IA32體系結構8(x86過程調用)

直觀感受及疑問

正常情況下,CPU執行指令都是一條條順序往下執行。所謂過程調用,就是CPU停止執行下一條指令,轉而去執行其它地方的指令。當這些指令執行完畢之後,CPU回到之前暫停的下一條指令,重新開始執行。有點類似C語言的“函數調用”。那麼,我們有幾個問題需要弄清楚:

1.我們的指令中,如何跳轉到過程調用指令,又如何回來?

2.跳過去又回來,CPU是如何記住之前的執行位置的?

3.如果過程程序需要參數,如何傳遞,有了返回值,如何傳遞?

瞭解堆棧

在回答上述問題之前,有一個非常重要的結構需要了解,那就是堆棧-stack。

堆棧是一段連續的內存位置,由棧選擇符SS和棧指針ESP指定。如果是32位保護模式,SS指向一個數據段描述符,這個數據段描述符用於堆棧,就必須是可讀可寫的。ESP指向棧頂數據的最低字節地址。x86的堆棧是一種滿遞減堆棧,從高地址向低地址擴展,ESP指向棧頂可用的元素。當push入棧時,ESP先遞減,然後再把數據拷貝到ESP開始的地址處。pop則相反,先拷貝數據,再遞增ESP。

堆棧的數量只受物理內存和線性地址空間的限制,但是任意時刻,只能操作使用一個堆棧,這個堆棧叫做當前堆棧,由SS來指定。

堆棧的push和pop操作,默認需要字節對齊,16位或者32位。這個由堆棧描述符裏面的D標誌決定。如果設置爲16位,則push和pop一次操作2字節。32位則操作4字節。

另外,還有一個堆棧基址寄存器EBP,用來方便過程調用的參數傳遞和建立局部變量。使用的時候,默認指向SS指向的數據段。一般在被調用程序裏面,直接拷貝ESP到EBP保存起來,指向一個獨立的“棧幀”。

返回指令指針(return instruction pointer)

跳到調用過程第一條指令之前,CALL指令會將EIP寄存器的值push到當前堆棧保存起來。此時,這個地址叫做返回指令指針。一旦從過程返回,RET指令會將這個地址彈出到EIP,從而恢復程序的執行。

使用CALL和RET進行過程調用

近過程調用

近跳轉過程如下:

1.push當前的EIP到堆棧

2.加載調用過程偏移到EIP

3.開始執行過程調用代碼

近返回過程如下:

1.pop棧頂元素到EIP寄存器

2.如果ret指令帶有可選的n參數,則遞增棧指針的值(n),以此釋放棧中的參數

3.恢復調用過程代碼的執行

遠過程調用

遠跳轉過程如下:

1.push當前的CS寄存器到堆棧

2.push當前的EIP寄存器到堆棧

3.加載過程代碼的段選擇子到CS寄存器

4.加載過程代碼的指令偏移到EIP寄存器

5.開始過程代碼的執行

遠返回過程如下:

1.pop棧頂數據到EIP寄存器

2.pop棧頂數據到CS寄存器(之前的棧頂數據已經彈出到EIP)

3.如果RET指令帶有n參數。遞增棧指針n字節,用以釋放棧中的參數

4.恢復原先程序的執行

參數傳遞

參數傳遞三種方式:

1.通過通用寄存器(把參數放在通過寄存器,然後執行程序,之後將返回值放到通過寄存器)

2.通過參數列表(參數存放在特定數據段,用一指針指定,放入通用寄存器)

3.通過堆棧(參數壓入堆棧,通過棧幀基址寄存器來定位參數位置)

linux內核主要使用了方式1和方式3,方式1多見於系統調用int 0x80。其中堆棧傳遞方式比較經典,其棧幀結構完美解決了傳參數量和局部變量數量的問題。具體參考《linux內核完全註釋》3.4節。在執行過程調用的時候,cpu不自動保存通用寄存器、段寄存器、EFLAGS等內容,如果需要使用這些狀態值,保存的責任在於調用者。

不同特權級過程調用

不同的特權級之間的過程調用,遵循特殊設計。IA架構分0/1/2/3這四種特權級,0最高特權級,3最低特權級。較低的特權級可以通過“門”來調用較高特權級上的過程代碼。否則,CPU會拋出一般保護異常。

CALL指令中提供的段選擇符指向一個特殊的數據結構-調用門描述符(call gate descriptor)。這個調用門描述符包含:

1.訪問權限信息

2.調用過程的代碼段選擇符

3.調用過程的代碼在段中的偏移

不同特權級的過程調用,處理器會執行堆棧切換。每個特權級,都有自己的堆棧。對於多特權級來說,特權級3的堆棧選擇符和堆棧指針使用SS和ESP,特權級0/1/2的堆棧選擇符和棧指針保存在任務狀態段(TSS)裏面。切換特權級時,對應的堆棧選擇符和棧指針會被自動傳送到SS和ESP,這對調用者來說,是透明的。

(注意:linux內核中沒有使用調用門,一下內容僅做了解)

當發起特權級更高的調用,處理器處理過程如下:

1.執行訪問權限檢查

2.臨時保存(內部)SS, ESP, CS和EIP寄存器的內容

3.從TSS中加載相應特權級(要轉到的調用代碼特權級)的堆棧選擇符和棧指針到SS和ESP,然後切換到新的堆棧

4.push之前臨時保存的調用者的SS和ESP到新堆棧(被調用者的堆棧)

5.從調用者堆棧複製參數到新堆棧(調用門描述符裏面確定的多少參數需要拷貝到新堆棧)

6.push之前臨時保存的調用者的CS和EIP到新棧

7.分別加載新代碼的段選擇符和調用門裏面的新指令指針到CS和EIP

8.以新的特權級開始執行過程調用

從特權級調用返回,處理器執行過程如下:

1.執行特權級檢查

2.加載CS和EIP寄存器爲調用之前的值

3.如果RET有n參數,則遞增棧指針n字節,彈出數據

4.加載SS和ESP寄存器爲調用之前的值

5.如果RET有n參數,遞增棧指針

6.恢復執行原先的代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章