系列文章目录

【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.cmath_asm.s编译为可执行文件math_demo

gcc -o math_demo main.c math_asm.s -marm

运行程序

在这里插入图片描述

调用流程图示

C层:main函数开始

C层:赋值 x=15, y=25

C层:调用 add_asm(x, y)

CPU自动操作:
1. x=15 → r0,y=25 → r1
2. 把C层下一条指令(printf1)的地址存入 lr
3. 跳转到汇编 add_asm 入口

汇编层:执行 add r0, r0, r1 → r0=40

汇编层:执行 bx lr → 跳回 lr 保存的地址(printf1)

C层:sum = r0(40)→ 赋值给sum变量

C层:执行 printf1 → 打印 15+25=40

C层:赋值 a=12, b=8

C层:调用 multiply_asm(a, b)

CPU自动操作:
1. a=12 → r0,b=8 → r1
2. 把C层下一条指令(printf2)的地址存入 lr
3. 跳转到汇编 multiply_asm 入口

汇编层:执行 mul r0, r0, r1 → r0=96

汇编层:执行 bx lr → 跳回 lr 保存的地址(printf2)

C层:product = r0(96)→ 赋值给product变量

C层:执行 printf2 → 打印 12×8=96

C层:main函数结束

功能说明

1、 关键知识点解释

  • extern 函数声明:
    • C 代码中必须用 extern int add_asm(int a, int b); 声明汇编函数,告诉编译器:
      • 这个函数的返回值是int,参数是两个int
      • 函数的实现不在当前 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); 这一行时:

  1. 编译器先把 C 参数映射到 ARM 寄存器:x=15 放入 r0y=25 放入 r1
  2. CPU 会把「调用完 add_asm 后要执行的下一条指令地址」自动存入 lr 寄存器—— 这个地址就是 C 代码中 printf("1. 加法运算:\n"); 这行代码的内存地址;
  3. 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 存返回值(被调用者无需保存)

规则拆解(结合咱们的代码)

  1. 参数传递

    • C 层调用 add_asm(x, y) 时,编译器自动把第 1 个参数 x 放入 r0,第 2 个参数 y 放入 r1(r2/r3 用于第 3/4 个参数,超过 4 个参数用栈);
    • 咱们的 add_asm/multiply_asm 直接用 r0/r1 计算,完全符合这个规则。
  2. 返回值传递

    • 汇编函数计算完后,结果必须放在 r0 中(比如 add r0, r0, r1 直接把和存入 r0);
    • C 层会自动从 r0 读取值,赋值给 sum/product,这就是为什么咱们的代码能正确拿到返回值。
  3. “被调用者无需保存” 的含义

    • 咱们的汇编函数可以随意修改 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           ; 返回

关键步骤解释

  1. 保存push {r4, lr}r4lr 压入栈(lr 也保存是因为循环里没有嵌套调用,但养成习惯更安全);
  2. 使用:随意修改 r4 做计数器,不影响外部;
  3. 恢复pop {r4, lr} 把栈里的值弹回 r4lr,还原到调用前的状态;
  4. 返回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 函数逻辑混乱,程序大概率崩溃。


调用约定部分:总结(结合前文代码)

  1. r0-r3 是 “临时寄存器”:咱们的加法 / 乘法函数直接用 r0/r1 传参、r0 存返回值,符合 “被调用者无需保存” 的规则,这也是代码能正确运行的核心;
  2. r4-r11 是 “保护寄存器”:只有用到时才需要保存 / 恢复,咱们的简单函数没用到,所以不用处理;
  3. 调用约定的本质:是 C 和汇编之间的 “协议”,双方都遵守这个协议,才能保证参数、返回值、寄存器状态的一致性,程序才不会出错。

简单记:r0-r3 随便用,r4-r11 用了就要 “先存后还”,返回值必须放 r0

总结

  1. lr 的核心作用

    • 调用汇编函数前,CPU 会把「C 层接下来要执行的代码地址」(比如 printf1、printf2)存到 lr 里,相当于给汇编函数留了一个 “回家的门牌号”;
    • 这个地址是自动存的,不需要你写代码干预。
  2. bx lr 的核心作用

    • 汇编函数执行完计算(add/mul)后,bx lr 就是 “按门牌号找回家的路”—— 跳回 lr 里存的地址,回到 C 层继续执行;
    • 图中 FM 两个节点就是 bx lr 的执行效果,是整个 “调用 - 返回” 链路的关键闭环。
  3. 寄存器映射

    • C 层的参数会自动映射到 ARM 寄存器(x→r0、y→r1),汇编计算后的结果存在 r0 里,回到 C 层后会自动赋值给 sum/product 变量,这是 ARM EABI 调用约定的具体体现。

bx lr 说明

结合 C 和 汇编代码,bx lr 的核心作用可以总结为 2 点:

  1. lr 是「C 层调用汇编函数时的 “回家地址”」(存的是 C 代码下一行的地址);
  2. bx lr 就是「汇编函数执行完后,按这个 “回家地址” 跳回 C 层」,让整个程序从调用处继续执行,不会中断或跑飞。

bx lr 就是汇编函数对 C 层说:“我算完了(加法 / 乘法结果在 r0 里),你接着从 printf 那行继续执行吧!”。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