Android不认Java编译的字节码,让我失去了一个周末


1、问题背景

我有一个SDK被集成进了SystemUI。SystemUI在周末进行Coverage构建的时候提示如下错误(省略了一些无关的信息,仅保留了关键文字):

dex2oatd method_verifier.cc:5126] void ...c(...) failed to verify: [0x115] expected to be within a catch-all for an instruction when a monitor is held
dex2oatd method_verifier.cc:867] Had a hard failure verifying all classes, and was asked to abort in such situation.

我被告知正常构建不会报错,只有Coverage构建会报错。由于我对SystemUI的构建不了解,无法从构建过程入手进行分析,故只能从错误信息本身着手。

2、问题分析

dex2oat的源码去分析对我来说显然不现实,所以我聚焦在这段文字本身:

expected to be within a catch-all for an instruction when a monitor is held

从这段文字可以看到几个关键字:monitorinstructioncatch-all。我决定从略有印象的monitor入手。

2.1、monitor

在网上一搜,我确认到monitorsynchronized的字节码(准确的说是smali,但区分他们在本文没有太大意义)。

假设有如下代码:

synchronized(lock) {
    data = 2
}

那么编译生成的字节码如下所示:

monitor-enter v0
const/4 v1, 0x2
:try_start_4
iput v1, p0, Lphantom/monitor/MainActivity;->data:I
:try_end_8
.catchall {:try_start_4 .. :try_end_8} :catchall_a
monitor-exit v0
return-void
:catchall_a
move-exception v1
monitor-exit v0
throw v1

这段字节码包含几个要点:

  1. synchronized关键字会生成成对的monitor-entermonitor-exit指令。
  2. 编译器会自动插入一段try-catch。插入try-catch的目的据说[1]是为了保障monitor在发生异常的时候不会被泄露。

综合上面两点,synchronized关键字的字节码大概会遵循如下模式:

monitor-enter
try_start
try_end
catchall
monitor-exit

在上面的字节码中出现了catchall,正好是SystemUI报错信息中提到的关键字。

2.2、catch和catchall

为了了解catchall的特别之处,对比下面代码的字节码。

// code 1
try {
    data = 3
} catch (e: Exception) {
    //
}

// code 2
try {
    data = 4
} catch (e: Throwable) {
    //
}

上述两端代码生成的字节码如下所示:

# code 1
const/4 v0, 0x3
:try_start_1
iput v0, p0, Lphantom/monitor/JavaClass;->data:I
:try_end_3
.catch Ljava/lang/Exception; {:try_start_1 .. :try_end_3} :catch_3

# code 2
const/4 v0, 0x4
:try_start_1
iput v0, p0, Lphantom/monitor/JavaClass;->data:I
:try_end_3
.catchall {:try_start_1 .. :try_end_3} :catchall_3

通过上面实验可以看出:

  1. catchall对应于catch(Throwable),即捕获所有异常。
  2. catch对应于具体类型的异常捕获。

综上,可以推测SystemUI报错的问题同时涉及synchronized语句和try-catch语句。

2.3、当代码同时涉及synchronized和try-catch的时候

结合报错信息,定位到SDK中的方法,在其中发现了这样一段代码:

try {
    // ... some other code
    synchronized (mLock) {
        // ... some code
    }
} catch (e: Exception) {
    // ... some other code
}

这段代码对应的字节码如下:

:try_start_135
# ... some other code
monitor-enter v4
:try_end_14d
.catch Ljava/lang/Exception; {:try_start_135 .. :try_end_14d} :catch_16d
:try_start_14f
# some code
:try_end_153
.catchall {:try_start_14f .. :try_end_153} :catchall_166
:try_start_153
# some code
monitor-exit v4
:try_end_158
.catch Ljava/lang/Exception; {:try_start_153 .. :try_end_158} :catch_16d
# ... some other code

该字节码有如下要点:

  1. 编译器自动插入的catchall仍然存在。
  2. 编译器额外插入了两段try-catch(Line5和Line14),且没有使用catchall

原设想synchronized外层的try-catch会生成一条catch指令,并把整个monitor包裹住。但真实的效果外层try-catchsynchronized劈成了两段,一段在Line15,另一段在Line1014,且前一段与后一段共享相同的异常类型和异常处理。猜测是try-catch不能嵌套,但并未去证实。

基于上述推论,将源码改为:

try {
    // ... some other code
    synchronized (mLock) {
        // ... some code
    }
} catch (e: Throwable) { // <- 把Exception改为Throwable
    // ... some other code
}

得到如下字节码:

:try_start_1
# ... some other code
monitor-enter v0
:try_end_6
.catchall {:try_start_1 .. :try_end_6} :catchall_11
:try_start_8
# some code
:try_end_c
.catchall {:try_start_8 .. :try_end_c} :catchall_e
:try_start_c
monitor-exit v0
:try_end_11
.catchall {:try_start_c .. :try_end_11} :catchall_11
# ... some other code

可见Line5和Line13都变成了catchall,而其他字节码并无本质变化。

综上,可以确认当try-catch中包含synchronized的时候,try-catch指令会被synchronized劈开,且有一条catch/catchall会插入在monitor-enter之后。

3、问题解决

3.1、修改效果及结论

经过上面的分析,SystemUI之所以报错是因为Android认为Java编译器在monitor-entermonitor-exit之间只能有catchall指令,而catch指令是不够安全的。

基于上述推论,将SDK中的catch异常类型从Exception修改为Throwable,重新集成到SystemUI做构建,本文开头的报错没有再出现。

由此得出一条潜规则:当内部直接包含synchronized语句的时候,catch的类型必须是Throwable

3.2、回过头来:Java编译输出的字节码真的不够安全吗?

源代码:

try {
    // ... some code #1
    synchronized(lock) {
        // ... some code #2
    }
} catch (e: Exception) {
    // ... some code #3
}

字节码:

:try_start_1
# ... some code #1
monitor-enter v0
:try_end_6
.catch Ljava/lang/Exception; {:try_start_1 .. :try_end_6} :catch_11
:try_start_8
# ... some code #2
:try_end_c
.catchall {:try_start_8 .. :try_end_c} :catchall_e
:try_start_c
monitor-exit v0
goto :goto_11
:catchall_e
move-exception v1
monitor-exit v0
throw v1
:try_end_11
.catch Ljava/lang/Exception; {:try_start_c .. :try_end_11} :catch_11
:catch_11
# ... some code #3
:goto_11
return-void
  1. 如果不发生任何异常,字节码会从Line1顺序执行到Line11,没有任何跳转,monitor-entermonitor-exit是配对的。
  2. 如果code #2发生异常,那么会命中Line9的catchall,跳过Line11的monitor-exit,跳转到Line13的异常处理,执行Line15的monitor-exitmonitor-entermonitor-exit仍然是配对的。
  3. 如果code #1发生异常,有两种情况:
    1. 发生的异常是Exception类型:命中Line5,那么Line3的monitor-enter不会执行,而是跳转到Line19,不会执行monitor-exit
    2. 发证的异常不是Exception类型:不会命中Line5,异常应该会中断代码执行,理论上Line3及之后的所有指令都不会执行,包括Line3的monitor-enter在内。

基于上述分析,可以相信:

  1. 如果执行进了synchronizedmonitor-entermonitor-exit总是配对的,monitor是安全的。
  2. 如果没有执行进synchronized,根本就不会进入monitor,monitor也是安全的。

综上可以得出结论:Java编译器生成的字节码是没有问题的。问题的本质可能是:

  1. Android机械的认为monitor内部不能有catch指令,只能有catchall指令。
  2. Java编译劈开try-catch时大可把catch指令放在monitor-enter之前,但却放在了monitor-enter之后。不管Java编译器的意图为何,但这个结果跟Android的校验规则不合。

3.3、遗留问题和其他结论

还有一些问题没能深入研究:

  1. 为什么Coverage构建会报错,而正常构建不报错?
  2. 为什么把APK直接安装到手机上运行的时候dex2oat不会报错,只有在构建的时候报错?
  3. Java编译器生成的字节码不能说是错误的,但为啥Android又不认可。既然Android不认可,为啥不在构建的更早环节报错呢?

在解决问题的过程中还得出了如下结论:

  1. try-catch语句和runCatching语句在异常类型为Throwable的时候,生成的字节码没有本质区别。
  2. Java和Kotlin的try-catch语句生成的字节码没有本质区别。

上述结论很容易验证,这里不再赘述。


原文链接:Android不认Java编译的字节码,让我失去了一个周末


  1. Android smali逆向还原之synchronized原理剖析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容