关于一个链接的BUG
疑难问题现象
现象
一个安卓程序需要调用一个so库实现加解密操作,但是在更新了新版本后一直发现程序崩溃。
kernel pain:
12345678910111213141516171819202122232017-08-21 10:43:58.724 F/libc ( 4844): Fatal signal 11 (SIGSEGV), code 1, fault addr 0xf4c3eb4 in tid 4844 (test.app)2017-08-21 10:43:58.827 I/DEBUG ( 266): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***2017-08-21 10:43:58.827 I/DEBUG ( 266): Build fingerprint: 'qcom/msm8909/msm8909:5.1.1/NewLand_N900/newland.20170815.163947:user/test-keys'2017-08-21 10:43:58.827 I/DEBUG ( 266): Revision: '0'2017-08-21 10:43:58.827 I/DEBUG ( 266): ABI: 'arm'2017-08-21 10:43:58.827 I/DEBUG ( 266): pid: 4844, tid: 4844, name: test.app >>> ./test.app <<<2017-08-21 10:43:58.827 I/DEBUG ( 266): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xf4c3eb42017-08-21 10:43:58.830 W/NativeCrashListener( 760): Couldn't find ProcessRecord for pid 48442017-08-21 10:43:58.835 I/DEBUG ( 266): r0 00000000 r1 becddf70 r2 becddf90 r3 0f4c3eb52017-08-21 10:43:58.835 E/DEBUG ( 266): AM write failure (32 / Broken pipe)2017-08-21 10:43:58.835 I/DEBUG ( 266): r4 b6f1efc8 r5 becde97c r6 00000001 r7 b6f1cc342017-08-21 10:43:58.835 I/DEBUG ( 266): r8 00000000 r9 00000000 sl 00000000 fp becde81c2017-08-21 10:43:58.835 I/DEBUG ( 266): ip 80000000 sp becddf50 lr b6f1d2b4 pc 0f4c3eb4 cpsr a00f00302017-08-21 10:43:58.836 I/DEBUG ( 266):2017-08-21 10:43:58.836 I/DEBUG ( 266): backtrace:2017-08-21 10:43:58.836 I/DEBUG ( 266): #00 pc 0f4c3eb4 <unknown>2017-08-21 10:43:58.836 I/DEBUG ( 266): #01 pc 000012b0 /data/test.app (mfg_request_auth+1196)2017-08-21 10:43:58.836 I/DEBUG ( 266): #02 pc 00000ca8 /data/test.app (main+116)2017-08-21 10:43:58.836 I/DEBUG ( 266): #03 pc 0000f3f9 /system/lib/libc.so (__libc_init+44)2017-08-21 10:43:58.836 I/DEBUG ( 266): #04 pc 00000bfc /data/test.app (_start+88)2017-08-21 10:43:58.994 I/DEBUG ( 266):2017-08-21 10:43:58.994 I/DEBUG ( 266): Tombstone written to: /data/tombstones/tombstone_092017-08-21 10:43:58.994 I/BootReceiver( 760): Copying /data/tombstones/tombstone_09 to DropBox (SYSTEM_TOMBSTONE)从日志上看到PC指针指向一个地址0xf4c3eb4,在kernel pain上可以看出这个地址是一个无效地址。
这点我们可以从后面的程序的map里面可以看出来。
观察brackstrace:中打印的调用栈的信息,在奔溃之前进入了一个叫做 mgf_request_auth 的函数中,调用位置在offet=1196的位置上。
此时,我们反汇编这个函数所在的bin文件:
123456700000e04 <mfg_request_auth>:e04: e92d4810 push {r4, fp, lr}e08: e28db008 add fp, sp, #8e0c: e24ddd23 sub sp, sp, #2240 ; 0x8c0e10: e24dd004 sub sp, sp, #4e14: e1a03000 mov r3, r0@ ---- 此处省略若干行 ----从这里我们可以看到函数的入口地址为:0x0E04,定位崩溃点位置为:0x0E04 + 1196(这个是十进制数) 得到偏移位置为,0x12B0。
定位汇编文件中offset=12b0的位置:
12345678910111213141516@ ----- 此处省略若干行 ------128c: ebfffe29 bl b38 <memset@plt>1290: e59f32fc ldr r3, [pc, #764] ; 1594 <mfg_request_auth+0x790>1294: e7943003 ldr r3, [r4, r3]1298: e5933000 ldr r3, [r3]129c: e24b1e8a sub r1, fp, #2208 ; 0x8a012a0: e241100c sub r1, r1, #1212a4: e24b2d22 sub r2, fp, #2176 ; 0x88012a8: e242200c sub r2, r2, #1212ac: e3a00000 mov r0, #012b0: e12fff33 blx r312b4: e3a03f63 mov r3, #396 ; 0x18c12b8: e2833002 add r3, r3, #212bc: e58d3000 str r3, [sp]12c0: e3a00003 mov r0, #3@ ----- 此处省略若干行 ------
请注意这里:
12b0: e12fff33 blx r3
这是一条跳转指令,跳转地址存放在r3寄存器中。所以我们可以查看在kernel pain 中打印的r3寄存器的值,r3=0f4c3eb5。
BLX 本身是一条跳转并切换指令集的指令,跳转地址为0xf4c3eb5,但是这个地址是一个无效地址。(奔溃的时PC=0xf4c3eb4, r3=0x0f4c3eb5,但是为什么PC指针是跳转地址的上一个字节地址,可能和ARM32指令集切换到Thumb16指令集的地址对齐规则有关,后续查阅资料后将补充该内容。)
分析
从上述描述的现象看是崩溃的原因是由于跳转到错误的地址导致的。
问题:
- 那么这个错误的地址是如何引入的?
具体的奔溃位置在源码中的哪个部分呢?
于是我们开始结合汇编代码查阅源码:
12345678910int mfg_request_auth(uint8_t auth_code, uint8_t *obuf, uint32_t *olen){// ... ... 此处省略N行TRACE("czl--------------------");unsigned int nLen = 0;char szPosType[123]={0};NDK_SysGetPosInfo(SYS_HWINFO_GET_POS_TYPE, &nLen, &szPosType[0]);// ..... 此处省略N行}发现崩溃位置就是调用了这个函数(NDK_SysGetPosInfo)的地方。(具体结合汇编定位源码位置的方法有很多种,靠谱的方式就是通过GDB单步调试的方式。限于一些商业上的问题所以不方便公布所有源码内容,只能摘取部分关键片段)初步以为是因为函数传参时候由于传递的参数不正确导致进出栈时出错而崩溃。后来我们全部按照参数表的规定进行了调整,并单独定义局部变量等方式进行了多种尝试后发现依然崩溃。奔溃点还是在相同的地方。百思不得其解。
后来在浏览汇编源码时发现有个奇怪的现象:崩溃的位置是一个地址跳转指令,但是我们调用的这个函数是被定义在一个函数库中,通过隐式调用方式进行调用的。
那么所有隐式调用过程在汇编展开时都会是这样的:
128c: ebfffe29 bl b38 memset@plt
每个函数名的后面都有个@plt,这个是用来标识这个函数是定义在外部库中,通过BL 指令进行带链接方式的跳转。
它会产生一次长调用过程切换到GOT中查找函数名的对应入口地址。
如果发现该函数不存在有效的已链接地址,则引发加载器搜索对应的库文件将函数实现的汇编代码导出并生产一个有效地址,链接到可执行文件的GOT表中。
关于GOT和PLT可参考 [GOT(全局偏移表)和PLT(过程链接表)]
原因
通过上述问题的分析我们可以大致分析出问题的原因,肯定是由于错误的链接方式导致的。
于是我查询了该函数所在的头文件,查看函数的声明方式:
1extern int (*NDK_SysGetPosInfo)(EM_SYS_HWINFO emFlag,uint *punLen,char *psBuf);看到这里,真相大白。
我们看到这个函数在头文件中被声明为一个函数指针。但是我们在代码中一直都没有看到这个指针的赋值过程,所以编译器在初始化过程中任意赋值,就导致了一个错误地址被引入到代码中的。
这也是一个将显示调用和隐式调用混合调用的BUG。
所以,在C语言中,我们应该鼓励在调用函数指针时都使用指针解引用方式进行调用:
int ret = (*NDK_SysGetPosInfo)(/* 这里省略若干参数*/);
这样我们就在调用时很清楚地知道现在我们是通过显式调用方式进行函数调用,在查看问题时,我们就能够更容易且直接地发现问题的原因。