如何解析远跳转FF汇编指令?虚函数调用跳转地址预测方法
C++虚函数调用底层实现与
FF 25指令解析 问题场景
先定义三个C++类:
class Base { virtual void foo() {...} }; class A : Base { virtual void foo() override {...} }; class B : A { virtual void bar() {...} /* 新增bar,未重写foo */ };
在分析虚函数调用的底层实现时,发现两种不同的汇编指令行为:
- 调用
A a; a.foo()时,编译器生成Call指令FF 10,指向近跳转指令E9 01 02 03 04,通过虚表记录的相对偏移找到实际函数,逻辑清晰。 - 调用
B b; b.foo()时,生成的Call指令指向FF 25跳转指令,Visual Studio反汇编如下:
00007FFA2CAB1C85 FF 25 AD 76 21 01 jmp qword ptr [A::foo (07FFA2DCC9338h)]
原本预期直接跳转到07FFA2DCC9338h,但实际跳转到了07FFA4AD05885h,该地址处是另一条跳转指令:
00007FFA4AD05885 E9 96 5D 01 00 jmp A::foo (07FFA4AD1B620h)
最终通过这条E9指令指向实际函数。
核心疑问:
FF 25指令的工作原理是什么?- 已知当前指令地址(
RIP)和跳转参数,能否预测最终跳转的目标位置?
指令解析与原理
FF 25指令本质:RIP相对寻址的间接跳转
FF 25并非远跳转(x86-64的远跳转是FF 2D配合前缀或JMP FAR),而是基于RIP的间接跳转,执行逻辑分为两步:
- 计算内存地址:
- 当前
RIP的值 = 当前指令地址 + 指令长度(FF 25是6字节指令,所以RIP = 00007FFA2CAB1C85 + 6 = 00007FFA2CAB1C8B) - 加上指令后4字节的偏移量(注意小端序,指令中的
AD 76 21 01实际是0x012176AD) - 得到目标内存地址:
00007FFA2CAB1C8B + 0x012176AD = 00007FFA2DCC9338,也就是VS反汇编标注的[A::foo (07FFA2DCC9338h)]
- 当前
- 读取跳转目标:
从上述内存地址中读取一个8字节的QWORD值(x86-64下是64位指针),这个值就是FF 25指令实际跳转到的地址——也就是你看到的07FFA4AD05885h。
为什么B的foo调用会走这个流程?
因为B没有重写foo,它的虚表中foo的条目指向的是编译器生成的跳转桩(thunk),而非直接指向A::foo的实际代码。跳转桩的作用通常包括:
- 处理COMDAT代码折叠(多个模块中相同的函数实现合并)
- 支持增量编译/链接时的地址重定向
- 统一函数的导出地址,方便动态链接
这个跳转桩就是07FFA4AD05885h处的E9指令,最终会跳转到A::foo的实际实现地址。
能否用RIP和参数预测跳转目标?
可以,步骤如下:
- 确定当前指令地址(即
FF 25指令的起始地址),计算RIP:RIP = 当前指令地址 + 6(因为FF 25是6字节指令) - 提取指令后4字节的偏移量,转换为32位有符号整数(注意小端序)
- 计算内存地址:
内存地址 = RIP + 偏移量 - 读取该内存地址处的8字节QWORD值,就是
FF 25指令直接跳转的目标地址 - 如果该目标地址是跳转桩(如
E9指令),则需要进一步解析该跳转指令的相对偏移,才能得到最终的函数地址
内容的提问来源于stack exchange,提问作者Vladislav
相关产品推荐
相关产品推荐

