ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入解析C++虚函数与虚表机制

2026/8/11 2:25:22 拓冰建站 浏览量
深入解析C++虚函数与虚表机制 代码classCalculator{public:intvalue;Calculator(intv):value(v){}intmultiply(intn){returnvalue* n;}intaddAndMultiply(inta,intb){returnmultiply(a b);}};classScientificCalculator:publicCalculator{public:ScientificCalculator(intv): Calculator(v){}virtual intpower(intexp){intresult1;for(inti0;i exp;i){result*value;}returnresult;}virtual intprocess(intx,inty){returnpower(x) multiply(y);}};intmain() {ScientificCalculator*calcnewScientificCalculator(2);intresultcalc-process(3,4);deletecalc;returnresult;}编译一些基础这个用到了虚函数简单说一下后面详细说先说普通调用假设函数int foo(int a)编译器会放到一个固定地址调用的时候汇编是call 0x1400003180然而虚函数调用是虚拟地址在对象里面去查表基本就是mov rax,[对象]mov rax,[rax]call rax主要是为了多态的情况还有一个需要解释的问题我们的虚函数为什么在逆向的时候要新建一个struct关键认知虚表不是代码是一块只读数据在 .rdata 里里面连续摆放着函数指针地址 内存内容 含义0x140005580 ┌───────────────────────────┐│ 0x1400031C0 │ ← 槽0: power 函数的地址 │0x140005588 │ 0x140003210 │ ← 槽1: process 函数的地址 │└───────────────────────────┘如果不创建的话不建结构体IDA 不知道 vptr 指向的是什么反编译就只能输出 (**this-value) 这种垃圾建结构体 ScientificCalculator_vtblIDA 就知道了——vptr 指向的是一块数据第0个字段是函数指针 slot_power第8个字段是 slot_process注意这个只是为了让源代码更加可读。接下来我们说一下vptr(虚表指针)对象 (new出来的内存)├─ [0x00] vptr ──→ 虚表 (静态数据, .rdata里的表)│ ├─ [0x00] 槽0: 第1个虚函数地址│ └─ [0x08] 槽1: 第2个虚函数地址└─ [0x08] 普通成员对象程序运行时才分配本例 new(16)每个实例一份虚表编译时就写死在 .rdata 里的地址表全程序只有一份所有同类的对象共享vptr对象和虚表之间的桥每个实例自己一份因为它可能属于不同类vptr存放位置只要有虚函数的类编译器强制对象第 0 个成员必须是 vptr8 字节。0x00 qword vptr 0x140005580 ← 指到虚表槽0位置0x08 dword value 2 ← 你的业务成员this 指针永远指向对象开头所以反汇编里 [this] 就经常是 vptr。vptr 什么时候被赋值vptr 不是你 C 源码里写的任何语句——它是编译器在你每个构造函数开头自动插入的指令。0x140003281 lea rdx, power ; 取虚表地址 0x140005580编译器认得这张表0x140003288 mov rax, [rbparg_0] ; this0x14000328c mov [rax], rdx ; 对象[0] 虚表地址 ← 就是 vptr 的赋值怎么确认一个类有没有 vptrmov [this], X ; X 指向 .rdata 里的函数指针表 → 有 vptr多态类mov [this], a2 ; 直接把参数写进对象[0] → 普通类无虚函数逆向main能确定是cint __fastcall main(int argc, const char **argv, const char **envp){ScientificCalculator *v3; // rbxint v5; // [rsp2Ch] [rbp-Ch]_main(argc, argv, envp);v3 operator new(0x10ui64);*(v3 2) 2;*v3 off_140005580;v5 ScientificCalculator::process(v3, 3, 4);operator delete(v3, 0x10ui64);return v5;}判断后应该是这样理由如下calculator方便理解就叫这个名字其实就是创建的对象result就是结果return不是标准的返回0而已有可能计算出来就是0。int __fastcall main(int argc, const char **argv, const char **envp){ScientificCalculator *calculator; // rbxint result; // [rsp2Ch] [rbp-Ch]_main(argc, argv, envp);calculator operator new(0x10ui64);*(calculator 2) 2; // ScientificCalculator 某个值2无默认值*calculator off_140005580;result ScientificCalculator::process(calculator, 3, 4);operator delete(calculator, 0x10ui64); // 删除对象return result; // 返回process计算结果}a2应该是结构体里面的东西是一个int看下面的这个只是优化之后的main_wu进行跟进找vptr找到this指向0x000000140005580在后面就是直接去对象那么对象[0]写入的地方就是0x000000140005580也就是vptr那么就有了0x00 vptrà0x000000140005580x64的指针是8字节0x08 int calve 2去ScientificCalculator 0x0000000140005570Itanium ABI 虚表标准布局这个我是gcc编译的后面在做msvc0是offset-to-top多重继承时是偏移这里单继承0第二个指向RTTI再看0x0000000140005510就是RTTI第1个 qword → libstdc 的 __class_type_info 虚表extern不用管第2个 qword → 0x140005550 _ZTS20ScientificCalculator 字符串内容是 20ScientificCalculator长度前缀类名整理下main先调用proccess还调用了multiply根据寄存器就有了main → vtable[1] process(3,4) → vtable[0] power(3) ─┐└ multiply(4) ─┴→ 相加接下来想计算的就计算我们吧逆向弄完缺少结构体struct Calculator {int value;};struct ScientificCalculator_vtbl {__int64 (__fastcall *slot_power)(void *self, int exp);__int64 (__fastcall *slot_process)(void *self, int a, int b);};解释一下SC_METHOD是个宏用于成员函数systemc.hstruct ScientificCalculator {ScientificCalculator_vtbl *vtable; // 0x00Calculator base; // 0x08};