嵌入式C语言——一文搞懂C与ARM汇编混合编程:从独立.s文件 到 内联asm()
·
什么是内联汇编?
- 简单来说,内联汇编(Inline Assembly)是一种允许你“C语言代码里,直接嵌入几行汇编语言代码”的功能。
- 它就像是你正在用高级语言(C)写信,但突然需要强调某个词,于是你切换到了古老的拉丁文(汇编)来书写那个词,然后再切换回 C 语言继续写信。
- C 语言是“高级”语言,你写的
c = a + b;最终由编译器来决定翻译成 ADD 指令。 - 而内联汇编,就是你放弃了编译器的“翻译”,你“C 位出道”,直接以“上帝视角”告诉编译器:“嘿,编译器,你别管了,在代码的这个地方,你必须给我原封不动地插入这几条汇编指令!”
为什么要用它
- 你可能会想,C 语言这么强大,为什么还要“手写”汇编?
- 因为在某些极端情况下,C 语言“做不到”或者“做得不够好”:
- 访问特定的 CPU 寄存器:C 语言没有“操作 CR0 寄存器”的函数。在内核开发中,你需要用汇编 mov cr0, eax 来直接控制 CPU 的行为(比如开关缓存、启用保护模式)。
- 调用特定的 CPU 指令:C 语言标准库里没有“CPUID”指令(用来获取 CPU 厂商、型号信息)。你必须用汇编的 cpuid 指令。
- 极致的性能优化:有时候,你(作为一个顶尖高手)确信你手写的汇编在当前场景下比编译器自动生成的汇编更高效。
- 访问硬件 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来返回。
- 第一个参数 (a) 传入
- 文件 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。 - 这和
x86的addq %rsi, %rax( 目标 = 目标 + 操作数 ) 有点不同。 add %0, %1, %2完美地对应了 C 语言的result = a + b,这甚至比x86的mov+add更直观!
- ARM 的
使用的疑问
- 我一边是变量
a和b,一边又是r0 r1,这是怎么对应上的?靠指针吗?
独立汇编文件
- 靠“C 编译器”和“调用约定” (Calling Convention)
- 在你调用
my_add(a, b);的那一刻,你(C语言)并不是直接把 a 和 b 的指针扔给 my_add 函数。 - 你遵守的是一个“调用约定” (AAPCS),这个“约定”就像一个快递公司的投递规则:
- C 编译器 (发货人):它想调用
my_add函数,并把 a 和 b 两个“包裹”(值)寄过去。 - “调用约定” (快递规则):规则手册上写着:“寄给函数的第一个包裹,必须放入
r0投递箱;第二个包裹,必须放入r1投递箱。” - 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 变量 - 你的汇编 (收货人):当你的
my_add.s代码开始执行第一行时,r0和r1早就已经被 C 编译器准备好了。你什么都不用管,你只需要相信“规则”,直接从r0和r1取货(add r0, r0, r1)就行了。
- C 编译器 (发货人):它想调用
- 所以: 这种方式下,是 C 编译器 扮演了“桥梁”,它主动生成
ldr (Load Register)指令,把内存中的a和b的值,拷贝到了寄存器r0和r1中。
内联汇编 (asm())
- 靠“编译器”和“约束占位符” (
%0, %1, %2) - asm() 语法是一种更高级、更灵活的“契约”。你不再和编译器“约定”必须用
r0和r1。 - 相反,你是在告诉编译器:“嘿,编译器,我需要你帮我个忙。我要做
add操作。”- “
add %0, %1, %2”:你写下的不是add r0, r1, r2。你写的是“占位符”。 %0= “请帮我找个地方放结果”%1= “请帮我找个地方放第一个输入”%2= “请帮我找个地方放第二个输入”: "=r"(result):你告诉编译器,“结果%0” 对应 C 语言的result变量。: "r"(a), "r"(b):你告诉编译器,“输入1 %1” 对应 a,“输入2 %2” 对应 b。"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 @ 编译器自动生成的“真实”汇编
更多推荐
所有评论(0)