_call_stub_entry入口中的pc( )函数

本文介绍了JVM初始化过程中的_call_stub_entry和call_stub函数,详细解析了_call_stub_entry如何存储函数入口地址,以及在生成CallStub函数指针时的角色。在初始化链路中,stubRoutines_initl负责例程初始化,generate_call_stub函数生成首地址,而pc()函数用于获取例程的起始位置。同时,文章还探讨了JVM堆、代码段和数据段,以及ip和bp寄存器在函数调用中的作用。

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

      在总结_call_stub_entry之前,先再次回顾下Java主函数调用必须经过的call_stub()函数,展开后得到的结构如下:

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

      CallStub是自定义类型的函数指针,有八个参数,call_stub()最后返回的就是这么一个函数指针变量类型。castable_address则是一个函数,接收一个地址类型address变量,并将其转换为基本类型unsigned int,最后返回出去:

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

      由此来看,_call_stub_entry是一个address类型,也就是标识的是某个内存地址,CallStub函数指针存放了这一地址后,相当于CallStub函数指针指向了某一个函数的入口,这样就可以调用函数了。_call_stub_entry在JVM初始化时就已经存放了某一个函数的入口地址,由generate_call_stub()函数完成首地址的生成。

 

初始化链路

      JVM在初始化时就要对_call_stub_entry进行内存地址赋值,初始化过程中,从main()函数开始,整个初始化链路是这样的:

java.c:main()
	java_md.c:LoadJavaVM()
	 jni.c:JNI_CreateJavaVM()
	 	Threads.c:create_vm()
			init.c:init_globals()
				StubRoutines.cpp:stubRoutines_initl()
					StubRoutines.cpp:initializel()
						stubGenerator_x86_x32.cpp:StubGenerator_generate()
							stubGenerator_x86_x32.cpp:StubCodeGenerator()
								stubGenerator_x86_x32.cpp:generate_initial()

      可与看到,在初始化链路中,stubRoutines_initl()负责例程的初始化,例程在上一篇日志里讲过,就是JVM初始化时写好的一些机器指令,为了实现特定的动作,例如函数调用与返回,异常处理等例程,CallStub函数指针里也有一个entry_point例程,它就是JVM调用Java方法时的入口,调用Java方法都必须先执行entry_point例程。

      再往后走,链路的最后调用了generate_initial()函数,对_call_stub_entry变量进行初始化,来看看generate_initial()函数中都做了哪些事情:

void generate_initial() {
// Generates all stubs and initializes the entry points

// This platform-specific settings are needed by generate_call_stub()
create_control_words();

// entry points that exist in all platforms Note: This is code
// that could be shared among different platforms - however the
// benefit seems to be smaller than the disadvantage of having a
// much more complicated generator structure. See also comment in
// stubRoutines.hpp.

StubRoutines::_forward_exception_entry = generate_forward_exception();

StubRoutines::_call_stub_entry =
  generate_call_stub (StubRoutines::_call_stub_return_address);
  
  //下面源码省略....
}

      在generate_initial()函数中看到了_call_stub_entry变量,它的初始化是得到genetate_call_stub()函数的返回值,该函数会产生一个首地址,赋值给_call_stub_entry进行初始化,产生首地址的过程比较复杂,一步一步慢慢来看它的源码,接着是看genetate_call_stub()函数:

address generate_call_stub(address& return_address) {
    StubCodeMark mark(this, "StubRoutines", "call_stub");
    address start = __ pc(); // 当前函数的入口地址
	
	assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
	bool sse_save = false;
	const Address rsp_after_call(rbp, -4 * wordSize);
	
	//下面源码省略....
}

第二行一个address类型的变量start,通过pc()函数获得首地址,保存的是当前这个例程的机器码起始位置。

pc()函数

      来看看pc()函数的结构:

