一段C語言和彙編的對應分析,揭示函式呼叫的本質
最近網易雲課堂開放了一節叫的課程。一直對作業系統和計算機本質很感興趣,於是進去看了下,才第一堂課,老師就要求學生寫一篇關於課時1的部落格作為作業。對於這種新穎的作業形式,筆者相當驚訝。好吧,作為任務,還是完成一下吧,剛好需要消化一下。本文將會按照要求,將一段C語言程式碼編譯成彙編,並給予分析和自己的思考。
本文作者周平,原創作品轉載請註明出處
首先對會涉及到的一些CPU暫存器和彙編的基礎知識羅列一下:
16位、32位、64位的CPU暫存器名稱有所不同,比如指令地址暫存器
ip
,在16位中叫ip
,32位中叫eip
,64位叫rip
32位的彙編指令通常以
l
結尾,比如movl
相當於mov
的含義ebp
: 堆疊基地址 暫存器,這個暫存器儲存的是當前執行緒的棧底地址
esp
: 堆疊棧頂 暫存器,這個暫存器儲存的是當前執行緒的棧頂地址
eip
: 指令地址 暫存器,這個暫存器儲存的是指令所在的地址,CPU會不斷的根據eip
所指向的指令去記憶體取指令並執行,並自行累加取下一條指令逐條執行。eip
無法直接賦值,call
、ret
、jmp
等指令可以起到修改eip
的作用%
用於直接定址暫存器,$
用於表示立即數。movl $8, %eax
表示把立即數8
存到eax
中()
用於記憶體間接定址,比如movl $10, (%esp)
表示將立即數10
儲存到esp
所指向的記憶體地址中8(%ebp)
表示先找到ebp
所指向的地址值+8
後得到的地址棧地址值是向下增長的,即棧頂從高地址向低地址移動
準備工作
準備一段C程式碼:
int g(int x) { return x+5; } int f(int x) { return g(x); } int main(void) { return f(10)+1; }
使用環境
編譯成彙編程式碼
使用如下命令編譯上面的c程式碼
gcc -S -o main.s main.c -m32
去掉不重要的部分後,得到:
彙編程式碼結果為:
g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $5, %eax popl %ebp ret f: pushl %ebp movl %esp, %ebp subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp) call g leave ret main: pushl %ebp movl %esp, %ebp subl $4, %esp movl $10, (%esp) call f addl $1, %eax leave ret
分析
具體的逐步分析,這裡就省了,老師課上講的很詳細了,這裡主要是要進行思考和歸納。
首先,我們看到3個C函式對應生成了3個部分的彙編程式碼,分別用函式名作為標號隔開了
int g(int x) -> g: int f(int x) -> f: int main(void) -> main:
我們知道程式是從main
函式開始執行的,那麼當程式被載入並執行時,上面的彙編程式碼會被載入到記憶體的某一個區域。而且,CPU中的很多暫存器都會初始化,當然其中最重要的是eip
,因為eip
是指向下一條將要執行的命令所在的記憶體地址,所以此時的eip
應該指向main
標號下的pushl %ebp
:
main: eip -> pushl %ebp
程式開始執行…
我們捆綁著看,首先先看這兩條:
pushl %ebp movl %esp, %ebp
再觀察一下整個程式碼,有沒有發現不僅僅是main
函式,函式f
和g
的開頭也是這兩個指令。分析一下,不難得出,這兩條指令是指將當前棧基地址壓棧後,重新將基地址定位到棧頂,這個含義其實是儲存好當前的基地址,重新開始一個新的棧。由於函式可以調函式,這裡的當前基地址,實際上是上一個函式的棧基地址。例如,在f
函式中的這兩句指令,實際上儲存的是main
函式的棧基地址。
接著來分析兩句:
subl $4, %esp movl $10, (%esp)
對照C程式碼不難發現,這是引數進棧,將立即數10
,儲存到棧頂(esp所指向的記憶體地址是棧頂)。而在f
函式中也可以發現類似的語句:
subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp)
所以,我們可以得出結論是,在呼叫函式前需要把引數逐個壓棧,而壓棧的順序根據筆者的測試是從右向左的。
接著呼叫call
指令,跳轉到f
函式,我們知道call
指令等同於下面的虛擬碼:
pushl %eip+1 movl %eip f
即把call
指令的後一條指令進棧後,將eip
賦值為目標函式的第一個指令地址。這樣做顯而易見:當所呼叫的函式結束後,需要返回當前函式繼續執行,所以必須要儲存下一條指令,否則回來的時候就找不到了。
來到f
函式,首先是儲存main函式的棧基地址,然後需要呼叫g
函式,於是需要引數先進棧:
subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp)
這裡重點思考一下,f
函式是如何獲得main函式傳遞過來的引數的,我們看到
movl 8(%ebp), %eax
為什麼引數是從8(%ebp)
中獲得的呢?我們知道8(%ebp)
表示的是以ebp為基準向棧底回溯8個位元組得到,為什麼是8個位元組呢?
回想一下,在main
函式中完成了引數進棧後做了兩件事情:
由於
call f
指令的作用,call f
下一條指令的地址被壓棧了,這佔用率4
個位元組進入
f
函式後,立即將main
函式的棧基地址進棧了,而且將ebp
靠向了棧頂esp
,這又佔用了4
個位元組
於是透過8(%ebp)
可以找到前一個函式的第一個整型引數的值。
一張圖告訴你怎麼回事:
看過了進入函式,呼叫函式的過程,再看一下函式是如何退出的。觀察main
和f
不難發現,退出函式使用的是如下指令
leave ret
leave
指令相當於如下指令:
movl %ebp, %esp popl %ebp
第一條語句是將
esp
重置到ebp
,可以理解為清空當前函式所使用的棧第二條語句是將棧頂值賦值給
ebp
,並彈出,棧頂值是什麼呢?透過上面的分析不難發現,此時的棧頂值實際上是前一個函式的棧基地址,所以第二條語句的意思就是把ebp
恢復到前一個函式的棧基地址
接著ret
就是相當於,恢復指令指向:
popl %eip
為什麼g函式沒有leave呢?因為g函式內部沒有任何的變數宣告和函式呼叫棧一直都是空的,所以編譯器最佳化了指令
總結
最後,透過這個例子,總結一下函式呼叫的過程:
進入函式:
當前棧基地址壓棧(當前棧基地址實際上是前一個函式的棧基地址)
呼叫其他函式:
引數從右到左進棧
下一條指令地址進棧
退出函式:
棧頂
esp
歸位,回到本函式的ebp
基地址回退到上一個函式的基地址
eip
退回到上一個函式即將要執行的那條語句的地址上
原文連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4550/viewspace-2805587/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 聊聊C語言和指標的本質C語言指標
- C++ 反彙編:關於函式呼叫約定C++函式
- C語言轉寫成MIPS指令集彙編以及MIPS指令集彙編中函式呼叫時棧的變化C語言函式
- C語言函式呼叫棧C語言函式
- 【C語言】函式的概念和函式的呼叫(引數傳遞)C語言函式
- c語言是如何處理函式呼叫的?C語言函式
- 從彙編視角解析函式呼叫中的堆疊運作函式
- AT&T彙編之使用C庫函式函式
- 組合語言-019(彙編程式與c\c++相互呼叫)組合語言C++
- C++中函式呼叫的用法C++函式
- 一次阿里面試後對函式本質的理解阿里面試函式
- C語言的函式C語言函式
- .Net7 CLR的呼叫函式和編譯函式函式編譯
- iOS逆向之旅(基礎篇) — 彙編(四) — 彙編下的函式iOS函式
- C 語言宏 + 內聯彙編實現 MIPS 系統呼叫
- C程式函式呼叫&系統呼叫C程式函式
- C 語言中的 time 函式函式
- C++建構函式和解構函式呼叫虛擬函式時使用靜態聯編C++函式
- 【C進階】26、指標的本質分析指標
- 聊聊C語言和ABAPC語言
- C++學習筆記-C++對C語言的函式擴充C++筆記C語言函式
- C語言中函式printf()和函式scanf()的用法C語言函式
- C語言中qsort函式的用法C語言函式
- C語言函式sscanf()的用法C語言函式
- C語言qsort函式的使用C語言函式
- 外部函式的呼叫函式
- C++:建構函式的分類和呼叫C++函式
- c語言if語句是如何變成彙編程式碼的?C語言
- 如何編寫高質量的函式 -- 敲山震虎篇函式
- C語言中函式的返回值C語言函式
- 淺談C語言中函式的使用C語言函式
- Java-呼叫R語言和呼叫Python(前後端展示)JavaR語言Python後端
- 分析350萬本書110億個詞彙後,演算法揭示出語言中「男女有別」的偏見現象演算法
- 如何編寫高質量的 JS 函式(3) --函數語言程式設計[理論篇]JS函式函數程式設計
- C++ 的函式分檔案編寫C++函式
- 一段js理解nodejs中js如果呼叫c++/c的NodeJSC++
- js 使用 DotNetObjectReference 呼叫 c# 函式JSObjectC#函式
- C++如何解析函式呼叫C++函式