为什么在允许某些Unicode字符的注释中执行Java代码?

Why is executing Java code in comments with certain Unicode characters allowed?

以下代码生成输出"hello world!"(不,真的,试试看)。

1
2
3
4
5
public static void main(String... args) {

   // The comment below is not a typo.
   // \u000d System.out.println("Hello World!");
}

这样做的原因是Java编译器将Unicode字符EDCOX1(0)作为新行解析并转换成:

1
2
3
4
5
6
public static void main(String... args) {

   // The comment below is not a typo.
   //
   System.out.println("Hello World!");
}

从而导致"执行"注释。

既然这可以用来"隐藏"恶意代码或者恶意程序员能想到的任何东西,为什么在注释中允许这样做?

为什么Java规范允许这样做?


Unicode解码发生在任何其他词汇翻译之前。这样做的主要好处是,在ASCII和任何其他编码之间来回切换都很简单。你甚至不需要知道评论从哪里开始和结束!

如JLS第3.3节所述,这允许任何基于ASCII的工具处理源文件:

[...] The Java programming language specifies a standard way of transforming a program written in Unicode into ASCII that changes a program into a form that can be processed by ASCII-based tools. [...]

这为平台独立性(支持字符集的独立性)提供了基本保证,这一直是Java平台的一个关键目标。

在用非拉丁语编写代码文档时,能够在文件中的任何位置编写任何Unicode字符是一个很好的特性,在注释中尤其重要。它能以如此微妙的方式干扰语义,这只是一个(不幸的)副作用。

在这个主题上有很多问题,Joshua Bloch和Neal Gafter的Java难题包括以下变型:

Is this a legal Java program? If so, what does it print?

1
2
3
4
5
6
7
8
9
10
11
\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020
\u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079
\u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020
\u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063
\u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028
\u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020
\u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b
\u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074
\u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020
\u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b
\u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d

(这是一个简单的"你好世界"节目。)

在解题中,他们指出了以下几点:

More seriously, this puzzle serves to reinforce the lessons of the previous three: Unicode escapes are essential when you need to insert characters that can’t be represented in any other way into your program. Avoid them in all other cases.

来源:Java:在注释中执行代码?!


由于这还没有解决,这里有一个解释,为什么Unicode转义在任何其他源代码处理之前发生:

其背后的思想是,它允许在不同字符编码之间进行Java源代码的无损翻译。如今,普遍存在对Unicode的支持,这看起来并不是问题,但在过去,西方国家的开发人员很难从他亚洲同事那里接收到包含亚洲字符的源代码,进行一些更改(包括编译和测试),并将结果发回,所有这些都不会对某些内容造成损害。G.

因此,Java源代码可以用任何编码来编写,并允许在标识符、字符和EDCOX1、6个字面和注释中包含大量字符。然后,为了无损地传输它,目标编码不支持的所有字符都将替换为它们的Unicode转义符。

这是一个可逆过程,有趣的一点是,翻译可以由一个工具来完成,它不需要知道Java源代码语法,因为翻译规则不依赖于它。这是因为编译器中的实际Unicode字符的翻译独立于Java源代码语法。它意味着您可以在两个方向上执行任意数量的翻译步骤,而不必更改源代码的含义。

这就是另一个甚至没有提到的奇怪特性的原因:\uuuuuuxxxx语法:

当翻译工具正在转义字符并且遇到已经是转义序列的序列时,它应该在序列中插入一个额外的u,将\ucafe转换为\uucafe。其含义没有改变,但是当转换到另一个方向时,工具应该只删除一个u,并用其unicode字符替换只包含单个u的序列。这样,即使Unicode转义符在来回转换时也保留在其原始形式中。我想,没人用过那个功能…


我将完全无效地添加这一点,仅仅是因为我无法控制自己,而且我还没有看到这一点,这个问题是无效的,因为它包含了一个隐藏的错误前提,即代码在注释中!

在Java源代码中,U000 0D等同于ASCII CR字符。它是一个线条的结束,无论发生在哪里,都是简单明了的。问题中的格式具有误导性,字符序列在语法上实际对应的是:

1
2
3
4
5
public static void main(String... args) {
   // The comment below is no typo.
   //
 System.out.println("Hello World!");
}

因此,最正确的答案是:代码执行是因为它不在注释中;它在下一行。"在爪哇中不允许执行注释,正如您所期望的那样。

很多混淆源于这样一个事实:语法高亮器和IDES不够复杂,无法考虑到这种情况。他们要么根本不处理unicode转义,要么在解析代码之后而不是之前处理,就像javac所做的那样。


