NO IMAGE

作為一個菜鳥,這個題目有點大,所以這篇部落格缺點是可能不夠深入,但應該還是很詳細的,希望能對大家有所幫助。

1.簡介加初步分析

在linux系統中,程式在記憶體中的分佈如下所示:

低地址.text.data.bss            heap(堆)      –>      unused   <–      stack(棧)      env高地址

其中 :

.text 部分是編譯後程式的主體,也就是程式的機器指令。

.data 和 .bss 儲存了程式的全域性變數,.data儲存有初始化的全域性變數,.bss儲存只有宣告沒有初始化的全域性變數。

heap(堆)中儲存程式中動態分配的記憶體,比如C的malloc申請的記憶體,或者C 中new申請的記憶體。堆向高地址方向增長。

stack(棧)用來進行函式呼叫,儲存函式引數,臨時變數,返回地址等。

下面是測試用的程式,比較簡單,用來輸出各個變數的地址。

#include <stdio.h>
#include <stdlib.h>
int ug;
int dg = 1;
void func(int);
void func2(int);
int main(int argc, char ** argv){
int ul;
int dl = 2;
int *pi = (int *)malloc(sizeof(int));
*pi = 4;
int *pi2 = (int *)malloc(sizeof(int));
*pi2 = 8;
printf("address of main:     %x\n", main);
printf("undefined global %d: %x\n", ug, &ug);
printf("defined global %d:   %x\n", dg, &dg);
printf("undefined local %d:  %x\n", ul, &ul);
printf("defined local %d:    %x\n", dl, &dl);
printf("address of func:     %x\n", func);
func(32);
printf("dynamic alloc %d:    %x\n", *pi, pi);
printf("dynamic alloc %d:    %x\n", *pi2, pi2);
free(pi);
free(pi2);
int a;
scanf("%d", &a);
return 0;
}
void func(int arg){
int uloc;
int dloc = 16;
printf("address of argument %d: %x\n", arg, &arg);
printf("undefined func local %d: %x\n", uloc, &uloc);
printf("defined func local %d: %x\n", dloc, &dloc);
func2();
}
void func2(){
int loc = 64;
printf("local of func2 %d: %x\n", loc, &loc);
}

程式輸出如下:

address of main:     4005f4
undefined global 0: 601050
defined global 1:   601038
undefined local 32767:  c2b96484
defined local 2:    c2b96488
address of func:     40075a
address of argument 32: c2b9643c
undefined func local -451161400: c2b96448
defined func local 16: c2b9644c
local of func2 64: c2b9641c
dynamic alloc 4:    16b1010
dynamic alloc 8:    16b1030

2.使用程序maps檔案深入分析

在linux下,可以檢視程序的maps檔案瞭解程式在記憶體中的分佈,上面那個程式執行後的程序的maps檔案內容如下:

cat /proc/2506/maps 
00400000-00401000 r-xp 00000000 08:03 4080116                            /home/yuduo/Workspace/C/sandbox/address.o
00600000-00601000 r--p 00000000 08:03 4080116                            /home/yuduo/Workspace/C/sandbox/address.o
00601000-00602000 rw-p 00001000 08:03 4080116                            /home/yuduo/Workspace/C/sandbox/address.o
016b1000-016d2000 rw-p 00000000 00:00 0                                  [heap]
7fc8e4bfc000-7fc8e4d91000 r-xp 00000000 08:03 4460234                    /lib/x86_64-linux-gnu/libc-2.13.so
7fc8e4d91000-7fc8e4f90000 ---p 00195000 08:03 4460234                    /lib/x86_64-linux-gnu/libc-2.13.so
7fc8e4f90000-7fc8e4f94000 r--p 00194000 08:03 4460234                    /lib/x86_64-linux-gnu/libc-2.13.so
7fc8e4f94000-7fc8e4f95000 rw-p 00198000 08:03 4460234                    /lib/x86_64-linux-gnu/libc-2.13.so
7fc8e4f95000-7fc8e4f9b000 rw-p 00000000 00:00 0 
7fc8e4f9b000-7fc8e4fbc000 r-xp 00000000 08:03 4460221                    /lib/x86_64-linux-gnu/ld-2.13.so
7fc8e5199000-7fc8e519c000 rw-p 00000000 00:00 0 
7fc8e51b7000-7fc8e51bb000 rw-p 00000000 00:00 0 
7fc8e51bb000-7fc8e51bc000 r--p 00020000 08:03 4460221                    /lib/x86_64-linux-gnu/ld-2.13.so
7fc8e51bc000-7fc8e51be000 rw-p 00021000 08:03 4460221                    /lib/x86_64-linux-gnu/ld-2.13.so
7fffc2b76000-7fffc2b97000 rw-p 00000000 00:00 0                          [stack]
7fffc2bbe000-7fffc2bbf000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

