由pthread库版本不一致导致的段错误
前几天工作中遇到一个奇怪的问题,程序编译好之后一运行,就发生 segmentation fault. 另一个奇怪的问题是,删掉部分无用的代码(至少在程序启动时不会被调用),编译出来的程序稍微小了一点,就可以运行了。
发生 Segmentation fault 的程序,写在 main() 函数内的 log 都没有打印出来,因此断定是库的问题,但要跟踪确定问题到底发生在哪里,还是费了一番力气。先截个图:
由于程序是在开发板上运行的,不能直接调试,而且是MIPS汇编,此前没有接触过,不过幸好还算简单。没有办法,只得开 gdbserver 远程调试。在发生 Segmentation fault 的地方首先加载一下动态库(shared library)的符号,然后看一下堆栈,是这样的:
1 2 3 4 5 6 7 8 9 10 | (gdb) info stack #0 0x2b17b4e8 in memcpy () from /home/roy/mips-libs/lib/libc.so.0 #1 0x2b19f3cc in __libc_pthread_init () from /home/roy/mips-libs/lib/libc.so.0 #2 0x0042a1b8 in __pthread_initialize_minimal () #3 0x2b19f50c in __uClibc_init () from /home/roy/mips-libs/lib/libc.so.0 #4 0x2b083e40 in _dl_get_ready_to_run () from /home/roy/mips-libs/lib/ld-uClibc.so.0 #5 0x2b0842ec in ?? () from /home/roy/mips-libs/lib/ld-uClibc.so.0 warning: GDB can't find the start of the function at 0x2b0842eb. Backtrace stopped: previous frame inner to this frame (corrupt stack?) (gdb) |
在 memcpy() 函数里面发生段错误,那不用说了,肯定传入了空指针。查看寄存器和汇编代码,进一步确认一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | (gdb) disas memcpy Dump of assembler code for function memcpy : 0x2b1af4d0 < memcpy +0>: b 0x2b1af4e8 < memcpy +24> 0x2b1af4d4 < memcpy +4>: move v1,a0 0x2b1af4d8 < memcpy +8>: addiu a2,a2,-1 0x2b1af4dc < memcpy +12>: addiu a1,a1,1 0x2b1af4e0 < memcpy +16>: sb v0,0(v1) 0x2b1af4e4 < memcpy +20>: addiu v1,v1,1 0x2b1af4e8 < memcpy +24>: bnezl a2,0x2b1af4d8 < memcpy +8> 0x2b1af4ec < memcpy +28>: lbu v0,0(a1) 0x2b1af4f0 < memcpy +32>: jr ra 0x2b1af4f4 < memcpy +36>: move v0,a0 End of assembler dump. (gdb) info registers zero at v0 v1 a0 a1 a2 a3 R0 00000000 181020a4 2b1d33a0 2b1f09f8 2b1f09f8 00000000 000000b8 00000000 t0 t1 t2 t3 t4 t5 t6 t7 R8 00000000 00000004 00000004 00000001 00000000 2b0c4000 00000018 0042a1b8 s0 s1 s2 s3 s4 s5 s6 s7 R16 2b0b36f4 00000005 2b0c4000 2b174234 00000000 00000004 00000001 0000002f t8 t9 k0 k1 gp sp s8 ra R24 000002b6 2b1af4d0 00000000 00000000 2b1f3510 7ffe7770 7ffe7880 2b1d33cc status lo hi badvaddr cause pc 01000313 0001257f 000002cb 00000000 80800408 2b1af4e8 fcsr fir restart 00000000 00000000 00000000 (gdb) |
发生段错误的原因用红色字体标出来了,$a1寄存器的值为0,也就是空指针,memcpy() 函数中还尝试从 $a1 的地址中读数据。
那么为什么 $a1 寄存器的值为0呢,它又应该是个什么值?继续按图索骥,向下查看堆栈,首先看__libc_pthread_init() 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | (gdb) disas __libc_pthread_init Dump of assembler code for function __libc_pthread_init: 0x2b1d33a0 <__libc_pthread_init+0>: lui gp,0x2 0x2b1d33a4 <__libc_pthread_init+4>: addiu gp,gp,368 0x2b1d33a8 <__libc_pthread_init+8>: addu gp,gp,t9 0x2b1d33ac <__libc_pthread_init+12>: addiu sp,sp,-32 0x2b1d33b0 <__libc_pthread_init+16>: sw ra,28(sp) 0x2b1d33b4 <__libc_pthread_init+20>: sw gp,16(sp) 0x2b1d33b8 <__libc_pthread_init+24>: move a1,a0 0x2b1d33bc <__libc_pthread_init+28>: lw t9,-32732(gp) 0x2b1d33c0 <__libc_pthread_init+32>: lw a0,-30536(gp) 0x2b1d33c4 <__libc_pthread_init+36>: jalr t9 0x2b1d33c8 <__libc_pthread_init+40>: li a2,184 0x2b1d33cc <__libc_pthread_init+44>: lw gp,16(sp) 0x2b1d33d0 <__libc_pthread_init+48>: lw ra,28(sp) 0x2b1d33d4 <__libc_pthread_init+52>: addiu sp,sp,32 0x2b1d33d8 <__libc_pthread_init+56>: jr ra 0x2b1d33dc <__libc_pthread_init+60>: lw v0,-30532(gp) End of assembler dump. (gdb) |
这里发现首先把 $a0 寄存器的值赋给了 $a1,然后 $a0 寄存器另作他用。那么在执行到红色代码那一行的时候,$a0 寄存器应该也是 0,我们向下再找,就要观察 $a0 寄存器了。继续看 __pthread_initialize_minimal () 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | (gdb) disas __pthread_initialize_minimal Dump of assembler code for function __pthread_initialize_minimal: 0x0042a194 <__pthread_initialize_minimal+0>: lui gp,0x5 0x0042a198 <__pthread_initialize_minimal+4>: addiu gp,gp,20252 0x0042a19c <__pthread_initialize_minimal+8>: addu gp,gp,t9 0x0042a1a0 <__pthread_initialize_minimal+12>: addiu sp,sp,-32 0x0042a1a4 <__pthread_initialize_minimal+16>: sw ra,28(sp) 0x0042a1a8 <__pthread_initialize_minimal+20>: sw gp,16(sp) 0x0042a1ac <__pthread_initialize_minimal+24>: lw t9,-30268(gp) 0x0042a1b0 <__pthread_initialize_minimal+28>: jalr t9 0x0042a1b4 <__pthread_initialize_minimal+32>: move a0,zero 0x0042a1b8 <__pthread_initialize_minimal+36>: lw gp,16(sp) 0x0042a1bc <__pthread_initialize_minimal+40>: lw v1,-32716(gp) 0x0042a1c0 <__pthread_initialize_minimal+44>: sw v0,2772(v1) 0x0042a1c4 <__pthread_initialize_minimal+48>: lw ra,28(sp) 0x0042a1c8 <__pthread_initialize_minimal+52>: jr ra 0x0042a1cc <__pthread_initialize_minimal+56>: addiu sp,sp,32 End of assembler dump. (gdb) |
哦,找到了,原来是在 __pthread_initialize_minimal() 函数中,$a0 寄存器被清空了,那么后面的 Segmentation fault 几乎顺理成章了。但是稍微一想,肯定就发现问题了,这个地方的代码没有发现什么分支,如果有另外一个程序可以正常运行的话,要么没有调用 __pthread_initialize_minimal() 函数,要么调用了另外一个不同版本的 __pthread_initialize_minimal() 函数。
带着这个疑问,我们跟踪一下正常运行的程序(暗自庆幸还有一个可供对比的样本)。但是调试这个正常运行的程序也费了一番力气,因为断点不好设置,因为 libc.so.0 并不是一起来就加载的,而是由一个过程,而且远程调试也不支持设置 load library 的断点。没有办法,只得我们自己寻找了,首先把断点设在 _dl_load_elf_shared_library() 函数上面,每断一次,就查看一下加载的是哪个库:
1 2 3 4 5 6 7 | (gdb) continue Continuing. Breakpoint 1, 0x2b83e1e8 in _dl_load_elf_shared_library () from /home/roy/mips-libs/lib/ld-uClibc.so.0 (gdb) printf "%s\n" , $s4+1 librt.so.0 (gdb) |
第一次加载了一个 librt.so.0,继续这个过程,直到 libc.so.0 被加载,然后就可以在 __libc_pthread_init () 这个函数上面打断点了:
1 2 3 4 5 6 7 8 | (gdb) break __libc_pthread_init Breakpoint 2 at 0x2b95b3bc (gdb) del 1 (gdb) continue Continuing. Breakpoint 2, 0x2b95b3bc in __libc_pthread_init () from /home/roy/mips-libs/lib/libc.so.0 (gdb) |
好,断下来了,看堆栈:
1 2 3 4 5 6 | (gdb) info stack #0 0x2b95b3bc in __libc_pthread_init () from /home/roy/mips-libs/lib/libc.so.0 #1 0x2b8a8eac in __pthread_initialize_minimal () from /home/roy/mips-libs/lib/libpthread.so.0 #2 0x2b95b50c in __uClibc_init () from /home/roy/mips-libs/lib/libc.so.0 #3 0x2b83fe40 in ?? () (gdb) |
已经发现问题了,我们看到,的确出现了一个不同的 __pthread_initialize_minimal() 函数,这个函数在 libpthread.so.0 中,而先前出错的那个,不知道在哪里,应该是在我们程序本身。看一下这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | (gdb) disas __pthread_initialize_minimal Dump of assembler code for function __pthread_initialize_minimal: 0x2b8a8e84 <__pthread_initialize_minimal+0>: lui gp,0x2 0x2b8a8e88 <__pthread_initialize_minimal+4>: addiu gp,gp,-10676 0x2b8a8e8c <__pthread_initialize_minimal+8>: addu gp,gp,t9 0x2b8a8e90 <__pthread_initialize_minimal+12>: addiu sp,sp,-32 0x2b8a8e94 <__pthread_initialize_minimal+16>: sw ra,28(sp) 0x2b8a8e98 <__pthread_initialize_minimal+20>: sw gp,16(sp) 0x2b8a8e9c <__pthread_initialize_minimal+24>: lw t9,-32292(gp) 0x2b8a8ea0 <__pthread_initialize_minimal+28>: lw a0,-32340(gp) 0x2b8a8ea4 <__pthread_initialize_minimal+32>: jalr t9 0x2b8a8ea8 <__pthread_initialize_minimal+36>: nop 0x2b8a8eac <__pthread_initialize_minimal+40>: lw gp,16(sp) 0x2b8a8eb0 <__pthread_initialize_minimal+44>: lw v1,-32744(gp) 0x2b8a8eb4 <__pthread_initialize_minimal+48>: sw v0,14948(v1) 0x2b8a8eb8 <__pthread_initialize_minimal+52>: lw ra,28(sp) 0x2b8a8ebc <__pthread_initialize_minimal+56>: jr ra 0x2b8a8ec0 <__pthread_initialize_minimal+60>: addiu sp,sp,32 End of assembler dump. (gdb) |
我们看到,$a0 寄存器的值是从 $gp-32340 这样寻址来的,看一下此时的 $a0 寄存器,发现它的内容和某个全局变量的地址是一样的:
1 2 3 4 5 6 7 8 | (gdb) p/x $a0 $2 = 0x2b8be410 (gdb) info variable __pthread_functions All variables matching regular expression "__pthread_functions" : Non-debugging symbols: 0x2b8be410 __pthread_functions (gdb) |
当然这是题外话。剩下的代码就一样了,$a0 赋给 $a1 ,从 $a1 读数据。
为什么一样的 Makefile 会生成两个不同的 __pthread_initialize_minimal() 函数?正当大惑不解的时候,灵光一闪,看一下运行时用到的 libpthread.so.0 和链接时用到的 libpthread.a 吧(用 objdump -S 看汇编代码):
在动态库 libpthread.so.0 中的 __pthread_initialize_minimal() 是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 0000be84 <__pthread_initialize_minimal>: be84: 3c1c0002 lui gp,0x2 be88: 279cd64c addiu gp,gp,-10676 be8c: 0399e021 addu gp,gp,t9 be90: 27bdffe0 addiu sp,sp,-32 be94: afbf001c sw ra,28(sp) be98: afbc0010 sw gp,16(sp) be9c: 8f9981dc lw t9,-32292(gp) bea0: 8f8481ac lw a0,-32340(gp) bea4: 0320f809 jalr t9 bea8: 00000000 nop beac: 8fbc0010 lw gp,16(sp) beb0: 8f838018 lw v1,-32744(gp) beb4: ac623a64 sw v0,14948(v1) beb8: 8fbf001c lw ra,28(sp) bebc: 03e00008 jr ra bec0: 27bd0020 addiu sp,sp,32 |
在静态库 libpthread.a 中的 __pthread_initialize_minimal() 是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 000010f4 <__pthread_initialize_minimal>: 10f4: 3c1c0000 lui gp,0x0 10f8: 279c0000 addiu gp,gp,0 10fc: 0399e021 addu gp,gp,t9 1100: 27bdffe0 addiu sp,sp,-32 1104: afbf001c sw ra,28(sp) 1108: afbc0010 sw gp,16(sp) 110c: 8f990000 lw t9,0(gp) 1110: 0320f809 jalr t9 1114: 00002021 move a0,zero 1118: 8fbc0010 lw gp,16(sp) 111c: 8f830000 lw v1,0(gp) 1120: ac620014 sw v0,20(v1) 1124: 8fbf001c lw ra,28(sp) 1128: 03e00008 jr ra 112c: 27bd0020 addiu sp,sp,32 |
一目了然了,正常运行的程序是从动态库中找符号,段错误的程序从静态库中找符号。那么,应该是我的代码中有使用 pthread 库的代码,恰恰是被删除的那一部分中,所以较大的程序,只是因为链接了 libpthread.a。毅然将 Makefile 中的 -lpthread 去掉,大功告成。
如今轻描淡写的几句话,当时不知费了多少力气。这个开发板是从某供应商买来的,他们提供的库不一样,我也没什么话可说了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步