什么是内联汇编?

  • 简单来说,内联汇编(Inline Assembly)是一种允许你“C语言代码里,直接嵌入几行汇编语言代码”的功能。
  • 它就像是你正在用高级语言(C)写信,但突然需要强调某个词,于是你切换到了古老的拉丁文(汇编)来书写那个词,然后再切换回 C 语言继续写信。
  • C 语言是“高级”语言,你写的 c = a + b; 最终由编译器来决定翻译成 ADD 指令。
  • 而内联汇编,就是你放弃了编译器的“翻译”,你“C 位出道”,直接以“上帝视角”告诉编译器:“嘿,编译器,你别管了,在代码的这个地方,你必须给我原封不动地插入这几条汇编指令!”

为什么要用它

  • 你可能会想,C 语言这么强大,为什么还要“手写”汇编?
  • 因为在某些极端情况下,C 语言“做不到”或者“做得不够好”:
    1. 访问特定的 CPU 寄存器:C 语言没有“操作 CR0 寄存器”的函数。在内核开发中,你需要用汇编 mov cr0, eax 来直接控制 CPU 的行为(比如开关缓存、启用保护模式)。
    2. 调用特定的 CPU 指令:C 语言标准库里没有“CPUID”指令(用来获取 CPU 厂商、型号信息)。你必须用汇编的 cpuid 指令。
    3. 极致的性能优化:有时候,你(作为一个顶尖高手)确信你手写的汇编在当前场景下比编译器自动生成的汇编更高效。
    4. 访问硬件 I/O 端口:在驱动开发中,你需要用 IN 和 OUT 这样的汇编指令去读写硬件的端口,C 语言没有这个能力。

如何使用?

  • 我们就以“在 C 语言中调用一个汇编函数来实现 c = a + b”为案例,用两种方式分别实现它。

方式一:独立汇编文件 (Standalone Assembly)

  • 关键:C 语言和汇编语言能“对话”的秘密,在于它们都遵守了“调用约定” (Calling Convention)。
  • 在 32 位 ARM (AArch32) 上,规则是:
    • 第一个参数 (a) 传入 r0 寄存器。
    • 第二个参数 (b) 传入 r1 寄存器。
    • 函数必须把返回值放入 r0 寄存器。
    • lr (Link Register) 寄存器保存了函数该返回到哪里,用 bx lr 来返回。
  • 文件 1:my_add_arm.s (纯 ARM 汇编代码)
    .global my_add  @ 告诉链接器,"my_add" 这个名字要暴露给C语言
    .syntax unified @ 使用统一的 ARM/Thumb 语法
    .thumb          @ 使用 Thumb 指令集 (在嵌入式中更常见,代码更紧凑)
    
    .text           @ 表示下面是代码
    
    @ 定义 "my_add" 函数
    @ 根据 ARM 调用约定:
    @   r0 = 第一个参数 (a)
    @   r1 = 第二个参数 (b)
    my_add:
        @ 核心指令:r0 = r0 + r1
        @ 把 r0 和 r1 相加,结果存回 r0
        add r0, r0, r1
        
        @ 函数返回
        @ "bx lr" (跳转到链接寄存器 lr 中保存的返回地址)
        @ 因为结果已经在 r0 中了,而 r0 恰好是返回值寄存器,
        @ 所以我们什么都不用做,直接返回即可。
        bx lr
    
    • @ 符号是 ARM 汇编中的注释。
    • add r0, r0, r1 这一行就完成了 C 语言 c = a + b 的所有工作,并且结果完美地留在了 r0 中,符合调用约定。
  • 文件 2:main_arm.c (C 语言调用代码)
    #include <stdio.h> // 在嵌入式中,你可能用的是你自己的日志/串口打印
    
    // “声明”我们用汇编写的函数
    extern int my_add(int a, int b);
    
    // 在嵌入式中,这可能是你的 "main" 任务
    int main() {
        int a = 10;
        int b = 25;
        
        // 像调用一个普通 C 函数一样,调用汇编函数
        int result = my_add(a, b);
        
        // 在 PC 上我们会用 printf
        printf("C 语言调用 ARM 汇编函数...\n");
        printf("%d + %d = %d\n", a, b, result); // 应该输出 35
        
        return 0;
    }
    
  • 上述两个文件交叉编译并链接到一起就可以使用了。

