CALL做了什麼
那麼一條CALL指令做了什麼事情呢?它做的就是對CPU執行指令所需要的充要條件相關因素進行處理,從而保證下一條指令能夠正確執行。CALL指令執行需要知道下一步調用的函數的地址(最簡單跳轉指令JMP需要知道的東東),而在它將CPU執行點給下一步需要執行的函數之前,需要先保存現有執行點的一些信息,最簡單的就是CS、EIP和ESP寄存器(自然的,也就是遵守調用約定的,函數調用ESP由調用函數自動計算,可以不存儲)。
圖 2 執行CALL之前的寄存器和內存使用情況
假定CPU現有代碼執行至004014A0時各寄存器的值如下:ESP(008B0010),EIP(004014A0),CS(0000)。如圖2所示。爲了方便描述,這裏假定各條指令的長度都是4字節(遠調用爲8字節)。
那麼執行CALL 004032A0後的各寄存器的值如下:ESP(008B000C),EIP(004032A0),CS(0000)。如圖3所示。執行一次近調用後CALL指令會將EIP入棧,並同時更新ESP和EIP的值,由於是近調用,CS寄存器的值不變。
圖 3 執行near CALL之後的寄存器和內存使用情況
同理,執行完CALL 0300: 004032A0後的各寄存器值如下:ESP(008B0008),EIP(004032A0),CS(0300)。如圖4所示。執行一次遠調用後CALL指令會將CS和EIP依次入棧,並同時更新ESP、EIP和CS的值,下一條指令的執行地址由最新的EIP和CS計算出來。
圖 4 執行FAR CALL之後的寄存器和內存使用情況
4 RET幹了些啥?
圖 5 執行RET之後的寄存器和內存使用情況
與CALL指令相對的,RET指令主要進行出棧操作,並更新相應的寄存器。出棧指令RET有四種形式。
(1) RET。可能是近返回也可能是遠返回。
(2) RETN。顯式指定近返回。
(3) RETF。顯式指定遠返回。
(4) RET N。同(1),不過ESP另外減去N字節。
圖5中給出了最簡單的一種調用RET且爲近返回調用後寄存器和內存的使用情況。
/*
* Calculate the delta between where we were compiled to run
* at and where we were actually loaded at. This can only be done
* with a short local call on x86. Nothing else will tell us what
* address we are running at. The reserved chunk of the real-mode
* data at 0x1e4 (defined as a scratch field) are used as the stack
* for this calculation. Only 4 bytes are needed.
*/
//計算編譯時的連接地址與實際加載地址之間的差值。。。。計算時使用實模式數據段偏移在0x1e4
//處的地址作爲棧,這次計算只需要4個字節的棧就可以了!這裏很是費解,把boot_params->scratch
//作爲堆棧使用。
//將前邊操作數的偏移地址賦值給esp,前邊參數的地址爲ds:( (BP_scratch+4)(%esi) ),那他的偏移就是
//(BP_scratch+4)(%esi),%esi裏放的是boot_params。
leal (BP_scratch+4)(%esi), %esp
call 1f //根據前面對call的解釋,知道,這裏call先將1處的加載地址eip+4放入堆棧
1 : popl %ebp //ebp就是1:的實際地址
subl $1b, %ebp //這裏$1b是1處的連接地址,因爲$1b是個標號,標號表示的都是連接地址