棧回溯技術


1.    前言

段錯誤、非法地址訪問等問題導致程序崩潰的現象屢屢發生,如果能找到發生錯誤的函數,往往一眼就能看出BUG所在——對於這類比較簡單的問題,比如使用空指針進行讀寫等,利用棧回溯技術可以很快定位。但是對於數組溢出、內存泄漏等問題導致的程序錯誤,往往隱藏很深,它們並不當場發作,即使我們一步一步跟蹤到發生錯誤的語句時,也經常會讓人覺得“這個地方根本不可能出錯啊”——錯誤在很早以前就隱藏下來了,只不過是這個“不可能出錯的語句”觸發了它。了解棧的作用、堆的實現,可以讓我們腦中對程序的運行、函數的調用、變量的操作有個感官的了解,對解決這類問題會有所幫助。

 

       關鍵詞:堆棧回溯堆實現棧作用

2.    棧的作用

一個程序包含代碼段、數據段、BSS段、堆、棧;其中數據段用來中存儲初始值不為0的全局數據,BSS段用來存儲初始值為0的全局數據,堆用於動態內存分配,棧用於實現函數調用、存儲局部變量。比如對於如下程序:

程序1 section.c

01 #include <stdlib.h>

02 #include <string.h>

03 #include <stdio.h>

04

05 int *g_pBuf;

06 int g_iCount = 10;

07

08 int main(int argc, char **argv)

09 {

10     char str[2];

11     g_pBuf = malloc(g_iCount);

12     printf("Address of main       = 0x%08x\n", (unsigned int)main);

13     printf("Address of g_pBuf     = 0x%08x\n", (unsigned int)&g_pBuf);

14     printf("Address of g_iCount   = 0x%08x\n", (unsigned int)&g_iCount);

15     printf("Address of malloc buf  = 0x%08x\n", (unsigned int)g_pBuf);

16     printf("Address of local buf str  = 0x%08x\n", (unsigned int)str);

17

18     return 0;

19 }

使用如下命令編譯得到可執行文件section,反匯編文件section.dis:

arm_v5t_le-gcc  -o  section  section.c -static

arm_v5t_le-objdump -D section > section.dis

 

PU S980上的linux環境下,這個程序的輸出結果為:

Address of main       = 0x000082b8

Address of g_pBuf     = 0x00082998

Address of g_iCount   = 0x00080bd0

Address of malloc buf  = 0x00083f80

Address of local buf str  = 0xbec80b36

 

        其中main函數的地址為0x000082b8,它處於代碼段中;全局變量g_pBuf位於BSS段;全局變量g_iCount位於數據段;使用malloc分配出來的內存地址為0x00083f80,它位於堆中;局部變量str數組的開始地址為0xbec80b36,位於棧中。它們的分布圖示如下:

 

1 程序各段示意圖

 

棧的作用有二:

①     保存調用者的環境——某些寄存器的值、返回地址

②     存儲局部變量

 

現在通過一個簡單的例子來說明棧的作用:

程序2 call.c

01 #include <stdlib.h>

02 #include <string.h>

03 #include <stdio.h>

04

05 void A(int a);

06 void B(int b);

07 void C(int c);

08

09 void A(int a)

10 {

11     printf("%d: A call B\n", a);

12     B(2);

13 }

14

15 void B(int b)

16 {

17     printf("%d: B call C\n", b);

18     C(3);

19 }

20

21 void C(int c)

22 {

23     printf("%d: function C\n", c);

24 }

25

26 int main(int argc, char **argv)

27 {  

28     A(1);

29     return 0;

30 }

 

使用如下命令編譯得到可執行文件call,反匯編文件call.dis:

arm_v5t_le-gcc -o  call  call.c  -static

arm_v5t_le-objdump -D call > call.dis

 

此程序的調用關系為main > A > B > C,現在來看看棧如何變化:

 

2 函數調用中棧的變化

 

       上圖中,main、A、B、C四個函數的棧大小都是16字節,返回地址都存在棧偏移地址為16的地方。我們是如何知道這點的呢?需要閱讀反匯編代碼:

……