address pc() const {
    return  _code_pos;
}

      返回类型是address,即一个内存地址,对应的是一个例程,JVM会初始化很多例程,每一个例程都是存放在一片连续的内存区域中的,最开始第一个例程的起始位置假设为0,例程所占内存为16个字节,那么_pc()函数就会返回0给start,JVM初始化第二个例程时,_pc()函数就会返回16,假设例程大小也为16个字节,那么结束位置就是32,第三个例程在初始化时_pc()函数得到的返回值就是32,以此类推。

      JVM的进程内存中分有几个部分,JVM堆,代码段和数据段等,所有的例程都会放在JVM堆里,所以JVM在初始化时会创建一个较大的堆内存区域,专门用来存放各种例程。每一个例程占用一片连续的区域,并且有一个对应的generate()函数,两个例程之间的内存区域也是相连的,当第一个例程对应的generate()函数执行完后,_code_pos变量的值就会自动增加,大小等于例程的大小,例如16,那么到第二个例程调用generate()函数时,得到的_pc()返回值就是16。每一个generate()函数中都会有address start = _pc();这段代码,start得到返回值_code_pos就是上一个例程的偏移量最后的位置,该位置也是下一个例程的开始位置。

 

generate_all_stub()入参

      ok,generate_all_stub()函数继续往下走,设置完start变量的偏移量后,接下来就是一些变量定义,寻址:

const Address call_wrapper  (rbp, 2  * 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);
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);

      可以看到,result、result_type、method、parameters甚至entry_point,都是前面CallStub函数指针里的参数,拿其中一行代码来看,const Address result (rbp, 3 * wordSize);表达的意思是result变量在JVM堆中的位置是 3 * wordSize(%rbp),在JVM为每一个Java方法分配的栈空间中,可以将其分为四个部分,存放变量的变量区,存放参数的入参区,ip代码段寄存器和bp栈基寄存器。

变量区和入参区

      变量区保存的是该Java方法中的一些局部变量,存放的是变量的引用,也就是地址,要注意的是,方法栈空间的变量区不是一定会初始化的,如果调用的Java方法中没有使用到局部变量,那么JVM不会分配出变量区。入参区在数据入参的时候用到,假如一个方法中又调用到了另一个方法,而且需要传入参数,那么就会将参数压栈到入参区,入参区存在与调用者的方法栈中,被调用这可以从里面拿到压栈后的参数。举个例子讲一下调用者和被调用者的关系,假设main()方法里调用了run()方法,那么main()方法就是调用者,run()方法就是被调用者,main()方法压栈的参数存放在了main()方法的方法栈中,run()方法从main()方法栈中获取入参,如果run()方法里面又调用其他方法,那么其他方法就从run()方法的方法栈中获取入参。

ip和bp寄存器

      ip是代码段寄存器,bp是栈的基地址寄存器(或者叫栈基寄存器),这两个寄存器在函数执行call add指令时就会自动压栈到栈顶位置,看个例子:

      main()函数中调用run()函数时,会自动将eip和ebp寄存器压入main()函数的栈顶,ip代码段寄存器的作用就是为了让main()函数在执行完run()函数返回后,能继续执行main()函数下面的代码,具体做法就是在执行函数调用时,自动将eip压栈,待被调用函数执行完成后,eip出栈,恢复调用函数的执行位置。

      bp栈基地址寄存器,作用就十分重要了,涉及到参数获取,例如一条指令movl8(%ebp) %eax,指的是从ebp寄存器向上偏移8个字节处获取参数数据,然后放到eax寄存器中。这里有一些地方需要注意的是,在JVM的栈空间中,内存地址从栈顶开始为低地址,向上分配,到栈底处为最高地址,例如还是上面那张图,main()函数栈底处为0(%esp),栈顶处为64(%esp),一共64字节内存空间,这是分配问题。还有一个是寻址问题,JVM在对数据进行寻址使用的是偏移量,用偏移量来确定数据的位置。拿回上面的图做例子,ebp寄存器的位置就是run()函数的栈底位置,那么它相对于run()栈底偏移量就是0,可以直接写成(%ebp),eip寄存器相对于run()函数栈底的位置是偏移了4个字节,所以用4(%ebp)表示,这是数据或变量通过被调用者栈底,也就是run()函数栈底来确定位置,还可以通过调用者的栈顶来确定数据的位置,一样的,如果取的数据在基准位置(例如ebp)的上方,也就是高地址位,那么指令前的数字就是正数,例如8(%ebp),意思是当前位置加上8个字节,相对的,如果位置在基准位置的下方,也就是低地址位,那么指令前的数字就是负数。

