手把手教你配置 ARM 交叉编译环境,实现 C 与汇编的相互调用,深入理解 ARM 调用约定
1. 引言:为什么要学习 ARM 混合编程?
在嵌入式开发和系统编程领域,C 语言与汇编语言的混合编程是一项至关重要的技能。通过混合编程,我们可以:
- 在汇编中实现极致性能优化
- 在 C 语言中方便地调用底层硬件功能
- 深入理解函数调用、参数传递等底层机制
- 为操作系统、驱动开发打下坚实基础
今天,我将带你从零开始,在 Windows 11 上搭建完整的 ARM 32 位开发环境,实现一个经典的混合编程示例:
- C 语言 (
main.c) 中定义main()和sum()函数 - 汇编语言 (
SUM.S) 中定义SUM_ASM子程序 main()函数调用汇编子程序SUM_ASMSUM_ASM内部又调用 C 语言的sum()函数
2. 开发环境搭建
2.1 为什么选择 WSL2?
虽然 Windows 11 是 x86_64 架构的主机,而我们要开发的是 ARM 32 位程序,但不用担心!我们通过 WSL2 (Windows Subsystem for Linux 2) 来搭建一个完美的开发环境。
WSL2 的优势:
- 原生 Linux 环境,工具链安装简单
- 高性能,接近原生 Linux 的速度
- 与 Windows 文件系统无缝集成
- 完美支持 QEMU 模拟器
2.2 安装 WSL2 和 Ubuntu
- 以管理员身份打开 PowerShell,执行以下命令:
powershell
# 启用WSL功能
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
# 设置WSL2为默认版本
wsl --set-default-version 2
# 安装Ubuntu发行版
wsl --install -d Ubuntu-24.04
-
重启计算机后,从开始菜单打开 Ubuntu,设置用户名和密码。
-
更换软件源(国内用户推荐,加速下载):
bash
# 备份原有源列表
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
# 使用华为镜像源(以Ubuntu 24.04为例)
sudo sed -i "s@http://.*archive.ubuntu.com@http://mirrors.huaweicloud.com@g" /etc/apt/sources.list
sudo sed -i "s@http://.*security.ubuntu.com@http://mirrors.huaweicloud.com@g" /etc/apt/sources.list
# 更新软件包列表
sudo apt update
2.3 安装开发工具链
在 Ubuntu 终端中执行以下命令,安装所有必要的工具:
bash
# 1. 安装ARM交叉编译工具链
# arm-linux-gnueabihf-gcc: ARM硬浮点交叉编译器
# gdb-multiarch: 支持多架构的调试器
# qemu-user-static: ARM模拟器
sudo apt install gcc-arm-linux-gnueabihf gdb-multiarch qemu-user-static -y
# 2. 验证安装是否成功
arm-linux-gnueabihf-gcc --version
qemu-arm-static --version
2.4 配置 VSCode
在 Windows 的 VSCode 中安装以下扩展:
- WSL 扩展(微软官方):在 VSCode 中直接使用 WSL 环境
- ARM Assembly:ARM 汇编语法高亮
- C/C++ 扩展:C 语言智能提示和调试支持
安装后,VSCode 左下角会出现绿色图标,点击后选择 " 连接到 WSL"。

