确定 catch 块在何处结束 ASM

Determine where a catch block ends ASM

在 ASM 中,我正在尝试确定 try-catch 块的标签。

目前我有:

public void printTryCatchLabels(MethodNode method) {

    if (method.tryCatchBlocks != null) {
        for (int i = 0; i < method.tryCatchBlocks.size(); ++i) {

            Label start = method.tryCatchBlocks.get(i).start.getLabel();
            Label end = method.tryCatchBlocks.get(i).end.getLabel();
            Label catch_start = method.tryCatchBlocks.get(i).handler.getLabel();

            System.out.println("try{      " + start.toString());
            System.out.println("}         " + end.toString());
            System.out.println("catch {   " + catch_start.toString());
            System.out.println("}         "  /*where does the catch block end?*/);

        }
    }

}

我正在尝试确定 catch 块末尾的标签位置,但我不知道如何确定。为什么我需要它?因为我想 "remove" 从字节码中尝试捕获块。

例如,我正在尝试更改:

public void test() {
    try {
        System.out.println("1");
    } catch(Exception e) {
        //optionally rethrow e.
    }
    System.out.println("2");
}

至:

public void test() {
    System.out.println("1");
    System.out.println("2");
}

所以要删除它,我认为我可以只获取标签并删除 catch-start 和 catch-end 之间的所有指令,然后删除所有标签。

有什么想法吗?

简而言之,您将不得不进行执行流程分析。在您的示例中:

public void test() {
    try {                         // (1) try start
        System.out.println("1");
    }                             // (2) try end
    catch(Exception e) {          
        //optionally rethrow e.   // (3) catch start
    }                             // (4) catch end
    System.out.println("2");      // (5) continue execution
}

图形上看起来像这样:

---(1)-+--(2)---------------------+
       |                          +--(5 execution path merged)
       +--(3 branched here)--(4)--+

因此,您需要构建代码块图,然后删除与 (3) 和 (4) 相关的节点。目前 ASM 不提供执行流分析工具,尽管一些用户报告说他们在 ASM 的树包之上构建了此类工具。

我建议阅读 JVM Spec §3.12. Throwing and Handling Exceptions。它包含一个非常简单但仍然存在问题的示例:

Compilation of try-catch constructs is straightforward. For example:

void catchOne() {
    try {
        tryItOut();
    } catch (TestExc e) {
        handleExc(e);
    }
}

is compiled as:

Method void catchOne()
0   aload_0             // Beginning of try block
1   invokevirtual #6    // Method Example.tryItOut()V
4   return              // End of try block; normal return
5   astore_1            // Store thrown value in local var 1
6   aload_0             // Push this
7   aload_1             // Push thrown value
8   invokevirtual #5    // Invoke handler method: 
                        // Example.handleExc(LTestExc;)V
11  return              // Return after handling TestExc
Exception table:
From    To      Target      Type
0       4       5           Class TestExc

此处,catch块以return指令结束,因此不加入原始代码流。但是,这不是必需的行为。相反,编译后的代码可以有一个分支到最后一个 return 指令来代替 4 return 指令,即

Method void catchOne()
0:  aload_0
1:  invokevirtual #6   // Method tryItOut:()V
4:  goto          13
7:  astore_1
8:  aload_0
9:  aload_1
10: invokevirtual #5   // Method handleExc:(LTestExc;)V
13: return
Exception table:
From    To      Target      Type
   0     4           7      Class TestExc

(例如,至少有一个 Eclipse 版本以这种方式编译示例)

但反之亦然,用指令 4 的分支代替最后一个 return 指令。

Method void catchOne()
0   aload_0
1   invokevirtual #6    // Method Example.tryItOut()V
4   return
5   astore_1
6   aload_0
7   aload_1
8   invokevirtual #5   // Method Example.handleExc(LTestExc;)V
11  goto 4
Exception table:
From    To      Target      Type
0       4       5           Class TestExc

所以你已经有三种可能性来编译这个不包含任何条件的简单示例。与循环或 if 指令关联的条件分支不一定指向条件代码块之后的指令。如果该代码块后跟另一个流控制指令,则条件分支(同样适用于 switch 目标)可能会使分支短路。

所以很难确定哪个代码属于 catch 块。在字节码级别,它甚至不必是一个连续的块,但可以与其他代码交错。

而这个时候我们连compiling finally and synchronized or the newer try(…) with resources声明都没有谈。他们最终都创建了在字节代码级别上看起来像 catch 块的异常处理程序。


由于异常处理程序中的分支指令在从异常中恢复时可能会以处理程序外部的代码为目标,因此遍历异常处理程序的代码图在这里无济于事,因为正确处理分支指令需要有关异常处理程序的信息您实际想要收集的分支目标。

所以处理这个任务的唯一方法是做相反的。您必须从非异常执行的方法开始遍历代码图,并将遇到的每条指令都视为不属于异常处理程序。对于剥离异常处理程序的简单任务,这已经足够了,因为您只需保留所有遇到的指令并删除所有其他指令。

在字节码级别,异常处理本质上是一个 goto。代码不必结构化,甚至根本不需要定义明确的 catch 块。即使您只处理正常编译的 Java 代码,一旦您考虑在 catch 块内尝试使用资源或复杂控制流结构的可能性,它仍然非常复杂。

如果您只想删除与 "catch block" 关联的代码,我建议您只删除关联的异常处理程序条目,然后执行无用代码消除过程。您可能可以在某处找到现有的 DCE 通行证(例如,Soot),或者您可以编写自己的通行证。

有一些比较常见的情况,catch 块的结束很容易被检测到。我在这里假设我们使用的是 Java 编译器。

  • 当 try 块(在开始和结束标签之间)以 GOTO(joinLabel) 结束时。如果块没有抛出异常或 return 总是,或 break/continue 退出周围循环,那么它将以指向最后一个处理程序结束的 GOTO 结束。
  • 对于不是最后一个块的 catch 块也是如此,它们将跳过处理程序以使用 GOTO 跟随,这可以帮助您识别最后一个处理程序的结尾。因此,如果 try 块没有这样的 GOTO,您可能会在其他处理程序中找到一个。
    • 可以通过比较具有相同开始和结束标签的 TRYCATCH 指令的处理程序标签来检测这些非最后的 catch 块。下一个处理程序的开始标签充当前一个处理程序的(独占)结束。