CallStub:函数指针直接触发机器指令

本文介绍了JVM如何通过CallStub和函数指针直接触发机器指令执行。JVM的字节码指令作为中间语言,解释器将字节码转化为C程序调用机器指令。CallStub是一个函数指针类型,用于调用Java方法的预定义入口,通过castable_address()函数获取目标函数地址,实现调用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        上篇日志总结CPU调用函数时的栈内存变化过程,用的C程序解释成汇编来描述,目的是想说明,JVM在执行Java程序时,函数调用的过程和C程序函数调用的过程是相同的,C作为静态编译型的语言,在程序执行先需要编译成CPU能直接执行的二进制码,JVM执行Java程序时也需要先将其解释成字节码,或者说字节码指令集更准确,通常指令集是计算机硬件才有的东西,在开发语言上包装一套指令集,好处是可以统一规范,这样“统一的接口”让开发者用更加接近人类语言来调用机器指令,想想如果让你用汇编来写程序,movl、pop、inc、shl左移右移等,程序可读性没那么高,效率也没那么高。

 

JVM字节码指令

        字节码指令作为中间语言,作用是帮助Java语言实现一些如栈操作,入栈出栈,传参读参,读写局部变化和函数调用等,例如最简单的控制指令,for循环、foreach循环、do…while循环、if…else和switch等。运算指令集有算术、逻辑、比较和位运算。数据交换指令集,用来操作栈内存、Java堆等,使用数据交换指令来实现数据在这些内存区域里面的交换,或者说传递,函数调用的指令集也在数据交换指令集中。

        因为JVM本身就是用C和C++共同编写的解释性虚拟机,所以在执行Java程序时,最终是JVM交由C语言来运行,也就是说,JVM是一边将字节码指令翻译成C程序,一边执行,通过C来调用执行机器指令。这就是JVM中模板解释器的实现思想,为每一个机器指令编写一段实现对应功能的汇编代码,在JVM初始化时,就会将汇编代码翻译成机器指令,加载到内存中,当JVM执行某一条Java字节码指令时,就可以从内存中直接执行对应的汇编代码,直接跳到该指令的内存地址就可以调用执行。

 

函数指针直接触发机器指令

        按照上面说的思路,JVM的模板解释器为每一个机器指令编写一段实现对应功能的汇编代码,在运行某一字节码指令时,可以直接执行对应的机器指令,具体的实现方式是怎样的,来看一个例子:

#include <stdio.h>
#include <stdlib.h>

const unsigned char run[] =  
{
	0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x40, 0x53, 0x56, 0x57,
	0x8d, 0x7d, 0xc0, 0xb9, 0x10, 0x00, 0x00, 0x00, 0xb8,
	0xcc, 0xcc, 0xcc, 0xcc, 0xf3, 0xab,
	0x8b, 0x45, 0x08, 0x03, 0x45, 0x0c,
	0x5f, 0x5e, 0x5b, 0x8b, 0xe5, 0x5d, 0xc3
};

int main(int argc, char const* argv[]) 
{
	int a = 4;
	int b = 5;
	int (*add)(int, int); // 定义函数指针
	add = (void*) run; // 函数指针add指向run机器码

	int result = add(a, b); 
	printf("%d + %d = %d", a, b, result);
	
	return 0;
}

        首先定义一个字符数组run,里面是一个函数的十六进制表示方式,这些字符组成机器指令,作用是对传入的两个数a和b进行求和,并返回结果。接着往下,main()函数里,还记得C直接操作机器指令的方式吗,就是用函数指针,通过函数指针变量(一个指针),存放某一段机器指令,在C程序编译阶段,C函数指针直接指向了某一段机器指令的首地址,实现直接调用该机器指令。

      所以在main()函数里,定义了一个函数指针add,下一行存放了run字符数组的首地址,最后通过add(a, b)来调用,程序执行到int result = add(a, b);时,就会直接将run数组里的一片连续内存区代码拿出来执行:

两种触发方式

上面的例子,通过函数指针直接触发机器指令,方式是先定义一个函数指针,函数指针就是一个指针变量,和其他普通变量如int,float,char等一样,存放的是一个值,指针变量存放的就是首地址,声明和调用正如上面的例子:

int (*add)(int, int); // 声明

arr = (void*) run; //存放某一片地址区域

int i = arr(a, b); // 调用

      还有一种方式,就是先声明其是一种类型,有点像面向对象中的类,首先通过typedef定义一种类型,一种函数指针类型,例如:

typedef (*addType)(int, int);

该语句声明了一种函数指针类型addType,是用户自定义的一种数据类型,然后就可以通过该类型是声明一个变量来用:

addType add = (void*) run;

int i = arr(a, b);

      无论是上面哪种声明,在调用时也有两种方式触发机器指令,第一张就是上面都用到的,直接int i = arr(a, b);看似最简洁明了,但是最好还是使用第二种方式:int i = (*add)(a, b);因为这样调用可以让别人一看就知道你使用了函数指针,而不是一个普通函数的显式调用。

 

call_stub函数指针

JVM中实现函数指针调用机器指令,用的是call_stub,它也是一个函数指针,函数原型如下:

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

函数的调用结果最终会被类型转换成CallStub,CallStub是一个自定义类型,函数指针类型,结构如下:

typedef void (*CallStub) {
	address	link,
	intptr_t*	result,
	BasicType	result_type,
	methodOopDesc*	method,
	address	entry_point,
	intptr_t*	parameters,
	int size_of_parameters,
	TRAPS
};

        可以看到一共有8个参数,link表示的是连接器,result是函数返回值的地址,result_type顾名思义就是函数的返回类型,method表示Java方法对象,entry_point这个参数很重要,表示的是JVM调用Java方法的事先定义好的入口,前面说到模板解释器,在JVM初始化时会将一些方法调用的“入口”代码编译成机器指令,加载到内存中,在JVM调用方法时,需要先调用这些入口指令,例如Java程序的主函数必须通过call_stub函数指针来执行。parameters指的是Java方法的参数集合,下一个size_of_parameters自然就是参数的数量了。

 

castable_address()

CallStub结构搞清楚后,回到call_stub的调用语句看:

return (CallStub)(castable_address(_call_stub_entry));

里面的参数是castable_address,也是一个函数,结构是这样的:

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

返回类型是address_word,顾名思义是一个地址类型,自定义的地址类型,很容易就能查到它经过了哪些包装:

typedef uintptr_t       address_word;

typedef unsigned int     uintptr_t;

        可以看到,address_word的最终原型是无符号整型类型unsigned int。最后,就只剩下_call_stub_entry这个参数了它的原封类型也是unsigned int,这些很容易查到它的封装:

static  address  _call_stub_entry;

        到这里把call_stub函数指针里的三个类型都搞明白了,JVM通过call_stub函数指针调用目标函数,call_stub函数相当于一个接口,里面又调用了castable_address()函数,传入原封类型是unsigned int的参数_address_stub_entry,标识的就是一个函数的地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值