x86-64 汇编 —— C++ 特性在汇编层的体现


本文件连接 C++ 语言特性与其编译后的汇编实现,是理解 C++ 教程底层原理部分的汇编基础。

内存分段

编译后的程序在内存中分为若干段:

graph TD
    subgraph "进程虚拟地址空间"
        STACK["栈<br/>← 局部变量、函数调用帧<br/>(向低地址增长)"]
        HEAP["堆<br/>← new/malloc 分配<br/>(向高地址增长)"]
        BSS[".bss<br/>← 未初始化全局/静态变量<br/>(零填充)"]
        DATA[".data<br/>← 已初始化全局/静态变量"]
        RODATA[".rodata<br/>← 只读数据<br/>(字符串常量、虚函数表)"]
        TEXT[".text<br/>← 代码(机器指令)"]
    end
    STACK --> HEAP --> BSS --> DATA --> RODATA --> TEXT
    L["低地址"] -.-> 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]()

对象内存布局:

graph TD
    subgraph "Base 对象 b (栈/堆)"
        OBJ["vptr (8B)<br/>data (4B)"]
    end
    subgraph "vtable (在 .rodata)"
        VT["[0] &Base::f()<br/>← 函数指针"]
    end
    OBJ -->|"指向"| VT

汇编(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++ 可以互调。

练习

以下题目从汇编/底层视角训练相关能力:

题号题目链接涉及知识点
146LRU缓存https://leetcode.cn/problems/lru-cache/构造函数/析构函数、生命周期
208实现Trie(前缀树)https://leetcode.cn/problems/implement-trie-prefix-tree/类的设计、多态思想
707设计链表https://leetcode.cn/problems/design-linked-list/虚函数表思想、间接调用