本文的實驗在一個虛擬機中進行,虛擬機模擬的cpu是x86-64(Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz),運行的是64bit ubuntu,安裝了ARM64的工具鏈:linux
$sudo apt-get install gcc-aarch64-linux-gnu $sudo apt-get install gcc-arm-linux-gnueabi
實驗使用的程序爲:ubuntu
#include<stdio.h> #include <stdlib.h> int func_C(int x1, int x2, int x3, int x4, int x5, int x6){ int sum = 0; sum = x1 + x2; sum = sum + x3 + x4; sum = sum + x5 + x6; return sum; } int func_B(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, char x9){ int sum = 0; sum = func_C(x1, x2, x3, x4, x5,x6); sum = sum + x7; sum = sum + x8; sum += x9; return sum; } void func_A(void){ int sum = 0; int x1 = 1; int x2 = 2; int x3 = 3; int x4 = 4; int x5 = 5; int x6 = 6; int x7 = 7; int x8 = 8; char x9 = 'c'; sum = func_B(x1, x2, x3, x4, x5, x6, x7, x8, x9); printf("sum = %d\n", sum); } int main(int argc, char *argv[], char *envp[]) { int count = argc; char **p_argv = argv; char **p_env = envp; func_A(); return 0; }
int main(int argc, char *argv[], char *envp[]) { int count = argc; char **p_argv = argv; char **p_env = envp; func_A(); return 0; }
首先,編譯源程序 $gcc test.c -o test -fno-stack-protector
,而後反彙編出main函數 $gdb test
:ide
(gdb) disas main Dump of assembler code for function main: 0x0000000000000790 <+0>: push %rbp 0x0000000000000791 <+1>: mov %rsp,%rbp 0x0000000000000794 <+4>: sub $0x40,%rsp 0x0000000000000798 <+8>: mov %edi,-0x24(%rbp) 0x000000000000079b <+11>: mov %rsi,-0x30(%rbp) 0x000000000000079f <+15>: mov %rdx,-0x38(%rbp) 0x00000000000007a3 <+19>: mov -0x24(%rbp),%eax 0x00000000000007a6 <+22>: mov %eax,-0x4(%rbp) 0x00000000000007a9 <+25>: mov -0x30(%rbp),%rax 0x00000000000007ad <+29>: mov %rax,-0x10(%rbp) 0x00000000000007b1 <+33>: mov -0x38(%rbp),%rax 0x00000000000007b5 <+37>: mov %rax,-0x18(%rbp) 0x00000000000007b9 <+41>: callq 0x6fd <func_A> 0x00000000000007be <+46>: mov $0x0,%eax 0x00000000000007c3 <+51>: leaveq 0x00000000000007c4 <+52>: retq End of assembler dump.
從上面對main
函數棧的分析能夠知道,一個函數棧大體會作如下幾件事:函數
0x0000000000000790 <+0>: push %rbp 0x0000000000000791 <+1>: mov %rsp,%rbp
在x86-64上rbp和rsp徹底能夠描繪出一個棧幀,rbp被稱爲幀指針(frame pointer),rsp被稱爲棧指針(stack pointer);rbp+0x08指向當前棧幀的底部,rsp指向棧幀的頂部。下面的第一句是將上一個棧幀的幀指針rbp壓棧保存起來;rbp保存起來後,緊接着下一句彙編就爲rbp賦予一個新值,將棧指針rsp的值賦給幀指針rbp,讓rbp指向當前棧幀的底部,其實rbp+0x08纔是當前棧幀的底部,只不過rbp+0x08處是上一個函數運行call指令時硬件自動存放的rip,對軟件不可見。工具
0x0000000000000794 <+4>: sub $0x40,%rsp
也就是開闢當前棧幀的空間,開闢的棧幀空間主要用於接收參數、存放局部變量以及運算的場所,下面的彙編開闢0x40個字節的空間。佈局
0x0000000000000798 <+8>: mov %edi,-0x24(%rbp) 0x000000000000079b <+11>: mov %rsi,-0x30(%rbp) 0x000000000000079f <+15>: mov %rdx,-0x38(%rbp)
前面也說過,x86-64參數傳遞的規則是rdi傳遞第一個參數、rsi傳遞第二個參數、rdx傳遞第三個參數....。mian
函數的一個形參是argc,第二個形參是argv,第三個形參是envp。從下面的彙編也能夠看出,實參在rdi、rsi和rdx中,而後分別放到rbp - 0x24 、rbp - 0x30和rbp -0x38形參的位置處。3d
0x00000000000007a3 <+19>: mov -0x24(%rbp),%eax 0x00000000000007a6 <+22>: mov %eax,-0x4(%rbp) 0x00000000000007a9 <+25>: mov -0x30(%rbp),%rax 0x00000000000007ad <+29>: mov %rax,-0x10(%rbp) 0x00000000000007b1 <+33>: mov -0x38(%rbp),%rax 0x00000000000007b5 <+37>: mov %rax,-0x18(%rbp) 0x00000000000007b9 <+41>: callq 0x6fd <func_A>
棧的最大做用就是做爲運算場所,main
的棧上並無安排太多的運算,僅僅作了三次賦值,而後主要工做就就轉給子函數了。下面的一二兩句的做用是將argc的值賦值給count;三四句的做用是將argv的值賦給p_argv;五六兩句的做用是將envp賦值給p_envl;最後一句就是調用子函數,咱們也把他看作是運算的一部分。指針
設置返回值。x86-64的函數返回值使用rax傳遞。從return 0
可知函數的返回值是0,所以將0賦給eax。code
0x00000000000007be <+46>: mov $0x0,%eax
leaveq
的做用,該語句的做用有兩個:一是將幀指針(rbp)的值賦值給棧指針(rsp),即mov %rbp, %rsp
;二是將幀指針(rbp)出棧,即pop %rbp
。正好和保存上一個棧幀結構的操做相反。leaveq
指令執行完後,幀指針(rbp)已經切換回caller的棧了,也即數據存儲區已經切換完成,只差函數控制切換。 0x00000000000007c3 <+51>: leaveq
爲了更加深刻的瞭解如何恢復棧幀,畫了一個以下所示的圖,rsp和rbp指向了current frame,也就是說寄存器rbp和rsp並無指向任何"previous frame",恢復上一個棧幀的核心問題在於如何讓rbp和rsp指向上一個棧幀,答案的鑰匙就是rbp寄存器。rbp指向的就是上一個rbp,rbp-16就是上一個rsp, 直覺上恢復上一個棧幀就是將rbp-16賦值給rsp,並將rbp指向的值賦值給rbp,顯示上述方法能夠恢復rsp和rbp,可是事實上並無使用上述方法,而是利用棧天然而然的恢復上一個棧幀,所謂的天然而然就是怎麼來怎麼回去。首先讓rbp賦值給rsp,而後pop一次棧便可恢復rbp,再pop棧一次便可恢復rsp。「首先讓rbp賦值給rsp,而後pop一次棧便可恢復rbp」是人類發明的指令leaveq
乾的,"再pop棧一次便可恢復rsp"是人類發明的指令ret
乾的。
ip
mov (%sp), %rip; addq $8,%rsp
或pop %rip
來理解。該指令執行完後,函數的控制以及棧指針(rsp)切換完成。 retq
指令改變了rsp 和 rip的值。 0x00000000000007c4 <+52>: retq
void func_A(void){ int sum = 0; int x1 = 1; int x2 = 2; int x3 = 3; int x4 = 4; int x5 = 5; int x6 = 6; int x7 = 7; int x8 = 8; char x9 = 'c'; sum = func_B(x1, x2, x3, x4, x5, x6, x7, x8, x9); printf("sum = %d\n", sum); }
首先,編譯源程序 $gcc test.c -o test -fno-stack-protector
,而後反彙編出func_A函數 $gdb test
:
(gdb) disas func_A Dump of assembler code for function func_A: 0x00000000000006fd <+0>: push %rbp 0x00000000000006fe <+1>: mov %rsp,%rbp 0x0000000000000701 <+4>: sub $0x30,%rsp 0x0000000000000705 <+8>: movl $0x0,-0x4(%rbp) 0x000000000000070c <+15>: movl $0x1,-0x8(%rbp) 0x0000000000000713 <+22>: movl $0x2,-0xc(%rbp) 0x000000000000071a <+29>: movl $0x3,-0x10(%rbp) 0x0000000000000721 <+36>: movl $0x4,-0x14(%rbp) 0x0000000000000728 <+43>: movl $0x5,-0x18(%rbp) 0x000000000000072f <+50>: movl $0x6,-0x1c(%rbp) 0x0000000000000736 <+57>: movl $0x7,-0x20(%rbp) 0x000000000000073d <+64>: movl $0x8,-0x24(%rbp) 0x0000000000000744 <+71>: movb $0x63,-0x25(%rbp) 0x0000000000000748 <+75>: movsbl -0x25(%rbp),%edi 0x000000000000074c <+79>: mov -0x1c(%rbp),%r9d 0x0000000000000750 <+83>: mov -0x18(%rbp),%r8d 0x0000000000000754 <+87>: mov -0x14(%rbp),%ecx 0x0000000000000757 <+90>: mov -0x10(%rbp),%edx 0x000000000000075a <+93>: mov -0xc(%rbp),%esi 0x000000000000075d <+96>: mov -0x8(%rbp),%eax 0x0000000000000760 <+99>: push %rdi 0x0000000000000761 <+100>: mov -0x24(%rbp),%edi 0x0000000000000764 <+103>: push %rdi 0x0000000000000765 <+104>: mov -0x20(%rbp),%edi 0x0000000000000768 <+107>: push %rdi 0x0000000000000769 <+108>: mov %eax,%edi 0x000000000000076b <+110>: callq 0x699 <func_B> 0x0000000000000770 <+115>: add $0x18,%rsp 0x0000000000000774 <+119>: mov %eax,-0x4(%rbp) 0x0000000000000777 <+122>: mov -0x4(%rbp),%eax 0x000000000000077a <+125>: mov %eax,%esi 0x000000000000077c <+127>: lea 0xd1(%rip),%rdi # 0x854 0x0000000000000783 <+134>: mov $0x0,%eax 0x0000000000000788 <+139>: callq 0x520 <printf@plt> 0x000000000000078d <+144>: nop 0x000000000000078e <+145>: leaveq 0x000000000000078f <+146>: retq End of assembler dump.
上述彙編作的事情也是那幾個,保存上一個棧幀的信息、開棧、接受參數、棧上運算....,下面將進行分析。
0x00000000000006fd <+0>: push %rbp 0x00000000000006fe <+1>: mov %rsp,%rbp
保存上一個棧幀的做用就是爲了該函數被調用完成後還能再回到caller函數的棧幀繼續執行,須要把caller的棧幀保存起來,保存地點就是callee的棧幀上。上述彙編的第一句就是把幀指針rbp入棧保存在棧上,第二句把棧指針rsp保存在幀指針rbp中。上述兩句執行完後,func_C的棧佈局以下圖所示。
0x0000000000000701 <+4>: sub $0x30,%rsp
開棧的操做比較簡單,就是把rsp的值減少,開闢出一片連續的內存區域用做接收參數,存放局部變量以及棧上運算。不過func_C沒有參數和棧上運算。執行完上述彙編後,棧的佈局以下圖所示。
0x0000000000000705 <+8>: movl $0x0,-0x4(%rbp) 0x000000000000070c <+15>: movl $0x1,-0x8(%rbp) 0x0000000000000713 <+22>: movl $0x2,-0xc(%rbp) 0x000000000000071a <+29>: movl $0x3,-0x10(%rbp) 0x0000000000000721 <+36>: movl $0x4,-0x14(%rbp) 0x0000000000000728 <+43>: movl $0x5,-0x18(%rbp) 0x000000000000072f <+50>: movl $0x6,-0x1c(%rbp) 0x0000000000000736 <+57>: movl $0x7,-0x20(%rbp) 0x000000000000073d <+64>: movl $0x8,-0x24(%rbp) 0x0000000000000744 <+71>: movb $0x63,-0x25(%rbp)
這裏的棧上運算比較簡單,只是對局部變量進行賦值。局部變量賦值事後的棧空間以下圖所示:
0x0000000000000748 <+75>: movsbl -0x25(%rbp),%edi 0x000000000000074c <+79>: mov -0x1c(%rbp),%r9d 0x0000000000000750 <+83>: mov -0x18(%rbp),%r8d 0x0000000000000754 <+87>: mov -0x14(%rbp),%ecx 0x0000000000000757 <+90>: mov -0x10(%rbp),%edx 0x000000000000075a <+93>: mov -0xc(%rbp),%esi 0x000000000000075d <+96>: mov -0x8(%rbp),%eax 0x0000000000000760 <+99>: push %rdi 0x0000000000000761 <+100>: mov -0x24(%rbp),%edi 0x0000000000000764 <+103>: push %rdi 0x0000000000000765 <+104>: mov -0x20(%rbp),%edi 0x0000000000000768 <+107>: push %rdi 0x0000000000000769 <+108>: mov %eax,%edi
前面也說過,在X86-64平臺,當參數小於7個時使用寄存器傳參 。當參數個數大於等於7時,參數arg1~arg6分別使用寄存器rdi,rsi, rdx, rcx, r8 and r9傳參,其他參數使用棧傳遞。以下圖所示,實參x1~x6使用寄存器rdi,rsi, rdx, rcx, r8 and r9傳參,實參x7~x9使用棧傳遞。
int func_B(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, char x9){ int sum = 0; sum = func_C(x1, x2, x3, x4, x5,x6); sum = sum + x7; sum = sum + x8; sum += x9; return sum; }
首先,編譯源程序 $gcc test.c -o test -fno-stack-protector
,而後反彙編出func_B函數 $gdb test
:
(gdb) disas func_B Dump of assembler code for function func_B: 0x0000000000000699 <+0>: push %rbp 0x000000000000069a <+1>: mov %rsp,%rbp 0x000000000000069d <+4>: sub $0x30,%rsp 0x00000000000006a1 <+8>: mov %edi,-0x14(%rbp) 0x00000000000006a4 <+11>: mov %esi,-0x18(%rbp) 0x00000000000006a7 <+14>: mov %edx,-0x1c(%rbp) 0x00000000000006aa <+17>: mov %ecx,-0x20(%rbp) 0x00000000000006ad <+20>: mov %r8d,-0x24(%rbp) 0x00000000000006b1 <+24>: mov %r9d,-0x28(%rbp) 0x00000000000006b5 <+28>: mov 0x20(%rbp),%eax 0x00000000000006b8 <+31>: mov %al,-0x2c(%rbp) 0x00000000000006bb <+34>: movl $0x0,-0x4(%rbp) 0x00000000000006c2 <+41>: mov -0x28(%rbp),%r8d 0x00000000000006c6 <+45>: mov -0x24(%rbp),%edi 0x00000000000006c9 <+48>: mov -0x20(%rbp),%ecx 0x00000000000006cc <+51>: mov -0x1c(%rbp),%edx 0x00000000000006cf <+54>: mov -0x18(%rbp),%esi 0x00000000000006d2 <+57>: mov -0x14(%rbp),%eax 0x00000000000006d5 <+60>: mov %r8d,%r9d 0x00000000000006d8 <+63>: mov %edi,%r8d 0x00000000000006db <+66>: mov %eax,%edi 0x00000000000006dd <+68>: callq 0x64a <func_C> 0x00000000000006e2 <+73>: mov %eax,-0x4(%rbp) 0x00000000000006e5 <+76>: mov 0x10(%rbp),%eax 0x00000000000006e8 <+79>: add %eax,-0x4(%rbp) 0x00000000000006eb <+82>: mov 0x18(%rbp),%eax 0x00000000000006ee <+85>: add %eax,-0x4(%rbp) 0x00000000000006f1 <+88>: movsbl -0x2c(%rbp),%eax 0x00000000000006f5 <+92>: add %eax,-0x4(%rbp) 0x00000000000006f8 <+95>: mov -0x4(%rbp),%eax 0x00000000000006fb <+98>: leaveq 0x00000000000006fc <+99>: retq End of assembler dump.
一個函數的彙編作的仍是那幾件事,下面分析:
0x0000000000000699 <+0>: push %rbp 0x000000000000069a <+1>: mov %rsp,%rbp
上一個棧幀的幀指針rbp保存在當前棧幀上, 上一個棧幀的棧指針rsp保存在當前棧幀的幀指針rbp寄存器中。
0x000000000000069d <+4>: sub $0x30,%rsp
在棧上開闢一塊連續的地址用於接收參數,局部變量,棧上運算。
0x00000000000006a4 <+11>: mov %esi,-0x18(%rbp) 0x00000000000006a7 <+14>: mov %edx,-0x1c(%rbp) 0x00000000000006aa <+17>: mov %ecx,-0x20(%rbp) 0x00000000000006ad <+20>: mov %r8d,-0x24(%rbp) 0x00000000000006b1 <+24>: mov %r9d,-0x28(%rbp) 0x00000000000006b5 <+28>: mov 0x20(%rbp),%eax 0x00000000000006b8 <+31>: mov %al,-0x2c(%rbp)
函數總共有9個參數,寄存器傳遞6個參數,棧傳遞三個參數。
0x00000000000006c2 <+41>: mov -0x28(%rbp),%r8d 0x00000000000006c6 <+45>: mov -0x24(%rbp),%edi 0x00000000000006c9 <+48>: mov -0x20(%rbp),%ecx 0x00000000000006cc <+51>: mov -0x1c(%rbp),%edx 0x00000000000006cf <+54>: mov -0x18(%rbp),%esi 0x00000000000006d2 <+57>: mov -0x14(%rbp),%eax 0x00000000000006d5 <+60>: mov %r8d,%r9d 0x00000000000006d8 <+63>: mov %edi,%r8d 0x00000000000006db <+66>: mov %eax,%edi
調用函數有6個參數,這6個參數都使用寄存器傳遞。
0x00000000000006dd <+68>: callq 0x64a <func_C> 0x00000000000006e2 <+73>: mov %eax,-0x4(%rbp) 0x00000000000006e5 <+76>: mov 0x10(%rbp),%eax 0x00000000000006e8 <+79>: add %eax,-0x4(%rbp) 0x00000000000006eb <+82>: mov 0x18(%rbp),%eax 0x00000000000006ee <+85>: add %eax,-0x4(%rbp) 0x00000000000006f1 <+88>: movsbl -0x2c(%rbp),%eax 0x00000000000006f5 <+92>: add %eax,-0x4(%rbp)
運算第一步就是將func_A的返回值%eax賦值給sum;第二步是將x7的值和sum相加放在sum中;第三步是將x8的和sum相加結果放在sum中;第三步是將x9的值擴展成32bit,和sum相加結果放在sum中。
int func_C(int x1, int x2, int x3, int x4, int x5, int x6){ int sum = 0; sum = x1 + x2; sum = sum + x3 + x4; sum = sum + x5 + x6; return sum; }
首先,編譯源程序 $gcc test.c -o test -fno-stack-protector
,而後反彙編出func_A函數 $gdb test
:
(gdb) disas func_C Dump of assembler code for function func_C: 0x000000000000064a <+0>: push %rbp 0x000000000000064b <+1>: mov %rsp,%rbp 0x000000000000064e <+4>: mov %edi,-0x14(%rbp) 0x0000000000000651 <+7>: mov %esi,-0x18(%rbp) 0x0000000000000654 <+10>: mov %edx,-0x1c(%rbp) 0x0000000000000657 <+13>: mov %ecx,-0x20(%rbp) 0x000000000000065a <+16>: mov %r8d,-0x24(%rbp) 0x000000000000065e <+20>: mov %r9d,-0x28(%rbp) 0x0000000000000662 <+24>: movl $0x0,-0x4(%rbp) 0x0000000000000669 <+31>: mov -0x14(%rbp),%edx 0x000000000000066c <+34>: mov -0x18(%rbp),%eax 0x000000000000066f <+37>: add %edx,%eax 0x0000000000000671 <+39>: mov %eax,-0x4(%rbp) 0x0000000000000674 <+42>: mov -0x4(%rbp),%edx 0x0000000000000677 <+45>: mov -0x1c(%rbp),%eax 0x000000000000067a <+48>: add %eax,%edx 0x000000000000067c <+50>: mov -0x20(%rbp),%eax 0x000000000000067f <+53>: add %edx,%eax 0x0000000000000681 <+55>: mov %eax,-0x4(%rbp) 0x0000000000000684 <+58>: mov -0x4(%rbp),%edx 0x0000000000000687 <+61>: mov -0x24(%rbp),%eax 0x000000000000068a <+64>: add %eax,%edx 0x000000000000068c <+66>: mov -0x28(%rbp),%eax 0x000000000000068f <+69>: add %edx,%eax 0x0000000000000691 <+71>: mov %eax,-0x4(%rbp) 0x0000000000000694 <+74>: mov -0x4(%rbp),%eax 0x0000000000000697 <+77>: pop %rbp 0x0000000000000698 <+78>: retq End of assembler dump.
func_A的棧結構和前幾個函數類型,可是編譯器識別到該函數時葉子函數(leaf function),其棧指針rsp再也不被使用,就會少一修改rsp的指令和一個恢復rsp的指令。
0x0000000000000699 <+0>: push %rbp 0x000000000000069a <+1>: mov %rsp,%rbp
不會有開棧的操做,即不會修改rsp指針。
0x000000000000064e <+4>: mov %edi,-0x14(%rbp) 0x0000000000000651 <+7>: mov %esi,-0x18(%rbp) 0x0000000000000654 <+10>: mov %edx,-0x1c(%rbp) 0x0000000000000657 <+13>: mov %ecx,-0x20(%rbp) 0x000000000000065a <+16>: mov %r8d,-0x24(%rbp) 0x000000000000065e <+20>: mov %r9d,-0x28(%rbp)
0x0000000000000662 <+24>: movl $0x0,-0x4(%rbp) 0x0000000000000669 <+31>: mov -0x14(%rbp),%edx 0x000000000000066c <+34>: mov -0x18(%rbp),%eax 0x000000000000066f <+37>: add %edx,%eax 0x0000000000000671 <+39>: mov %eax,-0x4(%rbp) 0x0000000000000674 <+42>: mov -0x4(%rbp),%edx 0x0000000000000677 <+45>: mov -0x1c(%rbp),%eax 0x000000000000067a <+48>: add %eax,%edx 0x000000000000067c <+50>: mov -0x20(%rbp),%eax 0x000000000000067f <+53>: add %edx,%eax 0x0000000000000681 <+55>: mov %eax,-0x4(%rbp) 0x0000000000000684 <+58>: mov -0x4(%rbp),%edx 0x0000000000000687 <+61>: mov -0x24(%rbp),%eax 0x000000000000068a <+64>: add %eax,%edx 0x000000000000068c <+66>: mov -0x28(%rbp),%eax 0x000000000000068f <+69>: add %edx,%eax 0x0000000000000691 <+71>: mov %eax,-0x4(%rbp)
彙編語言描述的和C語言一致。