news 2026/8/11 2:25:08

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析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){
intresult=1;
for(inti=0;i< exp;i++){
result*=value;
}
returnresult;
}
virtual intprocess(intx,inty){
returnpower(x)+ multiply(y);
}
};

intmain() {
ScientificCalculator*calc=newScientificCalculator(2);
intresult=calc->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_vtbl:IDA 就知道了——"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 个成员必须是 vptr(8 字节)。

+0x00 qword vptr = 0x140005580 ← 指到虚表槽0位置

+0x08 dword value = 2 ← 你的业务成员

this 指针永远指向对象开头,所以反汇编里 [this] 就经常是 vptr。

vptr 什么时候被赋值

vptr 不是你 C++ 源码里写的任何语句——它是编译器在你每个构造函数开头自动插入的指令。

0x140003281 lea rdx, power ; 取虚表地址 0x140005580(编译器认得这张表)

0x140003288 mov rax, [rbp+arg_0] ; this

0x14000328c mov [rax], rdx ; 对象[0] = 虚表地址 ← 就是 vptr 的赋值

怎么确认"一个类有没有 vptr"

mov [this], X ; X 指向 .rdata 里的函数指针表 → 有 vptr,多态类

mov [this], a2 ; 直接把参数写进对象[0] → 普通类,无虚函数

逆向

main

能确定是c++

int __fastcall main(int argc, const char **argv, const char **envp)

{

ScientificCalculator *v3; // rbx

int v5; // [rsp+2Ch] [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; // rbx

int result; // [rsp+2Ch] [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à0x000000140005580(x64的指针是8字节)

0x08 int calve = 2

去ScientificCalculator 0x0000000140005570

Itanium ABI 虚表标准布局,这个我是gcc编译的,后面在做msvc

0是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.h)

struct ScientificCalculator {

ScientificCalculator_vtbl *vtable; // +0x00

Calculator base; // +0x08

};

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 2:24:21

3步搭建原神私服:KCN-GenshinServer一键GUI服务端完全指南

3步搭建原神私服&#xff1a;KCN-GenshinServer一键GUI服务端完全指南 【免费下载链接】KCN-GenshinServer 基于GC制作的原神一键GUI多功能服务端。 项目地址: https://gitcode.com/gh_mirrors/kc/KCN-GenshinServer 你是否曾梦想在自己的电脑上搭建一个完全可控的原神私…

作者头像 李华
网站建设 2026/8/11 2:21:34

哈希表原理与实战:高效查找的核心技术

1. 哈希表&#xff1a;程序员的高效查找利器第一次听说哈希表时&#xff0c;我正被一个查找性能问题困扰。当时需要在十万条用户数据中快速匹配用户名&#xff0c;用普通数组遍历简直慢得像蜗牛。直到同事建议&#xff1a;"用哈希表吧&#xff0c;查找时间复杂度能降到O(1…

作者头像 李华
网站建设 2026/8/11 2:21:06

RAFT 检索增强微调技术深度解析:从域内知识注入到抗干扰推理的 LLM 领域适配新范式

RAFT 检索增强微调技术深度解析:从域内知识注入到抗干扰推理的 LLM 领域适配新范式 核心痛点:RAG 依赖检索质量、微调易遗忘通用能力——RAFT 将二者融合,让模型学会在噪声文档中精准引用并推理 适配人群:AI 工程师、LLM 应用开发者、RAG 系统架构师、NLP 研究者 收获能力:…

作者头像 李华
网站建设 2026/8/11 2:21:02

Hermes 与 ReAct 模式对比分析_2

Hermes 与 ReAct 模式对比分析 结论先行 Hermes 有 Thought → Action → Observation 的经典过程&#xff0c;但它不是用文本标记 Thought: / Action: / Observation: 显式表达的 ReAct&#xff0c;而是用 API 原生的结构化消息隐式完成了相同的三段循环。一、经典 ReAct 的三…

作者头像 李华
网站建设 2026/8/11 2:19:10

终极Unity游戏模组加载器MelonLoader:新手完全指南

终极Unity游戏模组加载器MelonLoader&#xff1a;新手完全指南 【免费下载链接】MelonLoader The Worlds First Universal Mod Loader for Unity Games compatible with both Il2Cpp and Mono 项目地址: https://gitcode.com/gh_mirrors/me/MelonLoader MelonLoader是全…

作者头像 李华
网站建设 2026/8/11 2:18:55

视频动态目标三维实时重建:边防机动目标长时序轨迹推演理论探究

### 视频动态目标三维实时重建&#xff1a;边防机动目标长时序轨迹推演理论探究摘要本研究旨在深入探究边防机动目标长时序轨迹推演理论&#xff0c;以提升边防安全监测的精准性与有效性。随着边境安全形势的日益复杂&#xff0c;对机动目标的持续、准确监测成为关键需求。研究…

作者头像 李华