java - 在java编译器中标记行的问题

标签 java javac bytecode javap

我是硕士生,正在研究静态分析。
在我的一次测试中,我遇到了在 java 编译器中标记行的问题。
我有以下java代码:

 226:   String json = "/org/elasticsearch/index/analysis/commongrams/commongrams_query_mode.json";
 227:   Settings settings = Settings.settingsBuilder()
 228:           .loadFromStream(json, getClass().getResourceAsStream(json))
 229:           .put("path.home", createHome())
 230:           .build();
编译此代码并执行命令 javap -p -v CLASSNAME 时,我得到一个表格,其中包含字节码中每条指令对应的源代码行。
见下图:
Bytecode table
问题是在调用.put (" path.home ", createHome ())方法,字节码基本上生成四个指令:
19: anewarray  
24: ldc - String path.home
30: invokespecial - createHome
34: invokevirtual - put

前两个标记为第 228 行(错误),后两个标记为第 229 行(正确)。
见下图:
Bytecode table
这是 .put("path.home", createHome()) 的原始实现方法:
     public Builder put(Object... settings) {
        if (settings.length == 1) {
            // support cases where the actual type gets lost down the road...
            if (settings[0] instanceof Map) {
                //noinspection unchecked
                return put((Map) settings[0]);
            } else if (settings[0] instanceof Settings) {
                return put((Settings) settings[0]);
            }
        }
        if ((settings.length % 2) != 0) {
            throw new IllegalArgumentException("array settings of key + value order doesn't hold correct number of arguments (" + settings.length + ")");
        }
        for (int i = 0; i < settings.length; i++) {
            put(settings[i++].toString(), settings[i].toString());
        }
        return this;
    }
我已经尝试使用 Oracle-JDK v8 和 Open-JDK v16 编译代码,并且都得到了结果。
我还通过更改 put() 进行了测试。方法通过删除其参数。编译此代码时,未出现标记行的问题。
我想知道为什么字节码指令映射行 229: .put (" path.home ", createHome ())在 java 源代码中的原始行以外的行上?有谁知道这是故意的吗?

最佳答案

这是连接的方式,行号关联存储在类文件和javac的历史记录中。编译器。
line number table仅包含将行号与标记其开头的代码位置相关联的条目。因此,假定该位置之后的所有指令都属于表中明确提及的下一个位置之前的同一行。
由于详细信息会占用空间,并且规范不要求行号表具有特定的精度,因此编译器供应商对包含哪些详细信息做出了不同的决定。
过去,即直到 Java 7,javac只为语句的开头生成了行号表条目,所以当我用 Java 7 的 javac 编译以下代码时

String settings = new StringBuilder() // this is line 7 in my .java file
    .append('a')
    .append(
      5
      +
      "".length())
    .toString();
我得到类似的东西
stack=3, locals=2, args_size=1
   0: new           #2                  // class java/lang/StringBuilder
   3: dup
   4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
   7: bipush        97
   9: invokevirtual #4                  // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
  12: iconst_5
  13: ldc           #5                  // String
  15: invokevirtual #6                  // Method java/lang/String.length:()I
  18: iadd
  19: invokevirtual #7                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  25: astore_1
  26: return
LineNumberTable:
  line 7: 0
  line 14: 26
这将导致属于该语句的所有指令仅与第 7 行相关联。
这已经被认为太少了,所以从Java 8开始,javac为跨越多行的表达式中的方法调用生成额外的条目。因此,当我使用 Java 8 或更高版本编译相同的代码时,我得到
stack=3, locals=2, args_size=1
   0: new           #2                  // class java/lang/StringBuilder
   3: dup
   4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
   7: bipush        97
   9: invokevirtual #4                  // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
  12: iconst_5
  13: ldc           #5                  // String
  15: invokevirtual #6                  // Method java/lang/String.length:()I
  18: iadd
  19: invokevirtual #7                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  25: astore_1
  26: return
LineNumberTable:
  line 7: 0
  line 8: 9
  line 12: 15
  line 9: 19
  line 13: 22
  line 14: 26
请注意每个附加条目(与 Java 7 版本相比)如何指向调用指令,以确保方法调用与正确的行号相关联。这极大地改进了异常堆栈跟踪以及步骤调试。
没有显式条目的非调用指令仍将与它们最近的具有条目的前面代码位置相关联。
因此,bipush 97 'a'对应的指令常量与第 7 行相关联,因为只有随后的 append消耗常量的调用有一个显式条目将它与第 8 行相关联。
下一个表达式的后果,5 + "".length() ,更是戏剧化。
推送常量的说明,iconst_5ldc [""] , 关联到第 8 行,前一个 append 的位置调用,而 iadd指令,实际上属于+ 5之间的运算符和 ""常量,与第 12 行相关联,因为最近获得显式行号的调用指令是 length()调用。
为了比较,Eclipse 是这样编译相同代码的:
stack=3, locals=2, args_size=1
   0: new           #20                 // class java/lang/StringBuilder
   3: dup
   4: invokespecial #22                 // Method java/lang/StringBuilder."<init>":()V
   7: bipush        97
   9: invokevirtual #23                 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
  12: iconst_5
  13: ldc           #27                 // String
  15: invokevirtual #29                 // Method java/lang/String.length:()I
  18: iadd
  19: invokevirtual #35                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  22: invokevirtual #38                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  25: astore_1
  26: return
LineNumberTable:
  line 6: 0
  line 7: 7
  line 9: 12
  line 11: 13
  line 9: 18
  line 8: 19
  line 12: 22
  line 6: 25
  line 13: 26
Eclipse 编译器没有 javac的历史,而是首先被设计为为表达式生成行号条目。我们可以看到它将属于调用表达式的第一条指令(不是调用指令)与右侧的行相关联,即 bipush 97append('a')ldc [""]"".length() .
此外,它还有 iconst_5 的附加条目。 , iadd , 和 astore_1 , 将它们与正确的行相关联。当然,这种更高的精度也会导致类文件稍大。

关于java - 在java编译器中标记行的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67409058/

相关文章:

java - ASM - 使用来自 LocalVariableSorter 的 newLocal 的奇怪 localVar 索引

java - Shiro 'Any' 权限实例

java - RuleBasedCollat​​or getInstance Locale.US getRules 在 Droid 中返回空字符串

仅 Java 注解处理

java - 无法编译和执行java程序

c# - C# 中的 native 代码?

java - 为什么 JUnit "Run as->JUnit"和 "Cover as -> Junit"有不同的行为?

c# - 使用 Adob​​e Air/Java 编写 Web 应用程序而不是平台特定语言的优点/缺点?

java - 我可以从 Java 写入 Beanshell 控制台吗?

javac.exe AST 编程访问示例