x86-64 汇编 —— C++ 特性在汇编层的体现
本文件连接 C++ 语言特性与其编译后的汇编实现,是理解 C++ 教程底层原理部分的汇编基础。
内存分段
编译后的程序在内存中分为若干段:
高地址
┌──────────┐
│ 栈 │ ← 局部变量、函数调用帧(向低地址增长)
├──────────┤
│ ... │
├──────────┤
│ 堆 │ ← new/malloc 分配(向高地址增长)
├──────────┤
│ .bss │ ← 未初始化全局/静态变量(零填充)
├──────────┤
│ .data │ ← 已初始化全局/静态变量
├──────────┤
│ .rodata │ ← 只读数据(字符串常量、虚函数表)
├──────────┤
│ .text │ ← 代码(机器指令)
└──────────┘
低地址
C++ 中各类内容的归属:
| C++ 内容 | 所属段 | 说明 |
|---|---|---|
| 成员函数体 | .text | 所有对象共享同一份代码 |
| 虚函数体 | .text | 与普通函数相同 |
| 虚函数表(vtable) | .rodata | 只读,每类一张 |
| 静态成员变量 | .data / .bss | 类外存储,全局唯一 |
| 字符串字面量 | .rodata | 只读 |
| 局部非静态对象 | 栈 | 函数返回后自动销毁 |
| new 分配的对象 | 堆 | 需手动 delete |
this 指针
非静态成员函数编译为普通函数 + 隐式第一参数 this:
struct Point {
int x, y;
void setX(int v) { x = v; } // 实际:void setX(Point* this, int v)
};编译后(Linux):
; Point::setX(Point* this, int v)
; this = rdi, v = esi
setX:
mov [rdi], esi ; this->x = v (x 在偏移 0)
ret
this->x就是*(this + 0),即[rdi]。这是所有成员访问的汇编真相。
虚函数与虚表
虚函数通过虚表(vtable)间接调用:
struct Base {
int data;
virtual void f() {}
};
Base b;
b.f(); // 直接调用 Base::f(静态绑定)
Base* p = &b;
p->f(); // 间接调用:p->vptr[0]()对象内存布局:
b:
┌─────────┬─────────┐
│ vptr │ data │
│ (8B) │ (4B) │
└─────────┴─────────┘
│
└──→ vtable (在 .rodata):
┌──────────────┐
[0] │ &Base::f() │ ← 函数指针
└──────────────┘
汇编(Linux,p 指向的 Base 对象):
; p->f() 等价于:
mov rax, [rdi] ; rax = p->vptr(取虚表地址)
call [rax] ; 调用 vtable[0] 指向的函数普通函数:
call 固定地址。虚函数:先取 vptr → 再取函数指针 → 间接 call。多一次内存访问,这就是虚函数有额外开销的汇编根源。详见 07_面向对象(三)多态与虚函数。
构造函数与析构函数
构造函数在对象分配后自动调用,析构函数在对象销毁前自动调用:
struct Foo {
int x;
Foo() : x(0) {} // 构造函数
~Foo() {} // 析构函数
};
{
Foo obj; // 栈对象 → 构造在此调用
} // 出作用域 → 析构在此调用汇编:
; 分配栈空间
sub rsp, 16
; 调用构造函数(this = 栈上的对象地址)
lea rdi, [rsp+4]
call Foo::Foo
; ... 使用 obj ...
; 调用析构函数
lea rdi, [rsp+4]
call Foo::~Foo
; 释放栈空间
add rsp, 16
new Foo等价于operator new(sizeof(Foo))+Foo::Foo(this)的调用。delete p等价于p->~Foo()+operator delete(p)的调用。
初始化列表 vs 函数体赋值
// 初始化列表:直接构造
Foo::Foo(int v) : x(v) {}
// 函数体赋值:先默认初始化,再赋值
Foo::Foo(int v) { x = v; }初始化列表生成更高效的汇编(少一条指令),对于 std::string 等非平凡类型差异更大。这也是 C++ 教程 05_面向对象(一)类与对象基础 中强调初始化列表的原因。
名称修饰(Name Mangling)
C++ 支持函数重载,编译器通过名称修饰区分不同重载版本:
void func(int x);
void func(double x);编译后的符号名类似:
_Z4funci ← func(int)
_Z4funcd ← func(double)
名称修饰解释了为什么不同编译器(GCC vs MSVC)编译的 .o 文件不能混链——修饰规则不同。
extern "C"禁止修饰,使 C 和 C++ 可以互调。