可以看到程式的.text的記憶體是:00400000-00401000,main函式和func函式的地址都在這個範圍內(4005f4、40075a),可以看到這部分記憶體許可權是可執行(r-xp),這裡面的程式碼也確實是需要執行的。寫這篇部落格時我又發現這段記憶體剛好一個頁面大小(4K),有趣。

.data的記憶體是:00601000-00602000,因為兩個全域性變數全在這裡(601050、601038),從許可權也可以看出來(rw-p),這裡w代表可寫,上面那部分記憶體(00600000-00601000)許可權是 r–p,估計是用來儲存常量(const)的。

然後就是堆(heap)了,地址範圍是016b1000-016d2000,兩個動態分配的變數剛好在這個範圍裡面:16b1010、16b1030,從他們的地址可以看出來他們是向高地址增長的。

堆後面直接就是高地址了,首先是一些動態連結庫,動態連結庫在記憶體中的位置在每個系統上都不一樣,有些系統放在.text前面,這個無所謂了,不關心。

然後就是棧(stack)了,地址範圍7fffc2b76000-7fffc2b97000。例子裡面很多變數都在這個範圍內,main的兩個區域性變數(c2b96484、c2b96488),func的引數和兩個區域性變數(c2b9643c、c2b96448、c2b9644c),func2的區域性變數(c2b9641c)。從這裡也可以看出棧是向低地址增長的,因為我們確定函式呼叫順序是main->func->func2,所以壓棧順序也一定是這個,從每個函式中找個代表出來按壓棧順序排列,c2b96484->c2b9643c->c2b9641c,發現地址越來越小了,所以棧向低地址增長沒有問題。

還又個問題,我們看到main裡面兩個區域性變數,先宣告的地址小(c2b96484),後宣告的地址大(c2b96488),其實這並不違背棧向低地址增長,因為在main函式這個棧幀裡面(stack frame),儲存區域性變數並沒有壓棧出棧等棧的操作,完全是兩碼事,比如我們看一下彙編程式碼,可以發現這區域性變數是這樣賦值的:

	movl	$2, -8(%rbp)

只和基地址有關(rbp)。我個人覺得區域性變數地址和編譯器有關,但是沒有測試,提出來算個想法吧 :)

stack後面還有兩個段:vdso,不知道是什麼;vsyscall,核心的程式碼,每個程式都少不了。

順便再說下,上面的測試還可以看出全域性變數沒初始化會預設賦值為0,而區域性變數不會,所以區域性變數使用前一定要初始化,否則會出現不知道的結果。

3. 使用objdump再深入分析

使用命令objdump -d address.o顯示程式的彙編內容,因為這個命令的輸出是在是太詳細,所以不能把結果都貼出來,但是並不影響大家理解。大家也可以在自己電腦上試試,再和後面內容對應起來看。如果還是有疑問,就給我留言吧:)
先把objdump輸出的開頭部分貼出來:
objdump -d address.o
address.o:     file format elf64-x86-64
Disassembly of section .init:
0000000000400498 <_init>:
400498:	48 83 ec 08          	sub    $0x8,%rsp
40049c:	e8 9b 00 00 00       	callq  40053c <call_gmon_start>
4004a1:	e8 2a 01 00 00       	callq  4005d0 <frame_dummy>
4004a6:	e8 f5 03 00 00       	callq  4008a0 <__do_global_ctors_aux>
4004ab:	48 83 c4 08          	add    $0x8,%rsp
4004af:	c3                   	retq   

首先說我的檔案格式是elf64-x86-64的,然後是.init的反彙編(從機器碼生成彙編碼),後面還有很多程式開始執行後main函式呼叫之前的很多初始化工作,這些都是編譯器和作業系統加的,不要以為程式開始執行後就直接開始執行main哦。不過這裡關心的還是main,main的反彙編部分如下:

00000000004005f4 <main>:
4005f4:	55                   	push   %rbp
4005f5:	48 89 e5             	mov    %rsp,%rbp
4005f8:	48 83 ec 30          	sub    $0x30,%rsp
4005fc:	89 7d dc             	mov    %edi,-0x24(%rbp)
4005ff:	48 89 75 d0          	mov    %rsi,-0x30(%rbp)
400603:	c7 45 f8 02 00 00 00 	movl   $0x2,-0x8(%rbp)
40060a:	bf 04 00 00 00       	mov    $0x4,%edi
40060f:	e8 dc fe ff ff       	callq  4004f0 <[email protected]>

