X86-64和ARM64用戶棧的結構 (3) ---_start到main
介紹將以如下函數為例:
#include<stdio.h> #include <stdlib.h> int func_A(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, char x7){ int sum = 0; sum = func_A(x1, x2, x3, x4, x5,x6); sum = sum + x7; return sum; } void func_C(void){ int sum = 0; int x1 = 1; int x2 = 2; int x3 = 3; int x4 = 4; int x5 = 5; int x6 = 6; char x7 = ‘c‘; sum = func_B(x1, x2, x3, x4, x5, x6, x7); printf("sum = %d\n", sum); } int main(int argc, char *argv[]) { int c = argc; char **p = argv; func_C(); return 0; }
x86-64
X86-64的寄存器相對於X86有擴展,主要不同體現在:
- 通用寄存器:X86-64有16個64bit通用寄存器
- 狀態寄存器:1個64bit狀態寄存器RFLAGS,僅僅低32bit被使用
- 指令寄存器:1個64bit指令寄存器RIP
- MMX寄存器:8個64bitMMX寄存器,16個128bitXMM寄存器。當使用這些寄存器時,數據的地址必須對齊到64bit、128bit。
16個64bit寄存器 為:RAX,RBX,RCX,RDX,RDI,RSI,RBP,RSP,R8,R9,R10,R11,R12,R13,R14,R15
在X86-64架構的處理器上,Windows和Linux的函數調用規則是不一樣。
Windows
暫不介紹
Linux
Stack Frame
Linux使用System V Application Binary Interface的函數調用規則。在《System V Applocation Binary Interface》中3.2.2 The Stack Frame中寫道:
In addition to registers, each function has a frame on the run-time stack. This stack grows downwards from high addresses. Figure 3.3 shows the stack organization. The end of the input argument area shall be aligned on a 16 (32 or 64, if __m256 or __m512 is passed on stack) byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 (32 or 64) when control is transferred to the function entry point. The stack pointer, %rsp, always points to the end of the latest allocated stack frame.
在輸入參數的結尾處rsp必須對齊到16字節,當調用函數時,首先rsp會減8,rip會壓棧,在棧中占8個字節,然後rip指向另一個函數的entry point,也即控制轉移到了函數的entry point。由於rip壓棧了,rsp+8應該是16字節對齊。
至於為什麽需要16字節對齊,查了一些資料發現和Sreaming SIMD Extensions(SSE)有關,它是一組CPU指令,用於像信號處理、科學計算或者3D圖形計算一樣的應用(SSE入門)。SIMD 也是幾個單詞的首寫字母組成的: Single Instruction, Multiple Data。 一個指令發出後,同一時刻被放到不同的數據上執行。16個128bitXMM寄存器可以被SSE指令操控,SSE利用這些寄存器可以同時做多個數據的運算,從而加快運算速度。但是數據被裝進XMM寄存器時,要求數據的地址需要16字節對齊,而數據經常會在棧上分配,因此只有要求棧以16字節對齊,才能更好的支持數據的16字節對齊。
Parameter Passing
當參數的數目小於7個時,使用rdi,rsi, rdx, rcx, r8 and r9傳遞參數,大於等於7個時使用stack傳參數。具體的規則見《System V Applocation Binary Interface》中3.2.3 Parameter Passing
- rax 作為函數返回值使用。
- rsp 棧指針寄存器,指向棧頂。
- rdi,rsi,rdx,rcx,r8,r9 用作函數參數,依次對應第1參數,第2參數...
- rbx,rbp,r12,r13,r14,r15 用作數據存儲,遵循被調用者(callee)使用規則,簡單說就是隨便用,調用子函數之前要備份它,以防他被修改
- r10,r11 用作數據存儲,遵循調用者(caller)使用規則,簡單說就是使用之前要先保存原值
_start函數
0000000000000540 <_start>:
540: 31 ed xor %ebp,%ebp
542: 49 89 d1 mov %rdx,%r9
545: 5e pop %rsi
546: 48 89 e2 mov %rsp,%rdx
549: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
54d: 50 push %rax
54e: 54 push %rsp
54f: 4c 8d 05 da 02 00 00 lea 0x2da(%rip),%r8 # 830 <__libc_csu_fini>
556: 48 8d 0d 63 02 00 00 lea 0x263(%rip),%rcx # 7c0 <__libc_csu_init>
55d: 48 8d 3d 2c 02 00 00 lea 0x22c(%rip),%rdi # 790 <main>
564: ff 15 76 0a 20 00 callq *0x200a76(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5>
56a: f4 hlt
56b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
跟據上述匯編:
r9 < ----- rdx
r8 <------ __libc_csu_fini
rcx <------ __libc_csu_init
rdx <------ argv
rsi <------ argc
rdi <------ main
rsp 的值壓棧
and $0xfffffffffffffff0,%rsp的目的是使rsp對齊到16字節。
push %rax 為了使rsp對齊到16字節
push %rsp, rsp的值入棧
執行_start的第一條指令時,rsp的值是多少呢?誰設置的呢?rsp的值是bprm->p,Linux內核設置的,在上面的內容中有介紹。下圖結合了Linux Kernel和_start設置的棧。其實_start
來自glibc,在x86-64平臺上,可以在文件sysdeps/x86_64/start.S
中找到代碼。這段代碼的目的很單純,只是給函數__libc_start_main
準備參數。函數__libc_start_main
同樣來自glibc,它定義在文件csu/libc-start.c中。
函數__libc_start_main
的原型如下:
int __libc_start_main(
(int (*main) (int, char**, char**),
int argc,
char **argv,
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void),
void* stack_end)
在《How statically linked programs run on Linux 》中介紹了__libc_start_main
的作用:
- Figure out where the environment variables are on the stack.
- Prepare the auxiliary vector, if required.
- Initialize thread-specific functionality (pthreads, TLS, etc.)
- Perform some security-related bookkeeping (this is not really a separate step, but is trickled all through the function).
- Initialize libc itself.
- Call the program initialization function through the passed pointer (init).
- Register the program finalization function (fini) for execution on exit.
- Call main(argc, argv, envp)
- Call exit with the result of main as the exit code.
ARM64
待完善
X86-64和ARM64用戶棧的結構 (3) ---_start到main