_call_stub_entry入口中的pc( )函數

      在總結_call_stub_entry之前,先再次回顧下Java主函數調用必須經過的call_stub()函數,展開後得到的結構如下:

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

      CallStub是自定義類型的函數指針,有八個參數,call_stub()最後返回的就是這麼一個函數指針變量類型。castable_address則是一個函數,接收一個地址類型address變量,並將其轉換爲基本類型unsigned int,最後返回出去:

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

      由此來看,_call_stub_entry是一個address類型,也就是標識的是某個內存地址,CallStub函數指針存放了這一地址後,相當於CallStub函數指針指向了某一個函數的入口,這樣就可以調用函數了。_call_stub_entry在JVM初始化時就已經存放了某一個函數的入口地址,由generate_call_stub()函數完成首地址的生成。

 

初始化鏈路

      JVM在初始化時就要對_call_stub_entry進行內存地址賦值,初始化過程中,從main()函數開始,整個初始化鏈路是這樣的:

java.c:main()
	java_md.c:LoadJavaVM()
	 jni.c:JNI_CreateJavaVM()
	 	Threads.c:create_vm()
			init.c:init_globals()
				StubRoutines.cpp:stubRoutines_initl()
					StubRoutines.cpp:initializel()
						stubGenerator_x86_x32.cpp:StubGenerator_generate()
							stubGenerator_x86_x32.cpp:StubCodeGenerator()
								stubGenerator_x86_x32.cpp:generate_initial()

      可與看到,在初始化鏈路中,stubRoutines_initl()負責例程的初始化,例程在上一篇日誌裏講過,就是JVM初始化時寫好的一些機器指令,爲了實現特定的動作,例如函數調用與返回,異常處理等例程,CallStub函數指針裏也有一個entry_point例程,它就是JVM調用Java方法時的入口,調用Java方法都必須先執行entry_point例程。

      再往後走,鏈路的最後調用了generate_initial()函數,對_call_stub_entry變量進行初始化,來看看generate_initial()函數中都做了哪些事情:

void generate_initial() {
// Generates all stubs and initializes the entry points

// This platform-specific settings are needed by generate_call_stub()
create_control_words();

// entry points that exist in all platforms Note: This is code
// that could be shared among different platforms - however the
// benefit seems to be smaller than the disadvantage of having a
// much more complicated generator structure. See also comment in
// stubRoutines.hpp.

StubRoutines::_forward_exception_entry = generate_forward_exception();

StubRoutines::_call_stub_entry =
  generate_call_stub (StubRoutines::_call_stub_return_address);
  
  //下面源碼省略....
}

      在generate_initial()函數中看到了_call_stub_entry變量,它的初始化是得到genetate_call_stub()函數的返回值,該函數會產生一個首地址,賦值給_call_stub_entry進行初始化,產生首地址的過程比較複雜,一步一步慢慢來看它的源碼,接着是看genetate_call_stub()函數:

address generate_call_stub(address& return_address) {
    StubCodeMark mark(this, "StubRoutines", "call_stub");
    address start = __ pc(); // 當前函數的入口地址
	
	assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
	bool sse_save = false;
	const Address rsp_after_call(rbp, -4 * wordSize);
	
	//下面源碼省略....
}

第二行一個address類型的變量start,通過pc()函數獲得首地址,保存的是當前這個例程的機器碼起始位置。

pc()函數

      來看看pc()函數的結構:

address pc() const {
    return  _code_pos;
}

      返回類型是address,即一個內存地址,對應的是一個例程,JVM會初始化很多例程,每一個例程都是存放在一片連續的內存區域中的,最開始第一個例程的起始位置假設爲0,例程所佔內存爲16個字節,那麼_pc()函數就會返回0給start,JVM初始化第二個例程時,_pc()函數就會返回16,假設例程大小也爲16個字節,那麼結束位置就是32,第三個例程在初始化時_pc()函數得到的返回值就是32,以此類推。

      JVM的進程內存中分有幾個部分,JVM堆,代碼段和數據段等,所有的例程都會放在JVM堆裏,所以JVM在初始化時會創建一個較大的堆內存區域,專門用來存放各種例程。每一個例程佔用一片連續的區域,並且有一個對應的generate()函數,兩個例程之間的內存區域也是相連的,當第一個例程對應的generate()函數執行完後,_code_pos變量的值就會自動增加,大小等於例程的大小,例如16,那麼到第二個例程調用generate()函數時,得到的_pc()返回值就是16。每一個generate()函數中都會有address start = _pc();這段代碼,start得到返回值_code_pos就是上一個例程的偏移量最後的位置,該位置也是下一個例程的開始位置。

 

