【问题标题】:Determine where a catch block ends ASM确定 catch 块结束 ASM 的位置
【发布时间】:2015-01-08 20:52:43
【问题描述】:

在 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 块末尾的位置,但我不知道如何。为什么我需要它?因为我想从字节码中“删除”try-catch 块。

例如,我正在尝试改变:

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之间的所有指令,然后删除所有标签。

有什么想法吗?

【问题讨论】:

    标签: java bytecode java-bytecode-asm bytecode-manipulation


    【解决方案1】:

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

    try-catch 结构的编译很简单。例如:

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

    编译为:

    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 finallysynchronized 或更新的 try(…) with resources 声明。它们最终都会创建在字节码级别上看起来像 catch 块的异常处理程序。


    由于异常处理程序中的分支指令在从异常中恢复时可能会针对处理程序之外的代码,因此遍历异常处理程序的代码图在这里没有帮助,因为正确处理分支指令需要有关分支目标的信息你真的很想聚集。

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

    【讨论】:

      【解决方案2】:

      简而言之,您必须进行执行流分析。在您的示例中:

      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 的树形包之上构建了此类工具。

      【讨论】:

      • 但是我如何找到(4)?
      • 按照指令链遍历所有可能的分支、条件跳转和嵌套的 try/catch 块。
      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

        如果您只想删除与“catch 块”相关的代码,我建议您只需删除相关的异常处理程序条目,然后执行死代码消除传递。您可能可以在某处找到现有的 DCE 通行证(例如,Soot),或者您可以编写自己的通行证。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-08-15
          • 2022-01-21
          • 2013-04-19
          • 2023-03-11
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多