CallStub:函數指針直接觸發機器指令

        上篇日誌總結CPU調用函數時的棧內存變化過程,用的C程序解釋成彙編來描述,目的是想說明,JVM在執行Java程序時,函數調用的過程和C程序函數調用的過程是相同的,C作爲靜態編譯型的語言,在程序執行先需要編譯成CPU能直接執行的二進制碼,JVM執行Java程序時也需要先將其解釋成字節碼,或者說字節碼指令集更準確,通常指令集是計算機硬件纔有的東西,在開發語言上包裝一套指令集,好處是可以統一規範,這樣“統一的接口”讓開發者用更加接近人類語言來調用機器指令,想想如果讓你用匯編來寫程序,movl、pop、inc、shl左移右移等,程序可讀性沒那麼高,效率也沒那麼高。

 

JVM字節碼指令

        字節碼指令作爲中間語言,作用是幫助Java語言實現一些如棧操作,入棧出棧,傳參讀參,讀寫局部變化和函數調用等,例如最簡單的控制指令,for循環、foreach循環、do…while循環、if…else和switch等。運算指令集有算術、邏輯、比較和位運算。數據交換指令集,用來操作棧內存、Java堆等,使用數據交換指令來實現數據在這些內存區域裏面的交換,或者說傳遞,函數調用的指令集也在數據交換指令集中。

        因爲JVM本身就是用C和C++共同編寫的解釋性虛擬機,所以在執行Java程序時,最終是JVM交由C語言來運行,也就是說,JVM是一邊將字節碼指令翻譯成C程序,一邊執行,通過C來調用執行機器指令。這就是JVM中模板解釋器的實現思想,爲每一個機器指令編寫一段實現對應功能的彙編代碼,在JVM初始化時,就會將彙編代碼翻譯成機器指令,加載到內存中,當JVM執行某一條Java字節碼指令時,就可以從內存中直接執行對應的彙編代碼,直接跳到該指令的內存地址就可以調用執行。

 

函數指針直接觸發機器指令

        按照上面說的思路,JVM的模板解釋器爲每一個機器指令編寫一段實現對應功能的彙編代碼,在運行某一字節碼指令時,可以直接執行對應的機器指令,具體的實現方式是怎樣的,來看一個例子:

#include <stdio.h>
#include <stdlib.h>

const unsigned char run[] =  
{
	0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x40, 0x53, 0x56, 0x57,
	0x8d, 0x7d, 0xc0, 0xb9, 0x10, 0x00, 0x00, 0x00, 0xb8,
	0xcc, 0xcc, 0xcc, 0xcc, 0xf3, 0xab,
	0x8b, 0x45, 0x08, 0x03, 0x45, 0x0c,
	0x5f, 0x5e, 0x5b, 0x8b, 0xe5, 0x5d, 0xc3
};

int main(int argc, char const* argv[]) 
{
	int a = 4;
	int b = 5;
	int (*add)(int, int); // 定義函數指針
	add = (void*) run; // 函數指針add指向run機器碼

	int result = add(a, b); 
	printf("%d + %d = %d", a, b, result);
	
	return 0;
}

        首先定義一個字符數組run,裏面是一個函數的十六進制表示方式,這些字符組成機器指令,作用是對傳入的兩個數a和b進行求和,並返回結果。接着往下,main()函數裏,還記得C直接操作機器指令的方式嗎,就是用函數指針,通過函數指針變量(一個指針),存放某一段機器指令,在C程序編譯階段,C函數指針直接指向了某一段機器指令的首地址,實現直接調用該機器指令。

      所以在main()函數裏,定義了一個函數指針add,下一行存放了run字符數組的首地址,最後通過add(a, b)來調用,程序執行到int result = add(a, b);時,就會直接將run數組裏的一片連續內存區代碼拿出來執行:

兩種觸發方式

上面的例子,通過函數指針直接觸發機器指令,方式是先定義一個函數指針,函數指針就是一個指針變量,和其他普通變量如int,float,char等一樣,存放的是一個值,指針變量存放的就是首地址,聲明和調用正如上面的例子:

int (*add)(int, int); // 聲明

arr = (void*) run; //存放某一片地址區域

int i = arr(a, b); // 調用

      還有一種方式,就是先聲明其是一種類型,有點像面向對象中的類,首先通過typedef定義一種類型,一種函數指針類型,例如:

typedef (*addType)(int, int);

該語句聲明瞭一種函數指針類型addType,是用戶自定義的一種數據類型,然後就可以通過該類型是聲明一個變量來用:

addType add = (void*) run;

int i = arr(a, b);

      無論是上面哪種聲明,在調用時也有兩種方式觸發機器指令,第一張就是上面都用到的,直接int i = arr(a, b);看似最簡潔明瞭,但是最好還是使用第二種方式:int i = (*add)(a, b);因爲這樣調用可以讓別人一看就知道你使用了函數指針,而不是一個普通函數的顯式調用。

 

call_stub函數指針

JVM中實現函數指針調用機器指令,用的是call_stub,它也是一個函數指針,函數原型如下:

static CallStub call_stub()
{
	return (CallStub)(castable_address(_call_stub_entry));
}

函數的調用結果最終會被類型轉換成CallStub,CallStub是一個自定義類型,函數指針類型,結構如下:

typedef void (*CallStub) {
	address	link,
	intptr_t*	result,
	BasicType	result_type,
	methodOopDesc*	method,
	address	entry_point,
	intptr_t*	parameters,
	int size_of_parameters,
	TRAPS
};

        可以看到一共有8個參數,link表示的是連接器,result是函數返回值的地址,result_type顧名思義就是函數的返回類型,method表示Java方法對象,entry_point這個參數很重要,表示的是JVM調用Java方法的事先定義好的入口,前面說到模板解釋器,在JVM初始化時會將一些方法調用的“入口”代碼編譯成機器指令,加載到內存中,在JVM調用方法時,需要先調用這些入口指令,例如Java程序的主函數必須通過call_stub函數指針來執行。parameters指的是Java方法的參數集合,下一個size_of_parameters自然就是參數的數量了。

 

castable_address()

CallStub結構搞清楚後,回到call_stub的調用語句看:

return (CallStub)(castable_address(_call_stub_entry));

裏面的參數是castable_address,也是一個函數,結構是這樣的:

inline address_word castable_address(address_x) 
{
	return address_word(x);
}

返回類型是address_word,顧名思義是一個地址類型,自定義的地址類型,很容易就能查到它經過了哪些包裝:

typedef uintptr_t       address_word;

typedef unsigned int     uintptr_t;

        可以看到,address_word的最終原型是無符號整型類型unsigned int。最後,就只剩下_call_stub_entry這個參數了它的原封類型也是unsigned int,這些很容易查到它的封裝:

static  address  _call_stub_entry;

        到這裏把call_stub函數指針裏的三個類型都搞明白了,JVM通過call_stub函數指針調用目標函數,call_stub函數相當於一個接口,裏面又調用了castable_address()函數,傳入原封類型是unsigned int的參數_address_stub_entry,標識的就是一個函數的地址。

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