可以看到main的起始地址是4005f4,和第一部分裡面結果一樣。

關於地址後面的內容我再解釋以下吧,55是機器碼,對應彙編碼push %rbp,因為55只有一個位元組,所以後面的地址是4005f5,下一個機器碼是48 89 e5,對應的彙編是:mov %rsp, %rbp,然後就以此類推了。不同的彙編對應的機器碼的位元組數是不同的,所以不要驚訝機器碼為什麼參差不齊的。再貼一部分func的反彙編(beautiful)吧:

000000000040075a <func>:
40075a:	55                   	push   %rbp
40075b:	48 89 e5             	mov    %rsp,%rbp
40075e:	48 83 ec 20          	sub    $0x20,%rsp
400762:	89 7d ec             	mov    %edi,-0x14(%rbp)
400765:	c7 45 fc 10 00 00 00 	movl   $0x10,-0x4(%rbp)
40076c:	8b 4d ec             	mov    -0x14(%rbp),%ecx
40076f:	b8 9e 09 40 00       	mov    $0x40099e,%eax
400774:	48 8d 55 ec          	lea    -0x14(%rbp),%rdx
400778:	89 ce                	mov    %ecx,%esi
40077a:	48 89 c7             	mov    %rax,%rdi
40077d:	b8 00 00 00 00       	mov    $0x0,%eax
400782:	e8 49 fd ff ff       	callq  4004d0 <[email protected]>

4. 使用gdb再再(裝b)深入分析

深入到這個份上,其實已經沒有什麼好分析的了,只是順便說說gdb下怎麼檢測變數和記憶體。
檢查變數就是print了,比如:
(gdb) print main
$1 = {int (int, char **)} 0x4005f4 <main>

可以看出main是個函式,起始地址0x4005f4,沒有什麼問題。下面重點介紹怎麼檢測記憶體。

gdb使用x檢測記憶體,使用格式是x/FMT ADDRESS,其中FMT是想要重複的次數 格式化字元(format letter) 大小字元(size letter),ADDRESS不用說就是想要檢測的地址了。
其中格式化字元有:o(octal   8進位制), x(hex   16進位制), d(decimal   10進位制), u(unsigned decimal    無符號10進位制), t(binary    2進位制), f(float   浮點數), a(address    地址), i(instruction    指令), c(char    字元) 和 s(string   字串).
大小字元有:b(byte   1個位元組), h(halfword   2個位元組), w(word   4個位元組), g(giant, 8個位元組)。
實際使用中格式化字元和大小字元位置貌似可以調換,我用的時候也不太在意。下面用三種方法檢測從地址main(0x4005f4)開始的8個位元組:
比如x/8xb main表示檢測mian開始的記憶體,輸出格式為16進位制(x),每次一個位元組(b),檢測8次(8),輸出如下:
(gdb) x/8xb main
0x4005f4 <main>:	0x55	0x48	0x89	0xe5	0x48	0x83	0xec	0x30

main的地址還是0x4005f4,然後可以回頭看看第三部分裡面main的反彙編,前8個位元組就是 0x55
0x48 0x89
0xe5 0x48
0x83 0xec
0x30。

使用x/8bx main效果是一樣的,不過話說回來gcc的一套工具大多引數位置可以隨便擺放。
再比如x/2xw main,檢測main開始的記憶體,輸出格式為16進位制(x),每次4個位元組,檢測2次(2),輸出如下:
(gdb) x/2xw main
0x4005f4 <main>:	0xe5894855	0x30ec8348

輸出貌似和上面按位元組輸出有些不同了,4個4個倒序了,因為我的處理器是intel的,intel採用小頭編碼方式(little-endian),低地址的位元組排在低位(十位、個位),高地址的位元組排在高位(千位,萬位),所以上面的0x55在低位,按4個位元組輸出就在最後面了(低位)。

我再換種方式輸出可能會更清楚:x/1xg main,同樣檢測main開始的記憶體,輸出格式還是16進位制(x),不同的是每次8個位元組(g),只檢測一次:
(gdb) x/1xg main
0x4005f4 <main>:	0x30ec8348e5894855

這個分析就留給讀者當小練習吧,如果不懂還是那句話,給我留言吧:)

參考文獻:
Linux assembly language programming. Bob Neveln. 2000
The art of debugging with gdb, ddd, and eclipse. Norman Matloff, Peter Jay Salzman. 2008
Professional Linux kernel architecture. Wolfgang Mauerer. 2008