Linux C语言如何嵌入汇编?

99ANYc3cd6
预计阅读时长 18 分钟
位置: 首页 C语言 正文

嵌入汇编允许你将汇编代码直接插入到 C 语言代码中,这对于需要直接操作硬件、优化关键性能代码或实现 C 语言无法直接表达的功能(如某些 CPU 指令)的场景非常有用。

linux c语言嵌入汇编
(图片来源网络,侵删)

我们将从基本语法、操作数约束、再到一个完整的实例,一步步进行说明。


基本语法

在 GCC 中,嵌入汇编使用 asm__asm__ 关键字(两者等价),后面跟着一个用括号括起来的字符串,最完整的语法如下:

asm ( "assembly code"
      : output_operands
      : input_operands
      : clobbered_registers
);

这四个部分分别是:

  1. 汇编代码:这是你要执行的汇编指令字符串。
  2. 输出操作数:告诉编译器哪些寄存器或内存位置会被你的汇编代码修改,这些值会最终写回到 C 语言的变量中。
  3. 输入操作数:告诉编译器哪些 C 语言的变量需要被加载到寄存器或内存中,以便汇编代码使用。
  4. 破坏的寄存器:告诉编译器你的汇编代码会修改哪些寄存器(即使这些寄存器没有被列为输出操作数),这样编译器在使用这些寄存器保存临时数据时就会知道。

四个部分详解

A. 汇编代码

这是最核心的部分,就是你写的汇编指令。movl $1, %eax

linux c语言嵌入汇编
(图片来源网络,侵删)

B. 输入操作数

告诉编译器哪些 C 变量需要作为输入,格式为: [约束] "C变量名" (C表达式)

  • C变量名:在汇编代码中,你通过 %[C变量名] 来引用它。
  • C表达式:通常是 C 语言的变量。
  • 约束:这是最关键的部分,它告诉编译器如何处理这个变量(是放入寄存器还是内存,放入哪个寄存器等)。

C. 输出操作数

告诉编译器哪些寄存器或内存中的值最终要写回到 C 变量中,格式为: [约束] "C变量名" (C表达式)

  • C变量名:在汇编代码中,你通过 %[C变量名] 来引用它。
  • C表达式:通常是 C 语言的变量,它的值将被修改。
  • 约束:同样用于指定操作数的位置。

D. 破坏的寄存器

一个字符串列表,列出你的汇编代码会修改但没有在输出操作数中声明的寄存器,如果你在汇编代码中使用了 eax 但没有用它来输出结果,你就应该在这里写上 "eax",编译器会认为这些寄存器的值是“脏”的,如果之前有数据,会自动保存和恢复。


操作数约束

约束是嵌入汇编的精髓,它定义了 C 变量和汇编操作数之间的映射关系,常见的约束有:

linux c语言嵌入汇编
(图片来源网络,侵删)
约束 含义 示例
r 任意通用寄存器 %eax, %ebx, %ecx, %edx
a %eax 寄存器 a
b %ebx 寄存器 b
c %ecx 寄存器 c
d %edx 寄存器 d
S %esi 寄存器 S
D %edi 寄存器 D
m 内存操作数 变量的内存地址
q a, b, c, d 中的任意一个 q
=r 只写的通用寄存器 表示输出
+r 读写(输入和输出)的通用寄存器 表示读写
g 任意通用寄存器或内存 g

一个完整的约束示例:"=a" (output)

  • 表示这是一个输出操作数。
  • a:表示这个操作数应该被放入 %eax 寄存器。

一个完整的实例:交换两个整数

这是一个非常经典且易于理解的例子,我们用嵌入汇编来实现两个整数的交换。

C 代码 (swap.c)