000082b8 <A>:

    82b8:       e92d4800        push    {fp, lr}

    82bc:       e28db004        add     fp, sp, #4      ; 0x4

    82c0:       e24dd008        sub     sp, sp, #8      ; 0x8

    82c4:       e50b0008        str     r0, [fp, #-8]

    82c8:       e59f0014        ldr     r0, [pc, #20]   ; 82e4 <A+0x2c>

    82cc:       e51b1008        ldr     r1, [fp, #-8]

    82d0:       eb0003f8        bl      92b8 <_IO_printf>

    82d4:       e3a00002        mov     r0, #2  ; 0x2

    82d8:       eb000002        bl      82e8 <B>

    82dc:       e24bd004        sub     sp, fp, #4      ; 0x4

    82e0:       e8bd8800        pop     {fp, pc}

    82e4:       00064584        .word   0x00064584

 

000082e8 <B>:

    82e8:       e92d4800        push    {fp, lr}

    82ec:       e28db004        add     fp, sp, #4      ; 0x4

    82f0:       e24dd008        sub     sp, sp, #8      ; 0x8

    82f4:       e50b0008        str     r0, [fp, #-8]

    82f8:       e59f0014        ldr     r0, [pc, #20]   ; 8314 <B+0x2c>

    82fc:       e51b1008        ldr     r1, [fp, #-8]

    8300:       eb0003ec        bl      92b8 <_IO_printf>

    8304:       e3a00003        mov     r0, #3  ; 0x3

    8308:       eb000002        bl      8318 <C>

    830c:       e24bd004        sub     sp, fp, #4      ; 0x4

    8310:       e8bd8800        pop     {fp, pc}

    8314:       00064594        .word   0x00064594

 

00008318 <C>:

    8318:       e92d4800        push    {fp, lr}

    831c:       e28db004        add     fp, sp, #4      ; 0x4

    8320:       e24dd008        sub     sp, sp, #8      ; 0x8

    8324:       e50b0008        str     r0, [fp, #-8]

    8328:       e59f000c        ldr     r0, [pc, #12]   ; 833c <C+0x24>

    832c:       e51b1008        ldr     r1, [fp, #-8]

    8330:       eb0003e0        bl      92b8 <_IO_printf>

    8334:       e24bd004        sub     sp, fp, #4      ; 0x4

    8338:       e8bd8800        pop     {fp, pc}

    833c:       000645a4        .word   0x000645a4

 

00008340 <main>:

    8340:       e92d4800        push    {fp, lr}

    8344:       e28db004        add     fp, sp, #4      ; 0x4

    8348:       e24dd008        sub     sp, sp, #8      ; 0x8

    834c:       e50b0008        str     r0, [fp, #-8]

    8350:       e50b100c        str     r1, [fp, #-12]

8354:       e3a00001        mov     r0, #1  ; 0x1

8358:       ebffffd6        bl      82b8 <A>

835c:       e3a03000        mov     r3, #0  ; 0x0

8360:       e1a00003        mov     r0, r3

8364:       e24bd004        sub     sp, fp, #4      ; 0x4

8368:       e8bd8800        pop     {fp, pc}

 

       上面紅色的指令:

push    {fp, lr}”表示將fp,lr 寄存器進行壓棧,同時 sp – 4×2,即向下移動 8 個字節。

sub     sp, sp, #8”表示將 sp 寄存器的值減 8,即向下移動 8 個字節。

pop     {fp, pc}”表示彈出棧頂值到 fp, lr 寄存器,同時 sp+4×2,即向上移動 8 個字節。

       局部變量也是存儲在棧中,當一個函數的局部變量越多,它的棧越大。

 

       從上圖可知:棧中保存着函數的返回地址、局部變量等,那么我們可以從這些返回地址來確定函數的調用關系、調用順序。這就是下節介紹的棧回溯。

3.    棧回溯

將程序2稍加修改,見程序3第23、24、30、32四行:

程序3 stack.c

01 #include <stdlib.h>

02 #include <string.h>

03 #include <stdio.h>

04

05 void A(int a);

06 void B(int b);

07 void C(int c);

08

09 void A(int a)

10 {

11     printf("%d: A call B\n", a);

12     B(2);

13 }

14

15 void B(int b)

16 {

17     printf("%d: B call C\n", b);

18     C(3);

19 }

20

21 void C(int c)

22 {

23     char *p = (char *)c;

24     *p = ‘A’;

25     printf("%d: function C\n", c);

26 }

27

28 int main(int argc, char **argv)

29 {   

30      char a;

31     A(1);

32     C(&a);

33     return 0;

34 }

 

       第23、24兩行必然導致段錯誤而使得程序崩潰,linux內核當發現發生段錯誤時,會打印出棧信息。我們可以使用棧回溯的方法找到發生錯誤的原因。

使用如下命令編譯得到可執行文件stack,反匯編文件stack.dis:

       arm_v5t_le-gcc -g -o stack stack.c -static

       arm_v5t_le-objdump -dS stack > stack.dis

 

運行結果如下(注意:如果你是通過telnet來運行程序,可以使用dmesg命令看到這些棧信息):

/ # ./stack

1: A call B

2: B call C

<2>stack: unhandled page fault (11) at 0x00000003, code 0x817

<1>pgd = c3ca0000

<1>[00000003] *pgd=81b86031, *pte=00000000, *ppte=00000000

<4>

<4>Pid: 662, comm:                stack

<4>CPU: 0

<4>PC is at 0x8338

<4>LR is at 0x830c

<4>pc : [<00008338>]    lr : [<0000830c>]    Not tainted

<4>sp : be897af0  ip : 00000001  fp : be897b04

<4>r10: 00000000  r9 : 00008ac8  r8 : 00008b10

<4>r7 : 00000000  r6 : 00000000  r5 : 00081fe8  r4 : 00000000

<4>r3 : 00000041  r2 : 00000003  r1 : 00000000  r0 : 00000003

<4>Flags: nZCv  IRQs on  FIQs on  Mode USER_32  Segment user

<4>Control: 5317F

<4>Table: 83CA0000  DAC: 00000015

<4>usr stack info

<4>stack mem: sp = be897af0

4> be897b24 03000000 b4450600 03000000 147b89be 0000830c a4450600 02000000

   247b89be 000082dc 00000000 01000000 3c7b89be 00008370 647e89be 01000000

   00000000 108b0000 00000000 000085b4 00000000 647e89be 01000000 00008354

   00004c69 6e757800 000000

<4> be897b24 03be897b 0003be89 000003be 00000003 b4000000 45b40000 0645b400

<4> 000645b4 03000645 00030006 00000300 00000003 14000000 7b140000 897b1400

<4> be897b14 0cbe897b 830cbe89 00830cbe 0000830c a4000083 45a40000 0645a400

……

<4>Backtrace: [<000082fc>] (0x82fc) from [<be897b14>] (0xbe897b14)

<4>Backtrace aborted due to bad frame pointer <000645b4>

<4>Code: e51b3010 e50b3008 e51b2008 e3a03041 (e5c23000)

~ $

 

上面的藍色部分

<4>PC is at 0x8338

<4>LR is at 0x830c

表示導致崩潰的指令的地址為0x8338,返回地址為0x830c。一個典型的進程的地址空間為:

~ $ cat /proc/660/maps

00008000-00112000 r-xp 00000000 1f:07 27         /bin/busybox

0011a000-0011b000 rw-p 0010a000 1f:07 27         /bin/busybox

0011b000-001ce000 rwxp 0011b000 00:00 0          [heap]

40000000-40001000 rw-p 40000000 00:00 0

bea24000-bea4e000 rwxp bea24000 00:00 0          [stack]

 

linux-S980:/home/sanya/yjt/bt # arm_v5t_le-addr2line 0x8338 -f -e stack

C

/home/sanya/yjt/bt/stack.c:24

linux-S980:/home/sanya/yjt/bt #

 

結合此程序的反匯編程序進行回溯:

PC is at 0x8338所在函數的部分反匯編代碼摘錄如下:

00008318 <C>:

 

void C(int c)

{

    8318:       e92d4800        push    {fp, lr}

    831c:       e28db004        add     fp, sp, #4      ; 0x4

    8320:       e24dd010        sub     sp, sp, #16     ; 0x10

    8324:       e50b0010        str     r0, [fp, #-16]

    char *p = (char *)c;

    8328:       e51b3010        ldr     r3, [fp, #-16]

    832c:       e50b3008        str     r3, [fp, #-8]

    *p = 'A';

    8330:       e51b2008        ldr     r2, [fp, #-8]

    8334:       e3a03041        mov     r3, #65 ; 0x41

PC->8338:       e5c23000        strb    r3, [r2]

    printf("%d: function C\n", c);

    833c:       e59f000c        ldr     r0, [pc, #12]   ; 8350 <C+0x38>

    8340:       e51b1010        ldr     r1, [fp, #-16]

    8344:       eb0003e3        bl      92d8 <_IO_printf>

}

    8348:       e24bd004        sub     sp, fp, #4      ; 0x4

    834c:       e8bd8800        pop     {fp, pc}

8350:       000645c4        .word   0x000645c4

 

下面對涉及的每個函數進行分析:

1.  函數C的堆棧:

  8318:       e92d4800        push    {fp, lr}

  831c:       e28db004        add     fp, sp, #4      ; 0x4

  8320:       e24dd010        sub     sp, sp, #16     ; 0x10

be897b24 03000000 b4450600 03000000 147b89be 0000830c a4450600 02000000

247b89be 000082dc 00000000 01000000 3c7b89be 00008370 647e89be 01000000

00000000 108b0000 00000000 000085b4 00000000 647e89be 01000000 54830000

00004c69 6e757800 000000

 

可知 lr 為 0x0000830c,根據這個地址值,

linux-S980:/home/sanya/yjt/bt # arm_v5t_le-addr2line 0x830c -f -e stack

B

/home/sanya/yjt/bt/stack.c:19

linux-S980:/home/sanya/yjt/bt #

在stack.dis中可以再次找到這個地址處於函數B的范圍內。

 

2. 函數B的堆棧:

函數B的匯編指令如下:

void B(int b)

{

    82e8:       e92d4800        push    {fp, lr}

    82ec:       e28db004        add     fp, sp, #4      ; 0x4

    82f0:       e24dd008        sub     sp, sp, #8      ; 0x8

    82f4:       e50b0008        str     r0, [fp, #-8]

    printf("%d: B call C\n", b);

    82f8:       e59f0014        ldr     r0, [pc, #20]   ; 8314 <B+0x2c>

    82fc:       e51b1008        ldr     r1, [fp, #-8]

    8300:       eb0003f4        bl      92d8 <_IO_printf>

    C(3);

    8304:       e3a00003        mov     r0, #3  ; 0x3

    8308:       eb000002        bl      8318 <C>

}

    830c:       e24bd004        sub     sp, fp, #4      ; 0x4

    8310:       e8bd8800        pop     {fp, pc}

8314:       000645b4        .word   0x000645b4

 

 

       由

82e8:       e92d4800        push    {fp, lr}

    82ec:       e28db004        add     fp, sp, #4      ; 0x4

82f0:       e24dd008        sub     sp, sp, #8      ; 0x8

可知函數B的堆棧大小為16字節,函數B執行完后的返回地址 lr 存儲在其堆棧偏移地址最外層。函數B棧的的數據緊挨着函數C的棧,取出羅列如下:

be897b24 03000000 b4450600 03000000 147b89be 0000830c a4450600 02000000

247b89be 000082dc 00000000 01000000 3c7b89be 00008370 647e89be 01000000

00000000 108b0000 00000000 000085b4 00000000 647e89be 01000000 54830000

00004c69 6e757800 000000

從上面信息可以知道,函數B的返回地址lr 為 000082dc,根據這個地址值,

linux-S980:/home/sanya/yjt/bt # arm_v5t_le-addr2line 0x82dc -f -e stack

A

/home/sanya/yjt/bt/stack.c:13

stack.dis可知處於函數A的地址范圍內。

 

3. 函數A的堆棧:

函數A的匯編指令如下:

void A(int a)

{

    82b8:       e92d4800        push    {fp, lr}

    82bc:       e28db004        add     fp, sp, #4      ; 0x4

    82c0:       e24dd008        sub     sp, sp, #8      ; 0x8

    82c4:       e50b0008        str     r0, [fp, #-8]

    printf("%d: A call B\n", a);

    82c8:       e59f0014        ldr     r0, [pc, #20]   ; 82e4 <A+0x2c>

    82cc:       e51b1008        ldr     r1, [fp, #-8]

    82d0:       eb000400        bl      92d8 <_IO_printf>

    B(2);

    82d4:       e3a00002        mov     r0, #2  ; 0x2

    82d8:       eb000002        bl      82e8 <B>

}

    82dc:       e24bd004        sub     sp, fp, #4      ; 0x4

    82e0:       e8bd8800        pop     {fp, pc}

    82e4:       000645a4        .word   0x000645a4

 

       由

82b8:       e92d4800        push    {fp, lr}

    82bc:       e28db004        add     fp, sp, #4      ; 0x4

    82c0:       e24dd008        sub     sp, sp, #8      ; 0x8

可知函數A的堆棧大小為16字節,函數B執行完后的返回地址 lr 存儲在其堆棧偏移地址最外層。函數A棧的的數據緊挨着函數B的棧,取出羅列如下:

be897b24 03000000 b4450600 03000000 147b89be 0000830c a4450600 02000000

247b89be 000082dc 00000000 01000000 3c7b89be 00008370 647e89be 01000000

00000000 108b0000 00000000 000085b4 00000000 647e89be 01000000 54830000

00004c69 6e757800 000000

       從上面信息可以知道,函數A的返回地址為0x00008370

linux-S980:/home/sanya/yjt/bt # arm_v5t_le-addr2line 0x8370 -f -e stack

main

/home/sanya/yjt/bt/stack.c:32

linux-S980:/home/sanya/yjt/bt #

stack.dis可知處於函數main的地址范圍內。

 

       至此,可以知道main調用A、A調用B、B再調用C時,在函數C中導致程序崩潰。現在認真看一下函數C:

21 void C(int c)

22 {

23     char *p = (char *)c;

24     *p = ‘A’;

25     printf("%d: function C\n", c);

26 }

       這個函數太過簡單,可以一眼就知道第23、24行代碼有問題。但是如果函數C有上千行代碼呢?除了睜大眼睛檢查C代碼外,我們還可以根據它的反匯編碼來差錯——內核打印出來的棧信息的前面還有些有用的信息:

<2>stack: unhandled page fault (11) at 0x00000003, code 0x817

<1>pgd = c3ca0000

<1>[00000003] *pgd=81b86031, *pte=00000000, *ppte=00000000

<4>

<4>Pid: 662, comm:                stack

<4>CPU: 0

<4>PC is at 0x8338

<4>LR is at 0x830c

<4>pc : [<00008338>]    lr : [<0000830c>]    Not tainted

<4>sp : be897af0  ip : 00000001  fp : be897b04

<4>r10: 00000000  r9 : 00008ac8  r8 : 00008b10

<4>r7 : 00000000  r6 : 00000000  r5 : 00081fe8  r4 : 00000000

<4>r3 : 00000041  r2 : 00000003  r1 : 00000000  r0 : 00000003

<4>Flags: nZCv  IRQs on  FIQs on  Mode USER_32  Segment user

<4>Control: 5317F

 

       現在回過頭來看看函數C的反匯編碼,從中找出出錯的原因:

00008318 <C>:

 

void C(int c)

{

    8318:       e92d4800        push    {fp, lr}

    831c:       e28db004        add     fp, sp, #4      ; 0x4

    8320:       e24dd010        sub     sp, sp, #16     ; 0x10

    8324:       e50b0010        str     r0, [fp, #-16]

    char *p = (char *)c;

    8328:       e51b3010        ldr     r3, [fp, #-16]

    832c:       e50b3008        str     r3, [fp, #-8]

    *p = 'A';

    8330:       e51b2008        ldr     r2, [fp, #-8]

    8334:       e3a03041        mov     r3, #65 ; 0x41

    8338:       e5c23000        strb    r3, [r2]

    printf("%d: function C\n", c);

    833c:       e59f000c        ldr     r0, [pc, #12]   ; 8350 <C+0x38>

    8340:       e51b1010        ldr     r1, [fp, #-16]

    8344:       eb0003e3        bl      92d8 <_IO_printf>

}

    8348:       e24bd004        sub     sp, fp, #4      ; 0x4

    834c:       e8bd8800        pop     {fp, pc}

8350:       000645c4        .word   0x000645c4

 

       出錯的指令為“8338:       e5c23000        strb    r3, [r2]”,它將寄存器r3的值存到地址(r2)中,只存儲1個字節。根據內核打印出來的寄存器值可知r3 : 00000041  r2 : 00000003,寫地址為0x03,當然<2>stack: unhandled page fault (11) at 0x00000003, code 0x817

——這不是可寫的地址。

       閱讀匯編代碼是件困難的事情,沒有其他辦法時再用這方法吧。


免責聲明!

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



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