在实际开发过程中大多数的使用情况是C语言与汇编语言混合编程的形式。在C代码中插入汇编语言的方法有内联汇编和内嵌汇编两种,通过插入汇编可以在C程序中实现C语言不能完成的一些工作。例如,在下面几种情况中必须使用内联汇编或嵌入型汇编。
(1)程序中使用饱和算术运算,如SSAT16和USAT16指令。
(2)程序中需要对协处理器进行操作。
(3)在C程序中完成对程序状态寄存器的操作。
当然,熟练运用汇编在项目中需要经历一些编程学习的过程:
(1)熟悉 ARM 汇编指令集(MIPS / RISC-V 类似)
(2)理解 CPU 模式、寄存器组织结构
(3)学会写简单的汇编函数
(4)掌握 C 与汇编的混合语法(GCC + Keil)
(5)应用于实际项目(如裸机 OS、RTOS 调度器)(在实际项目中进行混合开发)
✨ 为什么需要混合编程?
在嵌入式系统、操作系统内核、驱动开发等开发场景中,C语言的抽象能力 + 汇编的底层控制能力组合使用,可以:
(1)提高性能
(2)实现底层硬件操作
(3)控制特殊寄存器或处理器指令
🧱 常用方式:两种混合方法
方法 | 说明 | 适用 |
---|---|---|
内联汇编(Inline Assembly) | 在 C 函数中直接插入汇编语句 | GCC、Keil |
外联汇编(External Assembly) | 汇编代码写在 .s /.asm 文件中,通过链接使用 |
模块独立,结构清晰 |
方式 | 内联汇编(Inline Assembly) | 外联汇编(External Assembly) |
---|---|---|
📄 定义位置 | 写在 .c 文件中 写在 |
.s / .S / .asm 文件中 |
📌 使用方法 | 使用 __asm 或 asm 关键字 |
写标准汇编函数,通过 extern 引用 |
📦 可读性 | 嵌在 C 中,代码混杂 | 汇编与 C 分离,结构清晰 |
📚 维护性 | 差(混合逻辑) | 高(模块独立) |
✅ 优点 | 快速嵌入一两条汇编指令 | 可重用、结构清晰、支持复杂逻辑 |
🚫 缺点 | 语法复杂、可读性差 | 编译配置略复杂 |
⚙️ 支持平台 | Keil、GCC(语法不同) | 所有主流工具链 |
💡 典型用途 | 简单寄存器访问、特殊指令 | 中断处理、系统调用、启动代码 |
举个例子:应用场景(🧩 必须使用汇编的典型案例)
(1)使用特殊指令:饱和算术(Saturating Arithmetic)
指令如:SSAT16、USAT16
c
SSAT R0, #8, R1 ; 将 R1 的值限制在 8 位有符号范围内,结果存在 R0
- 作用:防止数值溢出,提高数值稳定性
- 示例用途:音频信号处理、图像计算
(2)访问协处理器(如 FPU、系统控制寄存器)
某些协处理器(如浮点协处理器 FPU、CP15)不能用 C 语言直接访问,此时需要使用 MRC/MCR 等指令:
c
MRC p15, 0, r0, c1, c0, 0 ; 从协处理器读取寄存器
(3)操作程序状态寄存器(Program Status Register)
如 CPSR(当前程序状态寄存器)、SPSR(保存的程序状态寄存器)
c
MRS R0, CPSR ; 读取 CPSR
ORR R0, R0, #0xC0 ; 关闭 IRQ/FIQ
MSR CPSR_c, R0 ; 写回 CPSR
- 用于控制中断、模式切换、标志位等
- 典型场景:进入/退出临界区、操作中断屏蔽位
✅ 一、内联汇编(Inline Assembly)
📌 GCC 风格语法(AT&T 语法)
c
int result;
__asm__ volatile("mov %1, %0" : "=r" (result) : "r" (123));
c
__asm volatile ("MRS %0, cpsr" : "=r" (result));
"MRS %0, cpsr"
:汇编部分: "=r" (result)
:输出部分,%0 表示第一个输出变量- 可加
: : : "memory"
等约束来控制寄存器、内存同步等
📌 Keil 风格语法(ARM 风格)
1、简单指令
c
__asm {
MOV R0, #1
ADD R0, R0, #2
}
2、函数中调用汇编
c
void disable_irq(void) {
__asm {
MRS R0, CPSR
ORR R0, R0, #0x80
MSR CPSR_c, R0
}
}
📌 应用场景
操作 PRIMASK、CONTROL、xPSR 等系统寄存器;
插入一两条汇编指令(如
NOP
,WFI
,BKPT
)实现临界区控制、快速运算等
✅ 二、外联汇编(External Assembly)
📄 示例:汇编文件 asm_func.s
c
.syntax unified
.cpu cortex-m3
.thumb
.global add_two_numbers
add_two_numbers:
ADD R0, R0, R1
BX LR
📄 示例:C 文件 main.c
c
#include <stdio.h>
extern int add_two_numbers(int a, int b);
int main() {
int result = add_two_numbers(10, 20);
printf("Result = %d\n", result);
}
📌 应用场景
启动文件(如
startup.s
)中断服务函数保存上下文
实现软中断、系统调用封装
独立汇编模块,如 DSP 优化函数
二者在实际开发中:
如果你要做关闭中断、读状态寄存器,或是 在 C 中快速插入 1-2 条指令,这类操作推荐使用内联汇编;如果你要写启动代码、上下文切换,或是某个函数全用汇编提速,这类操作推荐使用外联汇编。
🧰 附加工具链支持情况
工具链 / IDE | 内联汇编 | 外联汇编 | 说明 |
---|---|---|---|
Keil MDK | 支持 __asm |
.s 文件 |
ARM 风格 |
GCC (arm-none-eabi) | __asm__ |
.S 文件 |
AT&T 风格,推荐 .S |
IAR | __asm / asm |
.s87 文件等 |
支持外部汇编模块 |
📘 汇编与 C 程序相互调用的 AAPCS 规范
AAPCS(ARM Architecture Procedure Call Standard) 是 ARM ABI 中定义的子程序调用规则,它确保了 C 与汇编之间可以安全互相调用 ------ 不论是不同语言写的函数,还是不同编译器生成的代码,只要都遵守 AAPCS,就可以协同工作。
汇编程序、C 程序相互调用时,要特别注意遵守相应的 AAPCS 规则。
AAPCS 简介
AAPCS 是 ARM 的函数调用规范,规定了:
内容 | AAPCS 规则 |
---|---|
参数传递 | 前 4 个参数通过 R0~R3 传递,第 5 个及以上通过栈传递 |
返回值 | 通过 R0 (或 R0+R1 )返回 |
栈对齐 | 8 字节对齐 |
寄存器保存 | R0R3 、R12 调用者保存;R4R11 被调用者保存 |
返回方式 | 使用 BX LR 或 POP {..., PC} |
函数入口对齐 | 通常为 4 字节对齐 |
浮点参数 | 若启用 FPU ,使用 S0~S15 传递浮点参数 |
代码示例一:1. 从 C 调用汇编语言函数
📄 1.1 汇编文件 :asm_add.s
c
.syntax unified
.cpu cortex-m3
.thumb
.global add_numbers
.type add_numbers, %function
add_numbers:
ADD R0, R0, R1 // R0 = R0 + R1
BX LR // 返回结果在 R0
📄 1.2 C 文件 :main.c
c
#include <stdio.h>
// 声明汇编函数:符合 AAPCS 规则
extern int add_numbers(int a, int b);
int main(void) {
int result = add_numbers(10, 20);
printf("Result = %d\n", result); // 输出:30
return 0;
}
AAPCS 保证了:
10
→ R0,20
→ R1- 汇编函数用
R0 + R1 → R0
,返回值通过R0
返回 BX LR
返回调用点
代码示例二:2. 从汇编语言调用 C 函数
📄 2.1 C 文件 :math_func.c
c
#include <stdio.h>
// 被汇编调用的 C 函数
int multiply(int a, int b) {
return a * b;
}
📄 2.2 汇编文件 :asm_main.s
c
.syntax unified
.cpu cortex-m3
.thumb
.global main
.type main, %function
.extern multiply // 声明外部 C 函数
main:
MOV R0, #6 // 参数 a = 6
MOV R1, #7 // 参数 b = 7
BL multiply // 调用 C 函数 multiply(a, b)
// R0 中是返回值 6 * 7 = 42
BKPT #0 // 断点,结束程序
AAPCS 保证了:
- 汇编用
R0
、R1
传参 - C 函数使用
R0
、R1
接收参数,结果通过R0
返回 - 使用
BL
指令调用函数,链接地址自动保存在LR
汇编中使用 AAPCS 的技巧
🔒 寄存器保护示例(被调用方)
c
.global foo
.type foo, %function
foo:
PUSH {R4-R7, LR} // 保存被调用者需要保护的寄存器
; 函数体...
; 可使用 R4~R7 自由操作
POP {R4-R7, LR} // 恢复寄存器并返回
BX LR
遵守规则:
规则 | 原因 |
---|---|
使用 .global 导出函数名 |
供 C 链接器识别 |
使用 .type <name>, %function |
指明符号是函数 |
参数必须使用 R0~R3 |
否则参数错位 |
使用 BX LR 返回 |
避免非法返回地址 |
保证栈 8 字节对齐 | 否则有些库函数运行异常(如 printf ) |
不要破坏 R4~R11 寄存器 |
若修改,必须手动保存并恢复 |
🧾 附加:GCC 编译指令建议(裸机 Cortex-M):
bash
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -g -nostdlib main.c asm_add.s -o main.elf
AAPCS中定义了ARM寄存器使用规则如下
(1)在进行子函数调用时,当子函数中有参数时,子程序间通过寄存器 R0、R1、R2、R3 来传递参数,如果参数多于 4 个,则多出的部分用堆栈传递,被调用的子程序在返回前无须恢复寄存器 R0~R3 的内容。
(2)在子程序中,使用寄存器 R4~R11 来保存局部变量。如果在子程序中使用到了 R4 ~ R11 中的某些寄存器,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值;对于子程序中没有用到的寄存器则不必进行这些操作。在 Thumb 程序中,通常只能使用寄存器 R4 ~ R7 来保存局部变量。
(3)寄存器 R12 用作子程序间 scratch 寄存器(用于保存 SP,在函数返回时使用该寄存器出栈),记作 ip。在子程序间的连接代码段中常使用这种规则。
(4)寄存器 R13 用作数据栈指针,记作 sp,sp 中存放的是当前使用的栈的栈顶的地址。在子程序中寄存器 R13 不能用作其他用途。寄存器 sp 在进入子程序时的值和退出子程序时的值必须相等。
(5)寄存器 R14 称为连接寄存器,记作 lr。它用于保存子程序的返回地址。如果在子程序中保存了返回地址,寄存器 R14 则可以用作其他用途。
(6)寄存器 R15 是程序计数器,记作 pc。它不能用作其他用途。

在开发中使用混合编程的价值所在:
bash
性能优化 精准控制寄存器、减少指令周期
硬件访问 控制器件或寄存器、系统初始化
系统安全 中断屏蔽、临界区保护
内核开发 bootloader、上下文切换、调度器实
本文介绍了 ARM 程序设计的过程与方法,包括汇编语言程序、伪指令的使用、汇编器的使用、汇编和C混合编程等内容。这些内容是嵌入式编程的基础,希望有缘并看到这里的读者掌握,丰富自身技术栈,在这条路中更进一步。
以上,欢迎有从事同行业的电子信息工程、互联网通信、嵌入式开发的朋友共同探讨与提问,我可以提供实战演示或模板库。希望内容能够对你产生帮助!