Skip to content

第五章:内联汇编

涉及内核源码: , ,

楔子

林小源理解了寄存器,但他发现了一个矛盾。

内核是用 C 写的,但有些操作——比如读取控制寄存器、发起系统调用、执行原子操作——C 语言无法直接完成。它们需要特定的汇编指令。

但内核不能全是汇编。汇编难写、难读、难维护。

怎么办?

答案是内联汇编——在 C 代码中嵌入汇编指令。

他第一次看到 asm volatile("csrr %0, satp" : "=r"(val)) 这行代码时,完全不知道它在说什么。但当他理解了约束("=r""+r""i")的含义后,他意识到这是一种极其精妙的设计——C 编译器负责寄存器分配和代码布局,程序员负责指定关键的汇编指令。

两者配合,既高效又安全。

但"安全"这个词,只在你写对了的时候成立。

一、基本语法

林小源在工坊的尽头找到了一扇暗门。

门后是一间狭小的密室,空气中弥漫着金属和臭氧的味道。密室的中央坐着一个老者——不是编译器大殿里那个灰袍老者,而是一个更苍老、更沉默的存在。他的左半边身体是 C 语言的语法结构,右半边身体是汇编指令的二进制流。两种截然不同的语言在他身上交汇,却奇异地和谐。

"你是谁?"林小源问。

老者抬起头,声音像两种语言同时在说话:"我是内联汇编。C 与汇编之间的桥梁。"

他伸出左手——那只手由 C 语言的花括号和分号组成:"C 语言是内核的骨架。它可读、可维护、可移植。但有些事它做不到——读取控制寄存器、执行原子操作、发起系统调用。"

又伸出右手——那只手由汇编指令的二进制编码组成:"汇编是机器的母语。它能做任何事——但它难写、难读、难维护。内核不能全是汇编。"

然后他合起双手:"所以有了我。在 C 代码中嵌入汇编指令。编译器负责寄存器分配和代码布局,你负责指定关键的汇编指令。两者配合,既高效又安全。"

"怎么用?"

老者在空中写了两行字:

c
asm("汇编指令");
asm volatile("汇编指令");  /* volatile: 防止编译器优化掉 */

" 告诉编译器:这条汇编有副作用,不能省略,不能移动顺序。没有 ,编译器可能觉得这条指令'没用',直接删掉。内核中的汇编几乎都用 。"

二、带约束的内联汇编

老者从桌上拿起一张泛黄的羊皮纸,上面写着完整的内联汇编语法:

c
asm volatile(
    "汇编模板"
    : 输出操作数    (可选)
    : 输入操作数    (可选)
    : 破坏列表      (可选)
);

"汇编模板是你要执行的指令。操作数用 %0%1%2 引用——编译器会把它们替换成实际的寄存器或内存地址。约束告诉编译器:这个操作数应该放在哪里。"

林小源看着那些约束符号,一头雾水。

老者指着羊皮纸上的注释:""r" — 通用寄存器。编译器选一个空闲的寄存器存放这个操作数。"=r" — 输出操作数,只写。"+r" — 同一个寄存器既输入又输出。"i" — 编译期立即数。"m" — 内存操作数。"

他在空中演示了一个例子:

c
int x;
asm volatile("li %0, 42" : "=r"(x));

"li %0, 42 — 把立即数 42 加载到 %0。约束 "=r" 告诉编译器:%0 是一个输出操作数,放在通用寄存器里。编译器会选一个空闲寄存器,生成 li reg, 42,然后把 的值赋给 x。"

又演示了一个:

c
int a = 10, b = 20, result;
asm volatile(
    "addw %0, %1, %2"
    : "=r"(result)
    : "r"(a), "r"(b)
);

"addw %0, %1, %2 — 把 %1%2 相加,结果存到 %0%1a%2b%0。编译器负责把 ab 放到寄存器里,再把结果写回 。"

"如果我想让输入和输出用同一个寄存器呢?"

老者露出赞许的表情:""+r"。比如 addi %0, %0, 1——自增。%0 既是输入又是输出,必须是同一个寄存器。"

三、内核中的常用内联汇编

老者从密室的墙壁上取下三个锦囊,一一展开。

第一个锦囊里写着 rdcycle

c
static inline uint64_t read_cycle(void) {
    uint64_t val;
    asm volatile("rdcycle %0" : "=r"(val));
    return val;
}