#include <stdio.h>
// 函数声明
void swap(int *a, int *b);
int main() {
    int x = 10;
    int y = 20;
    printf("Before swap: x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("After swap:  x = %d, y = %d\n", x, y);
    return 0;
}
// 使用嵌入汇编实现的交换函数
void swap(int *a, int *b) {
    int temp;
    asm volatile (
        "movl %[src], %%eax;"     // 1. 将 src 指向的值加载到 eax
        "movl %[dest], %%ebx;"    // 2. 将 dest 指向的值加载到 ebx
        "movl %%eax, %[dest];"    // 3. 将 eax 的值(原src)存入 dest
        "movl %%ebx, %[src];"     // 4. 将 ebx 的值(原dest)存入 src
        : [src] "=m" (*a), [dest] "=m" (*b) // 输出操作数
        : [src] "m" (*a), [dest] "m" (*b)  // 输入操作数
        : "eax", "ebx", "memory"           // 破坏的寄存器
    );
    // 注意:这里的 "=m" 和 "m" 表示操作数在内存中。
    // 我们通过内存地址直接交换,不需要 temp 变量。
    // 但为了展示寄存器使用,我们下面提供一个更典型的版本。
}

上面的版本直接在内存中操作,虽然高效,但为了更好地展示寄存器约束,我们重写 swap 函数,使用一个临时寄存器 temp

更典型的 swap 函数版本

void swap(int *a, int *b) {
    int temp;
    asm volatile (
        "movl (%[src]), %%eax;"    // 1. 将 src 指向的内存中的值加载到 eax
        "movl (%[dest]), %%ebx;"   // 2. 将 dest 指向的内存中的值加载到 ebx
        "movl %%eax, (%[dest]);"   // 3. 将 eax 的值存入 dest 指向的内存
        "movl %%ebx, (%[src]);"    // 4. 将 ebx 的值存入 src 指向的内存
        : // 没有输出操作数,因为我们直接修改了内存
        : [src] "r" (a), [dest] "r" (b) // 输入操作数:将指针 a, b 放入任意寄存器
        : "eax", "ebx", "memory"        // 破坏的寄存器
    );
}

代码解析:

  1. asm volatile (...)

    • asm:关键字。
    • volatile:这是一个非常重要的修饰符,它告诉编译器不要优化这段汇编代码,即使编译器认为这段代码没有效果(它的输出没有被使用),也不要删除它,这对于硬件操作或需要精确时序的代码至关重要。
  2. 汇编代码

    • movl (%[src]), %%eax;%[src] 会被替换为 C 变量 a(即 &x)。(%[src]) 表示取 a 这个地址中的值,所以这行代码的意思是:把 x 的值放入 %eax 寄存器,注意, 用于寄存器, 用于在汇编字符串中表示一个 符号,因为 在汇编中有特殊含义。
    • movl (%[dest]), %%ebx;:同理,把 y 的值放入 %ebx
    • movl %%eax, (%[dest]);:把 %eax 中的值(原 x)存入 y 的地址。
    • movl %%ebx, (%[src]);:把 %ebx 中的值(原 y)存入 x 的地址。
  3. 输入操作数

    • [src] "r" (a)a&x)这个指针变量会被放入一个通用寄存器(由编译器选择,如 %esi%edi),在汇编代码中,我们通过 %[src] 来引用这个寄存器。
    • [dest] "r" (b):同理,b&y)也会被放入一个通用寄存器,通过 %[dest] 引用。
  4. 破坏的寄存器

    • "eax", "ebx":我们明确告诉编译器,%eax%ebx 的值在我们的汇编代码中被修改了,编译器会确保在进入 asm 块之前,如果这些寄存器中保存了有用的临时数据,会先将它们保存到栈上,并在 asm 块执行结束后恢复。
    • "memory":这是一个特殊的约束,它告诉编译器,我们的汇编代码会修改内存中的内容(通过指针 ab),这会阻止编译器在 asm 块之前和之后对内存进行不必要的缓存或重排操作,确保了内存操作的可见性。

编译和运行

将代码保存为 swap.c,使用 GCC 进行编译:

# -o 指定输出文件名
gcc -o swap swap.c

运行:

./swap

预期输出:

Before swap: x = 10, y = 20
After swap:  x = 20, y = 10

高级技巧与注意事项

  • 扩展汇编语法:我们上面使用的 "=r", "m" 等是 GCC 的扩展语法,不是标准的 AT&T 汇编语法,它极大地简化了与 C 编译器的交互。
  • 可移植性:嵌入汇编是高度平台相关的,上面的代码只能在 x86 或 x86-64 架构的 Linux 系统上运行,如果你需要为 ARM 编写,汇编语法和约束都会完全不同。
  • 性能优化:在大多数情况下,现代 C 编译器(如 GCC, Clang)的优化能力非常强大,手动嵌入汇编并不一定能带来性能提升,甚至可能因为破坏了编译器的优化而变得更慢。只有在经过性能分析(Profiling)确认是关键瓶颈,并且你对汇编和编译器行为有深入了解时,才应考虑使用嵌入汇编。
  • __attribute__((__always_inline__)):如果你希望一个包含嵌入汇编的函数总是被内联,可以使用这个属性,以避免函数调用的开销。
  • 查阅文档:GCC 的官方文档是学习嵌入汇编最权威的资料,可以搜索 "GCC Inline Assembly HOWTO"。

希望这份详细的指南能帮助你理解在 Linux C 语言中使用嵌入汇编!

-- 展开阅读全文 --
头像
织梦首页为何部分文字无法修改?
« 上一篇 今天
砍柴网dede最新模板,如何获取与使用?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]