CMM編譯器和C編譯器過程呼叫實現的比較
之前寫了一個cmm的直譯器,並且自己設計實現了函式呼叫。當時設計的時候並沒有看c編譯器是如何實現的,不過今天看了深入理解計算機系統之後,發現我的設計方法和c的還真的特別像,現在就簡單總結一下,因為裡面涉及了很多的彙編知識,所以不懂彙編的童鞋最好先看一下彙編:
首先我想說的是一個最重要的思想,那就是相對定址的思想,如果沒有這個方法,那麼編寫過程呼叫幾乎是不可能的。因為計算機指令是以變數的地址作為運算元的,那麼也就意味著你在生成程式碼的時候要事先把變數的地址都給算出來。但是過程呼叫有時候是不確定的,比如遞迴,你不確定它要呼叫多少次自身才會return,那麼自然而然所產生的區域性比變數的個數也是不確定的,那麼很顯然我們不能在運算元中使用絕對地址。那麼做法就是設定一個基址暫存器,在c中就是ebp,它記錄的是該過程呼叫開始處棧的基地址。有了這個東西,我們只需要確定過程呼叫程式碼中各個變數的相對過程呼叫開始處的地址,那麼不管我們呼叫多少次這個過程的程式碼塊,堆疊會往上成塊增長,而且在過程中的相對地址不會發生改變。
那麼下面我簡單說一下我的設計:
我在每次發生過程呼叫的時候,要把老的ebp儲存在新的棧幀的底部,然後利用回填的技術,將返回地址也儲存在棧幀的底部,這樣每發生一次過程呼叫,棧幀的底部必定是這兩個值。然後新的棧幀指標便是老的ebp的堆疊位置,當然我們更新ebp的時候是通過ADD方法,不斷累加上去的,即將ebp暫存器中的值加上目前相對位置的最大偏移量。
然後再壓入的便是過程的引數了。我直接壓入的是各個值,也就是說所有的過程呼叫都是傳值方式。這也是我設計的侷限性吧。把所有的引數壓入棧中之後,在之後如果在過程中使用它們的時候,便可以檢視符號表中有關函式的引數的資訊,查出它們的引數順序,然後通過計算轉換成它們的相對地址。之後如果有區域性變數宣告的時候,便在該函式的棧幀中繼續分配空間進行儲存。
當遇到return語句的時候,便是進行返回了。返回之前,需要恢復ebp,但是這之前需要先把返回地址儲存到臨時變數區中,否則改變了ebp之後便取不到它的值了,而且還需要將返回值載入到eax中,以便為返回之後的賦值。
我的設計確實可以實現過程呼叫,包括遞迴。下面我們看看C編譯器的設計,我是參考書以及通過檢視反彙編程式碼來感悟的:
首先我們先看一下網上有關過程呼叫的活動記錄的圖示吧:
很顯然,從圖中可以看出c編譯器和我的設計在堆疊儲存結構上是不同的,而且最大的不同是,在發生過程呼叫的時候,它是選擇將被調函式的引數和返回地址儲存在呼叫者的棧幀中的。由於棧幀是從高地址向低地址增長的,這就造成被調過程在訪問引數值的時候,需要將相對地址設定為正值,因為此時ebp已經更新了。而且,C中使用了esp暫存器,用來儲存當前棧頂的絕對地址(按照我的想法是這樣的)。為了更深入地理解這個過程呼叫的過程,我們來看下面的彙編程式碼。
呼叫者:main{
int start = 9;
int stride = start + 2;
char* tmp = extract_message1(start,stride);
}
被調者:char * extract_message1(int start, int stride) {.......return "cao";}
那麼我們通過反彙編器來看看在發生過程呼叫的前後都發生了什麼吧:(與過程呼叫無關的彙編程式碼都已經刪去)
首先在過程呼叫的外部:
00A5183C mov eax,dword ptr [stride]
00A5183F push eax
00A51840 mov ecx,dword ptr [start]
00A51843 push ecx
00A51844 call extract_message1 (0A51122h)
00A51849 add esp,8
00A5184C mov dword ptr [tmp],eax
那麼從這個彙編程式碼中其實已經很清楚了。首先是從右自左地壓入函式的引數。然後通過call指令發生函式呼叫並且跳轉到函式進行執行。返回之後會繼續執行下面的指令,注意這個add指令,其實它的意圖很明顯,相當於釋放引數的空間。但是要注意,其實在add之後我們緊接著還是可以訪問他們的。最後是執行賦值語句,將eax中的返回值移到變數tmp的儲存空間內。
那麼在call指令中都發生了什麼呢?
看下面的彙編程式碼:
00A515A0 push ebp
00A515A1 mov ebp,esp
00A515A3 sub esp,0F0h
00A515A9 push ebx
00A515AA push esi
00A515AB push edi
00A515AC lea edi,[ebp-0F0h]
00A515B2 mov ecx,3Ch
00A515B7 mov eax,0CCCCCCCCh
上面的程式碼就是在call指令開始執行之後所發生的。其實call指令做了兩部分工作,第一部分是將返回地址壓入到棧中,第二部分就是跳轉到過程開始處進行執行。兩部分工作都和pc暫存器相關。那麼接下來就是過程建立的準備工作。從彙編程式碼中可以看出。首先是儲存ebp,然後設定新的ebp,新的值就是當前esp的值。注意我是在ebp中直接更新的,也就是絕對地址是在ebp中不斷更新的,而c中明顯可以看出,它的絕對值的更新是通過esp的,更新ebp是藉助於esp中的絕對地址的。更新過之後,再分配一段空間(我不太清楚是幹嘛的,不過這個時候esp發生改變了,說明棧頂又上移了),然後是儲存被呼叫者儲存暫存器。
這裡簡單說一下呼叫者儲存暫存器和被呼叫者儲存暫存器。呼叫者儲存的意思就是呼叫者在呼叫過程之前都需要把這些暫存器push到自己的棧幀中。也就是說它不確定子過程是否要使用這些個暫存器,所以為了保險都全都push了吧。而被呼叫者儲存暫存器則是呼叫者不用儲存,而在呼叫的過程中如果使用了這些個暫存器的話,那麼就需要push到子過程的棧幀中,再進行覆蓋。在子過程返回之後,需要再次恢復這些值。如果子過程不使用這些暫存器的話,那麼就不必進行棧幀讀寫。所以這種方式可以潛在地減少棧幀讀寫的次數。因此C編譯器採用的後一種方式。按照慣例,在Ia32機器中,呼叫者儲存暫存器有eax,ecx,edx,而被呼叫者儲存暫存器有ebx,esi,edi.所以上面的那三句push語句你應該明白了吧。最後的三條指令我想大概做的是初始化工作吧。暫且不去管它。
那麼當遇到return語句之後,且看下面的彙編:
00A5164E pop edi
00A5164F pop esi
00A51650 pop ebx
00A51651 mov esp,ebp
00A51653 pop ebp
00A51654 ret
可以看出,在恢復被呼叫者儲存暫存器之後,計算機還恢復了ebp的值,pop ebp的效果就是將old ebp儲存到了ebp暫存器中。最後的ret語句,則是將返回地址pop掉,並且將pc的值設定到那即可。
從上面可以看出,其實我設計的思想和c的還是一樣的,都是相對,跳轉,棧幀。所以從這點來看,至少我的設計是正確的。
相關文章
- 編譯器的編譯基本過程編譯
- 編譯器的工作過程和原理編譯
- 編譯器的工作過程編譯
- C++ 編譯過程C++編譯
- 淺談彙編器、編譯器和直譯器編譯
- FreeBSD中的GNU C編譯器--編譯器GCC(轉)編譯GC
- 編譯C++ 程式的過程編譯C++
- C語言_來了解一下GCC編譯器編譯C可執行指令碼的過程C語言GC編譯指令碼
- 編譯器的自展和自舉、交叉編譯編譯
- gcc 編譯器與 clang 編譯器GC編譯
- 編譯過程編譯
- C/C++編譯過程詳解C++編譯
- C程式編譯過程淺析C程式編譯
- C語言編譯全過程C語言編譯
- [譯]iOS編譯器iOS編譯
- 使用C編譯器編寫shellcode編譯
- C/C++—— C++編譯器是如何實現多型C++編譯多型
- 方舟編譯器開源,華為自家開源平臺面世!(附編譯過程)編譯
- Javac編譯過程Java編譯
- 編譯核心過程編譯
- 在c++程式中呼叫被C編譯器編譯後的函式,為什麼要使用extern “C”C++編譯函式
- 從編譯原理看一個直譯器的實現編譯原理
- C++編譯器優化C++編譯優化
- GCC編譯和連結過程GC編譯
- 3- C語言編譯過程C語言編譯
- C語言編譯過程簡介C語言編譯
- 一張圖解析 編譯器編譯流程圖解編譯
- 在C,C++,java和python執行時直譯器和編譯器的區別C++JavaPython編譯
- 簡要介紹編譯器工作過程的11步編譯
- C++11各編譯器支援情況對比C++編譯
- C語言編譯和連結過程簡介C語言編譯
- 翻譯的未來:翻譯機器和譯後編譯編譯
- Linux上安裝GCC編譯器過程(轉)LinuxGC編譯
- GCC編譯過程(預處理->編譯->彙編->連結)GC編譯
- Vue3原始碼分析——編譯模組和編譯器Vue原始碼編譯
- CUDAFORTRAN編譯器編譯
- vue編譯器Vue編譯
- EVC編譯TCPMP的過程編譯TCP