"rdcycle 读取 CPU 的周期计数器。从上电开始,CPU 每执行一个时钟周期,这个计数器就加一。用它可以精确测量代码的执行时间——两个 rdcycle 的差值就是消耗的周期数。"

第二个锦囊里写着

c
static inline uint64_t read_time(void) {
    uint64_t val;
    asm volatile("rdtime %0" : "=r"(val));
    return val;
}

" 读取时间寄存器。和 rdcycle 不同,它不是 CPU 的周期数,而是经过校准的纳秒级时间戳。用于需要真实时间的场景。"

第三个锦囊里写着

c
static inline void mb(void) {
    asm volatile("fence rw, rw" ::: "memory");
}

"内存屏障。fence rw, rw 告诉 CPU:在这条指令之前的所有读写操作,必须在之后的任何读写操作开始之前完成。没有乱序,没有重排。"

林小源看着那三个锦囊。一个测量时间,一个读取时钟,一个强制顺序。它们看似简单,却是内核中最基础的原语——没有它们,性能测量、定时器、并发控制都无从谈起。

"破坏列表里的 "memory" 是什么?"林小源问。

"告诉编译器:这条指令会修改内存。编译器不能假设内存中的值在这条指令前后不变——它必须重新从内存读取,而不是用缓存在寄存器中的旧值。"

四、带内存操作数的汇编

老者从密室的地板下取出一个铁箱。箱子里装着三件武器——每一件都散发着危险的气息。

第一件是一把双刃剑,剑柄上刻着 lr.w / sc.w

c
static inline int atomic_cmpxchg(int *ptr, int old_val, int new_val) {
    int prev, tmp;
    asm volatile(
        "0: lr.w %0, (%2)\n"
        "   bne  %0, %3, 1f\n"
        "   sc.w %1, %4, (%2)\n"
        "   bnez %1, 0b\n"
        "1:"
        : "=&r"(prev), "=&r"(tmp)
        : "r"(ptr), "r"(old_val), "r"(new_val)
        : "memory"
    );
    return prev;
}

"Load-Reserved / Store-Conditional,"老者的声音低沉,"这是 RISC-V 实现比较并交换的基础。lr.w 从内存读取一个值,同时在那个地址上设置一个'预约'。sc.w 尝试写入——如果预约还在(没有其他 CPU 修改过那个地址),写入成功;否则写入失败,整个操作从头重试。"

"如果另一个 CPU 在 lrsc 之间修改了那个地址?"

"sc.w 失败,返回非零值。代码跳回 0: 重新开始。这就是'条件'的含义——只有在没有竞争的情况下,写入才会成功。"

第二件武器是一柄重锤,锤面上刻着 amoadd.w

c
static inline void atomic_add(int *ptr, int val) {
    asm volatile(
        "amoadd.w zero, %1, (%0)"
        :
        : "r"(ptr), "r"(val)
        : "memory"
    );
}

"Atomic Memory Operation — Add。一条指令完成读取、加法、写回。在多核系统中,这保证了原子性——不会有另一个 CPU 在中间插一脚。"

第三件武器是一面盾牌,盾面上刻着 amoswap.w

c
static inline int xchg(int *ptr, int new_val) {
    int old;
    asm volatile(
        "amoswap.w %0, %2, (%1)"
        : "=r"(old)
        : "r"(ptr), "r"(new_val)
        : "memory"
    );
    return old;
}

"Atomic Memory Operation — Swap。读取旧值的同时写入新值,一条指令完成。自旋锁的实现就依赖这个——用 原子地把锁的状态从'未锁定'改为'已锁定'。"

林小源看着那三件武器。每一件都是多核世界的基石。没有它们,并发就是一句空话。

"Linux 内核把这些指令封装成通用接口,"老者说,"——上层代码不需要知道底层用的是 lr/sc 还是 amo*。架构不同,接口相同。"

五、系统调用的汇编实现

老者最后带着林小源来到密室最深处的一面石壁前。石壁上刻着一个完整的系统调用流程——从用户态到内核态,再从内核态返回。

c
static long my_write(int fd, const void *buf, size_t count) {
    register long a0 asm("a0") = fd;
    register long a1 asm("a1") = (long)buf;
    register long a2 asm("a2") = (long)count;
    register long a7 asm("a7") = SYS_write;

    asm volatile(
        "ecall"
        : "+r"(a0)
        : "r"(a1), "r"(a2), "r"(a7)
        : "memory"
    );
    return a0;
}

