【ARM汇编】0x01_ARM和C混合编程实现基本运算
系列文章目录
【ARM汇编】0x00_基于AndroidStudio构建ARM32-v7a以及ARM64-v8a环境
【ARM汇编】0x01_ARM和C混合编程实现基本运算
更新日志
| 日期 | 变更内容 |
|---|---|
| 2026-03-18 | 完成初稿 |
代码
- math_asm.s
.text
; ==============================================
; ARM汇编基础算术运算函数 (ARMv7架构,APCS调用规范)
; 遵循ARM EABI调用约定:
; - 参数传递: r0-r3 用于传递前4个参数
; - 返回值: r0 用于返回函数结果
; - 函数返回: bx lr 返回到调用者
; ==============================================
.text ; 声明代码段(可执行代码存储区)
.global add_asm ; 将add_asm函数导出为全局符号,允许外部调用
.global multiply_asm ; 将multiply_asm函数导出为全局符号,允许外部调用
; 加法函数:r0=a, r1=b,返回r0=a+b
add_asm:
add r0, r0, r1 ; 执行加法:r0 = r0 + r1
bx lr ; 跳回调用者(lr保存返回地址)
; 乘法函数:r0=a, r1=b,返回r0=a*b
multiply_asm:
mul r0, r0, r1 ; 执行乘法:r0 = r0 * r1
bx lr ; 跳回调用者
- main.c
#include <stdio.h>
#include <stdlib.h>
extern int add_asm(int a, int b);
extern int multiply_asm(int a, int b);
int main(int argc, char** argv)
{
printf("ARM汇编与C语言混合编程示例\n");
printf("==========================\n\n");
// 1. 简单加法运算
int x = 15, y = 25;
int sum = add_asm(x, y);
printf("1. 加法运算:\n");
printf(" %d + %d = %d\n\n", x, y, sum);
// 2. 乘法运算
int a = 12, b = 8;
int product = multiply_asm(a, b);
printf("2. 乘法运算:\n");
printf(" %d × %d = %d\n\n", a, b, product);
return 0;
}
编译方法
Android环境
- Android.mk
LOCAL_PATH := $(call my-dir)
LOCAL_ARM_MODE := arm
LOCAL_MODULE := math_demo
LOCAL_SRC_FILES := src/main.c src/math_asm.s
LOCAL_LDLIBS := -llog
include $(BUILD_EXECUTABLE)
ARM环境
将main.c和math_asm.s编译为可执行文件math_demo
gcc -o math_demo main.c math_asm.s -marm
运行程序