3. 编写混合编程示例
3.1 创建项目目录
在 Ubuntu 终端中:
bash
# 创建项目文件夹
mkdir ~/arm_hybrid_demo
cd ~/arm_hybrid_demo
# 用VSCode打开
code .
VSCode 会以 WSL 模式打开当前文件夹,这时所有操作都在 Linux 环境中进行。
3.2 编写 C 语言文件(main.c)
创建 main.c 文件,内容如下:
c
#include <stdio.h>
// 声明外部汇编函数
extern int SUM_ASM(int a, int b);
// 供汇编调用的C函数
int sum(int a, int b) {
printf(" [C Function] sum() is running...\n");
printf(" [C Function] Calculating %d + %d\n", a, b);
return a + b;
}
int main() {
int val1 = 10;
int val2 = 20;
int result;
printf("---- ARM Hybrid Programming Demo ----\n");
printf("[Main] Calling ASM function SUM_ASM(%d, %d)...\n", val1, val2);
// C语言调用汇编函数
result = SUM_ASM(val1, val2);
printf("[Main] Return value from ASM: %d\n", result);
return 0;
}
代码说明:
extern int SUM_ASM(int a, int b);声明了一个外部汇编函数sum()函数会被汇编代码调用main()函数调用SUM_ASM(),完成 C→汇编的调用
3.3 编写汇编文件(SUM.S)
创建 SUM.S 文件(注意 S 是大写,这是 GNU 汇编器的惯例):
asm
.syntax unified @ 使用统一汇编语法
.thumb @ 使用Thumb指令集(ARM Cortex-M常用)
.text @ 代码段
.global SUM_ASM @ 导出符号,使C代码可以调用
.extern sum @ 声明外部C函数
@ 函数声明
.type SUM_ASM, %function
@ SUM_ASM函数定义
@ 参数:R0 = a, R1 = b
@ 返回值:R0 = a + b
SUM_ASM:
@ 保存现场(非常重要!)
@ 因为我们要调用C函数,BL指令会修改LR
@ 必须保存LR,否则无法返回main函数
PUSH {R11, LR}
@ 调用C函数sum(a, b)
@ 此时R0和R1已经是main函数传入的参数
@ 符合ATPCS调用约定
BL sum @ 调用sum函数,结果保存在R0
@ 恢复现场并返回
@ R0已经是sum函数的返回值,直接作为本函数的返回值
POP {R11, PC} @ 弹出LR到PC,实现函数返回
@ 告诉链接器这个代码不需要可执行栈
.section .note.GNU-stack,"",%progbits
.end
关键知识点:ARM ATPCS 调用约定
- 参数传递:前 4 个参数通过 R0-R3 传递
- 返回值:通过 R0 返回
- LR 寄存器:保存函数返回地址
- 栈对齐:必须是 8 字节对齐
为什么需要保存 LR?
当 SUM_ASM 使用 BL sum 调用 C 函数时,BL 指令会将返回地址(sum 执行完后应返回的地址)存入 LR。这会覆盖 SUM_ASM 自己的返回地址,所以必须先将 LR 压栈保存。
4. 编译与运行
4.1 编译步骤
在 VSCode 终端中执行:
bash
# 步骤1:编译C文件
# -c: 只编译不链接
# -g: 包含调试信息
arm-linux-gnueabihf-gcc -c main.c -o main.o -g
# 步骤2:编译汇编文件
# -mthumb: 生成Thumb指令集代码
arm-linux-gnueabihf-as -mthumb SUM.S -o SUM.o -g
# 步骤3:链接所有目标文件
# -static: 静态链接,避免QEMU运行时的动态库问题
arm-linux-gnueabihf-gcc main.o SUM.o -o demo -static
编译过程解析:
- 预处理 :处理
#include、#define等预处理指令 - 编译:将 C 代码转换为汇编代码
- 汇编:将汇编代码转换为机器码(目标文件)
- 链接:将多个目标文件合并为可执行文件
4.2 在 QEMU 中运行
bash
# 使用QEMU模拟ARM环境运行程序
qemu-arm-static -cpu cortex-a8 ./demo
运行结果:

---- ARM Hybrid Programming Demo ----
[Main] Calling ASM function SUM_ASM(10, 20)...
[C Function] sum() is running...
[C Function] Calculating 10 + 20
[Main] Return value from ASM: 30
执行流程分析:
main()开始执行,准备参数 10 和 20- 调用
SUM_ASM(10, 20),参数存入 R0 和 R1 SUM_ASM保存 LR,调用sum(10, 20)sum()执行加法,返回结果 30 到 R0SUM_ASM恢复现场,返回main()main()打印结果,程序结束
5. 使用 VSCode 调试(可视化学习)
调试是理解程序运行机制的最佳方式。我们配置 VSCode 的调试环境,可以单步执行、查看寄存器变化。
5.1 安装调试插件
在 VSCode 扩展商店搜索安装 Cortex-Debug 插件:

5.2 创建调试配置
在项目根目录创建 .vscode/launch.json 文件:
json
{
"version": "0.2.0",
"configurations": [
{
"name": "ARM Debug (QEMU)",
"type": "cortex-debug",
"request": "launch",
"servertype": "external",
"executable": "./demo",
"cwd": "${workspaceFolder}",
// 重要:指定工具链前缀
"toolchainPrefix": "arm-linux-gnueabihf",
// 使用gdb-multiarch
"gdbPath": "gdb-multiarch",
// 连接到QEMU的GDB服务器
"gdbTarget": "localhost:1234",
// 关键:清空默认启动命令
// 避免Cortex-Debug发送硬件复位指令
"overrideLaunchCommands": [],
// 自动运行到main函数
"runToEntryPoint": "main",
}
]
}
5.3 开始调试
- 启动调试服务器(在终端中):
bash
qemu-arm-static -g 1234 ./demo
-
在 VSCode 中:
- 在
main.c或SUM.S中设置断点 - 按 F5 启动调试
- 观察左侧的变量、寄存器和调用栈
- 在
-
关键观察点:
- 进入
SUM_ASM时,查看 R0、R1 的值(应该是 10 和 20) - 执行
PUSH {R11, LR}后,观察 SP(栈指针)的变化 - 调用
sum()后,查看 R0 的值(应该是 30) - 执行
POP {R11, PC}后,程序如何返回到main()
- 进入

