【JVM原始碼解析】虛擬機器解釋執行Java方法(下)

HeapDump效能社群發表於2021-12-10
本文由HeapDump效能社群首席講師鳩摩(馬智)授權整理髮布

第34篇-解析invokeinterface位元組碼指令

與invokevirtual指令類似,當沒有對目標方法進行解析時,需要呼叫LinkResolver::resolve_invoke()函式進行解析,這個函式會呼叫其它一些函式完成方法的解析,如下圖所示。

上圖中粉色的部分與解析invokevirtual位元組碼指令有所區別,resolve_pool()函式及其呼叫的相關函式在介紹invokevirtual位元組碼指令時詳細介紹過,這裡不再介紹。

呼叫LinkResolver::resolve_invokeinterface()函式對位元組碼指令進行解析。函式的實現如下:

void LinkResolver::resolve_invokeinterface(
 CallInfo& result,
 Handle recv,
 constantPoolHandle pool,
 int index, // 指的是常量池快取項的索引
 TRAPS
) {
  KlassHandle resolved_klass;
  Symbol* method_name = NULL;
  Symbol* method_signature = NULL;
  KlassHandle current_klass;
  // 解析常量池時,傳入的引數pool(根據當前棧中要執行的方法找到對應的常量池)和
  // index(常量池快取項的快取,還需要對映為原常量池索引)是有值的,根據這兩個值能夠
  // 解析出resolved_klass和要查詢的方法名稱method_name和方法簽名method_signature
  resolve_pool(resolved_klass, method_name, method_signature, current_klass, pool, index, CHECK);

  KlassHandle recvrKlass (THREAD, recv.is_null() ? (Klass*)NULL : recv->klass());
  resolve_interface_call(result, recv, recvrKlass, resolved_klass, method_name, method_signature, current_klass, true, true, CHECK);
}

我們接著看resolve\_interface\_call()函式的實現,如下:

void LinkResolver::resolve_interface_call(
 CallInfo& result,
 Handle recv,
 KlassHandle recv_klass,
 KlassHandle resolved_klass,
 Symbol* method_name,
 Symbol* method_signature,
 KlassHandle current_klass,
 bool             check_access,
 bool            check_null_and_abstract,
 TRAPS
) {
  methodHandle resolved_method;
  linktime_resolve_interface_method(resolved_method, resolved_klass, method_name, method_signature, current_klass, check_access, CHECK);
  runtime_resolve_interface_method(result, resolved_method, resolved_klass, recv, recv_klass, check_null_and_abstract, CHECK);
}

呼叫2個函式對方法進行解析。首先看linktime\_resolve\_interface_method()函式的實現。

呼叫linktime\_resolve\_interface\_method()函式會呼叫LinkResolver::resolve\_interface_method()函式,此函式的實現如下:

void LinkResolver::resolve_interface_method(
 methodHandle& resolved_method,
 KlassHandle resolved_klass,
 Symbol* method_name,
 Symbol* method_signature,
 KlassHandle current_klass,
 bool          check_access,
 bool          nostatics,
 TRAPS
) {
  // 從介面和父類java.lang.Object中查詢方法,包括靜態方法
  lookup_method_in_klasses(resolved_method, resolved_klass, method_name, method_signature, false, true, CHECK);

  if (resolved_method.is_null()) {
    // 從實現的所有介面中查詢方法
    lookup_method_in_interfaces(resolved_method, resolved_klass, method_name, method_signature, CHECK);
    if (resolved_method.is_null()) {
      // no method found
      // ...
    }
  }

  // ...
}

首先呼叫LinkResolver::lookup\_method\_in_klasses()函式進行方法查詢,在之前介紹過invokevirtual位元組碼指令時介紹過這個函式,不過只介紹了與invokevirtual指令相關的處理邏輯,這裡需要繼續檢視invokeinterface的相關處理邏輯,實現如下: 

void LinkResolver::lookup_method_in_klasses(
 methodHandle& result,
 KlassHandle klass,
 Symbol* name,
 Symbol* signature,
 bool checkpolymorphism,
 // 對於invokevirtual來說,值為false,對於invokeinterface來說,值為true
 bool in_imethod_resolve,
 TRAPS
) {
  Method* result_oop = klass->uncached_lookup_method(name, signature);

  // 在介面中定義方法的解析過程中,忽略Object類中的靜態和非public方法,如
  // clone、finalize、registerNatives
  if (
      in_imethod_resolve &&
      result_oop != NULL &&
      klass->is_interface() &&
      (result_oop->is_static() || !result_oop->is_public()) &&
      result_oop->method_holder() == SystemDictionary::Object_klass() // 方法定義在Object類中
  ) {
    result_oop = NULL;
  }

  if (result_oop == NULL) {
    Array<Method*>* default_methods = InstanceKlass::cast(klass())->default_methods();
    if (default_methods != NULL) {
      result_oop = InstanceKlass::find_method(default_methods, name, signature);
    }
  }
  // ...
  result = methodHandle(THREAD, result_oop);
}

呼叫uncached\_lookup\_method()函式從當前類和父類中查詢,如果沒有找到或找到的是Object類中的不合法方法,則會呼叫find_method()函式從預設方法中查詢。在Java8的新特性中有一個新特性為介面預設方法,該新特性允許我們在介面中新增一個非抽象的方法實現,而這樣做的方法只需要使用關鍵字default修飾該預設實現方法即可。

uncached\_lookup\_method()函式的實現如下:

Method* InstanceKlass::uncached_lookup_method(Symbol* name, Symbol* signature) const {
  Klass* klass = const_cast<InstanceKlass*>(this);
  bool dont_ignore_overpasses = true; 
  while (klass != NULL) {
    Method* method = InstanceKlass::cast(klass)->find_method(name, signature);
    if ((method != NULL) && (dont_ignore_overpasses || !method->is_overpass())) {
      return method;
    }
    klass = InstanceKlass::cast(klass)->super();
    dont_ignore_overpasses = false; // 不要搜尋父類中的overpass方法
  }
  return NULL;
}

從當前類和父類中查詢方法。當從類和父類中查詢方法時,呼叫find\_method()函式,最終呼叫另外一個過載函式find\_method()從InstanceKlass::\_methods屬性中儲存的方法中進行查詢;當從預設方法中查詢方法時,呼叫find\_method()函式從InstanceKlass::\_default\_methods屬性中儲存的方法中查詢。過載的find_method()函式的實現如下:

Method* InstanceKlass::find_method(Array<Method*>* methods, Symbol* name, Symbol* signature) {
  int hit = find_method_index(methods, name, signature);
  return hit >= 0 ? methods->at(hit): NULL;
}

其實呼叫find\_method\_index()函式就是根據二分查詢來找名稱為name,簽名為signature的方法,因為InstanceKlass::\_methods和InstanceKlass::\_default_methods屬性中的方法已經進行了排序,關於這些函式中儲存的方法及如何進行排序在《深入剖析Java虛擬機器:原始碼剖析與例項詳解(基礎卷)》一書中詳細介紹過,這裡不再介紹。

呼叫的LinkResolver::runtime\_resolve\_interface_method()函式的實現如下:

void LinkResolver::runtime_resolve_interface_method(
 CallInfo& result,
 methodHandle resolved_method,
 KlassHandle resolved_klass,
 Handle recv,
 KlassHandle recv_klass,
 bool check_null_and_abstract, // 對於invokeinterface來說,值為false
 TRAPS
) {
  // ...

  methodHandle sel_method;

  lookup_instance_method_in_klasses(
            sel_method, 
            recv_klass,
            resolved_method->name(),
            resolved_method->signature(), 
            CHECK);

  if (sel_method.is_null() && !check_null_and_abstract) {
    sel_method = resolved_method;
  }

  // ...
  // 如果查詢介面的實現時找到的是Object類中的方法,那麼要通過vtable進行分派,所以我們需要
  // 更新的是vtable相關的資訊
  if (!resolved_method->has_itable_index()) {
    int vtable_index = resolved_method->vtable_index();
    assert(vtable_index == sel_method->vtable_index(), "sanity check");
    result.set_virtual(resolved_klass, recv_klass, resolved_method, sel_method, vtable_index, CHECK);
  } else {
    int itable_index = resolved_method()->itable_index();
    result.set_interface(resolved_klass, recv_klass, resolved_method, sel_method, itable_index, CHECK);
  }
}

當沒有itable索引時,通過vtable進行動態分派;否則通過itable進行動態分派。 

呼叫的lookup\_instance\_method\_in\_klasses()函式的實現如下:

void LinkResolver::lookup_instance_method_in_klasses(
 methodHandle& result,
 KlassHandle klass,
 Symbol* name,
 Symbol* signature,
 TRAPS
) {
  Method* result_oop = klass->uncached_lookup_method(name, signature);
  result = methodHandle(THREAD, result_oop);
  // 迴圈查詢方法的實現,不會查詢靜態方法
  while (!result.is_null() && result->is_static() && result->method_holder()->super() != NULL) {
    KlassHandle super_klass = KlassHandle(THREAD, result->method_holder()->super());
    result = methodHandle(THREAD, super_klass->uncached_lookup_method(name, signature));
  }

  // 當從擁有Itable的類或父類中找到介面中方法的實現時,result不為NULL,
  // 否則為NULL,這時候就要查詢預設的方法實現了,這也算是一種實現
  if (result.is_null()) {
    Array<Method*>* default_methods = InstanceKlass::cast(klass())->default_methods();
    if (default_methods != NULL) {
      result = methodHandle(InstanceKlass::find_method(default_methods, name, signature));
    }
  }
}

如上在查詢預設方法實現時會呼叫find_method()函式,此函式在之前介紹invokevirtual位元組碼指令的解析過程時詳細介紹過,這裡不再介紹。

在LinkResolver::runtime\_resolve\_interface\_method()函式的最後有可能呼叫CallInfo::set\_interface()或CallInfo::set\_virtual()函式,呼叫這兩個函式就是將查詢到的資訊儲存到CallInfo例項中。最終會在InterpreterRuntime::resolve\_invoke()函式中根據CallInfo例項中儲存的資訊更新ConstantPoolCacheEntry相關的資訊,如下:

switch (info.call_kind()) {
  // ...
  case CallInfo::itable_call:
    cache_entry(thread)->set_itable_call(
      bytecode,
      info.resolved_method(),
      info.itable_index());
    break;
  default: ShouldNotReachHere();
}

當CallInfo中儲存的是itable的分派資訊時,呼叫set\_itable\_call()函式,這個函式的實現如下:

void ConstantPoolCacheEntry::set_itable_call(
 Bytecodes::Code invoke_code,
 methodHandle method,
 int index
) {
  assert(invoke_code == Bytecodes::_invokeinterface, "");
  InstanceKlass* interf = method->method_holder();
  // interf一定是介面,而method一定是非final方法
  set_f1(interf); // 對於itable,_f1儲存的是表示介面的InstanceKlass
  set_f2(index); // 對於itable,_f2儲存的是itable索引
  set_method_flags(as_TosState(method->result_type()),
                   0, // no option bits
                   method()->size_of_parameters());
  set_bytecode_1(Bytecodes::_invokeinterface);
}

使用CallInfo例項中的資訊更新ConstantPoolCacheEntry中的資訊即可。

第35篇-方法呼叫指令之invokespecial與invokestatic字

這一篇將詳細介紹invokespecial和invokestatic位元組碼指令的彙編實現邏輯

1、invokespecial指令

invokespecial指令的模板定義如下:

def(Bytecodes::_invokespecial , ubcp|disp|clvm|____, vtos, vtos, invokespecial , f1_byte );

生成函式為invokespecial(),生成的彙編程式碼如下:

0x00007fffe1022250: mov %r13,-0x38(%rbp)
0x00007fffe1022254: movzwl 0x1(%r13),%edx
0x00007fffe1022259: mov -0x28(%rbp),%rcx
0x00007fffe102225d: shl $0x2,%edx
0x00007fffe1022260: mov 0x10(%rcx,%rdx,8),%ebx
// 獲取ConstantPoolCacheEntry中indices[b2,b1,constant pool index]中的b1
0x00007fffe1022264: shr $0x10,%ebx
0x00007fffe1022267: and $0xff,%ebx
// 檢查invokespecial=183的bytecode是否已經連線,如果已經連線就進行跳轉
0x00007fffe102226d: cmp $0xb7,%ebx
0x00007fffe1022273: je 0x00007fffe1022312
 
// ... 省略呼叫InterpreterRuntime::resolve_invoke()函式
// 對invokespecial=183的bytecode進行連線,
// 因為位元組碼指令還沒有連線
 
// 將invokespecial x中的x載入到%edx中
0x00007fffe1022306: movzwl 0x1(%r13),%edx
// 將ConstantPoolCache的首地址儲存到%rcx中
0x00007fffe102230b: mov -0x28(%rbp),%rcx
// %edx中儲存的是ConstantPoolCacheEntry項的索引,轉換為字偏移
0x00007fffe102230f: shl $0x2,%edx
 
