计算机核心三大功能:方法调用、取指、运算
// 一个进行求和运算的汇编程序
main:
//保存调用者栈基地址,并为main()函数分配新栈空间
pushl %ebp
movl %esp, %ebp
subl $32, %esp //分配新栈空间,一共32字节
//初始化两个操作数,一个是5,一个是3
movl $5, 20(%esp)
movl $3, 24(%esp)
//将5和3压栈(参数入栈)
movl $24(%esp), %eax
movl %eax, 4(%esp)
movl 20(%esp), %eax
movl %eax, (%esp)
//调用add函数
calladd
movl %eax, 28(%esp) //得到add函数返回结果
//返回
movl $0, %eax
leave
ret
add:
//保存调用者栈基地址,并为add()函数分配新栈空间
pushl %ebp
mov %esp, %ebp
subl $16, %esp
//获取入参
movl 12(%ebp), %(eax)
movl 8(%ebp), %(edx)
//执行运算
addl %edx, %eax
movl %eax, -4(%ebp)
//返回
movl -4(%ebp), %eax
leave
ret
我们先来了解一下栈空间的分配,在Linux平台上,栈是向下增长的,也就是从内存的高地址向低地址增长,所以每次调用一个新的函数时,新函数的栈顶相对于调用者函数的栈顶,内存地址一定是低方位的。
栈模型.png
//初始化两个操作数,一个是5,一个是3
movl $5, 20(%esp)
movl $3, 24(%esp)
//将5和3压栈(参数入栈)
movl $24(%esp), %eax
movl %eax, 4(%esp)
movl 20(%esp), %eax
movl %eax, (%esp)
完成add参数复制栈布局.png
调用add的堆栈布局.png
//一个简单的带参数求和函数调用
#include
int add(int a, int b);
int main() {
int a = 5;
int b = 3;
int c = add(a, b);
return 0;
}
int add(int a,int b) {
int z = 1 + 2;
return z;
}
//main函数反汇编的代码
int main() {
//参数压栈、分配空间
002D1760 push ebp
002D1761 mov ebp,esp
002D1763 sub esp,0E4h
//以下部分代码不需要注意
002D1769 push ebx
002D176A push esi
002D176B push edi
002D176C lea edi,[ebp-0E4h]
002D1772 mov ecx,39h
002D1777 mov eax,0CCCCCCCCh
002D177C rep stos dword ptr es:[edi]
002D177E mov ecx,offset _F08B5E04_JVM1@cpp (02DC003h)
002D1783 call @__CheckForDebuggerJustMyCode@4 (02D120Dh)
//main函数正式代码部分
int a = 5;
002D1788 mov dword ptr [a],5
int b = 3;
002D178F mov dword ptr [b],3
int c = add(a, b);
002D1796 mov eax,dword ptr [b]
int c = add(a, b);
002D1799 push eax
002D179A mov ecx,dword ptr [a]
002D179D push ecx
002D179E call add (02D1172h)
002D17A3 add esp,8
002D17A6 mov dword ptr [c],eax
return 0;
002D17A9 xor eax,eax
}
//add函数汇编代码
int add(int a,int b) {
//参数压栈、分配空间
002D16F0 push ebp
002D16F1 mov ebp,esp
002D16F3 sub esp,0CCh
//一下部分代码不需要注意
002D16F9 push ebx
002D16FA push esi
002D16FB push edi
002D16FC lea edi,[ebp-0CCh]
002D1702 mov ecx,33h
002D1707 mov eax,0CCCCCCCCh
002D170C rep stos dword ptr es:[edi]
002D170E mov ecx,offset _F08B5E04_JVM1@cpp (02DC003h)
002D1713 call @__CheckForDebuggerJustMyCode@4 (02D120Dh)
//add函数正式代码部分
int z = 1 + 2;
002D1718 mov dword ptr [z],3
return z;
002D171F mov eax,dword ptr [z]
}
//一个简单的求和函数
package cn.leishida;
public class Test {
public static void main(String[] args) {
add(5,8);
}
public static int add(int a,int b) {
int c = a+b;
int d = c + 9;
return d;
}
}
//编译成字节码的内容
Classfile /G:/workspace/JVM/src/cn/leishida/Test.class
Compiled from "Test.java"
public class cn.leishida.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."":()V
#2 = Methodref #3.#16 // cn/leishida/Test.add:(II)I
#3 = Class #17 // cn/leishida/Test
#4 = Class #18 // java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 add
#12 = Utf8 (II)I
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #5:#6 // "":()V
#16 = NameAndType #11:#12 // add:(II)I
#17 = Utf8 cn/leishida/Test
#18 = Utf8 java/lang/Object
{
public cn.leishida.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iconst_5
1: bipush 8
3: invokestatic #2 // Method add:(II)I
6: pop
7: return
LineNumberTable:
line 5: 0
line 6: 7
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: bipush 9
7: iadd
8: istore_3
9: iload_3
10: ireturn
LineNumberTable:
line 8: 0
line 9: 4
line 10: 9
}
SourceFile: "Test.java"
现在我们还不用了解这些字节码的具体含义和作用,但我们知道机器只认识机器码的,所以JVM一定存在一个边界,边界的一边是C,一边是机器码。换言之,C可以直接调用汇编命令。
void (*fun)(int a, int b); //声明一个函数指针
void add(int x,int y); //声明一个函数原型
fun = add //将add函数首地址赋值给fun
#include
int (*addPointer)(int a, int b);
int add(int a, int b);
int main(){
int a = 5;
int b = 3;
addPointer = add;//初始化函数指针,使其指向add首地址
int c = addPointer(a, b);
printf("c = %d\n", c);
return 0;
}
int add(int a, int b) {
int c = a + b;
return c;
}
int *fun(int a,int b);
#include
int* add(int a, int b);
int main() {
int a = 5;
int b = 3;
int* c = add(a, b);
printf("c = %p\n", c);
return 0;
}
int* add(int a, int b) {
int c = a + b;
return &c;
}
call_stub在JVM内部具有举足轻重的意义,这个函数指针正式JVM内部C程序与机器指令的分水岭。JVM在调用这个函数之前,主要执行C程序,而JVM通过这个函数指针调用目标函数之后,就直接进入了机器指令的领域。
//call_stub指针原型:
#define CAST_TO_FN_PTR(func_type,value) ((func_type),(castable_address(value)))
static CallStub call_stub()
{ return CAST_TO_FN_PTR(CallStub, _call_stub_entry); }
宏替换后:
static CallStub call_stub()
{ return (CallStub))(castable_address(_call_stub_entry)); }
在执行函数对应的第一条字节码指令之前,必须经过call_stub函数指针进入对应例程,然后在目标例程中出发对Java主函数第一条字节码指令的调用。
这段代码之前的(CallStub)实际上就是转型,我们先来看一下CallStub的定义
// Calls to Java
typedef void (*CallStub)(
address link,
intptr_t* result,
BasicType result_type,
methodOopDesc* method,
address entry_point,
intptr_t* parameters,
int size_of_parameters,
TRAPS
);
由这段定义我们可以知道,CallStub定义了一个函数指针,他指向一个返回类型是void的,有8个入参的函数。
那么这个函数又是怎么调用的呢?
void JavaCalls::call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS) {
//省略部分代码
// do call
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
StubRoutines::call_stub()(
(address)&link,
// (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
result_val_address, // see NOTE above (compiler problem)
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
result = link.result(); // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
// Preserve oop return value across possible gc points
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)
//省略部分代码...
}
可以看到,JVM直接调用了call_stub并传入了8个入参,可是我们再看call_stub定义的时候是没有入参的,这是为什么呢。
static CallStub call_stub()
{ return (CallStub))(castable_address(_call_stub_entry)); }
//我们将这一行展开
{
//声明一个指针变量
CallStub functionPointer;
//调用castable_address
address returnAddress = castable_address(_call_stub_entry);
functionPointer = (CallStub)returnAddress;
//返回CallStub类型的变量
return functionPointer;
}
实际上,JVM调用的是call_stub()函数所返回的函数指针变量,而CallStub在定义时就规定了这种函数指针类型有8个入参,所以JVM在调用这种类型的函数指针前也必须传入8个入参。
typedef uintptr_t address_word; //unsigned integer which will hold a //pointer,
//except for some implementations of a C++,linkage pointer to function.
//Should never need one of those to be placed in thisype anyway.
inline address_word castable_address(address x)
{ return address_word(x) ; }
而uintptr_t实际上就是无符号整数,我们接着分析call_stub函数,实际上就是下面
static CallStub call_stub()
{ return (CallStub))(unsigned int(_call_stub_entry)); }
所以这个函数的逻辑总体包含两部
可是这个函数指针究竟指向哪里呢?其实在JVM初始化过程中就将_call_stub_entry这一变量指向了某个内存地址,在x86 32位Linux平台上,JVM初始化过程存在这样一条链路,这条链路从JVM的main函数开始,调用到init_global()这个全局数据初始化模块,最后在调用到StubRoutines这个例程生成模块,最终在stubGenerator_x86_32.cpp:generate_initial()函数中对_call_stub_entry变量初始化。如下图
StubRoutines::_call_stub_entry=generate_call_stub(StubRoutines::_call_stub_return_address);
这一句可以说是JVM最核心的功能,不过在分析之前我们先要弄清楚call_stub的入参:
StubRoutines::call_stub()(
(address)&link,
// (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
result_val_address, // see NOTE above (compiler problem)
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
参数名 | 含义 |
---|---|
link | 这是一个连接器 |
result_val_address | 函数返回值地址 |
result_type | 函数返回值类型 |
method() | JVM内部所表示的Java对象 |
entry_point | JVM调用Java方法例程入口,经过这段例程才能跳转到Java字节码对应的机器指令运行 |
args->parameters() | Java方法的入参集合 |
args->size_of_parameters() | Java方法的入参数量 |
CHECK | 当前线程对象 |
连接器link的所属类型是JavaCallWrapper,实际上是在Java函数的调用者与被调用者之间打起了一座桥梁,通过这个桥梁,我们可以实现堆栈追踪,得到整个方法的调用链路。在Java函数调用时,link指针江北保存到当前方法的堆栈中。
是当前Java方法在JVM内部的表示对象,每一个Java方法在被JVM加载时,都会在内部为这个Java方法建立函数模型,保存一份Java方法的全部原始描述信息。包括:
* Java函数的名称,所属类
* Java函数的入参信息,包括入参类型、入参参数名、入参数量、入参顺序等
* Java函数编译后的字节码信息,包括对应的字节码指令、所占用的总字节数等
* Java函数的注解信息
* Java函数的继承信息
* Java函数的返回信息
是继_call_stub_entry例程之后的又一个最主要的例程入口
JVM调用Java函数时通过_call_stub_entry所指向的函数地址最终调用到Java函数。而通过_call_stub_entry所指向的函数调用Java函数之前,必须要先经过entry_point例程。在这个例程里面会真正从method()对象上拿到Java函数编译后的字节码,通过entry_point可以得到Java函数所对应的第一个字节码指令,然后开始调用。
描述Java函数的入参信息,在JVM真正调用Java函数的第一个字节码指令之前,JVM会在CallStub()函数中解析Java函数的入参,然后为Java函数分配堆栈,并将Java函数的入参逐个入栈
Java函数的入参个数。JVM根据这个值计算Java堆栈空间大小。
_call_stub_entry的值是generate_call_stub()函数赋值的,现在我们来分析一下。
address generate_call_stub(address& return_address) {
StubCodeMark mark(this, "StubRoutines", "call_stub");
address start = __ pc();
// stub code parameters / addresses
assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
bool sse_save = false;
const Address rsp_after_call(rbp, -4 * wordSize); // same as in generate_catch_exception()!
const int locals_count_in_bytes (4*wordSize);
const Address mxcsr_save (rbp, -4 * wordSize);
const Address saved_rbx (rbp, -3 * wordSize);
const Address saved_rsi (rbp, -2 * wordSize);
const Address saved_rdi (rbp, -1 * 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); // same as in generate_catch_exception()!
sse_save = UseSSE > 0;
// stub code
__ enter();
__ movptr(rcx, parameter_size); // parameter counter
__ shlptr(rcx, Interpreter::logStackElementSize); // convert parameter count to bytes
__ addptr(rcx, locals_count_in_bytes); // reserve space for register saves
__ subptr(rsp, rcx);
__ andptr(rsp, -(StackAlignmentInBytes)); // Align stack
// save rdi, rsi, & rbx, according to C calling conventions
__ movptr(saved_rdi, rdi);
__ movptr(saved_rsi, rsi);
__ movptr(saved_rbx, rbx);
// save and initialize %mxcsr
if (sse_save) {
Label skip_ldmx;
__ stmxcsr(mxcsr_save);
__ movl(rax, mxcsr_save);
__ andl(rax, MXCSR_MASK); // Only check control and mask bits
ExternalAddress mxcsr_std(StubRoutines::addr_mxcsr_std());
__ cmp32(rax, mxcsr_std);
__ jcc(Assembler::equal, skip_ldmx);
__ ldmxcsr(mxcsr_std);
__ bind(skip_ldmx);
}
// make sure the control word is correct.
__ fldcw(ExternalAddress(StubRoutines::addr_fpu_cntrl_wrd_std()));
#ifdef ASSERT
// make sure we have no pending exceptions
{ Label L;
__ movptr(rcx, thread);
__ cmpptr(Address(rcx, Thread::pending_exception_offset()), (int32_t)NULL_WORD);
__ jcc(Assembler::equal, L);
__ stop("StubRoutines::call_stub: entered with pending exception");
__ bind(L);
}
#endif
// pass parameters if any
BLOCK_COMMENT("pass parameters if any");
Label parameters_done;
__ movl(rcx, parameter_size); // parameter counter
__ testl(rcx, rcx);
__ jcc(Assembler::zero, parameters_done);
// parameter passing loop
Label loop;
// Copy Java parameters in reverse order (receiver last)
// Note that the argument order is inverted in the process
// source is rdx[rcx: N-1..0]
// dest is rsp[rbx: 0..N-1]
__ movptr(rdx, parameters); // parameter pointer
__ xorptr(rbx, rbx);
__ BIND(loop);
// get parameter
__ movptr(rax, Address(rdx, rcx, Interpreter::stackElementScale(), -wordSize));
__ movptr(Address(rsp, rbx, Interpreter::stackElementScale(),
Interpreter::expr_offset_in_bytes(0)), rax); // store parameter
__ increment(rbx);
__ decrement(rcx);
__ jcc(Assembler::notZero, loop);
// call Java function
__ BIND(parameters_done);
__ movptr(rbx, method); // get methodOop
__ movptr(rax, entry_point); // get entry_point
__ mov(rsi, rsp); // set sender sp
BLOCK_COMMENT("call Java function");
__ call(rax);
BLOCK_COMMENT("call_stub_return_address:");
return_address = __ pc();
#ifdef COMPILER2
{
Label L_skip;
if (UseSSE >= 2) {
__ verify_FPU(0, "call_stub_return");
} else {
for (int i = 1; i < 8; i++) {
__ ffree(i);
}
// UseSSE <= 1 so double result should be left on TOS
__ movl(rsi, result_type);
__ cmpl(rsi, T_DOUBLE);
__ jcc(Assembler::equal, L_skip);
if (UseSSE == 0) {
// UseSSE == 0 so float result should be left on TOS
__ cmpl(rsi, T_FLOAT);
__ jcc(Assembler::equal, L_skip);
}
__ ffree(0);
}
__ BIND(L_skip);
}
#endif // COMPILER2
// store result depending on type
// (everything that is not T_LONG, T_FLOAT or T_DOUBLE is treated as T_INT)
__ movptr(rdi, result);
Label is_long, is_float, is_double, exit;
__ movl(rsi, result_type);
__ cmpl(rsi, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(rsi, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(rsi, T_DOUBLE);
__ jcc(Assembler::equal, is_double);
// handle T_INT case
__ movl(Address(rdi, 0), rax);
__ BIND(exit);
// check that FPU stack is empty
__ verify_FPU(0, "generate_call_stub");
// pop parameters
__ lea(rsp, rsp_after_call);
// restore %mxcsr
if (sse_save) {
__ ldmxcsr(mxcsr_save);
}
// restore rdi, rsi and rbx,
__ movptr(rbx, saved_rbx);
__ movptr(rsi, saved_rsi);
__ movptr(rdi, saved_rdi);
__ addptr(rsp, 4*wordSize);
// return
__ pop(rbp);
__ ret(0);
// handle return types different from T_INT
__ BIND(is_long);
__ movl(Address(rdi, 0 * wordSize), rax);
__ movl(Address(rdi, 1 * wordSize), rdx);
__ jmp(exit);
__ BIND(is_float);
// interpreter uses xmm0 for return values
if (UseSSE >= 1) {
__ movflt(Address(rdi, 0), xmm0);
} else {
__ fstp_s(Address(rdi, 0));
}
__ jmp(exit);
__ BIND(is_double);
// interpreter uses xmm0 for return values
if (UseSSE >= 2) {
__ movdbl(Address(rdi, 0), xmm0);
} else {
__ fstp_d(Address(rdi, 0));
}
__ jmp(exit);
return start;
}
这段代码的主要作用就是生成机器码,使用C语言动态生成,弄懂这段指令的逻辑,对理解JVM的字节码执行引擎至关重要。
address start = __ pc();
pc()函数定义:
address pc() const{
return _code_pos;
}
JVM启动过程中,会生成许多例程,例如函数调用、字节码例程、异常处理、函数返回等。每一个例程的开始都是address start = __pc();(JVM的例程都会写入JVM堆内存)。偏移量_code_pos指向上一个例程的最后字节的位置,新的例程开始时会先保存这个起始位置,以便JVM通过CallStub这个函数指针执行这段动态生成的机器指令。
// stub code parameters / addresses
assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
bool sse_save = false;
const Address rsp_after_call(rbp, -4 * wordSize); // same as in generate_catch_exception()!
const int locals_count_in_bytes (4*wordSize);
const Address mxcsr_save (rbp, -4 * wordSize);
const Address saved_rbx (rbp, -3 * wordSize);
const Address saved_rsi (rbp, -2 * wordSize);
const Address saved_rdi (rbp, -1 * 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); // same as in generate_catch_exception()!
sse_save = UseSSE > 0;
一个函数的堆栈空间大体上可以分为3部分:
- 堆栈变量区:
保存方法的局部变量,或者对数据的地址引用(指针),如果没有局部变量,则不分配本区- 入参区域
如果当前方法调用了其他方法,并且传了参数,那么这些入参会保存在调用者堆栈中,就是所谓的压栈- ip和bp区:一个是代码段寄存器,一个是堆栈栈基急促安琪,一个用于恢复调用者方法的代码位置,一个用于恢复调用方法的堆栈位置。这部分前面有提到过
入参 | 位置 |
---|---|
(address)&link:连接器 | 8(%ebp) |
result_val_address:返回地址 | 12(%ebp) |
result_type:返回类型 | 16(%ebp) |
method():方法内部对象 | 20(%ebp) |
entry_point:Java方法调用入口例程 | 24(%ebp) |
parameters():Java方法的入参 | 28(%ebp) |
size_of_parametres():Java方法的入参数量 | 32(%ebp) |
CHECK:当前线程 | 36(%ebp) |
如果按N=4字节来寻址,那我们可以这样写
入参 | 位置 | C++类标记 |
---|---|---|
(address)&link:连接器 | 2N(%ebp) | Address link(rbp, 2N) |
result_val_address:返回地址 | 3N(%ebp) | Address link(rbp, 3N) |
result_type:返回类型 | 4N(%ebp) | Address link(rbp, 4N) |
method():方法内部对象 | 5N(%ebp) | Address link(rbp, 5N) |
entry_point:Java方法调用入口例程 | 6N(%ebp) | Address link(rbp, 6N) |
parameters():Java方法的入参 | 7N(%ebp) | Address link(rbp, 7N) |
size_of_parametres():Java方法的入参数量 | 8N(%ebp) | Address link(rbp, 8N) |
CHECK:当前线程 | 9N(%ebp) | Address link(rbp, 9N) |
如此一来,generate_call_stub函数前面十几行的类声明就不难理解了
const Address mxcsr_save (rbp, -4 * wordSize);
const Address saved_rbx (rbp, -3 * wordSize);
const Address saved_rsi (rbp, -2 * wordSize);
const Address saved_rdi (rbp, -1 * wordSize);
这四个相对于rbp偏移量为负,说明这四个参数位置在CalStub()函数的堆栈内部,而这四个变量用于保存调用者的信息,后面会详细分析。
这部分逻辑从这行代码开始
// stub code
__ enter();
这行代码在不同的硬件平台对应不同的机器指令。
enter()的定义:
push(rbp);
mov(rbp,rsp);
很明显是把rbp压栈的操作,前面已经讲过了。
JVM为了能够调用Java函数,需要在运行期指导一个Java函数的入参大小,然后动态计算出所需要的堆栈空间。由于物理机器不能识别Java程序,因此JVM必然要通过自己作为中间桥梁连接到Java程序,并让Java被调用的函数的堆栈能够计生在JVM的CallStub()函数堆栈。
想要实现寄生在CallStub的函数堆栈中,我们就要对它的空间进行扩展,物理机器为扩展堆栈提供了简单指令:
sub operand,%esp
在JVM内部,一切Java对象实例及其成员变量和成员方法的访问,最终皆通过指针得以寻址,同理,JVM在传递Java函数参数时所传递的也只是Java入参对象实例的指针,而指针的宽度在特定的硬件平台上是一样的。
__ movptr(rcx, parameter_size); // parameter counter
__ shlptr(rcx, Interpreter::logStackElementSize); // convert parameter count to bytes,将参数长度转化为字节,保存到rcx
__ addptr(rcx, locals_count_in_bytes); // reserve space for register saves,保存rdi,rsi,rbx,mxcsr四个寄存器的值
__ subptr(rsp, rcx); //扩展堆栈空间,rcx是之前计算出的参数的字节
__ andptr(rsp, -(StackAlignmentInBytes)); // Align stack内存对齐
至此,JVM完成了动态堆栈内存分配!!
esi、edi、ebx属于调用者函数的私有数据,在发生函数调用之前,调用者函数必须将这些数据保存起来,以便被调用函数执行完毕从新回到调用者函数中时能够正常运行。JVM直接将其保存到了被调用者函数的堆栈中。
// save rdi, rsi, & rbx, according to C calling conventions
__ movptr(saved_rdi, rdi);
__ movptr(saved_rsi, rsi);
__ movptr(saved_rbx, rbx);
//...这部分是保存mxcsr的,属于Intel的SSE技术
// pass parameters if any
BLOCK_COMMENT("pass parameters if any");
Label parameters_done;
__ movl(rcx, parameter_size); // parameter counter参数数量,控制循环次数
__ testl(rcx, rcx); //判断参数数量是否为0,如果是,直接跳过参数处理
__ jcc(Assembler::zero, parameters_done);
// parameter passing loop
Label loop;
// Copy Java parameters in reverse order (receiver last)
// Note that the argument order is inverted in the process
// source is rdx[rcx: N-1..0]
// dest is rsp[rbx: 0..N-1]
__ movptr(rdx, parameters); // parameter pointer,第一个入参地址
__ xorptr(rbx, rbx);
__ BIND(loop);
此时物理寄存器状态:
寄存器名称 | 指向 |
---|---|
edx | parameters首地址 |
ecx | Java函数入参数量 |
现在开始循环将Java函数参数压栈
// get parameter
__ movptr(rax, Address(rdx, rcx, Interpreter::stackElementScale(), -wordSize));
__ movptr(Address(rsp, rbx, Interpreter::stackElementScale(),
Interpreter::expr_offset_in_bytes(0)), rax); // store parameter
__ increment(rbx);
__ decrement(rcx);
__ jcc(Assembler::notZero, loop);
至此,Java函数的3个参数全部被压栈,离函数调用越来越近
前面经过调用者框架栈帧保存(栈基),堆栈动态扩展、现场保存、Java函数参数压栈这一系列处理,JVM终于为Java函数的调用演完前奏,而标志开始的则是entry_point例程。
// call Java function
__ BIND(parameters_done);
__ movptr(rbx, method); // get methodOop
__ movptr(rax, entry_point); // get entry_point
__ mov(rsi, rsp); // set sender sp
BLOCK_COMMENT("call Java function");
__ call(rax);
到目前为止各个参数的位置
参数 | 保存位置 |
---|---|
link | 仍在堆栈中8(%ebp) |
result_val_address | 仍在堆栈中12(%ebp) |
result_type | 仍在堆栈中16(%ebp) |
method | ebx寄存器 |
entry_point | eax寄存器 |
parameters | edx寄存器 |
size_of_parameters | ecx寄存器 |
CHECK | 仍在堆栈中32(%ebp) |
物理寄存器保存的重要信息
寄存器名 | 指向 |
---|---|
edx | parameters首地址 |
ecx | Java函数入参数量 |
ebx | 指向Java函数,即Java函数所对应的的method对象 |
esi | CallStub栈顶 |
eax | entry_point例程入口 |
此时eax已经指向了entry_point例程入口,只需要call一下就可以赚到entry_point例程,去执行entry_point例程
调用完entry_point例程之后会有返回值,CallStub会获取返回值并继续处理。
// store result depending on type
// (everything that is not T_LONG, T_FLOAT or T_DOUBLE is treated as T_INT)
__ movptr(rdi, result);
Label is_long, is_float, is_double, exit;
__ movl(rsi, result_type);
上面的代码存储了结果和结果类型,JVM会将这两个值分别存进edi和esi这两个寄存器,这是基于约定的技术实现方式。
call_stub例程的讲解至此告一段落,置于JVM内部如何从C/C++程序完成Java函数的调用,会在之后讲解完entry_point例程后揭开真相。