"这就是手动发起系统调用,"老者指着石壁,"register long a0 asm("a0") 告诉编译器:把变量 a0 固定在寄存器 a0 上。然后 触发陷入。返回值也放在 a0 中——用 "+r" 约束表示它既是输入又是输出。"

林小源看着石壁上的代码。每一步都精确到寄存器——a7 存系统调用号,a0a2 存参数, 触发陷入,a0 接收返回值。没有 libc 的包装,没有额外的开销——从用户态到内核态,只有这一条路。

"这就是 的本质,"老者说,"RISC-V 用户态进入内核的门户。每次用户态程序调用 read()write(),最终都会通过 进入内核。只不过 libc 帮你把参数放好了,你感受不到它的存在。"

老者转身走向密室的出口。他的半边 C 身体和半边汇编身体在门口的光线中融为一体。

"内联汇编是最后的手段,"他的声音从远处传来,"能用 C 解决的问题,不要用汇编。但有些事——读取 CSR、执行原子操作、发起系统调用——C 语言无能为力。那时候,你就需要我。"

"但记住——我是一把没有剑鞘的剑。用对了,削铁如泥。用错了,伤的是你自己。"


道藏笔记

内核启示

指令是 RISC-V 用户态进入内核的门户。 每次用户态程序调用 read()write(),最终都会通过 进入内核。

RISC-V 的系统调用入口在 附近展开:硬件从 U 态陷入 S 态,内核保存用户上下文,依据 a7 中的调用号查表分派,最后恢复寄存器并返回用户态。

原子操作同样体现了架构差异。RISC-V 用 lr/scamo* 指令表达原子读改写;内核把它们封装在统一的 atomic_*() 和锁原语后面,让上层代码不用关心具体指令。

代码典籍

c
int x;
asm volatile("li %0, 42" : "=r"(x));

int a = 10, b = 20, result;
asm volatile(
    "addw %0, %1, %2"
    : "=r"(result)
    : "r"(a), "r"(b)
);

int val = 100;
asm volatile(
    "addi %0, %0, 1"
    : "+r"(val)
);
c
static inline uint64_t read_cycle(void) {
    uint64_t val;
    asm volatile("rdcycle %0" : "=r"(val));
    return val;
}

static inline uint64_t read_time(void) {
    uint64_t val;
    asm volatile("rdtime %0" : "=r"(val));
    return val;
}

static inline void mb(void) {
    asm volatile("fence rw, rw" ::: "memory");
}

static inline void wmb(void) {
    asm volatile("fence w, w" ::: "memory");
}

static inline void rmb(void) {
    asm volatile("fence r, r" ::: "memory");
}
c
/* RISC-V LR/SC 实现的比较并交换 */
static inline int atomic_cmpxchg(int *ptr, int old_val, int new_val) {
    int prev, tmp;
    asm volatile(
        "0: lr.w %0, (%2)\n"
        "   bne  %0, %3, 1f\n"
        "   sc.w %1, %4, (%2)\n"
        "   bnez %1, 0b\n"
        "1:"
        : "=&r"(prev), "=&r"(tmp)
        : "r"(ptr), "r"(old_val), "r"(new_val)
        : "memory"
    );
    return prev;
}

static inline void atomic_add(int *ptr, int val) {
    asm volatile(
        "amoadd.w zero, %1, (%0)"
        :
        : "r"(ptr), "r"(val)
        : "memory"
    );
}

static inline int xchg(int *ptr, int new_val) {
    int old;
    asm volatile(
        "amoswap.w %0, %2, (%1)"
        : "=r"(old)
        : "r"(ptr), "r"(new_val)
        : "memory"
    );
    return old;
}
c
static long my_write(int fd, const void *buf, size_t count) {
    register long a0 asm("a0") = fd;
    register long a1 asm("a1") = (long)buf;
    register long a2 asm("a2") = (long)count;
    register long a7 asm("a7") = SYS_write;

    asm volatile(
        "ecall"
        : "+r"(a0)
        : "r"(a1), "r"(a2), "r"(a7)
        : "memory"
    );
    return a0;
}

static long my_getpid(void) {
    register long a0 asm("a0") = 0;
    register long a7 asm("a7") = SYS_getpid;

    asm volatile(
        "ecall"
        : "+r"(a0)
        : "r"(a7)
        : "memory"
    );
    return a0;
}

破关试炼

内联汇编试炼

RISC-V 从用户态陷入内核态发起系统调用的指令是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道