\u000d转义终止注释,因为\u转义在程序标记化之前统一转换为相应的Unicode字符。您也可以使用\u0057\u0057而不是//来开始评论。

这是您的IDE中的一个bug,它的语法应该突出显示该行,以明确说明\u000d结束了注释。

这也是语言中的设计错误。它现在不能被纠正,因为这会破坏依赖它的程序。\u转义应该由编译器仅在"有意义"的上下文(字符串文本和标识符,可能没有其他地方)转换为相应的Unicode字符,或者应该禁止它们在U+0000–007F范围内生成字符,或者两者都转换。这些语义中的任何一个都会阻止注释被\u000d转义终止,而不会干扰\u转义是有用的情况。请注意,这包括使用\u转义在注释内作为一种非拉丁语脚本中对注释进行编码的方法,因为文本编辑器可以采用r查看\u转义的位置比编译器重要。(不过,我不知道任何编辑器或IDE会在任何上下文中将\u转义显示为相应的字符。)

在C族中有一个类似的设计错误,1,其中反斜杠换行是在确定注释边界之前处理的,例如。

1
2
// this is a comment \
   this is still in the comment!

我提出这一点是为了说明,如果您习惯于思考标记化技术和分析编译器程序员思考标记化技术和分析的方式,那么很容易犯下这个特定的设计错误,并且直到纠正它为时已晚时才意识到这是一个错误。基本上,如果您已经定义了形式语法,然后有人提出了一个语法特殊情况,即三角图、反斜杠换行符、在仅限于ASCII的源文件中编码任意Unicode字符,无论需要嵌入什么,在标记器之前添加转换过程比添加到红色更容易。定义记号赋予器以注意在何处使用该特殊情况是有意义的。

1对于学究:我知道C的这一方面是100%有意的,其基本原理是,我不是在编造这一点,它允许您在穿孔卡片上机械地强制使用任意长行的代码。这仍然是一个错误的设计决定。


这是一个有意的设计选择,它可以追溯到Java的最初设计。

对于那些询问"谁希望Unicode在注释中转义?"我想他们是母语使用拉丁字符集的人。换句话说,Java的原始设计固有的是,无论Java程序中的合法性如何,用户都可以使用任意的Unicode字符,最常见的是注释和字符串。

可以说,在用于查看源文本的程序(如IDES)中,这样的程序无法解释Unicode转义并显示相应的glyph,这是一个缺点。


我同意@zwl的说法,这是一个设计错误,但我更批评它。

\u转义在字符串和字符文本中很有用;这是它唯一应该存在的地方。它的处理方式应与其他逃逸的处理方式相同,如
"\u000A"应完全指"
"

绝对没有必要让\uxxxx发表评论——没人能读到。

同样,在程序的其他部分也没有使用\uxxxx的意义。唯一的例外可能是在强制包含一些非ASCII字符的公共API中——我们最后一次看到它是什么时候?

设计师们在1995年有他们的理由,但20年后,这似乎是一个错误的选择。

(向读者提问——为什么这个问题一直得到新的选票?这个问题是从某个流行的地方链接的吗?)


唯一能够回答为什么实现Unicode转义的人是编写规范的人。

一个合理的原因是,希望让整个BMP成为Java源代码的可能特征。但这会带来一个问题:

  • 您希望能够使用任何BMP字符。
  • 您希望能够输入任何BMP字符相当容易。一种方法是使用Unicode转义。
  • 您希望使词汇规范易于人类阅读和编写,并合理地易于实现。

当unicode逃逸进入混乱状态时,这是非常困难的:它创建了一整套新的lexer规则。

最简单的方法是分两步进行词法分析:首先搜索并用它所代表的字符替换所有Unicode转义,然后像Unicode转义不存在一样解析结果文档。

这样做的好处是它易于指定,因此它使规范更简单,并且易于实现。

缺点是,你的例子。


编译器不仅将Unicode转义符转换为它们在将程序解析为标记之前表示的字符,而且在丢弃注释和空白之前也会这样做。

此程序包含一个Unicode转义符(u000d),位于其唯一注释中。正如注释所告诉您的,这个转义表示换行符,编译器在放弃注释之前会对其进行适当的翻译。

这取决于平台。在某些平台上,如Unix,它会工作;在其他平台上,如Windows,它不会工作。虽然肉眼看输出可能相同,但如果将其保存在文件中或通过管道传输到其他程序进行后续处理,则很容易导致问题。