调用流程图示
功能说明
1、 关键知识点解释
- extern 函数声明:
- C 代码中必须用
extern int add_asm(int a, int b);声明汇编函数,告诉编译器:- 这个函数的返回值是
int,参数是两个int; - 函数的实现不在当前 C 文件中,需要链接时从汇编文件中找。
- 这个函数的返回值是
- C 代码中必须用
2、 参数 / 返回值的映射:
- C 代码中调用
add_asm(x, y)时,编译器会自动把x放入r0、y放入r1; - 汇编函数执行完后,
r0中的值会被编译器当作返回值,赋值给 C 代码中的sum。
3、 编译选项 -marm:
- 强制 gcc 使用
ARM指令集(而非Thumb指令集)。
4、bx lr 是汇编函数返回的关键,确保执行完汇编逻辑后回到 C 代码继续运行。
拆解「调用→执行→返回」全流程
C代码:sum = add_asm(x, y) 调用汇编函数;
汇编层:add_asm 执行加法后,靠 bx lr 回到 C 代码继续执行。
步骤 1:C 调用 add_asm 前,CPU 自动给 lr 赋值
当 C 代码执行到 int sum = add_asm(x, y); 这一行时:
- 编译器先把 C 参数映射到 ARM 寄存器:
x=15放入r0,y=25放入r1; - CPU 会把「调用完
add_asm后要执行的下一条指令地址」自动存入lr寄存器—— 这个地址就是 C 代码中printf("1. 加法运算:\n");这行代码的内存地址; - CPU 跳转到汇编函数
add_asm的第一条指令(add r0, r0, r1)开始执行。
此时 lr 里存的是:printf("1. 加法运算:\n"); 的内存地址(记为 0xXXXX1234)。
步骤 2:汇编函数执行核心逻辑
add_asm 执行 add r0, r0, r1:
r0 = 15 + 25 = 40(加法结果);- 此时
lr里的地址(0xXXXX1234)没有被修改,依然保存着返回地址。
步骤 3:bx lr 完成返回,回到 C 层继续执行
执行 bx lr 时:
bx指令的作用是「跳转到指定寄存器存储的地址」;- 这里就是跳转到
lr里的0xXXXX1234(也就是printf那行代码的地址); - CPU 跳过去后,C 代码就从
printf("1. 加法运算:\n");开始继续执行,同时把汇编函数中r0=40的值赋值
调用约定要点
r0–r3:用作参数和临时寄存器,r0 为返回值寄存器;被调用者无需保存(caller-saved)。
如果函数使用了需保留的寄存器(如 r4–r11),需在函数内保存/恢复。
先明确核心概念(新手友好版)
在 ARM EABI 调用约定中,寄存器被分为两类,核心目的是平衡 “执行效率” 和 “程序稳定性”:
表格
| 寄存器分类 | 包含寄存器 | 核心规则(谁负责保存) | 通俗理解 |
|---|---|---|---|
| 调用者保存(Caller-Saved) | r0-r3, lr | 被调用函数可随意修改,调用者自己负责保存 | “临时工作台”,用完不用还 |
| 被调用者保存(Callee-Saved) | r4-r11 | 被调用函数如果用了,必须先保存、用完恢复 | “公用储物柜”,用完要还原 |
要点 1:r0–r3 用作参数 / 临时寄存器,r0 存返回值(被调用者无需保存)
规则拆解(结合咱们的代码)
-
参数传递:
- C 层调用
add_asm(x, y)时,编译器自动把第 1 个参数x放入r0,第 2 个参数y放入r1(r2/r3 用于第 3/4 个参数,超过 4 个参数用栈); - 咱们的
add_asm/multiply_asm直接用r0/r1计算,完全符合这个规则。
- C 层调用
-
返回值传递:
- 汇编函数计算完后,结果必须放在
r0中(比如add r0, r0, r1直接把和存入r0); - C 层会自动从
r0读取值,赋值给sum/product,这就是为什么咱们的代码能正确拿到返回值。
- 汇编函数计算完后,结果必须放在
-
“被调用者无需保存” 的含义:
- 咱们的汇编函数可以随意修改
r0-r3(比如把r0改成加法结果),不用管修改前的值; - 因为调用者(C 层的 main 函数)如果需要保留
r0-r3里的原有数据,会自己提前保存(比如压入栈),不需要被调用的汇编函数操心。
- 咱们的汇编函数可以随意修改
代码示例(符合规则)
add_asm:
add r0, r0, r1 ; 直接修改r0,无需保存原有值
bx lr ; 符合规则:r0-r3无需保存
违反规则的后果(反面例子)
如果把返回值存到 r1 而不是 r0:
add_asm:
add r1, r0, r1 ; 错误:结果存到r1
bx lr
→ C 层读取 r0 时拿到的还是原来的 15,最终打印 15+25=15,完全错误。
要点 2:使用 r4–r11 需保存 / 恢复(被调用者负责)
咱们的加法 / 乘法函数只用到了 r0-r1,所以不需要处理 r4-r11;但如果汇编函数用到了 r4-r11,必须遵循 “先保存、后使用、最后恢复” 的原则,否则会破坏调用者(C 层)的寄存器数据,导致程序崩溃。
正面示例(使用 r4 的正确写法)
假设写一个 “累加 10 次” 的汇编函数,需要用 r4 做循环计数器:
; 函数:sum_10_asm(r0=初始值) → 返回r0=初始值+1+2+...+10
sum_10_asm:
push {r4, lr} ; 1. 保存r4(被调用者需保存)和lr(防止嵌套调用覆盖)
mov r4, #1 ; 2. 使用r4作为计数器,初始值1
mov r1, r0 ; 临时存初始值到r1
loop:
add r0, r0, r4 ; 累加:r0 = r0 + r4
add r4, r4, #1 ; 计数器+1
cmp r4, #10 ; 判断是否到10
ble loop ; 没到10就继续循环
pop {r4, lr} ; 3. 恢复r4和lr的原始值
bx lr ; 返回
关键步骤解释
- 保存:
push {r4, lr}把r4和lr压入栈(lr也保存是因为循环里没有嵌套调用,但养成习惯更安全); - 使用:随意修改
r4做计数器,不影响外部; - 恢复:
pop {r4, lr}把栈里的值弹回r4和lr,还原到调用前的状态; - 返回:
bx lr正常返回。
违反规则的后果(反面例子)
如果不用 push/pop 保存 r4:
sum_10_asm:
mov r4, #1 ; 直接修改r4,未保存
loop:
add r0, r0, r4
add r4, r4, #1
cmp r4, #10
ble loop
bx lr
→ C 层的 main 函数如果之前用 r4 存了重要数据(比如循环变量),会被这个汇编函数覆盖,导致 main 函数逻辑混乱,程序大概率崩溃。
调用约定部分:总结(结合前文代码)
- r0-r3 是 “临时寄存器”:咱们的加法 / 乘法函数直接用
r0/r1传参、r0存返回值,符合 “被调用者无需保存” 的规则,这也是代码能正确运行的核心; - r4-r11 是 “保护寄存器”:只有用到时才需要保存 / 恢复,咱们的简单函数没用到,所以不用处理;
- 调用约定的本质:是 C 和汇编之间的 “协议”,双方都遵守这个协议,才能保证参数、返回值、寄存器状态的一致性,程序才不会出错。
简单记:r0-r3 随便用,r4-r11 用了就要 “先存后还”,返回值必须放 r0。
总结
-
lr 的核心作用:
- 调用汇编函数前,CPU 会把「C 层接下来要执行的代码地址」(比如 printf1、printf2)存到
lr里,相当于给汇编函数留了一个 “回家的门牌号”; - 这个地址是自动存的,不需要你写代码干预。
- 调用汇编函数前,CPU 会把「C 层接下来要执行的代码地址」(比如 printf1、printf2)存到
-
bx lr 的核心作用:
- 汇编函数执行完计算(add/mul)后,
bx lr就是 “按门牌号找回家的路”—— 跳回lr里存的地址,回到 C 层继续执行; - 图中
F和M两个节点就是bx lr的执行效果,是整个 “调用 - 返回” 链路的关键闭环。
- 汇编函数执行完计算(add/mul)后,
-
寄存器映射:
- C 层的参数会自动映射到 ARM 寄存器(x→r0、y→r1),汇编计算后的结果存在 r0 里,回到 C 层后会自动赋值给 sum/product 变量,这是 ARM EABI 调用约定的具体体现。
bx lr 说明
结合 C 和 汇编代码,bx lr 的核心作用可以总结为 2 点:
lr是「C 层调用汇编函数时的 “回家地址”」(存的是 C 代码下一行的地址);bx lr就是「汇编函数执行完后,按这个 “回家地址” 跳回 C 层」,让整个程序从调用处继续执行,不会中断或跑飞。
,bx lr 就是汇编函数对 C 层说:“我算完了(加法 / 乘法结果在 r0 里),你接着从 printf 那行继续执行吧!”。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)