在前一篇文章 第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. SQL 小知识笔记

    1.自动生成序列号 select row_number() over(order by field1) as row_number,* from t_table

  2. SpringBoot Redis 2.0.x

    redis的安装 在笔者之前的文章中有介绍redis的安装,不会的可以去看 笔者之前写的文章redis安装 完成安装后如果不熟悉redis的操作,redis官方文档也有基本操作指南,redis基本操作 ...

  3. MySQL 插入中文后,显示为空白

    https://blog.csdn.net/sun_hj_95/article/details/79488583 在MySQL中插入中文后,显示如下: 解决方案: 在my.ini (在MySQL的安装 ...

  4. Spring Boot中文文档(官方文档翻译 基于1.5.2.RELEASE)

    作者:Phillip Webb, Dave Syer, Josh Long, Stéphane Nicoll, Rob Winch, Andy Wilkinson, Marcel Overdijk, ...

  5. XDFZOI 月赛 201905 Sliver

    组题人自己组完过后,才发现自己还是太弱了... T1 简单模拟. 按照游戏规则直接模拟显然是不明智的,所以我们可以像石头剪刀布一样,将判断改变为检验. 同时,我们发现,一共只有48种牌,所以我们可以直 ...

  6. python 10篇 操作mysql

    一.操作数据库 使用pip install pymysql,安装pymysql模块,使用此模块连接MySQL数据库并操作数据库. import pymysql host = 'ip地址' # 链接的主 ...

  7. 团队开发day04

    通过myurl.openConnection()连接一直连接失败,问题解决: 在一般的Java Web程序开发中,我们通常使用localhost或者127.0.0.1来访问本机的Web服务, 但是如果 ...

  8. [009] - JavaSE面试题(九):集合之Set

    第一期:Java面试 - 100题,梳理各大网站优秀面试题.大家可以跟着我一起来刷刷Java理论知识 [009] - JavaSE面试题(九):集合之Set 第1问:List和Set的区别? List ...

  9. [刘阳Java]_CSS数字分页效果

    先给出效果图,见下图.下图主要的完成当鼠标放到分页数字的上会呈现一个变大的效果 实现思路: (1). 使用浮动属性,以便让li元素水平排列. (2).将a元素设置为块级元素,然后设置它们的尺寸. (3 ...

  10. C++中dynamic_cast与static_cast浅析与实例演示

    1. static_cast 1.1 static_cast语法 static_cast< new_type >(expression) 备注:new_type为目标数据类型,expres ...