generate_all_stub()入參

      ok,generate_all_stub()函數繼續往下走,設置完start變量的偏移量後,接下來就是一些變量定義,尋址:

const Address call_wrapper  (rbp, 2  * wordSize);
const Address result        (rbp, 3  * wordSize);
const Address result_type   (rbp, 4  * wordSize);
const Address method        (rbp, 5  * wordSize);
const Address entry_point   (rbp, 6  * wordSize);
const Address parameters    (rbp, 7  * wordSize);
const Address parameter_size(rbp, 8  * wordSize);
const Address thread        (rbp, 9  * wordSize);
const Address r15_save(rbp, r15_off * wordSize);
const Address r14_save(rbp, r14_off * wordSize);
const Address r13_save(rbp, r13_off * wordSize);
const Address r12_save(rbp, r12_off * wordSize);
const Address rbx_save(rbp, rbx_off * wordSize);

      可以看到,result、result_type、method、parameters甚至entry_point,都是前面CallStub函數指針裏的參數,拿其中一行代碼來看,const Address result (rbp, 3 * wordSize);表達的意思是result變量在JVM堆中的位置是 3 * wordSize(%rbp),在JVM爲每一個Java方法分配的棧空間中,可以將其分爲四個部分,存放變量的變量區,存放參數的入參區,ip代碼段寄存器和bp棧基寄存器。

變量區和入參區

      變量區保存的是該Java方法中的一些局部變量,存放的是變量的引用,也就是地址,要注意的是,方法棧空間的變量區不是一定會初始化的,如果調用的Java方法中沒有使用到局部變量,那麼JVM不會分配出變量區。入參區在數據入參的時候用到,假如一個方法中又調用到了另一個方法,而且需要傳入參數,那麼就會將參數壓棧到入參區,入參區存在與調用者的方法棧中,被調用這可以從裏面拿到壓棧後的參數。舉個例子講一下調用者和被調用者的關係,假設main()方法裏調用了run()方法,那麼main()方法就是調用者,run()方法就是被調用者,main()方法壓棧的參數存放在了main()方法的方法棧中,run()方法從main()方法棧中獲取入參,如果run()方法裏面又調用其他方法,那麼其他方法就從run()方法的方法棧中獲取入參。

ip和bp寄存器

      ip是代碼段寄存器,bp是棧的基地址寄存器(或者叫棧基寄存器),這兩個寄存器在函數執行call add指令時就會自動壓棧到棧頂位置,看個例子:

      main()函數中調用run()函數時,會自動將eip和ebp寄存器壓入main()函數的棧頂,ip代碼段寄存器的作用就是爲了讓main()函數在執行完run()函數返回後,能繼續執行main()函數下面的代碼,具體做法就是在執行函數調用時,自動將eip壓棧,待被調用函數執行完成後,eip出棧,恢復調用函數的執行位置。

      bp棧基地址寄存器,作用就十分重要了,涉及到參數獲取,例如一條指令movl8(%ebp) %eax,指的是從ebp寄存器向上偏移8個字節處獲取參數數據,然後放到eax寄存器中。這裏有一些地方需要注意的是,在JVM的棧空間中,內存地址從棧頂開始爲低地址,向上分配,到棧底處爲最高地址,例如還是上面那張圖,main()函數棧底處爲0(%esp),棧頂處爲64(%esp),一共64字節內存空間,這是分配問題。還有一個是尋址問題,JVM在對數據進行尋址使用的是偏移量,用偏移量來確定數據的位置。拿回上面的圖做例子,ebp寄存器的位置就是run()函數的棧底位置,那麼它相對於run()棧底偏移量就是0,可以直接寫成(%ebp),eip寄存器相對於run()函數棧底的位置是偏移了4個字節,所以用4(%ebp)表示,這是數據或變量通過被調用者棧底,也就是run()函數棧底來確定位置,還可以通過調用者的棧頂來確定數據的位置,一樣的,如果取的數據在基準位置(例如ebp)的上方,也就是高地址位,那麼指令前的數字就是正數,例如8(%ebp),意思是當前位置加上8個字節,相對的,如果位置在基準位置的下方,也就是低地址位,那麼指令前的數字就是負數。

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