a photo of Whexy

Wenxuan

CyberSecurity Researcher at Northwestern University

C 中的内联汇编语言

Whexy /
November 14, 2020

编写汇编代码既困难又无聊!然而,如果你想设置寄存器、读取内存,有时你必须做这些 “脏活”。

幸运的是我们有 GCC 的帮助。GCC 提供了一个关键字 asm,允许你在 C 代码中嵌入汇编指令。C 语言绝对救了我们的命。

基本 Asm

基本 asm 语句具有以下语法:

asm asm-qualifiers ( Assembler Instructions );

例如,在 PMU 设置 中,我们想读取系统寄存器 PMCR_EL0,以确保 PMU 是否设置成功。相应的汇编指令是 MRS x0, PMCR_EL0。在 C 代码中,我们可以写:

asm ("mrs x0, PMCR_EL0");

请注意,asm-qualifiers 被省略了。所有的基本 asm 块都隐式地是 “volatile” 的。

你应该被警告,GCC 不解析汇编指令,对它们的意思甚至是否有效都一无所知

扩展 Asm

基本 asm 对周围的 C 语法一无所知。例如,如果你想将存储在 C 变量 int_a 中的整数放入寄存器,你不能使用基本 asm

使用扩展 asm,你可以从汇编中读写 C 变量,并从汇编代码执行跳转到 C 标签。扩展 asm 具有以下语法:

asm qualifiers (AssemblerTemplate
                 : output_constraint (C lvalue)
                 : input_constraint (C expression)
                 : Clobbers)

一些汇编指令会产生副作用,我们应该显式地使用限定符 volatile 告诉 GCC 不要优化我们的代码(是的,GCC 真的很聪明,有时我们需要告诉它不要这么聪明)。

约束

约束有点令人困惑。我们只需要知道一个在配置 Ninja 时可能使用的常见约束。

约束 r 标记相关的计算结果可以存储在通用寄存器中,通常用于输入约束。另一方面,=r 意思相同,但是只写寄存器,通常用于输出约束。

例如,以下代码改变系统寄存器中的值。

asm volatile("msr pmintenset_el1, %0" : : "r" ((u64)(0 << 31)));

它标记 C 表达式 (u64) (0<<31) 存储在任何通用寄存器中。运行时,GCC 将分配一个通用寄存器来填充汇编模板中的 %0,并用表达式的计算结果填充它。

另一个打印存储在系统寄存器中的值的例子。

long long f;
asm("mrs %0, PMCR_EL0" : "=r" (f));
printk(KERN_INFO "PMCR_EL0 = %llu\n", f);

它标记 C 变量 (long long) f 被处理为任何通用寄存器。运行时,GCC 将分配一个通用寄存器来填充汇编模板中的 %0,并将其与变量 f 绑定。

Clobber 列表

扩展 asm 被设计为将 C 表达式作为输入,并将 C 左值写入输出。但有时我们的汇编代码有副作用,比如我们会改变另一个寄存器中存储的值。

不幸的是,GCC 不知道这种影响,因为它无法理解汇编指令(还记得 GCC 是为了理解 C 代码而设计的吗?)。所以我们必须显式地列出受影响的寄存器,这就是我们有 clobber 列表的原因。

这里是一个例子:

asm( "movl %0,%%eax;\n\tmovl %1,%%ecx;\n\tcall _foo"
        : /*no outputs*/
        : "g" (from), "g" (to)
        : "eax", "ecx"
    );

这是一个 x86 的例子,不用担心你无法理解,因为我也不能。我们唯一关心的是我们将 eaxecx 标记为 clobber,所以 GCC 不会依赖这两个寄存器来维护其他东西。

© LICENSED UNDER CC BY-NC-SA 4.0