// 獲取ConstantPoolCache::_f1屬性的值
0x00007fffe1022312: mov 0x18(%rcx,%rdx,8),%rbx 
// 獲取ConstantPoolCache::_flags屬性的值
0x00007fffe1022317: mov 0x28(%rcx,%rdx,8),%edx 
 
 
// 將flags移動到ecx中
0x00007fffe102231b: mov %edx,%ecx
// 從flags中取出引數大小
0x00007fffe102231d: and $0xff,%ecx
// 獲取到recv,%rcx中儲存的是引數大小,最終計算為 %rsp+%rcx*8-0x8,
// flags中的引數大小可能對例項方法來說,已經包括了recv的大小
// 如呼叫例項方法的第一個引數是this(recv)
0x00007fffe1022323: mov -0x8(%rsp,%rcx,8),%rcx 
// 從flags中獲取return type,也就是從_flags的高4位儲存的TosState
0x00007fffe1022328: shr $0x1c,%edx
// 將TemplateInterpreter::invoke_return_entry地址儲存到%r10
0x00007fffe102232b: movabs $0x7ffff73b6380,%r10 
// 找到對應return type的invoke_return_entry的地址
0x00007fffe1022335: mov (%r10,%rdx,8),%rdx 
// 通過invokespecial指令呼叫函式後的返回地址
0x00007fffe1022339: push %rdx 
               
// 空值檢查
0x00007fffe102233a: cmp (%rcx),%rax 
 
// ...
 
// 設定呼叫者棧頂
0x00007fffe102235c: lea 0x8(%rsp),%r13
// 向棧中last_sp的位置儲存呼叫者棧頂
0x00007fffe1022361: mov %r13,-0x10(%rbp)
 
// 跳轉到Method::_from_interpretered_entry入口去執行
0x00007fffe1022365: jmpq *0x58(%rbx)

invokespecial指令在呼叫private和構造方法時,不需要動態分發。在這個位元組碼指令解析完成後,ConstantPoolCacheEntry中的\_f1指向目標方法的Method例項,\_f2沒有使用,所以如上彙編的邏輯非常簡單,這裡不再過多介紹。

2、invokestatic指令

invokestatic指令的模板定義如下:

def(Bytecodes::_invokestatic , ubcp|disp|clvm|____, vtos, vtos, invokestatic , f1_byte);

生成函式為invokestatic(),生成的彙編程式碼如下:

0x00007fffe101c030: mov %r13,-0x38(%rbp)
0x00007fffe101c034: movzwl 0x1(%r13),%edx
0x00007fffe101c039: mov -0x28(%rbp),%rcx
0x00007fffe101c03d: shl $0x2,%edx
0x00007fffe101c040: mov 0x10(%rcx,%rdx,8),%ebx
0x00007fffe101c044: shr $0x10,%ebx
0x00007fffe101c047: and $0xff,%ebx
0x00007fffe101c04d: cmp $0xb8,%ebx
// 檢查invokestatic=184的bytecode是否已經連線,如果已經連線就進行跳轉 
0x00007fffe101c053: je 0x00007fffe101c0f2
 
 
// 呼叫InterpreterRuntime::resolve_invoke()函式對invokestatic=184的
// 的bytecode進行連線,因為位元組碼指令還沒有連線
// ... 省略瞭解析invokestatic的彙編程式碼 
 
// 將invokestatic x中的x載入到%edx中
0x00007fffe101c0e6: movzwl 0x1(%r13),%edx
// 將ConstantPoolCache的首地址儲存到%rcx中
0x00007fffe101c0eb: mov -0x28(%rbp),%rcx
// %edx中儲存的是ConstantPoolCacheEntry項的索引,轉換為字偏移
0x00007fffe101c0ef: shl $0x2,%edx
 
 
// 獲取ConstantPoolCache::_f1屬性的值
0x00007fffe101c0f2: mov 0x18(%rcx,%rdx,8),%rbx
// 獲取ConstantPoolCache::_flags屬性的值
0x00007fffe101c0f7: mov 0x28(%rcx,%rdx,8),%edx
 
 
// 從flags中獲取return type,也就是從_flags的高4位儲存的TosState
0x00007fffe101c0fb: shr $0x1c,%edx
// 將TemplateInterpreter::invoke_return_entry地址儲存到%r10
0x00007fffe101c0fe: movabs $0x7ffff73b5d00,%r10
// 找到對應return type的invoke_return_entry的地址
0x00007fffe101c108: mov (%r10,%rdx,8),%rdx
// 通過invokespecial指令呼叫函式後的返回地址
0x00007fffe101c10c: push %rdx
 
 
// 設定呼叫者棧頂
0x00007fffe101c10d: lea 0x8(%rsp),%r13
// 向棧中last_sp的位置儲存呼叫者棧頂
0x00007fffe101c112: mov %r13,-0x10(%rbp)
 
// 跳轉到Method::_from_interpretered_entry入口去執行
0x00007fffe101c116: jmpq *0x58(%rbx)

invokespecial指令在呼叫靜態方法時,不需要動態分發。在這個位元組碼指令解析完成後,ConstantPoolCacheEntry中的\_f1指向目標方法的Method例項,\_f2沒有使用,所以如上彙編的邏輯非常簡單,這裡不再過多介紹。

關於invokestatic與invokespecial的解析過程這裡就不再過多介紹了,有興趣的可從LinkResolver::resolve_invoke()函式檢視具體的解析過程。

第36篇-方法返回指令之return

方法返回的位元組碼相關指令如下表所示。 
image.png

模板定義如下:

def(Bytecodes::_ireturn , ____|disp|clvm|____, itos, itos, _return , itos );
def(Bytecodes::_lreturn , ____|disp|clvm|____, ltos, ltos, _return , ltos );
def(Bytecodes::_freturn , ____|disp|clvm|____, ftos, ftos, _return , ftos );
def(Bytecodes::_dreturn , ____|disp|clvm|____, dtos, dtos, _return , dtos );
def(Bytecodes::_areturn , ____|disp|clvm|____, atos, atos, _return , atos );
def(Bytecodes::_return , ____|disp|clvm|____, vtos, vtos, _return , vtos );

def(Bytecodes::_return_register_finalizer , ____|disp|clvm|____, vtos, vtos, _return , vtos );

生成函式都為TemplateTable::\_return()。但是如果是Object物件的構造方法中的return指令,那麼這個指令還可能會被重寫為\_return\_register\_finalizer指令。

生成的return位元組碼指令對應的彙編程式碼如下: 

第1部分

// 將JavaThread::do_not_unlock_if_synchronized屬性儲存到%dl中
0x00007fffe101b770: mov 0x2ad(%r15),%dl
// 重置JavaThread::do_not_unlock_if_synchronized屬性值為false
0x00007fffe101b777: movb $0x0,0x2ad(%r15)

// 將Method*載入到%rbx中
0x00007fffe101b77f: mov -0x18(%rbp),%rbx
// 將Method::_access_flags載入到%ecx中
0x00007fffe101b783: mov 0x28(%rbx),%ecx
// 檢查Method::flags是否包含JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test $0x20,%ecx
// 如果方法不是同步方法,跳轉到----unlocked----
0x00007fffe101b78c: je 0x00007fffe101b970


// 如果在%dl暫存器中儲存的_do_not_unlock_if_synchronized的值不為0,
// 則跳轉到no_unlock,表示不要釋放和鎖相關的資源 
0x00007fffe101b792: test $0xff,%dl 
0x00007fffe101b795: jne 
0x00007fffe101ba90 // 跳轉到----no_unlock----處

在JavaThread類中定義了一個屬性\_do\_not\_unlock\_if\_synchronized,這個值表示在丟擲異常的情況下不要釋放receiver(在非靜態方法呼叫的情況下,我們總是會將方法解析到某個物件上,這個物件就是這裡的receiver,也可稱為接收者),此值僅在解釋執行的情況下才會起作用。初始的時候會初始化為false。在如上彙編中可以看到,當\_do\_not\_unlock\_if\_synchronized的值為true時,表示不需要釋放receiver,所以雖然當前是同步方法,但是卻直接呼叫到了no_unlock處。

第2部分

如果執行如下彙編程式碼,則表示%dl暫存器中儲存的\_do\_not\_unlock\_if_synchronized的值為0,需要執行釋放鎖的操作。

// 將之前位元組碼指令執行的結果儲存到表示式棧頂,
// 由於return不需要返回執行結果,所以不需要設定返回值等資訊,
// 最終在這裡沒有生成任何push指令

// 將BasicObjectLock儲存到%rsi中,由於%rsi在呼叫C++函式時可做為
// 第2個引數傳遞,所以如果要呼叫unlock_object就可以傳遞此值
0x00007fffe101b79b: lea -0x50(%rbp),%rsi

// 獲取BasicObjectLock::obj屬性地址儲存到%rax中
0x00007fffe101b79f: mov 0x8(%rsi),%rax 

// 如果不為0,則跳轉到unlock處,因為不為0,表示
// 這個obj有指向的鎖物件,需要進行釋放鎖的操作
0x00007fffe101b7a3: test %rax,%rax
0x00007fffe101b7a6: jne 0x00007fffe101b8a8 // 跳轉到----unlock----處

// 如果是其它的return指令,則由於之前通過push指令將結果儲存在
// 表示式棧上,所以現在可通過pop將表示式棧上的結果彈出到對應暫存器中

第1個指令的-0x50(%rbp)指向了第1個BasicObjectLock物件,其中的sizeof(BasicObjectLock)的值為16,也就是16個位元組。在之前我們介紹棧幀的時候介紹過Java解釋棧的結構,如下:

假設當前的棧幀中有2個鎖物件,則會在棧幀中儲存2個BasicObjectLock物件,BasicObjectLock中有2個屬性,\_lock和\_obj,分別佔用8位元組。佈局如下圖所示。

由於return位元組碼指令負責要釋放的是加synchronized關鍵字的、解釋執行的Java方法,所以為synchronized關鍵字建立的第1個鎖物件儲存在離當前棧幀最靠近棧底的地方,也就是上圖中灰色部分,而其它鎖物件我們暫時不用管。灰色部分表示的BasicObjectLock的地址通過-0x50(%rbp)就能獲取到,然後對其中的\_lock和\_obj屬性進行操作。

由於現在還沒有介紹鎖相關的知識,所以這裡不做過多介紹,在後面介紹完鎖相關知識後還會詳細介紹。 

第3部分

在變數throw\_monitor\_exception為true的情況下,通過呼叫call\_VM()函式生成丟擲鎖狀態異常的彙編程式碼,這些彙編程式碼主要是為了執行C++函式InterpreterRuntime::throw\_illegal\_monitor\_state\_exception()。完成執行後還會執行由should\_not\_reach\_here()函式生成的彙編程式碼。

在變數throw\_monitor\_exception為false並且install\_monitor\_exception為true的情況下,通過呼叫call\_VM()函式生成彙編程式碼來執行C++函式InterpreterRuntime::new\_illegal\_monitor\_state_exception()。最後跳轉到unlocked處執行。

第4部分

在InterpreterMacroAssembler::remove\_activation()函式中,bind完unlock後就會呼叫InterpreterMacroAssembler::unlock\_object()函式生成如下的彙編程式碼。InterpreterMacroAssembler::unlock_object()函式的作用如下:

Unlocks an object. Used in monitorexit bytecode and remove_activation. Throws an IllegalMonitorException if object is not locked by current thread.

生成的彙編程式碼如下:

// **** unlock ****

// ============呼叫InterpreterMacroAssembler::unlock_object()函式生成如下的彙編程式碼==================

// 將%r13儲存到棧中,防止異常破壞了%r13暫存器中的值
0x00007fffe101b8a8: mov %r13,-0x38(%rbp)

// 將BasicObjectLock::_lock的地址儲存到%rax暫存器中
0x00007fffe101b8ac: lea (%rsi),%rax
// 將BasicObjectLock::_obj儲存到%rcx暫存器中
0x00007fffe101b8af: mov 0x8(%rsi),%rcx

// 將BasicObjectLock::_obj的值設定為NULL,表示釋放鎖操作
0x00007fffe101b8b3: movq $0x0,0x8(%rsi)

// ----------當UseBiasedLocking的值為true時,呼叫MacroAssembler::biased_locking_exit()生成如下的彙編程式碼------------
// 從BasicObjectLock::_obj物件中取出mark屬性值並相與
0x00007fffe101b8bb: mov (%rcx),%rdx
0x00007fffe101b8be: and $0x7,%rdx
// 如果BasicObjectLock::_obj指向的oop的mark屬性後3位是偏向鎖的狀態,則跳轉到---- done ----
0x00007fffe101b8c2: cmp $0x5,%rdx
0x00007fffe101b8c6: je 0x00007fffe101b96c
// ------------------------結束呼叫MacroAssembler::biased_locking_exit()生成的彙編程式碼---------------------

// 將BasicObjectLock::_lock這個oop物件的_displaced_header屬性值取出
0x00007fffe101b8cc: mov (%rax),%rdx
// 判斷一下是否為鎖的重入,如果是鎖的重入,則跳轉到---- done ----
0x00007fffe101b8cf: test %rdx,%rdx
0x00007fffe101b8d2: je 0x00007fffe101b96c

// 讓BasicObjectLock::_obj的那個oop的mark恢復為
// BasicObjectLock::_lock中儲存的原物件頭
0x00007fffe101b8d8: lock cmpxchg %rdx,(%rcx)
// 如果為0,則表示鎖的重入,跳轉到---- done ---- ????
0x00007fffe101b8dd: je 0x00007fffe101b96c

// 讓BasicObjectLock::_obj指向oop,這個oop的物件頭已經替換為了BasicObjectLock::_lock中儲存的物件頭
0x00007fffe101b8e3: mov %rcx,0x8(%rsi)

// -----------呼叫call_VM()函式生成彙編程式碼來執行C++函式InterpreterRuntime::monitorexit()----------------
0x00007fffe101b8e7: callq 0x00007fffe101b8f1
0x00007fffe101b8ec: jmpq 0x00007fffe101b96c
0x00007fffe101b8f1: lea 0x8(%rsp),%rax
0x00007fffe101b8f6: mov %r13,-0x38(%rbp)
0x00007fffe101b8fa: mov %r15,%rdi
0x00007fffe101b8fd: mov %rbp,0x200(%r15)
0x00007fffe101b904: mov %rax,0x1f0(%r15)
0x00007fffe101b90b: test $0xf,%esp
0x00007fffe101b911: je 0x00007fffe101b929
0x00007fffe101b917: sub $0x8,%rsp
0x00007fffe101b91b: callq 0x00007ffff66b3d22
0x00007fffe101b920: add $0x8,%rsp
0x00007fffe101b924: jmpq 0x00007fffe101b92e
0x00007fffe101b929: callq 0x00007ffff66b3d22
0x00007fffe101b92e: movabs $0x0,%r10
0x00007fffe101b938: mov %r10,0x1f0(%r15)
0x00007fffe101b93f: movabs $0x0,%r10
0x00007fffe101b949: mov %r10,0x200(%r15)
0x00007fffe101b950: cmpq $0x0,0x8(%r15)
0x00007fffe101b958: je 0x00007fffe101b963
0x00007fffe101b95e: jmpq 0x00007fffe1000420
0x00007fffe101b963: mov -0x38(%rbp),%r13
0x00007fffe101b967: mov -0x30(%rbp),%r14
0x00007fffe101b96b: retq 
// ------------------------結束call_VM()函式呼叫生成的彙編程式碼--------------------------------

// **** done ****

0x00007fffe101b96c: mov -0x38(%rbp),%r13
0x00007fffe101b970: mov -0x40(%rbp),%rsi

// ==========結束呼叫InterpreterMacroAssembler::unlock_object()函式生成如下的彙編程式碼============

第5部分

// 如果是其它的return指令,則由於之前通過push指令將結果儲存在
// 表示式棧上,所以現在可通過pop將表示式棧上的結果彈出到對應暫存器中


// **** unlocked ****
// 在執行這裡的程式碼時,表示當前的棧中沒有相關的鎖,也就是
// 相關的鎖物件已經全部釋放

// **** restart ****
// 檢查一下,是否所有的鎖都已經釋放了

// %rsi指向當前棧中最靠棧頂的BasicObjectLock
0x00007fffe101b970: mov -0x40(%rbp),%rsi
// %rbx指向當前棧中最靠棧底的BasicObjectLock
0x00007fffe101b974: lea -0x40(%rbp),%rbx

// 跳轉到----entry----
0x00007fffe101b978: jmpq 0x00007fffe101ba8b

第6部分

執行如下程式碼,會通過呼叫call\_VM()函式來生成呼叫InterpreterRuntime::throw\_illegal\_monitor\_state_exception()函式的程式碼:

// **** exception ****
// Entry already locked, need to throw exception

// 當throw_monitor_exception的值為true時,執行如下2個函式生成的彙編程式碼:
// 執行call_VM()函式生成的彙編程式碼,就是呼叫C++函式InterpreterRuntime::throw_illegal_monitor_state_exception()
// 執行should_not_reach_here()函式生成的彙編程式碼 

// 當throw_monitor_exception的值為false,執行如下彙編:
// 執行呼叫InterpreterMacroAssembler::unlock_object()函式生成的彙編程式碼
// install_monitor_exception的值為true時,執行call_VM()函式生成的彙編程式碼,就是呼叫C++函式InterpreterRuntime::new_illegal_monitor_state_exception() 
// 無條件跳轉到----restart ----

第7部分

// **** loop ****

// 將BasicObjectLock::obj與NULL比較,如果不相等,則跳轉到----exception----
0x00007fffe101ba79: cmpq $0x0,0x8(%rsi)
0x00007fffe101ba81: jne 0x00007fffe101b97d // 則跳轉到----exception----

第8部分

// **** entry ****

// 0x10為BasicObjectLock,找到下一個BasicObjectLock
0x00007fffe101ba87: add $0x10,%rsi 
// 檢查是否到達了鎖物件儲存區域的底部
0x00007fffe101ba8b: cmp %rbx,%rsi
// 如果不相等,跳轉到loop
0x00007fffe101ba8e: jne 0x00007fffe101ba79 // 跳轉到----loop----

第9部分

// **** no_unlock ****

// 省略jvmti support
 
// 將-0x8(%rbp)處儲存的old stack pointer(saved rsp)取出來放到%rbx中
0x00007fffe101bac7: mov -0x8(%rbp),%rbx

// 移除棧幀
// leave指令相當於:
// mov %rbp, %rsp
// pop %rbp
0x00007fffe101bacb: leaveq 
// 將返回地址彈出到%r13中
0x00007fffe101bacc: pop %r13
// 設定%rsp為呼叫者的棧頂值
0x00007fffe101bace: mov %rbx,%rsp
0x00007fffe101bad1: jmpq *%r13

其中的解釋方法返回地址為return address,由於當前是C++函式呼叫Java,所以這個返回地址其實是C++函式的返回地址,我們不需要考慮。

整個的呼叫轉換如下圖所示。

其中的紅色部分表示終結這個流程。 

在return位元組碼指令中會涉及到鎖釋放的流程,所以上面的流程圖看起來會複雜一些,等我們介紹完鎖相關知識後會再次介紹return指令,這裡不再過多介紹。

第37篇-恢復呼叫者棧幀例程Interpreter::\_invoke\_return_entry

我們在之前介紹過return位元組碼指令的執行邏輯,這個位元組碼指令只會執行釋放鎖和退出當前棧幀的操作,但是當控制權轉移給呼叫者時,還需要恢復呼叫者的棧幀狀態,如讓%r13指向bcp、%r14指向區域性變數表等,另外還需要彈出壓入的實參、跳轉到呼叫者的下一個位元組碼指令繼續執行,而這一切操作都是由Interpreter::\_return\_entry例程負責的。這個例程在之前介紹invokevirtual和invokeinterface等位元組碼指令時介紹過,當使用這些位元組碼指令呼叫方法時,會根據方法的返回型別壓入Interpreter::\_return\_entry一維陣列中儲存的對應例程地址,這樣return位元組碼指令執行完成後就會執行這段例程。

在invokevirtual和invokeinterface等位元組碼指令中通過呼叫如下函式獲取對應的例程入口:

address* TemplateInterpreter::invoke_return_entry_table_for(Bytecodes::Code code) {
  switch (code) {
  case Bytecodes::_invokestatic:
  case Bytecodes::_invokespecial:
  case Bytecodes::_invokevirtual:
  case Bytecodes::_invokehandle:
    return Interpreter::invoke_return_entry_table();
  case Bytecodes::_invokeinterface:
    return Interpreter::invokeinterface_return_entry_table();
  default:
    fatal(err_msg("invalid bytecode: %s", Bytecodes::name(code)));
    return NULL;
  }
}