<think>我们正在使用Google Test框架进行单元测试,并且需要为带有lambda函数参数的函数创建stub(打桩)示例。 由于lambda函数是匿名且类型唯一的,我们不能直接创建一个匹配任意lambda类型的存根。但是,我们可以通过模板或者通用可调用对象来设计存根。 思路: 1. 定义一个存根类模板,它可以存储任意可调用对象(包括lambda),并提供一个调用运算符来匹配原函数的签名。 2. 在测试中,我们可以创建该存根类的实例,并将其传递给被测试的函数,以替换原有的lambda参数。 但是,请注意,如果原函数的参数是lambda(即函数期望一个可调用对象),那么我们可以通过模板参数或std::function来传递。这里我们假设原函数使用std::function来接收lambda,这样我们可以方便地替换。 示例场景: 假设我们有一个函数`process`,它接受一个整数和一个可调用对象(lambda),然后使用这个可调用对象来处理整数。 我们将编写: - 被测试的代码(process函数) - 存根类(用于替换传入的lambda) - 测试用例(使用Google Test) 步骤: 1. 定义被测试函数: ```cpp #include <functional> int process(int value, std::function<int(int)> func) { return func(value); } ``` 2. 设计一个存根类,它可以替换func。在测试中,我们希望控制func的行为,以便测试process函数。 3. 使用Google Test编写测试用例。 然而,直接替换传入的lambda,我们可以直接在测试中构造一个std::function对象传入,而不需要复杂的存根机制。但是,如果我们要模拟一个复杂的依赖(比如需要记录调用次数、改变行为等),我们可以创建一个模拟类。 下面是一个简单的示例,展示如何用自定义的std::function对象来打桩: 但是,如果问题要求使用类似于打桩技术(比如拦截对真实函数的调用)来替换一个函数内部使用的lambda,那可能涉及其他技术,例如依赖注入。这里我们假设我们可以通过参数传递可调用对象。 另一种情况:如果被测试的函数内部使用了固定的lambda(即不是通过参数传入),那么我们就不能通过参数来替换。这时,我们可能需要重构代码,将lambda提取出来,使其可以被替换。或者使用链接期替换(例如使用桩函数替换原函数)但lambda通常定义在函数内部,无法直接替换。 因此,根据问题描述,我们关注的是“为带有lambda函数参数的函数”创建存根,所以我们可以通过传入不同的可调用对象(存根)来测试。 示例代码: 被测试代码(假设在头文件demo.h中): ```cpp // demo.h #include <functional> int process(int value, std::function<int(int)> func) { return func(value); } ``` 测试代码: ```cpp #include "gtest/gtest.h" #include "demo.h" TEST(ProcessTest, WithStub) { // 创建一个存根函数对象,它返回传入值的两倍 auto stub = [](int x) { return x * 2; }; int result = process(5, stub); EXPECT_EQ(result, 10); } // 我们还可以创建一个模拟存根,记录调用次数等 class MockStub { public: MOCK_METHOD(int, call, (int)); }; // 但是Google Mock需要对象有明确的类型,我们可以用适配器 TEST(ProcessTest, WithGoogleMock) { MockStub mock; EXPECT_CALL(mock, call(5)).WillOnce(testing::Return(10)); // 将mock对象适配到std::function std::function<int(int)> func = [&mock](int x) { return mock.call(x); }; int result = process(5, func); // 在mock中我们已经设置了期望,并且通过EXPECT_CALL验证 } ``` 但是注意:上面的Google Mock使用需要包含gmock头,并且MockStub类需要继承自testing::Test等(或者使用TEST_F,但这里我们使用TEST)。更完整的做法: 然而,问题要求使用lambda函数作为参数的stub打桩,上面的第一种方法(使用lambda直接作为存根)已经足够简单。第二种方法展示了如何结合Google Mock来验证调用。 但是,如果原函数的参数不是std::function,而是模板参数(例如接受任意可调用对象),那么测试代码依然可以类似地传递存根lambda。 因此,我们给出两种方案: 方案1:直接传递lambda作为存根(适用于简单行为模拟) 方案2:使用Google Mock创建模拟对象(适用于需要验证调用行为的场景) 下面我们写一个完整的测试文件示例,包括使用Google Mock的情况(需要链接gtest和gmock)。 注意:在Google Test中使用Google Mock,需要包含gmock.h。 测试代码文件: ```cpp #include "gtest/gtest.h" #include "gmock/gmock.h" #include "demo.h" // 方案1:直接使用lambda TEST(ProcessTest, DirectStub) { auto stub = [](int x) { return x * 2; }; EXPECT_EQ(process(5, stub), 10); } // 方案2:使用Google Mock class MockStub { public: MOCK_METHOD(int, call, (int)); }; TEST(ProcessTest, GoogleMockStub) { MockStub mock; EXPECT_CALL(mock, call(5)) .Times(1) .WillOnce(testing::Return(10)); // 将mock对象适配到std::function std::function<int(int)> func = [&mock](int x) { return mock.call(x); }; int result = process(5, func); // 这里不需要再EXPECT_EQ,因为如果mock.call没有被调用或者参数不对,测试会失败 // 但为了结果清晰,我们也可以检查返回值 EXPECT_EQ(result, 10); } int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } ``` 编译时需要链接gtest和gmock库。 总结:对于带有lambda函数参数的函数,我们可以通过传递不同的可调用对象(包括lambda表达式和std::function对象)来打桩。结合Google Mock,我们可以对调用进行更细致的验证。 注意:如果原函数内部使用lambda,而不是作为参数,那么上述方法不适用。此时需要重构代码,将内部lambda改为通过参数传入(依赖注入)或者改为可替换的组件。 相关问题: 1. 如何在C++中使用Google Mock来模拟一个带有函数指针参数的函数? 2. 在单元测试中,如何测试内部使用lambda的函数? 3. 如何为C++中的模板函数(接受任意可调用对象)编写单元测试? 4. 使用Google Test时,如何测试回调函数的调用次数和参数? 5. 在C++单元测试中,如何处理对虚函数的模拟?</think>### 在C++中使用Google Test框架为带lambda参数的函数创建Stub示例 #### 1. **问题分析** 当函数参数包含lambda时,打桩的关键在于: - Lambda是匿名且类型唯一的,需通过模板匹配 - 需使用`std::function`封装可调用对象 - 通过桩函数替换原函数行为 #### 2. **解决方案** ```cpp #include <gtest/gtest.h> #include <functional> // 必需头文件 // 被测试函数(接收lambda参数) int process_data(int value, std::function<int(int)> transformer) { return transformer(value * 2); // 业务逻辑 } // 桩函数模板(核心实现) template <typename Fn> class LambdaStub { public: static testing::Action<int(int)> action; // 存储桩行为 // 桩函数入口 static int stub_entry(int arg) { return action.Perform(std::tuple<int>(arg)); } // 绑定桩行为 static void set_action(std::function<int(int)> new_action) { action = testing::Invoke(new_action); } }; template <typename Fn> testing::Action<int(int)> LambdaStub<Fn>::action; // 静态成员初始化 // 测试用例 TEST(DataProcessingTest, LambdaStubWorks) { // 设置桩行为(返回输入值+10) auto stub_behavior = [](int x) { return x + 10; }; LambdaStub<decltype(stub_behavior)>::set_action(stub_behavior); // 执行测试(验证桩函数替换) int result = process_data(5, LambdaStub
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值