方式二:内联汇编 (Inline Assembly)

  • 核心思想:我们直接在 C 代码的 main 函数内部,“塞入” 几行 ARM 汇编指令。
    #include <stdio.h>
    
    int main() {
        int a = 10;
        int b = 25;
        int result = 0; // C 语言变量,用来接收结果
    
        printf("C 语言变量: a = %d, b = %d\n", a, b);
    
        // ======== 这就是 ARM 的内联汇编块 ========
        asm volatile (
            // %0, %1, %2 是 C 变量的占位符
            // "add %0, %1, %2"
            // 意思是: result = a + b
            "add %0, %1, %2"
    
            /* : 输出操作数列表 */
            : "=r" (result)   // %0: result。 "=r"表示:
                              // "=": 这是只写的
                              // "r": 放到任意通用寄存器(Register)
    
            /* : 输入操作数列表 */
            : "r" (a),        // %1: a。 "r"表示:
                              //     放到任意通用寄存器
              "r" (b)         // %2: b。 "r"同上
    
            /* : 被弄乱的列表 */
            : "cc"            // 告诉编译器:
                              // "我的 add 指令修改了CPU的标志位(cc)"
                              // (ARM的ADD指令会更新 N, Z, C, V 标志)
        );
        // ===========================================
    
        printf("内联汇编计算结果: result = %d\n", result); // 应该输出 35
    
        return 0;
    }
    
    • ARM 的 add 指令(非 Thumb-2)通常是三操作数指令 (ADD Rd, Rn, Rm),即 目标 = 操作数1 + 操作数2。
    • 这和 x86addq %rsi, %rax ( 目标 = 目标 + 操作数 ) 有点不同。
    • add %0, %1, %2 完美地对应了 C 语言的 result = a + b,这甚至比 x86mov+add 更直观!

使用的疑问

  • 我一边是变量ab,一边又是r0 r1,这是怎么对应上的?靠指针吗?

独立汇编文件

  • 靠“C 编译器”和“调用约定” (Calling Convention)
  • 在你调用 my_add(a, b); 的那一刻,你(C语言)并不是直接把 a 和 b 的指针扔给 my_add 函数。
  • 你遵守的是一个“调用约定” (AAPCS),这个“约定”就像一个快递公司的投递规则:
    1. C 编译器 (发货人):它想调用 my_add 函数,并把 a 和 b 两个“包裹”(值)寄过去。
    2. 调用约定” (快递规则):规则手册上写着:“寄给函数的第一个包裹,必须放入 r0 投递箱;第二个包裹,必须放入 r1 投递箱。”
    3. C 编译器 (遵守规则):OK。于是在调用 my_add 之前,gcc 编译器自动在 main_arm.c 中插入了隐藏的汇编代码。这些代码在C里是“隐身”的,但它真实存在:
      ; C 语言的 main 函数里...
      ; 假如 a 存在 [sp, #8] (栈上第8字节)
      ; 假如 b 存在 [sp, #4] (栈上第4字节)
      
      ldr   r0, [sp, #8]    @ 1. 把 a (10) 的值从内存加载到 r0
      ldr   r1, [sp, #4]    @ 2. 把 b (25) 的值从内存加载到 r1
      
      bl    my_add          @ 3. 现在,带着 r0=10 和 r1=25,跳转去执行 my_add
      
      ; ...my_add 返回后...
      mov   r2, r0          @ 4. 编译器知道返回值在 r0,把它存到 result 变量
      
    4. 你的汇编 (收货人):当你的 my_add.s 代码开始执行第一行时,r0r1 早就已经被 C 编译器准备好了。你什么都不用管,你只需要相信“规则”,直接从 r0r1 取货(add r0, r0, r1)就行了。
  • 所以: 这种方式下,是 C 编译器 扮演了“桥梁”,它主动生成 ldr (Load Register) 指令,把内存中的 ab 的值,拷贝到了寄存器 r0r1 中。

内联汇编 (asm())

  • 靠“编译器”和“约束占位符” (%0, %1, %2)
  • asm() 语法是一种更高级、更灵活的“契约”。你不再和编译器“约定”必须用 r0r1
  • 相反,你是在告诉编译器:“嘿,编译器,我需要你帮我个忙。我要做 add 操作。”
    1. add %0, %1, %2”:你写下的不是 add r0, r1, r2。你写的是“占位符”。
    2. %0 = “请帮我找个地方放结果”
    3. %1 = “请帮我找个地方放第一个输入”
    4. %2 = “请帮我找个地方放第二个输入”
    5. : "=r"(result):你告诉编译器,“结果 %0” 对应 C 语言的 result 变量。
    6. : "r"(a), "r"(b):你告诉编译器,“输入1 %1” 对应 a,“输入2 %2” 对应 b。
    7. "r" 约束:你告诉编译器:“你(编译器)是自由的!你可以把 a 放到 r0,也可以放到 r4、r7… 随便哪个通用寄存器 (r) 都可以,你看着办,只要你方便就行。”
  • 所以,“对应”是这样发生的:
  • 编译器在编译你的 asm() 块时,它可能会临时决定:
    • “嗯,现在 r3 是空闲的,我就用 r3 来放 a(%1) 吧。”
    • r5 也是空闲的,我就用 r5 来放 b(%2) 吧。”
    • r2 也空闲,我就用 r2 来放 result(%0) 吧。”
  • 然后,编译器把你写的模板 “add %0, %1, %2”,自动“翻译”成了:add r2, r3, r5 @ 编译器自动生成的“真实”汇编
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