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
——這不是可寫的地址。
閱讀匯編代碼是件困難的事情,沒有其他辦法時再用這方法吧。