generate_call_stub()保存調用者方法棧

   

目錄

入參位置

物理寄存器Register類

保存調用者方法棧

棧空間對接


       在總結generate_call_stub()保存調用者方法棧過程之前,還是先來回顧下,JVM初始化鏈是怎麼到達該函數的。call_stub()函數裏返回一個CallStub類型的函數指針,由_call_stub_entry這一基本類型轉換而得到,_call_stub_entry在前面的日誌裏說到,它最終是unsigned int類型,存放的是某一內存地址,類型轉換爲CallStub後,也就是call_stub()會將返回值函數指針所存放的地址,標識的是某一個函數,直接調用。_call_stub_entry變量存放的內存地址,在JVM初始化時就會指定好,上一篇日誌中給出的初始化鏈最後,調用了generate_initial()函數,函數內部又調用了generate_call_stub(),該函數就是對_call_stub_entry變量進行初始化。初始化過程中,需要將一些參數進行壓棧,也就是保存到方法棧中,前面的日誌裏說到CallStub需要的參數有8個,連接器link、函數返回地址result_val_address、函數返回類型result_type,Java方法method()和entry_point例程入口等等,從generate_call_stub()函數中我們也看到了初始化參數的壓棧操作。

 

入參位置

      可以看到,在generate_call_stub()初始化時,會對一系列的參數進行定義,之後壓棧,那麼它們的入參位置在哪呢?從它們後面的參數可以看到,link的入參位置在rbp棧基地址寄存器往上偏移2*wordSize的地方,在32位的系統環境下,wordSize表示4個字節,64位系統下表示8個字節,以32位爲例,所以第一個參數link的棧地址是8(%ebp),result棧地址是3*4爲12(%ebp),下面的以此類推,待所有參數初始化好後,它們在棧中的位置就變成了下圖所示:

      像8(%ebp),12(%ebp)這樣的位置標記是彙編語言的方式,在JVM中入參位置用4個字節(32位系統下)爲一個單位長度來表示,所以第一個參數link,彙編表示8(%ebp),JVM表示爲2N(%ebp),result彙編表示12(%ebp),JVM表示爲3N(%ebp),這裏的N表示爲4個字節。最後,CallStub裏的八個參數便依次從2N表示到9N,對應上圖從2*wordSize到9*wordSize。

 

物理寄存器Register類

      從上面的generate_call_stub()函數中可以看到,無論是哪一個參數初始化,最後都是返回Address類型的變量,Address類表示的是一個物理寄存器,是一個C++類,裏面有保存該寄存器的棧基地址和偏移量信息,結構如下:

      Registert類標識一個物理寄存器,用來存放棧基地址,從上面的初始化信息可以看到CallStub裏那幾個參數都傳入rbp棧基地址寄存器,作用是確定參數地址和恢復調用方法的棧位置。第二個參數int型的_disc,標識的就是偏移量,通過rbp寄存器和相對於其的偏移量來確定入參位置,清楚明白。有了這個C++類後,就可以對參數進行尋址了,例如想尋找method參數在棧中的地址,可以直接:

Address position(rbp, 5N);

回到generate_call_stub()函數中,wordSize在JVM中也是有定義的,它的類型是int:

const int wordSize = sizeof(*char);

      這就很好對應了前面說的,在32位操作系統環境下,wordSize大小爲4個字節,因爲一個char類型指針大小爲4,sizeof(*char)便返回4,在64偉系統環境下,char型大小爲8個字節,所以wordSize等於8。從前面的generate_call_stub()函數中看有的參數初始化棧位置是正數*wordSize,有的是負數*wordSize,前面說過,如果根據rbp寄存器來確定棧位置,在寄存器往上偏移的地方,數字爲正,如果是負數*wordSize,則表示在寄存器往下偏移多少個字節的位置。

 

保存調用者方法棧

繼續沿着generate_call_stub()函數往下走,在一系列參數初始化後,下一個要執行的是_entrer()函數

該函數的定義如下:

      看到這裏是不是很熟悉?(雖然我前面日誌沒總結過函數彙編代碼的執行過程,因爲彙編代碼我還不太熟,但是如果你熟悉彙編代碼函數調用過程,就會明白這是怎麼回事)這就是函數調用前會執行的保存調用者棧基地址和重新指定棧基地址的指令,因爲在generate_call_stub()函數中需要調用_entrer()函數,那麼首先進行push(rbp)操作,保存當前調用者函數的棧基位置,接着mov(rbp, rsp),讓rbp寄存器指向rsp寄存器的位置,也就是generate_call_stub()函數的棧頂,這樣就可以往下爲其他調用函數分配棧空間,_enter函數的作用就是這樣,兩條代碼翻譯成彙編指令就是:

push %ebp

mov %esp, %ebp

在每一個函數調用之前,都會執行這兩條指令。

 

棧空間對接

      寄存器指向棧頂位置後,接下來就應該爲被調用者函數分配方法棧空間了,rbp寄存器當前指向了調用者函數的棧頂位置,其實也就指向了被調用者函數的棧底位置:

      接下來爲被調用者分配的方法棧空間,將會“對接”在調用者方法棧棧頂處,也就是從調用者方法棧頂處開始往下爲被調用函數分配棧空間,依靠的彙編指令是sub operand, %sp,該指令爲棧空間擴展容量,例如:

sub $32 %esp

      該彙編指令表示分配大小爲32字節的棧空間,並將esp寄存器指向棧頂位置。這種對接方式,或者可以理解爲從原本的方法棧下擴展了容量,可以讓被調用函數很方便地對調用者函數中壓棧的參數進行入參,因爲有ebp寄存器的存在,還記得它當前指向的位置嗎,就在調用者函數的棧頂,也同時是被調用者函數的棧底處,如果被調用者函數想要拿調用者函數中的參數進行入參,那麼可以通過ebp寄存器來進行定位,獲得調用者函數方法棧中的壓棧參數位置:

movl8(%ebp), %eax

movl12(%ebp), %edx

      拿這兩條指令舉例,8(%ebp)表示取出ebp寄存器往上偏移8個字節位置的數據,保存到eax寄存器中,ebp寄存器此時指向的是調用者函數的棧頂,所以往上偏移就是從自己的方法棧內讀取數據。12(%ebp)同理,表示從ebp寄存器往上偏移12個字節位置處,取出數據,放到edx寄存器中,這兩條指令通常是在被調用者函數裏面的,由於需要入參,取出數據後,被調用者函數便可以那參數進行其他操作,例如如果想把兩者相加,那麼可以執行指令:

addl %eax, %edx

      表示將兩個寄存器內的數據進行相加,結果保存到edx寄存器處,這就是被調用者函數讀參過程,得益於JVM這種對接棧空間的分配方式,調用者和被調用這函數之間可以通過寄存器很好地定位參數的棧位置,然後在兩個函數之間傳遞。

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