本節主要核心是介紹x86-64體系結構下的_start
函數,該函數是由x86-64彙編寫成;調用__libc_start_main
函數向其傳遞參數。所以須要先了解一些x86-64的棧幀結構、寄存器、以及參數傳遞規則。git
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.
github
在輸入參數的結尾處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個128bit XMM寄存器能夠被SSE指令操控,SSE利用這些寄存器能夠同時作多個數據的運算,從而加快運算速度。可是數據被裝進XMM寄存器時,要求數據的地址須要16字節對齊,而數據常常會在棧上分配,所以只有要求棧以16字節對齊,才能更好的支持數據的16字節對齊。ide
X86-64的寄存器相對於X86有擴展,主要不一樣體如今:函數
16個64bit寄存器 爲:RAX,RBX,RCX,RDX,RDI,RSI,RBP,RSP,R8,R9,R10,R11,R12,R13,R14,R15
在X86-64架構的處理器上,Windows和Linux的函數調用規則不同。post
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)
跟據上述彙編,其實也就作了一件事,調用__libc_start_main
函數,並向其傳遞了7個參數:操作系統
__libc_csu_fini
__libc_csu_init
上述彙編有幾句比較晦澀:.net
__libc_start_main
以前,幫助rsp對齊到16字節,%rax入棧無其它意義。顯然,這一句執行後,rsp尚未對齊到16字節,下一句彙編執行後就將對齊到16字節。__libc_start_main
函數,且使rsp對齊到16字節。執行_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)
和x86-64節的套路相似,先了解一些ARM64的棧幀結構、寄存器、以及參數傳遞規則。3d
下圖取自<Procedure Call Standard for the ARM 64-bit Architecture>,大體說明了ARM64的棧幀結構。
看ARM64的彙編會常常遇到adr、ldr,adrp指令,下面將進行簡短的介紹。
主要用於造成pc相對地址,把相對地址load到寄存器中,使用方法爲:
adr <xd>, <label>
當前指令到label的偏移 offset_to_label 加上PC的值,而後將結果賦值給xd。offset_to_label能夠是個負數,實際在執行過程當中會將offse_to_label擴展成64爲有符號數。可是ARM指令的長度是固定爲32bit,offset_to_label最多隻能爲21位,也便可以尋PC +/-1MB的範圍。
常常會被編譯器轉換成add或sub指令:
add <xd>,[PC, #offset_to_label] or sub <xd>,[PC, #-offset_to_label]
這個指令的本質做用是把地址中的數據加載到寄存器中,根據地址的表達形式不一樣能夠分爲幾種狀況:
ldr <Xd>, <label>
將程序label處的數據load到Xd中,label是一個地址。指令記錄的不是label的絕對地址,是當前指令到label的偏移,記做offset_to_labe,l和adr指令描述中的 offset_to_label 有所不一樣。在彙編時,彙編器會計算當前指令到label的偏移量(以字節爲單位),而後將偏移量右移兩位獲得 offset_to_label 。在執行執行指令時效果以下:
Xd <=== [PC + (offset_to_label << 2)]
另外幾種以下:
ldr <Xt>,[<Xn|SP>],#<simm> post_index ldr <Xt>,[<Xn|SP>,#<simm>]! pre_index ldr <Xt>,[<Xn|SP>,#<pimm>] unsigned_offset
該指令在ARMv8中首次被設計出來,是ARM指令集的一個重大創新,能夠減小指令條數以及訪存的次數。有幾篇博客介紹了該指令的做用,可是沒有講清楚,如《ARM指令淺析2(adrp、b)》、《彙編7、ADRP指令》。
指令的使用方式爲:
adrp <Rd>, <label>
adrp就是address page 的簡寫,這裏的page指的是大小爲4KB的連續內存,和操做系統中的頁不是一回事。該指令的做用是將label所在頁且4KB對其的頁基地址放入寄存器Xd中。Labe表示的地址確定在這個頁基地址肯定的頁內。要想完全搞懂這個指令的做用,還須要從指令彙編的過程和譯碼的過程進行分析。
也就是將這個指令變成二進制機器碼的過程,根據ARM文檔,adrp指令的二進制格式爲:
32bit中的21bit immhi和immlo是由lable的地址(L)和當前指令所在的地址計算來的,第一步獲取label和當前指令所在頁的頁基地址,二者相減獲得差值;第二步將差值右移12位,再取低21位做爲immhi:immlo。在進行指令彙編的時候,數據和指令在最終的二進制文件中的位置都肯定了,固然也能夠肯定當前指令在所在的頁基地址和lable所在的頁基地址。
如上圖所示,在彙編時 immhi:immlo=(pageoffset_to_label>>12)&0x1FFFFF,Rd也是肯定的,就能夠造成一條二進制機器碼指令。
在cpu執行adrp 機器碼指令時,能夠根據PC和機器碼指令中的immhi:immlo找到label所在頁的基地址。在adrp指令發明後,對二進制文件的映射提出了一個要求,即二進制文件映射的虛擬地址必須4K對齊。在CPU執行adrp的機器碼時,PC時已知的,根據PC就能夠計算出label所在頁的基地址:Rd=(PC & 0xFFFFFFFFFFFF0000) + (immhi:immlo << 12).
到這裏adrp指令的前先後後基本上也就介紹完了,還值得一提的是,獲取label所在頁的基地址自己沒有什麼用,因此通常在adrp指令的後面都會在跟一條add指令:add Rd, Rd,offset_inpage
, label所在的地址就在寄存器Rd中了,就可使用load指令加載label處的數據了;或者直接使用ldr Rd, [Rd, #offset_inpage]
加載label處的數據。
adrp的優點是什麼? ARM是RISC指令集,每一個指令都是等長的32bit,這32bit能容下的東西頗有限,一個尋址指令除去自己的操做碼,留給地址的bit位就沒幾個了,而有了adrp指令,相對尋址能力大大提高,能夠尋址距離PC 4GB遠的數據,既能夠尋址PC前的4GB範圍,也能夠尋址PC後的4GB範圍,由於immhi:immlo是21bit,offset_inpage是12bit,21+12=33。
在glibc的 sysdeps/aarch64/start.S中有_start函數,通過簡單的處理以下所示:
_start: /* Create an initial frame with 0 LR and FP */ 1: mov x29, #0 2: mov x30, #0 /* Setup rtld_fini in argument register */ 3: mov x5, x0 /* Load argc and a pointer to argv */ 4: ldr x1, [sp, #0] 5: add x2, sp, #8 /* Setup stack limit in argument register */ 6: mov x6, sp 7: adrp x0, :got:main 8: ldr x0, [x0, #:got_lo12:main] 9: adrp x3, :got:__libc_csu_init 10: ldr x3, [x3, #:got_lo12:__libc_csu_init] 11: adrp x4, :got:__libc_csu_fini 12: ldr x4, [x4, #:got_lo12:__libc_csu_fini] /* __libc_start_main (main, argc, argv, init, fini, rtld_fini, stack_end) */ /* Let the libc call main and exit with its return code. */ 13: bl __libc_start_main /* should never get here....*/ 14: bl abort
上面的彙編,1~2行表示狀況LR(Link Register) 和FP(Frame Pointer); 第4行是將argc傳遞給x1;第5行是將argv傳遞給x2,這裏的argc和argv就是咱們平時寫的C
程序int main(int argc, char *argv[])
函數的兩個參數;其他幾行相似,都是使用寄存器傳遞參數。ARM64的_start函數和X86-64的_start函數目的是同樣的,都是調用__libc_start_mian
函數,該函數的聲明爲:
__libc_start_main (int (*main) (int, char **, char **), int argc, char *argv, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void *stack_end);
其中寄存器傳遞的參數爲:
x0 main x1 argc x2 argv x3 init x4 fini x5 rtld_fini x6 stack_end
_start
函數的做用以下圖所示,下圖的上半部分是Linux Kernel完成的和平臺無關的設置,創建起了用戶棧最初的部分,SP指向棧頂,棧中存放傳遞給__libc_start_main
函數的參數argc和argv,Linux Kernel在這一點完成將用戶的參數傳遞給用戶程序的角色,同時也將棧的控制權轉移給libc,而libc的__libc_start_main
函數在將棧的控制權完成轉移給用戶的main
函數以前,還會作一些額外的工做,發揮一些額外的做用