6. 深入理解:ARM 调用约定
6.1 ATPCS(ARM-Thumb Procedure Call Standard)
ATPCS 是 ARM 架构的函数调用标准,规定了:
| 项目 | 规则 |
|---|---|
| 参数传递 | R0-R3 传递前 4 个参数,其余通过栈传递 |
| 返回值 | 通过 R0 返回,64 位值通过 R0 和 R1 返回 |
| 保存寄存器 | R4-R11 必须由被调用者保存 |
| 栈对齐 | 栈指针必须保持 8 字节对齐 |
6.2 函数调用栈帧
典型的 ARM 函数序言和结语:
asm
@ 函数序言(Prologue)
PUSH {R11, LR} @ 保存帧指针和返回地址
MOV R11, SP @ 设置新的帧指针
SUB SP, SP, #N @ 为局部变量分配栈空间
@ 函数结语(Epilogue)
MOV SP, R11 @ 恢复栈指针
POP {R11, PC} @ 恢复帧指针,返回调用者
6.3 我们的示例分析
在 SUM_ASM 中,我们只保存了 R11 和 LR,没有分配局部变量空间,因为:
- 我们没有使用额外的局部变量
- 参数传递和函数调用都通过寄存器完成
- 这是最简单的叶子函数(leaf function)实现
7. 常见问题与解决方案
7.1 编译工具链问题
问题 :找不到 arm-linux-gnueabihf-gcc
bash
# 解决方案:重新安装工具链
sudo apt remove gcc-arm-linux-gnueabihf
sudo apt update
sudo apt install gcc-arm-linux-gnueabihf
问题:编译时报 "undefined reference"
bash
# 可能原因:链接顺序错误
# 正确链接顺序:
arm-linux-gnueabihf-gcc main.o SUM.o -o demo
# 而不是:
arm-linux-gnueabihf-gcc SUM.o main.o -o demo
7.2 QEMU 运行问题
问题:运行时报 "Exec format error"
bash
# 原因:QEMU没有正确识别可执行文件格式
# 解决方案:
sudo update-binfmts --display | grep arm
# 如果没有正确配置,重新安装:
sudo apt install --reinstall qemu-user-static
问题:调试连接失败
bash
# 检查1234端口是否被占用
netstat -tlnp | grep 1234
# 如果被占用,修改launch.json中的端口号
7.3 汇编语法问题
问题:汇编编译错误
bash
# 检查语法:
# 1. 标签必须以冒号结尾
# 2. 指令必须使用Tab或空格缩进
# 3. 注释使用@或/*
# 4. 确保.syntax unified在文件开头
8. 扩展练习
掌握了基础后,可以尝试以下扩展:
练习 1:传递更多参数
修改程序,让 sum() 函数接受 4 个参数,测试 ATPCS 的参数传递规则。
练习 2:使用软中断
在汇编中添加软中断调用,了解 ARM 的 SWI 机制。
练习 3:优化性能
对比纯 C 实现和汇编优化的性能差异。
练习 4:浮点运算
使用 ARM 的 VFP(向量浮点单元)进行浮点计算。
9. 总结
通过这个完整的 ARM 混合编程示例,你学会了:
- 环境搭建:在 Windows 11 上通过 WSL2 搭建 ARM 开发环境
- 工具使用:ARM 交叉编译器、QEMU 模拟器、VSCode 调试
- 混合编程:C 语言与汇编语言的相互调用
- 调用约定:理解 ARM ATPCS 的参数传递和栈帧管理
- 调试技巧:使用 GDB 可视化调试 ARM 程序
核心要点回顾:
- ARM 使用 R0-R3 传递参数,R0 返回值
- 调用函数前必须保存 LR 寄存器
- 栈操作必须保持 8 字节对齐
- 混合编程的关键是遵循统一的调用约定
这个简单的示例是你 ARM 开发之旅的起点。掌握了这些知识,你已具备开发更复杂 ARM 程序的基础,可以继续探索中断处理、内存管理、操作系统内核等高级主题。
记住:理解底层机制是成为优秀系统开发者的关键。混合编程让你能够 " 看见 "C 语言背后的机器指令,这是提升编程能力的绝佳途径。