Linux Source Code Tree Overview跟System Boot up我就跳過了…
x32 Calling Conventions
這邊要介紹的是,當C呼叫一個function時參數的傳遞。關於stack frame我在網路攻防這邊有提到一些基礎知識,重複的在這邊就不再贅述。
假設今天G function要呼叫H function,則我們稱呼叫者G為caller,被呼叫的H function我們稱callee。為了方便講解老師投影片的圖,看一下下面這段code:
1 | #include <stdlib.h> |
可以看到main function呼叫了G function,G function再去呼叫H function。現在把焦點放在G function裡面呼叫的H(3),當G呼叫H之前,它會先把之後要給H的參數push到stack上,之後再去call H function,下圖為上面這支程式的執行檔經過objdump產生的內容:
上圖可以看到G在call H之前將3這個值push到stack,那callee要如何使用這參數呢?看一下下面這張stack frame:
當G call了H function後,它裡面會有一個動作是將return address push上去,之後到H的stack frame時會先push ebp,因此可以利用ebp的相對位置找到參數,如下圖:
由於我程式裡面做的事情是i = b - 3,可以看到compiler將ebp+8的位置載入eax,接著再將它減去3。看完caller如何傳參數給callee後,接下來我們看callee如何將參數return給caller,其實很簡單,caller只要把要return的值塞進去eax暫存器就好了:
看一下剛剛G function,我直接將傳進來的參數a return回去,所以assembler就直接將ebp + 8位置的值丟進eax了。
x64 Calling Conventions
x64與x32不一樣的是,如果傳遞的參數在六個以下時,它是利用暫存器去傳送的,看一下下面程式:
1 | int G(int a, int b, int c, int d, int e, int f, int g, int h) |
我把上面的程式碼丟到64 bit ubuntu編譯後,objdump如下圖:
可以看到紅色框的部分6個參數都移到了暫存器當中,而當到第七個參數時,compiler是把它放到了rsp的位置,從stack底端往上放。
而callee要拿參數時,前六個當然是從暫存器拿,第七個開始就從rbp + 16開始:
由於我是return g,它是第七個參數,所以將rbp + 10(hex)移到eax。
Callee-Save Register vs. Caller-Save Register
這可能會被文字混淆…
caller saved register意思是當caller要呼叫callee時,caller會預先把某幾個暫存器的值存在stack上,然後呼叫了callee後callee可以使用者些暫存器。所以caller saved register是給callee用的。
而callee saved register是當caller呼叫了callee時,為了確保不會更動到register的值,callee會先把暫存器的值存起來,等到做完所有動作要return回caller之前再把剛存起來的值還給暫存器,所以callee saved register是callee幫caller存的。
IA32 Process Address Space Layout
Address space意思是一堆address的集合,在32bit的linux環境中每個process都會有4G byte的address space,前3G byte為user address space,從0x00000000 ~ 0xBFFFFFFF;最後1G byte則是kernel address space,從0xC0000000 ~ 0xFFFFFFFF,所以學linux kernel的人要知道0xC0000000,這非常重要!!!!(老師說的XDD)。下圖就是IA32 linux的address space:
x64 Process Address Space Layout
x64的address space比較特別,由於他有$2^{64}$bit的address,相當於16EB,如果要將這麼大的virtual address都implement到實際的記憶體上的話,會造成page table複雜度提升,況且正常作業系統也用不到這麼龐大的記憶體空間,於是AMD決定只用前48bit的address,使用空間大概是256TB,創造了一個叫做canonical form的規則。
這規則其實還蠻人性化的(? 看一下目前48bit的設計,整個address space分為兩大區塊,一邊從底部長另一邊從頂部長,先說明一下canonical form的規則,它除了規定只能用前48個bit之外還外加了一個規定,就是沒用到的bit,從第49個bit開始往後都要跟第48個bit一樣,也就是說如果第48個bit為0之後就全部都要是0,如果第48個bit為1之後就都要是1,有了這個規則後,如果你的記憶體是單向長的話,當你長到00008000 00000000的時候,你的第48個bit為1,但第49bit後卻全都是0,這樣就違反了canonical form。
而user space跟kernel space剛好一人一半(從kernel 2.6.11版本後),高位址的部分是kernel address,另一半低位址則是user space。下面的位址分部是參考這邊。
1 | 0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm |