在前一篇文章 第2篇-JVM虚拟机这样来调用Java主类的main()方法  中我们介绍了在call_helper()函数中通过函数指针的方式调用了一个函数,如下:

StubRoutines::call_stub()(
(address)&link,
result_val_address,
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);

其中调用StubRoutines::call_stub()函数会返回一个函数指针,查清楚这个函数指针指向的函数的实现是我们这一篇的重点。 调用的call_stub()函数的实现如下:

来源:/src/share/vm/runtime/stubRoutines.hpp

static CallStub  call_stub() {
return CAST_TO_FN_PTR(CallStub, _call_stub_entry);
}

call_stub()函数返回一个函数指针,指向依赖于操作系统和cpu架构的特定的方法,原因很简单,要执行native代码,得看看是什么cpu架构以便确定寄存器,看看什么os以便确定ABI。

其中CAST_TO_FN_PTR是宏,具体定义如下:

源代码位置:/src/share/vm/runtime/utilities/globalDefinitions.hpp
#define CAST_TO_FN_PTR(func_type, value) ((func_type)(castable_address(value)))

对call_stub()函数进行宏替换和展开后会变为如下的形式:

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

CallStub的定义如下:

源代码位置:/src/share/vm/runtime/stubRoutines.hpp

typedef void (*CallStub)(
// 连接器
address link,
// 函数返回值地址
intptr_t* result,
//函数返回类型
BasicType result_type,
// JVM内部所表示的Java方法对象
Method* method,
// JVM调用Java方法的例程入口。JVM内部的每一段
// 例程都是在JVM启动过程中预先生成好的一段机器指令。
// 要调用Java方法, 必须经过本例程,
// 即需要先执行这段机器指令,然后才能跳转到Java方法
// 字节码所对应的机器指令去执行
address entry_point,
intptr_t* parameters,
int size_of_parameters,
TRAPS
); 

如上定义了一种函数指针类型,指向的函数声明了8个形式参数。 

在call_stub()函数中调用的castable_address()函数定义在globalDefinitions.hpp文件中,具体实现如下:

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

address_word是一定自定义的类型,在globalDefinitions.hpp文件中的定义如下:

typedef   uintptr_t     address_word;

其中uintptr_t也是一种自定义的类型,在Linux内核的操作系统下使用globalDefinitions_gcc.hpp文件中的定义,具体定义如下:

typedef  unsigned int  uintptr_t;

这样call_stub()函数其实等同于如下的实现形式:

static CallStub call_stub(){
return (CallStub)( unsigned int(_call_stub_entry) );
}

将_call_stub_entry强制转换为unsigned int类型,然后以强制转换为CallStub类型。CallStub是一个函数指针,所以_call_stub_entry应该也是一个函数指针,而不应该是一个普通的无符号整数。  

在call_stub()函数中,_call_stub_entry的定义如下:

address StubRoutines::_call_stub_entry = NULL; 

_call_stub_entry的初始化在在/src/cpu/x86/vm/stubGenerator_x86_64.cpp文件下的generate_initial()函数,调用链如下:

StubGenerator::generate_initial()   stubGenerator_x86_64.cpp
StubGenerator::StubGenerator() stubGenerator_x86_64.cpp
StubGenerator_generate() stubGenerator_x86_64.cpp
StubRoutines::initialize1() stubRoutines.cpp
stubRoutines_init1() stubRoutines.cpp
init_globals() init.cpp
Threads::create_vm() thread.cpp
JNI_CreateJavaVM() jni.cpp
InitializeJVM() java.c
JavaMain() java.c

其中的StubGenerator类定义在src/cpu/x86/vm目录下的stubGenerator_x86_64.cpp文件中,这个文件中的generate_initial()方法会初始化call_stub_entry变量,如下:

StubRoutines::_call_stub_entry = generate_call_stub(StubRoutines::_call_stub_return_address);

现在我们终于找到了函数指针指向的函数的实现逻辑,这个逻辑是通过调用generate_call_stub()函数来实现的。

不过经过查看后我们发现这个函数指针指向的并不是一个C++函数,而是一个机器指令片段,我们可以将其看为C++函数经过C++编译器编译后生成的指令片段即可。在generate_call_stub()函数中有如下调用语句:

__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize);

这两段代码直接生成机器指令,不过为了查看机器指令,我们借助了HSDB工具将其反编译为可读性更强的汇编指令。如下:

push   %rbp
mov %rsp,%rbp
sub $0x60,%rsp 

这3条汇编是非常典型的开辟新栈帧的指令。之前我们介绍过在通过函数指针进行调用之前的栈状态,如下:

那么经过运行如上3条汇编后这个栈状态就变为了如下的状态: 

我们需要关注的就是old %rbp和old %rsp在没有运行开辟新栈帧(CallStub()栈帧)时的指向,以及开辟新栈帧(CallStub()栈帧)时的new %rbp和new %rsp的指向。另外还要注意saved rbp保存的就是old %rbp,这个值对于栈展开非常重要,因为能通过它不断向上遍历,最终能找到所有的栈帧。

下面接着看generate_call_stub()函数的实现,如下:

address generate_call_stub(address& return_address) {
...
address start = __ pc(); const Address rsp_after_call(rbp, rsp_after_call_off * wordSize); const Address call_wrapper (rbp, call_wrapper_off * wordSize);
const Address result (rbp, result_off * wordSize);
const Address result_type (rbp, result_type_off * wordSize);
const Address method (rbp, method_off * wordSize);
const Address entry_point (rbp, entry_point_off * wordSize);
const Address parameters (rbp, parameters_off * wordSize);
const Address parameter_size(rbp, parameter_size_off * wordSize); const Address thread (rbp, thread_off * 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); // 开辟新的栈帧
__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize); // save register parameters
__ movptr(parameters, c_rarg5); // parameters
__ movptr(entry_point, c_rarg4); // entry_point __ movptr(method, c_rarg3); // method
__ movl(result_type, c_rarg2); // result type
__ movptr(result, c_rarg1); // result
__ movptr(call_wrapper, c_rarg0); // call wrapper // save regs belonging to calling function
__ movptr(rbx_save, rbx);
__ movptr(r12_save, r12);
__ movptr(r13_save, r13);
__ movptr(r14_save, r14);
__ movptr(r15_save, r15); const Address mxcsr_save(rbp, mxcsr_off * wordSize);
{
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);
} // ... 省略了接下来的操作
}

其中开辟新栈帧的逻辑我们已经介绍过,下面就是将call_helper()传递的6个在寄存器中的参数存储到CallStub()栈帧中了,除了存储这几个参数外,还需要存储其它寄存器中的值,因为函数接下来要做的操作是为Java方法准备参数并调用Java方法,我们并不知道Java方法会不会破坏这些寄存器中的值,所以要保存下来,等调用完成后进行恢复。

生成的汇编代码如下:

mov      %r9,-0x8(%rbp)
mov %r8,-0x10(%rbp)
mov %rcx,-0x18(%rbp)
mov %edx,-0x20(%rbp)
mov %rsi,-0x28(%rbp)
mov %rdi,-0x30(%rbp)
mov %rbx,-0x38(%rbp)
mov %r12,-0x40(%rbp)
mov %r13,-0x48(%rbp)
mov %r14,-0x50(%rbp)
mov %r15,-0x58(%rbp)
// stmxcsr是将MXCSR寄存器中的值保存到-0x60(%rbp)中
stmxcsr -0x60(%rbp)
mov -0x60(%rbp),%eax
and $0xffc0,%eax // MXCSR_MASK = 0xFFC0
// cmp通过第2个操作数减去第1个操作数的差,根据结果来设置eflags中的标志位。
// 本质上和sub指令相同,但是不会改变操作数的值
cmp 0x1762cb5f(%rip),%eax # 0x00007fdf5c62d2c4
// 当ZF=1时跳转到目标地址
je 0x00007fdf45000772
// 将m32加载到MXCSR寄存器中
ldmxcsr 0x1762cb52(%rip) # 0x00007fdf5c62d2c4

加载完成这些参数后如下图所示。

下一篇我们继续介绍下generate_call_stub()函数中其余的实现。

推荐阅读:

第1篇-关于JVM运行时,开篇说的简单些

第2篇-JVM虚拟机这样来调用Java主类的main()方法

如果有问题可直接评论留言或加作者微信mazhimazh

关注公众号,有HotSpot源码剖析系列文章!

 

第3篇-CallStub新栈帧的创建的更多相关文章

  1. 第6篇-Java方法新栈帧的创建

    在 第2篇-JVM虚拟机这样来调用Java主类的main()方法 介绍JavaCalls::call_helper()函数的实现时提到过如下一句代码: address entry_point = me ...

  2. C语言的函数调用过程(栈帧的创建与销毁)

    从汇编的角度解析函数调用过程 看看下面这个简单函数的调用过程: int Add(int x,int y) { ; sum = x + y; return sum; } int main () { ; ...

  3. 第5篇-调用Java方法后弹出栈帧及处理返回结果

    在前一篇 第4篇-JVM终于开始调用Java主类的main()方法啦 介绍了通过callq调用entry point,不过我们并没有看完generate_call_stub()函数的实现.接下来在ge ...

  4. 第7篇-为Java方法创建栈帧

    在 第6篇-Java方法新栈帧的创建 介绍过局部变量表的创建,创建完成后的栈帧状态如下图所示. 各个寄存器的状态如下所示. // %rax寄存器中存储的是返回地址 rax: return addres ...

  5. Windows x64 栈帧结构

    0x01 前言 Windows 64位下函数调用约定变为了快速调用约定,前4个参数采用rcx.rdx.r8.r9传递,多余的参数从右向左依次使用堆栈传递.本次文章是对于Windows 64位下函数调用 ...

  6. HotSpot的执行引擎-CallStub栈帧

    之前多次提到接触到调用JavaCalls::call()方法来执行Java方法,如: (1)Java主类装载时,调用JavaCalls::call()方法执行的Java方法checkAndLoadMa ...

  7. 对抗栈帧地址随机化/ASLR的两种思路和一些技巧

    栈帧地址随机化是地址空间布局随机化(Address space layout randomization,ASLR)的一种,它实现了栈帧起始地址一定程度上的随机化,令攻击者难以猜测需要攻击位置的地址. ...

  8. X86-64寄存器和栈帧

    简介 通用寄存器可用于传送和暂存数据,也可参与算术逻辑运算,并保存运算结果.除此之外,它们还各自具有一些特殊功能.通用寄存器的长度取决于机器字长,汇编语言程序员必须熟悉每个寄存器的一般用途和特殊用途, ...

  9. CTF必备技能丨Linux Pwn入门教程——调整栈帧的技巧

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

随机推荐

  1. Spring Boot 无侵入式 实现RESTful API接口统一JSON格式返回

    前言 现在我们做项目基本上中大型项目都是选择前后端分离,前后端分离已经成了一个趋势了,所以总这样·我们就要和前端约定统一的api 接口返回json 格式, 这样我们需要封装一个统一通用全局 模版api ...

  2. Linux中ls的用法

    在linux系统中,可以说一切皆文件.文件类型包含:普通文件,目录,字符设备文件,块设备文件,符号链接文件等 我们可以用file这个命令来查看文件的属性: 这里可以看到1.sh是个脚本文件 下面开始介 ...

  3. Redis:Java链接redis单节点千万级别数据 写入,读取 性能测试

    本文是对Redis 单节点,针对不同的数据类型,做插入行测试. 数据总条数为:10058624 环境说明:             Redis 未做任何优化, 单节点    (服务器上, 内存64G) ...

  4. [心得]zookeeper

    1. 什么是zookeeper? 分布式协调服务 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅.负载均衡.命名服务.分布式协调/通知.集群管 ...

  5. Linux守护进程列表/守护进程

      在linux或者unix操作系统中在系统引导的时候会开启很多服务,这些服务就叫做守护进程.为了增加灵活性,root可以选择系统开启的模式,这些模式叫做运行级别,每一种运行级别以一定的方式配置系统. ...

  6. leetcode 数组分成和相等的三个部分

    题目: 给你一个整数数组 A,只有可以将其划分为三个和相等的非空部分时才返回 true,否则返回 false. 形式上,如果可以找出索引 i+1 < j 且满足 (A[0] + A[1] + . ...

  7. 职场人都该了解<荷花定律>

    先看再点赞,给自己一点思考的时间,如果对自己有帮助,微信搜索[程序职场]关注这个执着的职场程序员.我有什么:职场规划指导,技能提升方法,讲不完的职场故事,个人成长经验. 荷花定律 ,听起来很新奇的一个 ...

  8. android开发相关知识笔记

    1.xpage页面打开: openPage(TestFragment.class) openPage("标识") // 页面打开等待结果返回: openPageForResult( ...

  9. 从新建文件夹开始构建ShadowPlay Engine游戏引擎(3)

    本篇序言 各位可能看到博文的名字换了,也就是引擎名字换了,其实是在下想到了一个更棒的名字:皮影戏(ShadowPlay),取这个名字的含义是因为,游戏中的角色(Puppet)不也是由于我们的操作而动起 ...

  10. C++11标准特性的一些理解

    (1)auto 和 decltype 关键字 在C++11之前,auto关键字用来指定存储期(C++98中指的是自动生命周期).在新标准中,它的功能变为类型推断.C++11引入auto关键词与之前C语 ...