可以看到invokeinterface位元組碼從Interpreter::\_invokeinterface\_return\_entry陣列中獲取對應的例程,而其它的從Interpreter::\_invoke\_return\_entry一維陣列中獲取。如下:

address TemplateInterpreter::_invoke_return_entry[TemplateInterpreter::number_of_return_addrs];
address TemplateInterpreter::_invokeinterface_return_entry[TemplateInterpreter::number_of_return_addrs];

當返回一維陣列後,會根據方法返回型別進一步確定例程入口地址。下面我們就看一下這些例程的生成過程。 

TemplateInterpreterGenerator::generate\_all()函式中會生成Interpreter::\_return_entry入口,如下:

{
    CodeletMark cm(_masm, "invoke return entry points");
    const TosState states[] = {itos, itos, itos, itos, ltos, ftos, dtos, atos, vtos};
    const int invoke_length = Bytecodes::length_for(Bytecodes::_invokestatic); // invoke_length=3
    const int invokeinterface_length = Bytecodes::length_for(Bytecodes::_invokeinterface); // invokeinterface=5
 
    for (int i = 0; i < Interpreter::number_of_return_addrs; i++) { // number_of_return_addrs = 9
       TosState state = states[i]; // TosState是列舉型別
       Interpreter::_invoke_return_entry[i] = generate_return_entry_for(state, invoke_length, sizeof(u2)); 
       Interpreter::_invokeinterface_return_entry[i] = generate_return_entry_for(state, invokeinterface_length, sizeof(u2));
    }
}

除invokedynamic位元組碼指令外,其它的方法呼叫指令在解釋執行完成後都需要呼叫由generate\_return\_entry\_for()函式生成的例程,生成例程的generate\_return\_entry\_for()函式實現如下:

address TemplateInterpreterGenerator::generate_return_entry_for(TosState state, int step, size_t index_size) {
 
  // Restore stack bottom in case萬一 i2c adjusted stack
  __ movptr(rsp, Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize)); // interpreter_frame_last_sp_offset=-2
  // and NULL it as marker that esp is now tos until next java call
  __ movptr(Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize), (int32_t)NULL_WORD);
 
  __ restore_bcp();
  __ restore_locals();
 
  // ...
 
  const Register cache = rbx;
  const Register index = rcx;
  __ get_cache_and_index_at_bcp(cache, index, 1, index_size);
 
  const Register flags = cache;
  __ movl(flags, Address(cache, index, Address::times_ptr, ConstantPoolCache::base_offset() + ConstantPoolCacheEntry::flags_offset()));
  __ andl(flags, ConstantPoolCacheEntry::parameter_size_mask);
  __ lea(rsp, Address(rsp, flags, Interpreter::stackElementScale()) ); // 棧元素標量為8
  __ dispatch_next(state, step);
 
  return entry;
}

根據state的不同(方法的返回型別的不同),會在選擇執行呼叫者方法的下一個位元組碼指令時,決定要從位元組碼指令的哪個入口處開始執行。我們看一下,當傳遞的state為itos(也就是當方法的返回型別為int時)時生成的彙編程式碼如下:

// 將-0x10(%rbp)儲存到%rsp後,置空-0x10(%rbp)
0x00007fffe1006ce0: mov -0x10(%rbp),%rsp // 更改rsp
0x00007fffe1006ce4: movq $0x0,-0x10(%rbp) // 更改棧中特定位置的值
// 恢復bcp和locals,使%r14指向本地變數表,%r13指向bcp
0x00007fffe1006cec: mov -0x38(%rbp),%r13
0x00007fffe1006cf0: mov -0x30(%rbp),%r14
 // 獲取ConstantPoolCacheEntry的索引並載入到%ecx
0x00007fffe1006cf4: movzwl 0x1(%r13),%ecx 
 
 // 獲取棧中-0x28(%rbp)的ConstantPoolCache並載入到%ecx
0x00007fffe1006cf9: mov -0x28(%rbp),%rbx 
// shl是邏輯左移,獲取字偏移
0x00007fffe1006cfd: shl $0x2,%ecx 
// 獲取ConstantPoolCacheEntry中的_flags屬性值
0x00007fffe1006d00: mov 0x28(%rbx,%rcx,8),%ebx
// 獲取_flags中的低8位中儲存的引數大小
0x00007fffe1006d04: and $0xff,%ebx 
 
// lea指令將地址載入到記憶體暫存器中,也就是恢復呼叫方法之前棧的樣子
0x00007fffe1006d0a: lea (%rsp,%rbx,8),%rsp 
 
// 跳轉到下一指令執行
0x00007fffe1006d0e: movzbl 0x3(%r13),%ebx 
0x00007fffe1006d13: add $0x3,%r13
0x00007fffe1006d17: movabs $0x7ffff73b7ca0,%r10
0x00007fffe1006d21: jmpq *(%r10,%rbx,8)

如上彙編程式碼的邏輯非常簡單,這裡不再過多介紹。  

第38篇-解釋方法之間的呼叫小例項

這一篇我們介紹一下解釋執行的main()方法呼叫解析執行的add()方法的小例項,這個例子如下:

package com.classloading;

public class TestInvokeMethod {
  public int add(int a, int b) {
    return a + b;
  }

  public static void main(String[] args) {
    TestInvokeMethod tim = new TestInvokeMethod();
    tim.add(2, 3);
  }
}

通過Javac編譯器編譯為位元組碼檔案,如下: 

Constant pool:
   #1 = Methodref #5.#16 // java/lang/Object."<init>":()V
   #2 = Class #17 // com/classloading/TestInvokeMethod
   #3 = Methodref #2.#16 // com/classloading/TestInvokeMethod."<init>":()V
   #4 = Methodref #2.#18 // com/classloading/TestInvokeMethod.add:(II)I
   #5 = Class #19 // java/lang/Object
   #6 = Utf8 <init>
   #7 = Utf8 ()V
   #8 = Utf8 Code
   #9 = Utf8 LineNumberTable
  #10 = Utf8 add
  #11 = Utf8 (II)I
  #12 = Utf8 main
  #13 = Utf8 ([Ljava/lang/String;)V
  #14 = Utf8 SourceFile
  #15 = Utf8 TestInvokeMethod.java
  #16 = NameAndType #6:#7 // "<init>":()V
  #17 = Utf8 com/classloading/TestInvokeMethod
  #18 = NameAndType #10:#11 // add:(II)I
  #19 = Utf8 java/lang/Object
{
  public com.classloading.TestInvokeMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new #2 // class com/classloading/TestInvokeMethod
         3: dup
         4: invokespecial #3 // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_2
        10: iconst_3
        11: invokevirtual #4 // Method add:(II)I
        14: pop
        15: return
}

下面分幾部分介紹呼叫相關的內容。

1.C++函式呼叫main()方法

現在我們從位元組碼索引為8的aload_1開始看,此時的棧幀狀態如下:

由於aload\_1的tos\_out為atos,所以在棧頂快取的暫存器中會快取有TestInvokeMethod例項的地址,當執行iconst\_2時,會從atos進入。iconst\_2指令的彙編如下: 

// aep
push %rax
jmpq // 跳轉到下面那條指令執行

// ...

mov $0x2,%eax // 指令的彙編程式碼

由於iconst\_2的tos\_out為itos,所以在進入下一個指令時,會從iconst\_3的tos\_int為itos中進入,如下:  

// iep
push %rax

mov $0x3,%eax

接下來就是執行invokevirtual位元組碼指令了,此時的2已經壓入了表示式棧,而3在%eax暫存器中做為棧頂快取,但是invokevirtual的tos_in為vtos,所以從invokevirtual位元組碼指令的iep進入時會將%eax暫存器中的值也壓入表示式棧中,最終的棧狀態如下圖所示。

 

2.main()方法呼叫add()方法

invokevirtual位元組碼指令在執行時,假設此位元組碼指令已經解析完成,也就是對應的ConstantPoolCacheEntry中已經儲存了方法呼叫相關的資訊,則執行的相關彙編程式碼如下:

0x00007fffe1021f90: mov %r13,-0x38(%rbp) // 將bcp儲存到棧中
// invokevirtual x中取出x,也就是常量池索引儲存到%edx,
// 其實這裡已經是ConstantPoolCacheEntry的index,因為在類的連線
// 階段會對方法中特定的一些位元組碼指令進行重寫
0x00007fffe1021f94: movzwl 0x1(%r13),%edx 
// 將ConstantPoolCache的首地址儲存到%rcx
 
 
0x00007fffe1021f99: mov -0x28(%rbp),%rcx 
 
// 左移2位,因為%edx中儲存的是ConstantPoolCacheEntry索引,左移2位是因為
// ConstantPoolCacheEntry佔用4個字
0x00007fffe1021f9d: shl $0x2,%edx 
        
// 計算%rcx+%rdx*8+0x10,獲取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices
// 因為ConstantPoolCache的大小為0x16位元組,%rcx+0x10定位
// 到第一個ConstantPoolCacheEntry的位置
// %rdx*8算出來的是相對於第一個ConstantPoolCacheEntry的位元組偏移
0x00007fffe1021fa0: mov 0x10(%rcx,%rdx,8),%ebx 
 
// 獲取ConstantPoolCacheEntry中indices[b2,b1,constant pool index]中的b2
0x00007fffe1021fa4: shr $0x18,%ebx 
 
// 取出indices中含有的b2,即bytecode儲存到%ebx中
0x00007fffe1021fa7: and $0xff,%ebx 
 
// 檢視182的bytecode是否已經連線 
0x00007fffe1021fad: cmp $0xb6,%ebx 
  
// 如果連線就進行跳轉,跳轉到resolved 
0x00007fffe1021fb3: je 0x00007fffe1022052

我們直接看方法解析後的邏輯實現,如下:

// **** resolved ****
// resolved的定義點,到這裡說明invokevirtual位元組碼已經連線
// 獲取ConstantPoolCacheEntry::_f2,這個欄位只對virtual有意義
// 在計算時,因為ConstantPoolCacheEntry在ConstantPoolCache之後儲存,
// 所以ConstantPoolCache為0x10,而
// _f2還要偏移0x10,這樣總偏移就是0x20
// ConstantPoolCacheEntry::_f2儲存到%rbx
0x00007fffe1022052: mov 0x20(%rcx,%rdx,8),%rbx 
 // ConstantPoolCacheEntry::_flags儲存到%edx
0x00007fffe1022057: mov 0x28(%rcx,%rdx,8),%edx 
 // 將flags移動到ecx中
0x00007fffe102205b: mov %edx,%ecx 
// 從flags中取出引數大小 
0x00007fffe102205d: and $0xff,%ecx 
 
          
// 獲取到recv,%rcx中儲存的是引數大小,最終計算引數所需要的大小為%rsp+%rcx*8-0x8,
// flags中的引數大小對例項方法來說,已經包括了recv的大小
// 如呼叫例項方法的第一個引數是this(recv)
0x00007fffe1022063: mov -0x8(%rsp,%rcx,8),%rcx // recv儲存到%rcx 
 
// 將flags儲存到r13中
0x00007fffe1022068: mov %edx,%r13d 
// 從flags中獲取return type,也就是從_flags的高4位儲存的TosState
0x00007fffe102206b: shr $0x1c,%edx 
 
// 將TemplateInterpreter::invoke_return_entry地址儲存到%r10
0x00007fffe102206e: movabs $0x7ffff73b6380,%r10 
// %rdx儲存的是return type,計算返回地址
// 因為TemplateInterpreter::invoke_return_entry是陣列,
// 所以要找到對應return type的入口地址
0x00007fffe1022078: mov (%r10,%rdx,8),%rdx 
// 向棧中壓入返回地址
0x00007fffe102207c: push %rdx 
 
// 還原ConstantPoolCacheEntry::_flags 
0x00007fffe102207d: mov %r13d,%edx 
// 還原bcp
0x00007fffe1022080: mov -0x38(%rbp),%r13

執行完如上的程式碼後,已經向相關的暫存器中儲存了相關的值。相關的暫存器狀態如下:

rbx: 儲存的是ConstantPoolCacheEntry::_f2屬性的值
rcx: 就是呼叫例項方法時的第一個引數this
rdx: 儲存的是ConstantPoolCacheEntry::_flags屬性的值

棧的狀態如下圖所示。

需要注意的是return address也是一個例程的地址,是TemplateInterpreter::invoke\_return\_entry一維陣列中型別為整數對應的下標儲存的那個地址,因為呼叫add()方法返回的是整數型別。如何得出add()方法的返回型別呢?是從ConstantPoolCacheEntry的_flags的TosState中得出的。

下面繼續看invokevirtual位元組碼指令將要執行的彙編程式碼,如下:

// flags儲存到%eax
0x00007fffe1022084: mov %edx,%eax 
// 測試呼叫的方法是否為final 
0x00007fffe1022086: and $0x100000,%eax 
// 如果不為final就直接跳轉到----notFinal---- 
0x00007fffe102208c: je 0x00007fffe10220c0 
 
// 通過(%rcx)來獲取receiver的值,如果%rcx為空,則會引起OS異常
0x00007fffe1022092: cmp (%rcx),%rax 
 
// 省略統計相關程式碼部分
 
// 設定呼叫者棧頂並儲存
0x00007fffe10220b4: lea 0x8(%rsp),%r13
0x00007fffe10220b9: mov %r13,-0x10(%rbp)
 
// 跳轉到Method::_from_interpretered_entry入口去執行
0x00007fffe10220bd: jmpq *0x58(%rbx)

