第50篇-調用約定(2)
- 2022 年 1 月 7 日
- 筆記
前面已經介紹了解釋執行的Java方法、編譯執行的Java方法和native方法的調用約定。這一篇我們看一下HotSpot VM中輔助實現調用約定的相關函數。
1、SharedRuntime::java_calling_convention()函數
當需要編譯執行Java方法時,會調用SharedRuntime::java_calling_convention()函數,此函數的實現如下:
int SharedRuntime::java_calling_convention(
const BasicType *sig_bt, // sig_bt相當於是數組
VMRegPair *regs,
int total_args_passed,
int is_outgoing // 值一般為false
) {
// Register的類型為RegisterImpl*,而VMReg的類型為VMRegImpl*
// 通過數組來將相關的參數存儲到對應的暫存器上
static const Register INT_ArgReg[Argument::n_int_register_parameters_j] =
{
j_rarg0, // 6
j_rarg1, // 2
j_rarg2, // 1
j_rarg3, // 8
j_rarg4, // 9
j_rarg5 // 7
};
static const XMMRegister FP_ArgReg[Argument::n_float_register_parameters_j] =
{
j_farg0, // 0
j_farg1, // 1
j_farg2, // 2
j_farg3, // 3
j_farg4, // 4
j_farg5, // 5
j_farg6, // 6
j_farg7 // 7
};
// ...
}
在調用如上函數時,會傳入表示Java方法參數的類型數組sig_bt,總的參數數量total_args_passed。我們將要傳遞參數使用的暫存器和棧slot通過regs數組來保存。其中的BasicType枚舉類的定義如下:
enum BasicType {
T_BOOLEAN = 4,
T_CHAR = 5,
T_FLOAT = 6,
T_DOUBLE = 7,
T_BYTE = 8,
T_SHORT = 9,
T_INT = 10,
T_LONG = 11,
T_OBJECT = 12,
T_ARRAY = 13,
T_VOID = 14,
T_ADDRESS = 15, // t_address ret指令用到的表示返回地址的returnAddress類型
T_NARROWOOP = 16, // t_narrowoop
T_METADATA = 17, // t_metadata
T_NARROWKLASS = 18, // t_narrowklass
T_CONFLICT = 19, // t_conflict for stack value type with conflicting contents
T_ILLEGAL = 99 // t_illegal
};
如上枚舉類中定義的類型已經足夠表示Java位元組碼中的任何類型了,所以Java方法中的類型會統一使用如上枚舉類來表示。
VMRegPair類的定義如下:
class VMRegPair {
private:
VMReg _second;
VMReg _first;
public:
void set_bad () {
_second=VMRegImpl::Bad();
_first=VMRegImpl::Bad();
}
void set1 (VMReg v) {
_second=VMRegImpl::Bad(); // 值為-1
_first=v;
}
void set2 (VMReg v) {
_second=v->next(); // 就是v的值加1
_first=v;
}
// ...
}
我們看到了這個類中定義了_first和_second這一對暫存器,這主要是為32位實現考慮的,因為32位在傳遞long或double類型的參數時,需要2個暫存器來完成,一個存儲高32位,一個存儲低32位。對於64位來說,通過只使用_first暫存器就可完成任務。所以我們在討論64位實現時,可不用太在意_second屬性。
VMRegPair中的_first和_second屬性的類型為VMReg。VMReg是VMRegImpl*的別名,定義如下:
typedef VMRegImpl* VMReg;
// 這個類中只有靜態屬性,並且也沒虛函數,所以佔用的記憶體大小為1個字
class VMRegImpl {
private:
enum { BAD = -1 };
static VMReg stack0;
public:
static VMReg as_VMReg(int val, bool bad_ok = false) {
assert(val > BAD || bad_ok, "invalid");
// 一個整數轉換為VMRegImpl*,注意VMReg是VMRegImpl*的別名
return (VMReg) (intptr_t) val;
}
static VMReg stack2reg( int idx ) {
intptr_t x = stack0->value(); // x的值為184
return (VMReg) (intptr_t) (x + idx); // stack0->value()的值為184
}
uintptr_t reg2stack() {
return value() - stack0->value();
}
// ...
}
需要注意的是,stack0是VMReg類型,也就是指針類型,指針類型是可以直接和整數相互轉換的,所以我們通常會在stack0中存儲一個整數。通過判斷這個整數,我們能夠知道,當前的VMRegImpl實例到底代表的是通用暫存器、浮點暫存器還是棧上的位置。
在C/C++函數中,可將整數轉換為指針類型,因為指針類型表示地址,其實地址也是一個數值。舉個例子,如下:
// 定義一個空類a,佔用的記憶體空間大小為1
class a{};
int num = 2;
// 將整數轉換為指針,這是被允許的
a* res = (a*)num;
直接將一個整數轉換為指針類型。但是我們在使用時要記住,這通常不是一個合法的地址。
繼續看函數的實現邏輯:
int SharedRuntime::java_calling_convention(
const BasicType *sig_bt, // sig_bt相當於是數組
VMRegPair *regs,
int total_args_passed,
int is_outgoing
) {
// ...
uint int_args = 0;
uint fp_args = 0;
// 如果暫存器使用完,則多出來的參數需要通過棧來傳遞,這個變數記錄需要的
// slot(這裡為了考慮32位情況,每個slot是4個位元組,所以在64位情況下,
// 每次需要2個slot,所以stk_args每次需要增加2
uint stk_args = 0;
for (int i = 0; i < total_args_passed; i++) {
switch (sig_bt[i]) { // sig_bt[i]的類型為字,當前是64位,8個位元組
case T_BOOLEAN:
case T_CHAR:
case T_BYTE:
case T_SHORT:
case T_INT:
// 當小於6個參數時,參數放在暫存器上,n_int_register_parameters_j=6
if (int_args < Argument::n_int_register_parameters_j) {
VMReg tmp = INT_ArgReg[int_args++]->as_VMReg(); // VMReg是VMRegImpl*類型的別名
regs[i].set1(tmp);
} else { // 放在棧上
VMReg tmp = VMRegImpl::stack2reg(stk_args);
regs[i].set1(tmp);
stk_args += 2;
}
break;
case T_VOID:
// halves of T_LONG or T_DOUBLE
// long和double需要2個slot(這裡的slot為8位元組)
assert(i != 0 && (sig_bt[i - 1] == T_LONG || sig_bt[i - 1] == T_DOUBLE), "expecting half");
regs[i].set_bad();
break;
case T_LONG:
assert(sig_bt[i + 1] == T_VOID, "expecting half");
case T_OBJECT:
case T_ARRAY:
case T_ADDRESS:
if (int_args < Argument::n_int_register_parameters_j) {
VMReg tmp = INT_ArgReg[int_args++]->as_VMReg();
regs[i].set2(tmp);
} else {
VMReg tmp = VMRegImpl::stack2reg(stk_args);
regs[i].set2(tmp);
stk_args += 2;
}
break;
case T_FLOAT:
if (fp_args < Argument::n_float_register_parameters_j) {
VMReg tmp = FP_ArgReg[fp_args++]->as_VMReg();
regs[i].set1(tmp);
} else {
VMReg tmp = VMRegImpl::stack2reg(stk_args);
regs[i].set1(tmp);
stk_args += 2;
}
break;
case T_DOUBLE:
assert(sig_bt[i + 1] == T_VOID, "expecting half");
if (fp_args < Argument::n_float_register_parameters_j) {
VMReg tmp = FP_ArgReg[fp_args++]->as_VMReg();
regs[i].set2(tmp);
} else {
VMReg tmp = VMRegImpl::stack2reg(stk_args);
regs[i].set2(tmp);
stk_args += 2;
}
break;
default:
ShouldNotReachHere();
break;
}
}
return round_to(stk_args, 2);
}
當類型為非浮點數類型時,通過通用暫存器來傳遞。通過通用暫存器傳遞參數時,如果為boolean、byte、short、char和int時,調用VMRegPair::set1()函數,否則調用VMRegPair::set2()函數。對於64位來說,我們不需要關注VMRegPair::_second屬性的值,所以我們只關心_first參數的值即可。
最終會在regs數組中存儲與參數個數相同的VMRegPair個實例,我們總結一下:
(1)當存儲T_BOOLEAN、T_BYTE、T_SHORT、T_CHAR和T_INT時,_first的值小於32,表示使用通用暫存器來傳遞參數;
(2)當存儲T_OBJECT、T_ARRAY、T_ADDRESS和T_LONG類型時,_first的值是仍然小於32,表示使用通用暫存器來傳遞參數;
(3)當存儲T_FLOAT和T_DOUBLE時,_first的值大於等於48,小於148,表示使用浮點暫存器來傳遞參數;
(4)當存儲的_first的值大於等於148時,表示對應的參數需要通過棧來傳遞;
(5)當_first的值為其它時,非法;
只有在暫存器用完後才會通過棧來傳遞參數,所以要通過stk_args來統計需要開闢多大的棧空間。舉個例子如下:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
本地方法共有5個參數,所以可以通過前5個暫存器來傳遞參數。函數入參為:
const int total_args_passed=5 BasicType* sig_bbt=[T_OBJECT,T_INT,T_INT,T_OBJECT,T_INT,T_INT]
最終stk_args為0,而regs的值如下:
VMRegPair* in_regs=[ VMRegPair(_first=6*2,_second=13) // 傳遞的是T_OBJECT VMRegPair(_first=2*2,_second=-1) VMRegPair(_first=1*2,_second=-1) VMRegPair(_first=8*2,_second=17) // 傳遞的是T_OBJECT VMRegPair(_first=9*2,_second=-1) ]
我們可以通過判斷_first的值來區分出浮點類型與其它剩餘類型。由於如上的5個參數都是通過通用暫存器傳遞的,所以_first的值都小於32。
2、SharedRuntime::c_calling_convention()函數
調用的函數的實現如下:
int SharedRuntime::c_calling_convention(
const BasicType *sig_bt,
VMRegPair *regs,
int total_args_passed
){ // 共需要向C傳遞的參數數量
// Register的定義為RegisterImpl*
static const Register INT_ArgReg[Argument::n_int_register_parameters_c] = {
c_rarg0, // 0x7
c_rarg1, // 0x6
c_rarg2, // 0x2
c_rarg3, // 0x1
c_rarg4, // 0x8
c_rarg5 // 0x9
};
static const XMMRegister FP_ArgReg[Argument::n_float_register_parameters_c] = {
c_farg0,
c_farg1,
c_farg2,
c_farg3,
c_farg4,
c_farg5,
c_farg6,
c_farg7
};
uint int_args = 0;
uint fp_args = 0;
uint stk_args = 0; // inc by 2 each time
// 參數優先向暫存器中分配,如果沒有暫存器時再向棧中分配
for (int i = 0; i < total_args_passed; i++) {
switch (sig_bt[i]) {
case T_BOOLEAN:
case T_CHAR:
case T_BYTE:
case T_SHORT:
case T_INT:
if (int_args < Argument::n_int_register_parameters_c) {
VMReg tmp = INT_ArgReg[int_args++]->as_VMReg();
regs[i].set1(tmp);
} else {
VMReg tmp = VMRegImpl::stack2reg(stk_args);
regs[i].set1(tmp);
stk_args += 2;
}
break;
case T_LONG:
assert(sig_bt[i + 1] == T_VOID, "expecting half");
// fall through
case T_OBJECT:
case T_ARRAY:
case T_ADDRESS:
case T_METADATA:
// n_int_register_parameters_c的值為6
if (int_args < Argument::n_int_register_parameters_c) {
VMReg tmp = INT_ArgReg[int_args++]->as_VMReg() ;
regs[i].set2( tmp );
} else {
VMReg tmp = VMRegImpl::stack2reg(stk_args);
regs[i].set2(tmp);
stk_args += 2;
}
break;
case T_FLOAT:
if (fp_args < Argument::n_float_register_parameters_c) {
VMReg tmp = FP_ArgReg[fp_args++]->as_VMReg();
regs[i].set1(tmp);
} else {
VMReg tmp = VMRegImpl::stack2reg(stk_args);
regs[i].set1(tmp);
stk_args += 2;
}
break;
case T_DOUBLE:
assert(sig_bt[i + 1] == T_VOID, "expecting half");
if (fp_args < Argument::n_float_register_parameters_c) {
VMReg tmp =FP_ArgReg[fp_args++]->as_VMReg();
regs[i].set2(tmp);
} else {
VMReg tmp = VMRegImpl::stack2reg(stk_args);
regs[i].set2(tmp);
stk_args += 2;
}
break;
case T_VOID: // Halves of longs and doubles
assert(i != 0 && (sig_bt[i - 1] == T_LONG || sig_bt[i - 1] == T_DOUBLE),
"expecting half");
regs[i].set_bad();
break;
default:
ShouldNotReachHere();
break;
}
}
return stk_args;
}
其實現非常類似於SharedRuntime::java_calling_convention()函數,這裡不再過多介紹。
arraycopy()對應的本地函數的實現如下:
JVM_ENTRY(void, JVM_ArrayCopy(
JNIEnv *env, jclass ignored,
jobject src, jint src_pos,
jobject dst, jint dst_pos,
jint length))
// ...
arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));
// 進行數組的拷貝操作
s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END
共有7個參數,所以在調用本地函數時,需要將1個參數存儲在棧上。入參及計算的最終的regs的值如下:
const int total_args_passed=5 BasicType* sig_bbt=[T_OBJECT,T_INT,T_INT,T_OBJECT,T_INT,T_INT] VMRegPair* regs=[ VMRegPair(_first=6*2,_second=13) // 傳遞的是T_OBJECT VMRegPair(_first=2*2,_second=-1) VMRegPair(_first=1*2,_second=-1) VMRegPair(_first=8*2,_second=17) // 傳遞的是T_OBJECT VMRegPair(_first=9*2,_second=-1) ]
6個值都可以通過通用暫存器傳遞,所以_first的值都小於32。另外還有個整數需要傳遞,所以stk_args的值為2(表示用2個、每個大小為4位元組的slot傳遞整數類型參數)。需要注意的是,對於64位來說,如果要傳遞long和double類型的值,其實也需要2個4位元組大小的slot即可,也就是1個8位元組的slot即可,並不是需要2個8位元組的slot,這是由調用約定規定的。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源程式碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流


