Java执行引擎工作原理:方法调用
- 方法调用如何实现
- 函数指针和指针函数
- CallStub源码详解
Git链接(有HotSpot源码)
1 方法调用如何实现
计算机核心三大功能:方法调用、取指、运算
1.1 真实机器如何实现方法调用
- 参数入栈。有几个参数就把几个参数入栈,此时入的是调用者自己的栈
- 代码指针(eip)入栈。以便物理机器执行完调用函数之后返回继续执行原指令
- 调用函数的栈基址入栈,为物理机器从被调用者函数返回做准备
- 为调用方法分配栈空间,每个函数都有自己的栈空间。
// 一个进行求和运算的汇编程序
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
完成add参数压栈后的main()函数堆栈布局
//初始化两个操作数,一个是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
- 返回值约定保存在eax寄存器中
- ebp:栈基地址
- esp:栈顶地址
- 相对寻址:28(%esp)相对于栈顶向上偏移28字节
- 方法内的局部变量分配在靠近栈底位置,而传递的参数分配在靠近栈顶的位置
调用add函数时的函数堆栈布局
调用add的堆栈布局.png
- 在调用函数之前,会自动向栈顶压如eip,以便调用结束后可正常执行原程序
- 执行函数调用时,需要手动将ebp入栈
物理机器执行函数调用的主要步骤
- 保存调用者栈基址,当前IP寄存器入栈
- 调用函数时,在x86平台上,参数从右到左依次入栈
- 一个方法所分配的栈空间大小,取决于该方法内部的局部变量空间、为被调用者所传递的入参大小
- 被调用者在接收入参时,从8(%ebp)处开始,往上逐个获得每一个入参参数
- 被调用者将返回的结果保存到eax寄存器中,调用者从该寄存器中获取返回值。
1.2 C语言函数调用
//一个简单的带参数求和函数调用
#include<stdio.h>
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]
}
- 其实这就是物理机器函数调用是的步骤
有参数传递场景下的C程序函数调用机制
- 压栈
- 参数传递顺序
- 读取入参
JVM函数调用机制
//一个简单的求和函数
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."<init>":()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 <init>
#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 // "<init>":()V
#16 = NameAn