執行Method::\_from\_interpretered_entry例程,這個例程在之前詳細介紹過,執行完成後會為add()方法建立棧幀,此時的棧狀態如下圖所示。

執行iload\_0與iload\_1指令,由於連續出現了2個iload,所以是\_fast\_iload2,彙編如下:

movzbl  0x1(%r13),%ebx
neg     %rbx
mov     (%r14,%rbx,8),%eax
push    %rax
movzbl  0x3(%r13),%ebx
neg     %rbx
mov     (%r14,%rbx,8),%eax

注意,只有第1個變數壓入了棧,第2個則儲存到%eax中做為棧頂快取。 

呼叫iadd指令,由於tos_in為itos,所以彙編如下:

mov (%rsp),%edx
add    $0x8,%rsp
add    %edx,%eax

最後結果快取在%eax中。 

3.退出add()方法

執行ireturn位元組碼指令進行add()方法的退棧操作。對於例項來說,執行的相關彙編程式碼如下:

// 將JavaThread::do_not_unlock_if_synchronized屬性儲存到%dl中
0x00007fffe101b770: mov 0x2ad(%r15),%dl
// 重置JavaThread::do_not_unlock_if_synchronized屬性值為false
0x00007fffe101b777: movb $0x0,0x2ad(%r15)
 
// 將Method*載入到%rbx中
0x00007fffe101b77f: mov -0x18(%rbp),%rbx
// 將Method::_access_flags載入到%ecx中
0x00007fffe101b783: mov 0x28(%rbx),%ecx
// 檢查Method::flags是否包含JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test $0x20,%ecx
// 如果方法不是同步方法,跳轉到----unlocked----
0x00007fffe101b78c: je 0x00007fffe101b970

unlocked處的彙編實現如下:

// 將-0x8(%rbp)處儲存的old stack pointer(saved rsp)取出來放到%rbx中
0x00007fffe101bac7: mov -0x8(%rbp),%rbx
 
// 移除棧幀
// leave指令相當於:
// mov %rbp, %rsp
// pop %rbp
0x00007fffe101bacb: leaveq 
// 將返回地址彈出到%r13中
0x00007fffe101bacc: pop %r13
// 設定%rsp為呼叫者的棧頂值
0x00007fffe101bace: mov %rbx,%rsp
0x00007fffe101bad1: jmpq *%r13

執行leaveq指令進行退棧操作,此時的棧狀態如下圖所示。

然後我們就要彈出返回地址,跳轉到TemplateInterpreter::invoke\_return\_entry陣列中儲存的相關地址去執行對應的例程了。

4.執行返回例程

對於例項來說,傳遞的state為itos時生成的彙編程式碼如下:

// 將-0x10(%rbp)儲存到%rsp後,置空-0x10(%rbp)
0x00007fffe1006ce0: mov -0x10(%rbp),%rsp // 更改rsp
0x00007fffe1006ce4: movq $0x0,-0x10(%rbp) // 更改棧中特定位置的值
// 恢復bcp和locals,使%r14指向本地變數表,%r13指向bcp
0x00007fffe1006cec: mov -0x38(%rbp),%r13
0x00007fffe1006cf0: mov -0x30(%rbp),%r14
 // 獲取ConstantPoolCacheEntry的索引並載入到%ecx
0x00007fffe1006cf4: movzwl 0x1(%r13),%ecx 

 // 獲取棧中-0x28(%rbp)的ConstantPoolCache並載入到%ecx
0x00007fffe1006cf9: mov -0x28(%rbp),%rbx 
// shl是邏輯左移,獲取字偏移
0x00007fffe1006cfd: shl $0x2,%ecx 
// 獲取ConstantPoolCacheEntry中的_flags屬性值
0x00007fffe1006d00: mov 0x28(%rbx,%rcx,8),%ebx
// 獲取_flags中的低8位中儲存的引數大小
0x00007fffe1006d04: and $0xff,%ebx 

// lea指令將地址載入到記憶體暫存器中,也就是恢復呼叫方法之前棧的樣子
0x00007fffe1006d0a: lea (%rsp,%rbx,8),%rsp 

// 跳轉到下一指令執行
0x00007fffe1006d0e: movzbl 0x3(%r13),%ebx 
0x00007fffe1006d13: add $0x3,%r13
0x00007fffe1006d17: movabs $0x7ffff73b7ca0,%r10
0x00007fffe1006d21: jmpq *(%r10,%rbx,8)

如上的彙編程式碼也是執行的退棧操作,最主要的就是把在呼叫解釋執行方法時壓入的實參從棧中彈出,接著就是執行main()方法中invokevirtual中的下一條指令pop。此時的棧狀態如下圖所示。

需要注意的是,此時的棧頂快取中儲存著呼叫add()方法的執行結果,那麼在跳轉到下一條指令pop時,必須要從pop的iep入口進入,這樣就能正確的執行下去了。 

5.退出main()方法 

當執行pop指令時,會從iep入口進入,執行的彙編程式碼如下:

// iep
push %rax

// ...

add    $0x8,%rsp

由於main()方法呼叫add()方法不需要返回結果,所以對於main()方法來說,這個結果會從main()方法的表示式棧中彈出。下面接著執行return指令,這個指令對應的彙編程式碼如下:

// 將JavaThread::do_not_unlock_if_synchronized屬性儲存到%dl中
0x00007fffe101b770: mov 0x2ad(%r15),%dl
// 重置JavaThread::do_not_unlock_if_synchronized屬性值為false
0x00007fffe101b777: movb $0x0,0x2ad(%r15)

// 將Method*載入到%rbx中
0x00007fffe101b77f: mov -0x18(%rbp),%rbx
// 將Method::_access_flags載入到%ecx中
0x00007fffe101b783: mov 0x28(%rbx),%ecx
// 檢查Method::flags是否包含JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test $0x20,%ecx
// 如果方法不是同步方法,跳轉到----unlocked----
0x00007fffe101b78c: je 0x00007fffe101b970

main()方法為非同步方法,所以跳轉到unlocked,在unlocked邏輯中會執行一些釋放鎖的邏輯,對於我們本例項來說這不重要,我們直接看退棧的操作,如下:

// 將-0x8(%rbp)處儲存的old stack pointer(saved rsp)取出來放到%rbx中
0x00007fffe101bac7: mov -0x8(%rbp),%rbx

// 移除棧幀
// leave指令相當於:
// mov %rbp, %rsp
// pop %rbp
0x00007fffe101bacb: leaveq 
// 將返回地址彈出到%r13中
0x00007fffe101bacc: pop %r13
// 設定%rsp為呼叫者的棧頂值
0x00007fffe101bace: mov %rbx,%rsp
0x00007fffe101bad1: jmpq *%r13

最後的棧狀態如下圖所示。

其中的return address是C++語言的返回地址,接下來如何退出如上的一些棧幀及結束方法就是C++的事兒了。


相關文章