由pthread庫版本不一致導致的段錯誤


前幾天工作中遇到一個奇怪的問題,程序編譯好之后一運行,就發生 segmentation fault. 另一個奇怪的問題是,刪掉部分無用的代碼(至少在程序啟動時不會被調用),編譯出來的程序稍微小了一點,就可以運行了。

發生 Segmentation fault 的程序,寫在 main() 函數內的 log 都沒有打印出來,因此斷定是庫的問題,但要跟蹤確定問題到底發生在哪里,還是費了一番力氣。先截個圖:

由於程序是在開發板上運行的,不能直接調試,而且是MIPS匯編,此前沒有接觸過,不過幸好還算簡單。沒有辦法,只得開 gdbserver 遠程調試。在發生 Segmentation fault 的地方首先加載一下動態庫(shared library)的符號,然后看一下堆棧,是這樣的:

(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() 函數里面發生段錯誤,那不用說了,肯定傳入了空指針。查看寄存器和匯編代碼,進一步確認一下:

(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() 函數:

(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 () 函數:

(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() 函數上面,每斷一次,就查看一下加載的是哪個庫:

(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 () 這個函數上面打斷點了:

(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)

 好,斷下來了,看堆棧:

(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 中,而先前出錯的那個,不知道在哪里,應該是在我們程序本身。看一下這個函數:

(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 寄存器,發現它的內容和某個全局變量的地址是一樣的:

(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() 是這樣的:

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() 是這樣的:

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 去掉,大功告成。

如今輕描淡寫的幾句話,當時不知費了多少力氣。這個開發板是從某供應商買來的,他們提供的庫不一樣,我也沒什么話可說了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM