日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

揭示C语言函数调用的本质解析

發布時間:2025/3/8 编程问答 14 豆豆
生活随笔 收集整理的這篇文章主要介紹了 揭示C语言函数调用的本质解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

C語言是面向過程的,而C++是面向對象的C和C++的區別:

C是一個結構化語言,它的重點在于算法和數據結構。C程序的設計首要考慮的是如何通過一個過程,對輸入(或環境條件)進行運算處理得到輸出(或實現過程(事務)控制)。

C++,首要考慮的是如何構造一個對象模型,讓這個模型能夠契合與之對應的問題域,這樣就可以通過獲取對象的狀態信息得到輸出或實現過程(事務)控制。 所以C與C++的最大區別在于它們的用于解決問題的思想方法不一樣。之所以說C++比C更先進,是因為“ 設計這個概念已經被融入到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退回到上一個函數即將要執行的那條語句的地址上

  • 總結

    以上是生活随笔為你收集整理的揭示C语言函数调用的本质解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。