虚表指针(vptr)是编译器在含虚函数的对象内存最前端插入的8字节(x64)隐藏指针,指向只读段中的虚函数表(vtable),其存在可通过sizeof比成员变量总和多8字节直接验证。

虚表指针在内存里到底长什么样
虚表指针(vptr)是编译器在每个含虚函数的类对象开头自动插入的一个隐藏指针,指向该类的虚函数表(vtable)。它通常位于对象内存布局的最前面,大小等于指针宽度(x64 下为 8 字节)。不是所有对象都有 vptr——只有定义了虚函数(或继承自含虚函数的类)的对象才存在;且一旦对象构造完成,vptr 就固定指向其静态类型对应的虚表。
直接修改 vptr 是可行的,但风险极高:
- 对象可能被优化为栈上局部变量,生命周期短,hook 后立即析构导致虚表访问野指针
- 多重继承、虚继承会改变 vptr 位置,偏移不固定
- 编译器可能内联虚调用,绕过虚表查找(尤其在 O2/O3 下)
- Windows 上若对象在只读内存页(如某些 DLL 的全局对象),写入 vptr 会触发 ACCESS_VIOLATION
怎么安全地替换虚表里的函数地址
真正可操作的是修改虚表本身(即 vtable 数组中的函数指针),而非频繁改每个对象的 vptr。虚表本身通常位于只读数据段(.rdata 或 .rodata),所以必须先解除内存保护:
- Windows 下用
VirtualProtect将虚表所在页设为PAGE_READWRITE - Linux/macOS 下用
mprotect配合getpagesize()对齐地址 - 虚表地址可通过取某个虚函数地址反推:例如
&MyClass::func是虚函数指针,减去其在虚表中的索引偏移(单位为sizeof(void*))即可得到虚表起始地址
示例(Windows x64,假设 func 是类第一个虚函数):
// 假设 obj 是 MyClass* 类型的有效对象 uintptr_t* vtable = *(uintptr_t**)obj; // 读出 vptr 指向的虚表首地址 DWORD old_protect; VirtualProtect(vtable, sizeof(void*), PAGE_READWRITE, &old_protect); vtable[0] = (uintptr_t)&my_hook_func; // 替换第一个虚函数 VirtualProtect(vtable, sizeof(void*), old_protect, &old_protect);
为什么 hook 后调用崩溃或跳错地址
常见原因不是代码写错,而是 ABI 和调用约定不匹配:
立即学习“C++免费学习笔记(深入)”;
- hook 函数签名必须与原虚函数**完全一致**:参数个数、类型、const/volatile 限定、返回值——哪怕差一个
const,MSVC 可能生成不同 thiscall 约定 - 成员函数有隐式
this指针,hook 函数也必须是普通函数(非 static),且不能用 lambda(无稳定地址) - 若原函数是
__thiscall(x86 默认),而你用__cdecl实现,栈平衡会被破坏 - 开启 /GS(缓冲区安全检查)时,编译器可能在虚函数入口插入校验,hook 后跳转会失败
验证方式:反汇编原虚函数和你的 hook 函数,确认前几条指令的调用约定特征(如 this 是否从 ecx 传入、是否清理栈)。
跨平台和现代 C++ 的现实约束
这种手法在实际工程中几乎不可维护:
- C++11 起,
final关键字会阻止虚函数被重写,编译器可能直接内联或去掉虚表项 - Clang/LLVM 的 LTO(Link Time Optimization)可能合并、重排甚至删除虚表
- ASLR + CFG(Control Flow Guard)在 Windows 上默认启用,对虚表写入或间接跳转做运行时校验
- std::variant、std::any 或策略模式比硬 patch 虚表更安全、更易测试
真正需要 runtime 替换行为的场景(如插件系统、热更新),应优先考虑依赖注入、接口抽象或 std::function 成员变量,而不是触碰虚表——后者本质上是绕过语言语义的底层 hack,一旦编译器或运行时环境升级,就容易无声失效。


















