Pwn 基础之C/C++(十):C++ 对象模型与反编译阅读基础
本篇目标:掌握继承、虚函数、对象复制与常用标准库,并建立阅读反编译代码的基础模型。
预计阅读时间:25~30 分钟。
1. 继承
class Animal {
public:
void eat()
{
std::cout << "eat\n";
}
};
class Dog : public Animal {
public:
void bark()
{
std::cout << "bark\n";
}
};
Dog 继承 Animal 的可访问成员:
Dog dog;
dog.eat();
dog.bark();
派生类对象通常包含基类子对象,但具体布局由 ABI、编译器和继承方式决定。
2. 虚函数与动态多态
class Animal {
public:
virtual void speak() const
{
std::cout << "animal\n";
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override
{
std::cout << "dog\n";
}
};
Dog dog;
Animal *animal = &dog;
animal->speak();
输出 dog。程序根据对象的动态类型选择函数实现,这叫动态多态。
3. 虚表的概念模型
C++ 标准规定多态行为,但不强制唯一底层实现。主流编译器通常使用虚表和虚表指针。
概念上:
对象
+------------------+
| 虚表指针 |
+------------------+
| 普通成员 |
+------------------+
虚表
+------------------+
| speak 对应入口 |
+------------------+
| 析构函数相关入口 |
+------------------+
实际布局需要结合:
- 编译器;
- ABI;
- 单继承或多继承;
- 虚继承;
- 优化选项;
- 目标平台。
4. 虚析构函数
Animal *animal = new Dog;
delete animal;
如果基类被用于多态删除,应提供虚析构函数:
virtual ~Animal() = default;
这样析构过程才能根据动态类型正确执行。
5. 复制构造
struct Point {
int x;
int y;
};
Point first{1, 2};
Point second = first;
second 是新的对象,其成员由 first 复制而来。
6. 浅复制与深复制
若对象只含数值成员,逐成员复制通常足够。
若对象直接拥有动态资源,简单复制裸指针只会复制地址:
对象 A ──→ 同一资源
对象 B ──┘
此时必须明确:
- 是否允许复制;
- 是否创建独立副本;
- 是否共享资源;
- 谁负责最终释放。
优先使用 std::string、std::vector 和智能指针表达资源语义。
7. Rule of Zero
如果所有资源都由标准库成员管理,尽量不手写特殊成员函数:
class User {
private:
std::string name_;
std::vector<int> scores_;
};
通常无需手写析构、复制和移动逻辑,编译器生成的默认行为即可正确组合成员行为。
8. std::string
std::string text = "hello";
text += " world";
std::cout << text << '\n';
std::cout << text.size() << '\n';
需要 C 字符串接口时:
const char *raw = text.c_str();
该指针的有效性与字符串对象及后续修改有关,不应脱离生命周期长期保存。
9. std::vector
std::vector<int> numbers = {10, 20, 30};
numbers.push_back(40);
for (int number : numbers) {
std::cout << number << '\n';
}
常用操作:
numbers.size();
numbers.empty();
numbers.push_back(100);
numbers.pop_back();
numbers.clear();
numbers[index]:普通下标访问;numbers.at(index):越界时抛出异常。
容器扩容时,原元素地址、指针和迭代器可能失效,具体规则需要查看对应容器接口。
10. std::array 与 std::map
固定长度数组:
std::array<int, 4> numbers = {10, 20, 30, 40};
键值映射:
std::map<std::string, int> scores;
scores["Alice"] = 100;
查找:
auto iterator = scores.find("Alice");
if (iterator != scores.end()) {
std::cout << iterator->second << '\n';
}
11. auto 与范围 for
auto value = 100;
auto text = std::string("hello");
auto 不表示没有类型,而是让编译器推导类型。
只读遍历较大对象:
for (const auto &item : items) {
// 使用 item
}
修改元素:
for (auto &item : items) {
// 修改 item
}
12. 模板基础
template <typename T>
T maximum(T a, T b)
{
return a > b ? a : b;
}
int a = maximum(10, 20);
double b = maximum(1.5, 2.5);
标准库中的 std::vector<int>、std::array<int, 4>、std::unique_ptr<User> 都建立在模板之上。
13. 反编译器常见类型
反编译工具可能生成:
| 类型名 | 常见含义 |
|---|---|
_BYTE |
1 字节 |
_WORD |
2 字节 |
_DWORD |
4 字节 |
_QWORD |
8 字节 |
这些通常是工具生成的辅助类型,不一定等于原源码中的类型名。
也可能看到:
unsigned __int8
unsigned __int16
unsigned __int32
unsigned __int64
可以先按对应位宽理解,再结合访问方式验证符号属性和真实语义。
14. 阅读地址计算
*(int *)(base + 4 * index)
分解:
base是起始地址;4 * index是字节偏移;- 相加得到目标地址;
- 转成
int *; - 解引用读取一个
int。
如果 base 指向整数数组,它可能等价于:
((int *)base)[index]
15. 恢复结构体字段
*(int *)(object + 8)
可能表示从对象起始地址偏移 8 字节处读取一个 int。
可以暂时建立模型:
struct Object {
unsigned char unknown[8];
int field;
};
但这是推测,必须结合:
- 多处访问的固定偏移;
- 读写宽度;
- 字段使用方式;
- 函数调用关系;
- 构造与析构行为。
16. 综合练习路线
C 基础
- 四则运算程序;
- 质数判断;
- 数组最大值;
- 手写字符串长度;
- 指针交换;
- 结构体学生管理;
- 动态数组扩容;
- 文件保存与读取。
C++ 基础
- 使用
class Student重写学生管理; - 用
std::string管理姓名; - 用
std::vector管理学生列表; - 用构造函数建立有效对象;
- 使用 RAII 管理文件或动态资源;
- 建立简单基类和派生类,观察虚函数调用。
工具链
- 查看预处理结果;
- 生成汇编文件;
- 使用 GDB 查看变量和内存;
- 对照
-O0与-O2; - 使用 Sanitizer 检查练习项目。
本篇练习
- 建立
Animal/Dog/Cat多态示例并观察析构顺序。 - 使用 GDB 打印带虚函数对象的大小和原始字节。
- 用
std::vector实现通讯录。 - 编写函数模板求两个值中的较大者。
- 将一段数组访问代码手动改写为“基址 + 偏移”的形式。
本篇小结
- 继承建立类型间的扩展关系;
- 虚函数实现动态多态;
- 主流 ABI 常通过虚表机制实现动态分派;
- 对象复制必须考虑资源所有权;
- Rule of Zero 优先依赖标准库管理资源;
- 阅读反编译代码时,要把类型、访问宽度、基址和偏移组合起来判断。
后续学习方向
完成本系列后,可以按以下路线正式进入二进制基础:
x86-64 寄存器与汇编
↓
函数调用约定与栈帧
↓
ELF 文件结构
↓
动态链接、PLT 与 GOT
↓
GDB 调试与反汇编
↓
常见程序保护机制
↓
在合法 CTF/靶场环境中学习 Pwn
继续学习时,坚持把每个结论落到:
源码 → 类型 → 字节 → 地址 → 指令 → 实际运行结果

说些什么吧!