Dalvik Verifier 中的引用与精确引用

Reference vs. Precise Reference in Dalvik Verifier

我在 Dalvik 字节码上编写工具,它为各种方法调用条目执行一些日志记录。具体来说,在各种方法调用站点,我将插入一组收集参数的指令,将它们放入 Object[] 数组中,然后将其传递给日志记录函数。

一切都很好,我已经实施并克服了大多数应用程序的所有问题。但是我遇到了一个特别难以理解的 Dalvik 验证器错误:

java.lang.VerifyError: Verifier rejected class io.a.a.g: void io.a.a.g.r() 
failed to verify: void io.a.a.g.r(): [0x570] register v5 has type Reference: 
java.lang.Object but expected Precise Reference: java.lang.String

我查看了我的工具生成的代码,我所做的只是将寄存器 v5 放入一个对象数组中。

我有几个问题:

编辑:

这是我正在谈论的方法的字节码转储。 https://gist.github.com/kmicinski/c8382f0521b19643bb24379d91c47d36 如您所见,0x570 不是一条指令的开始,并且(据我所知)r5 与应该是对象的 String 没有任何冲突。

如果您仔细查看错误,它会告诉您您传递的是 Object,而应该是 String。无论如何,除非您 post 导致问题的实际字节码,否则没有更多可以说的了。

你确定0x570指向一条指令的中间吗?它不应该。无论如何,调试它的方法是查看相关指令并弄清楚为什么 r5 是一个对象,而它应该是一个字符串。或者你可以 post 字节码让我看看。

编辑:现在您已经 post 编辑了代码,实际上有一条路径导致 v5 成为对象,但它有点微妙

异常处理程序 .catch JSONException {:5D8 .. :938} :BDE 跳转到 :BDE

异常处理程序的代码将捕获的异常存储在v5 中,这意味着此时v5 不再是String。然后跳转到 :162

:BDE
00000BDE  move-exception      v5
00000BE0  const               v0, 0x00488B36
00000BE6  invoke-static       Logger->logBasicBlockEntry(I)V, v0
00000BEC  goto/16             :162

:162 在另一个异常处理程序的范围内:.catch ClassNotFoundException {:2E .. :594} :BF0

:Bf0 保持 v5 不变并跳转到 :A28

:BF0
00000BF0  move-exception      v6
00000BF2  const               v0, 0x00488B3E
00000BF8  invoke-static       Logger->logBasicBlockEntry(I)V, v0
00000BFE  goto/16             :A28

:A28 是假定 v5 是字符串的代码块的开头。特别是,在指令 :AE0 上,v5 被传递给一个接受字符串的函数。

00000AE0  invoke-virtual      StringBuilder->append(String)StringBuilder, v7, v5

0xAE0 恰好是 0x570 的两倍,这解释了错误中显示的偏移量,一旦您按照 JesusFreke 的建议调整了代码单元。

请注意,这不一定是唯一损坏的代码路径,它只是我在查看您的代码时发现的第一个。然而,一个错误的路径足以将 v5 的类型与 JSONException 统一起来,从而将其转换为 Object.

0x570 可能是代码单元中的偏移量,每个代码单元为两个字节。所以字节偏移量实际上是0xAE0,它确实对应了一条指令,而那条指令确实引用了v5.

我预计发生的情况是在 v5 中存储字符串的某处有代码,但是在字符串存储在 v5 中的位置和使用它的位置之间存在另一个代码路径,并且该代码路径具有不同的v5 中存储的对象类型。当代码路径合并时,它使用两种类型的公共超类作为寄存器的类型。因此,如果这两种类型完全不相关,则 java.lang.Object 将是超类。

调试此问题的方法是 运行 baksmali 使用 --register-info ARGS,DEST,FULLMERGE 选项(以及 --code-offsets,因此您可以轻松找到 0xAE0),然后从0xAE0 并查看 v5 的类型设置为对象的位置。

我想添加我的答案,因为其他人非常乐于贡献他们的时间来回答一个可能不会概括太多的棘手问题!

正如@Antimony 指出的那样,我的代码中有一条控制路径从异常处理程序开始,将异常存储在 v5 中(导致 v5 成为 Object ) 然后 goto 在 异常处理程序中 点。该异常处理程序随后导致 v5 被用作字符串,从而导致验证器错误。

在应用程序的原始代码中,goto 目标的唯一薄弱部分是一条 return-void 指令。因此,Dalvik 验证器没有将路径传播到异常处理程序。

不幸的是,当我重写这个应用程序时,它导致该异常处理程序的目标包含的不仅仅是这个 return-void 指令,使验证原因通过该块进入捕获的异常处理程序。特别是,在 return-void 之前,我插入了对 Logger.logMethodExit 的调用,然后验证器假设它可以将控制权转移回异常处理程序(在本例中为 :BF0)并最终转移到该位置其中 v5 用作字符串。在原始应用程序中,它被杀死了(在 gen/kill 数据流意义上)。但是在重写时,我包含了这个打破数据流不变量的额外调用...... Crud.

我想我知道如何在我的实施中解决这个问题,但弄清楚这确实很痛苦!

更多一般经验教训在这里:

  • 验证器错误偏移实际上只是字节码中的 2*index

  • 与 JVM 字节码不同,Dalvik 字节码认为一部分操作码是不可抛出的,包括 return。这会影响数据流分析

  • 精确引用意味着某些东西在一个基本块中被限制为对象的特定细化,在另一个基本块中被限制为 Object(尽管这个错误对我来说似乎有点深奥..)

  • 当你重写字节码时,你需要认识到你正在隐式工作的 gen/kill 集,特别是 return-* 指令会立即杀死东西,而跳转到 try.. 中基本块的开头将继续保持这些东西的存在。