首先介紹下面會用到的幾個寄存器:
rsp : 棧指針寄存器,指向棧頂
rbp : 椞椎伲基址寄存器歉秫,指向棧底
edi : 函數(shù)參數(shù)
rsi/esi : 函數(shù)參數(shù)
eax : 累加器或函數(shù)返回值用
int test2(int a, int b) {
int v1 = a + 1;
int v2 = b + 2;
int c = v1 + v2 + 3;
return c + 4;
}
void test1() {
int a = 1;
int b = 2;
int c = a + b + test2(a, b);
}
int main(int argc, const char * argv[]) {
test1();
return 0;
}
首先我們要知道然想,函數(shù)棧里面的內(nèi)存地址是從高到低的。
下面從main函數(shù)開始一句句匯編進行解讀:
0齿尽、初始時
rsp = 0x00007ffeefbff418
rbp = 0x00007ffeefbff428
1掠河、 pushq %rbp
將rbp的地址壓棧亮元,rsp繼續(xù)指向棧頂,所以我們可以看到
rsp = 0x00007ffeefbff418 - 0x8 = 0x00007ffeefbff410
此時棧頂?shù)刂反娣诺膬?nèi)容就是剛才壓棧的rbp的地址唠摹,即
*0x00007ffeefbff410 = 0x00007ffeefbff428
(我這里就用*表示取地址的內(nèi)容爆捞,如果有不懂打印的為什么是反的,可以先去了解下大小端)勾拉。
2煮甥、 movq %rsp, %rbp
將棧頂rsp的值賦值給棧底rbp,即
rsp = rbp = 0x00007ffeefbff410
3藕赞、 subq $0x10, %rsp
棧頂往下移16個字節(jié)成肘,可以理解成給后面預(yù)留的16字節(jié)的空間。此時
rsp = 0x00007ffeefbff410 - 0x10 = 0x00007ffeefbff400
4斧蜕、 movl $0x0, -0x4(%rbp)
5双霍、 movl %edi, -0x8(%rbp)
6、 movq %rsi, -0x10(%rbp)
這三句可以理解成將寄存器edi和rsi之前的值先用第三步預(yù)留的內(nèi)存存儲下來批销,因為下面調(diào)用函數(shù)里面可能會修改這兩個寄存器里面的值洒闸。
7、 callq 0x100002f80
call表示調(diào)用函數(shù)均芽,同時call有一個作用:將call指令的下一條指令地址壓棧丘逸。所以此時棧頂
rsp = 0x00007ffeefbff400 - 0x8 = 0x00007ffeefbff3f8
rbp = 0x00007ffeefbff410
并且里面存放的內(nèi)容就是call下一條指令的地址,即
*0x00007ffeefbff3f8 = 0x100002fdb
從這里開始進入test1函數(shù)
8掀宋、 pushq %rbp
rbp壓棧鸣个,則
rsp = 0x00007ffeefbff3f8 - 0x8 = 0x00007ffeefbff3f0
*0x00007ffeefbff3f0 = 0x00007ffeefbff410
9、 movq %rsp, %rbp
將棧頂rsp的值賦值給棧底rbp布朦,即
rsp = rbp = 0x00007ffeefbff3f0
10囤萤、 subq $0x10, %rsp
棧頂往下移16個字節(jié),可以理解成給test1函數(shù)検桥浚空間分配16字節(jié)內(nèi)存涛舍。
rsp = 0x00007ffeefbff3f0 - 0x10 = 0x00007ffeefbff3e0
11、 movl $0x1, -0x4(%rbp)
將1放入內(nèi)存地址0x00007ffeefbff3f0 - 0x4
中唆途,即
*0x00007ffeefbff3ec = 1
正好對應(yīng)a = 1
富雅,所以可以猜測0x00007ffeefbff3ec
就是a的地址
12掸驱、 movl $0x2, -0x8(%rbp)
同上,可知將2放入內(nèi)存地址0x00007ffeefbff3f0 - 0x8
中没佑,即
*0x00007ffeefbff3e8 = 2
正好對應(yīng) b = 2
毕贼,可猜測0x00007ffeefbff3e8
就是b的地址
13、 movl -0x4(%rbp), %eax
14蛤奢、 addl -0x8(%rbp), %eax
15鬼癣、 movl %eax, -0x10(%rbp)
在文章最前面提過eax寄存器一般作為累加器,所以源碼里面的int c = a + b + test2(a, b);
這里就很好理解啤贩,前面不是分配了16個字節(jié)的內(nèi)存么待秃,我們只用到了高8個字節(jié)的內(nèi)存分別存儲a、b痹屹,這里將a章郁、b的值通過累加器加起來,再用低8個字節(jié)的內(nèi)存 -0x10(%rbp)存儲a+b的和志衍,即
*0x00007ffeefbff3e0 = 3
16暖庄、 movl -0x4(%rbp), %edi
17、 movl -0x8(%rbp), %esi
前面提到過寄存器edi楼肪、esi一般用來做函數(shù)參數(shù)存儲培廓。所以這里就是將a的值用edi存儲,b的值用esi存儲淹辞,即
edi = 1
esi = 2
此時rsp = 0x00007ffeefbff3e0
rbp = 0x00007ffeefbff3f0
18、 callq 0x100002f50
同第7步俘侠,call的下一條匯編指令地址壓棧象缀,所以
rsp = 0x00007ffeefbff3e0 - 0x8 = 0x00007ffeefbff3d8
*0x00007ffeefbff3d8 = 0x100002faa
從這里開始進入test2函數(shù)
19、 pushq %rbp
rbp壓棧爷速,則
rsp = 0x00007ffeefbff3d8 - 0x8 = 0x00007ffeefbff3d0
*0x00007ffeefbff3d0 = 0x00007ffeefbff3f0
20央星、 movq %rsp, %rbp
將棧頂rsp的值賦值給棧底rbp,即
rsp = rbp = 0x00007ffeefbff3d0
21惫东、 movl %edi, -0x4(%rbp)
22莉给、 movl %esi, -0x8(%rbp)
把前面用edi、esi存儲的參數(shù)用test2的椓冢空間存儲颓遏,即
*0x00007ffeefbff3cc = 1
*0x00007ffeefbff3c8 = 2
23、 movl -0x4(%rbp), %eax
24滞时、 addl $0x1, %eax
25叁幢、 movl %eax, -0xc(%rbp)
將參數(shù)0x00007ffeefbff3cc
里面存放的值(1)通過累加器eax,計算之后的值放入地址-0xc(%rbp)坪稽,即0x00007ffeefbff3c4 = 2
曼玩,正好對應(yīng)源代碼int v1 = a + 1;
鳞骤,所以這里v1的地址就是0x00007ffeefbff3c4
26、 movl -0x8(%rbp), %eax
27黍判、 addl $0x2, %eax
28豫尽、 movl %eax, -0x10(%rbp)
將參數(shù)0x00007ffeefbff3c8
里面存放的值(2)通過累加器eax,計算之后的值放入地址-0x10(%rbp)顷帖,即0x00007ffeefbff3c0 = 2
美旧,正好對應(yīng)源代碼int v2 = b + 2;;
,所以這里v2的地址就是0x00007ffeefbff3c0
29窟她、 movl -0xc(%rbp), %eax
30陈症、 addl -0x10(%rbp), %eax
31、 addl $0x3, %eax
32震糖、 movl %eax, -0x14(%rbp)
這里就是通過累加器eax將-0xc(%rbp)和-0x10(%rbp)里面的值相加录肯,再加上3,放入地址-0x14(%rbp)中吊说,正好對應(yīng)int c = v1 + v2 + 3;
论咏,所以這里c的地址就是-0x14(%rbp)即0x00007ffeefbff3bc
,且*0x00007ffeefbff3bc = 9
33颁井、 movl -0x14(%rbp), %eax
34厅贪、 addl $0x4, %eax
這里將前面計算的c的值加4,將結(jié)果放入寄存器eax雅宾,前面提到eax也做函數(shù)返回养涮,所以此時eax = 13
35、 popq %rbp
這句指令表示出棧眉抬,同時將出棧的值放入寄存器rbp贯吓,所以有
rsp = 0x00007ffeefbff3d0 + 0x8 = 0x00007ffeefbff3d8
rbp = *0x00007ffeefbff3d0 = 0x00007ffeefbff3f0
從這里開始退出test2函數(shù)
36、 retq
這句表示退出test2函數(shù)蜀变,同時出棧悄谐,并且斷點跳到出棧值的地址,所以可以看到
rsp = 0x00007ffeefbff3d8 + 0x8 = 0x00007ffeefbff3e0
库北,而之前
*0x00007ffeefbff3d8 = 0x100002faa
爬舰,所以此時跳到0x100002faa
。
同時我們可以發(fā)現(xiàn)棧頂rsp = 0x00007ffeefbff3e0
棧底rbp = 0x00007ffeefbff3f0
寒瓦,與進入test2函數(shù)之前的值保持一致情屹,說明函數(shù)在調(diào)用前后會保持棧平衡,即從哪里開始杂腰,最后又會回到哪里
屁商。
從這里開始回到test1函數(shù)
37、 movl %eax, %ecx
前面提到test2的返回值存放在寄存器eax,這里先將返回值用寄存器ecx存儲蜡镶,即ecx = 13
38雾袱、 movl -0x10(%rbp), %eax
39、 addl %ecx, %eax
40官还、 movl %eax, -0xc(%rbp)
因為之前a+b的值存在地址-0x10(%rbp)中芹橡,這里這三句正好對應(yīng)int c = a + b + test2(a, b);
,其中-0xc(%rbp)就是c的地址望伦,所以有
*0x7ffeefbff3e4 = 16
41林说、 addq $0x10, %rsp
這句正好對應(yīng)前面的subq $0x10, %rsp
,此時
rsp = 0x00007ffeefbff3e0 + 0x10 = 0x00007ffeefbff3f0
42屯伞、 popq %rbp
這句指令表示出棧腿箩,同時將出棧的值放入寄存器rbp,所以有
rsp = 0x00007ffeefbff3f0 + 0x8 = 0x00007ffeefbff3f8
rbp = *0x00007ffeefbff3f0 = 0x00007ffeefbff410
從這里開始退出test1函數(shù)
43劣摇、 retq
這句表示退出test1函數(shù)珠移,同時出棧,并且斷點跳到出棧值的地址末融,所以可以看到
rsp = 0x00007ffeefbff3f8 + 0x8 = 0x00007ffeefbff400
钧惧,而之前
*0x00007ffeefbff3f8 = 0x100002fdb
,所以此時跳到0x100002fdb
勾习。
同時我們可以發(fā)現(xiàn)棧頂rsp = 0x00007ffeefbff400
棧底rbp = 0x00007ffeefbff410
浓瞪,與進入test1函數(shù)之前的值保持一致,再次驗證了前面的觀點巧婶。
44乾颁、 xorl %eax, %eax
因為函數(shù)test1無返回值,所以這里eax也沒實際用到
45艺栈、 addq $0x10, %rsp
這句正好對應(yīng)前面的subq $0x10, %rsp
英岭,此時
rsp = 0x00007ffeefbff400 + 0x10 = 0x00007ffeefbff410
46、 popq %rbp
這句指令表示出棧眼滤,同時將出棧的值放入寄存器rbp巴席,所以有
rsp = 0x00007ffeefbff410 + 0x8 = 0x00007ffeefbff418
rbp = *0x00007ffeefbff410 = 0x00007ffeefbff428
此時也正好對應(yīng)初始時棧頂和棧底的值历涝。
總結(jié):函數(shù)調(diào)用會保持棧平衡
補充:函數(shù)遞歸沒有退出條件之所以會造成死循環(huán)诅需,就是因為rsp會一直往下減,直至減到棧區(qū)范圍外荧库,這樣就會造成棧溢出堰塌。