嵌入式 - ARM(3)从基础调用到 C / 汇编互调
ARM 汇编是嵌入式开发中的核心技术之一,尤其在底层驱动、实时系统等场景中应用广泛。本文将系统讲解 ARM 汇编中的函数机制,包括函数定义与调用、栈操作、现场保护、以及 C 语言与汇编的混合编程等关键技术,帮助开发者深入理解 ARM 架构下的函数执行原理。
一、ARM 函数的基本定义与调用机制
1.1 函数的定义格式
在 ARM 汇编中,函数的基本定义格式如下:
函数名:; 函数体指令序列bx lr ; 函数返回
其中,bx lr
是函数返回的关键指令,lr
(Link Register) 寄存器存储了函数调用后的返回地址,通过该指令可将程序执行流切回到调用处。
1.2 函数调用的核心:PC 与 LR 的交互
ARM 架构中,函数调用通过b
(branch) 和bl
(branch with link) 指令实现,两者的核心区别在于是否保存返回地址:
b 标签
:单纯跳转,不修改 LR 寄存器bl 标签
:跳转前自动将下一条指令地址存入 LR,为函数返回做准备
示例代码:
b main ; 跳转到main函数,不保存返回地址func: ; 被调用函数mov r0, #1 ; 初始化r0mov r1, #2 ; 初始化r1add r3, r0, r1 ; 计算r3 = r0 + r1bx lr ; 函数返回,跳回LR指向的地址main: ; 主函数mov r0, #100 ; 初始化r0mov r1, #200 ; 初始化r1bl func ; 调用func,自动将下条指令地址存入LRmov r3, #300 ; func返回后执行此指令
执行流程解析:
bl func
执行时,自动将mov r3, #300
的地址存入 LR- 执行 func 函数体
bx lr
将 PC 设置为 LR 的值,跳回 main 函数继续执行
二、栈操作:保护现场与恢复现场的基础(压栈/弹栈)
arm体系采用的方案是满减,但是在进行操作之前,我们必须告诉2440栈底的位置,
这里我们把栈底设置为0x40001000,从地址0x40000000开始的0x1000这段内存空间对应的
是2440内部的一段ram,总共4k。
实际能够使用的内存空间为[0x40000000~0x40000FFF],
设置栈底指针寄存器: ldr sp =0x40001000
在函数调用过程中,栈是实现现场保护的关键结构,用于解决两个核心问题:
- 被调函数修改主调函数使用的寄存器
- 函数嵌套调用时的返回地址正确传递
2.1 ARM 的四种栈类型
ARM 架构定义了四种栈操作模式,由 "空 / 满" 和 "增 / 减" 组合而成:
类型 | 定义 | 操作逻辑 |
---|---|---|
满增 (FA) | 栈指针指向最后一个已使用元素,入栈时先移动指针再存数据 | 入栈:sp += 4; *sp = 数据 |
满减 (FD) | 栈指针指向最后一个已使用元素,入栈时先存数据再移动指针 | 入栈:*sp = 数据;sp -= 4 |
空增 (EA) | 栈指针指向第一个空闲位置,入栈时先存数据再移动指针 | 入栈:*sp = 数据;sp += 4 |
空减 (ED) | 栈指针指向第一个空闲位置,入栈时先移动指针再存数据 | 入栈:sp -= 4; *sp = 数据 |
ARM 2440 常用模式:满减 (FD),对应指令后缀
fd
(如stmfd
、ldmfd
)
2.2寄存器与内存之间的数据传输 ---- Idr str
加载与存储指令 LDR/STR
LDR(加载)和 STR(存储)指令用于在内存和寄存器之间传送数据:
LDR<c> <Rt>, <label> ; 从label指向的内存地址加载数据到Rt寄存器
STR<c> <Rt>, <label> ; 将Rt寄存器的数据存储到label指向的内存地址
这些指令在初始化内存、设置异常向量表等操作中非常重要。
1、str
指令的基本语法--- 基于昨天拓展
str
指令的完整语法格式如下:str{<cond>}{<type>} <src>, [<base>{, <offset>}]
各部分含义:
{<cond>}
:可选条件码,用于条件执行(如eq
、ne
、gt
等)。例如streq
表示 “相等时才执行存储”。{<type>}
:可选数据类型,指定存储数据的长度及符号性:
- 无后缀:默认存储 32 位字(word)
b
:存储 8 位字节(byte)h
:存储 16 位半字(halfword)sb
:存储 8 位有符号字节(带符号扩展)sh
:存储 16 位有符号半字(带符号扩展)<src>
:源寄存器,即要存储到内存的数据所在的寄存器(如r0
、r1
等)。[<base>]
:基址寄存器,存储内存地址的基地址(如r2
表示以r2
中的值为基地址)。{, <offset>}
:可选偏移量,用于计算最终的内存地址,有多种形式(见下文)。
地址计算方式(偏移量类型)str
指令通过 “基址寄存器 + 偏移量” 计算目标内存地址,支持多种偏移模式:
1. 立即数偏移(最常用)
直接指定一个立即数作为偏移量,格式为 #<imm>
。
前索引(pre-indexed):先计算地址(基址 + 偏移),再存储数据。
示例:str r0, [r1, #4]
含义:将r0
的值存储到地址r1 + 4
处(r1
本身不变)。后索引(post-indexed):先以基址存储数据,再更新基址(基址 + 偏移)。
示例:str r0, [r1], #4
含义:先将r0
的值存储到r1
指向的地址,再将r1
的值更新为r1 + 4
。自动索引(with writeback):前索引 + 自动更新基址,通过
!
标记。
示例:str r0, [r1, #4]!
含义:将r0
的值存储到r1 + 4
处,同时将r1
更新为r1 + 4
(!
表示 “写回” 基址)。
2、str
与ldr
的配合使用
str
和ldr
通常成对出现,用于实现 “寄存器→内存→寄存器” 的数据流转:; 1. 将 r0 的值存入内存 mov r0, #100 ldr r1, =0x40000000 str r0, [r1] ; 内存 0x40000000 处的值 = 100; 2. 从内存读取值到 r2 ldr r2, [r1] ; r2 = 100(与 r0 原值一致)
2.3 栈顶指针的初始化
栈指针 (SP) 需要初始化到合法的内存区域,由于 ARM 的mov
指令只能操作立即数,对于大地址需用ldr
伪指令:
; 错误:0x40001000不是合法立即数,mov无法直接赋值
; mov sp, #0x40001000; 正确:使用ldr伪指令加载地址
ldr sp, =0x40001000
内存配置(以 Keil 为例):
- 魔术棒 -> Target -> IRAM1:
- Start:0x40000000(内部 RAM 起始地址)
- Size:0x1000(RAM 大小,需与 SP 初始化地址匹配)
2.4 现场保护与恢复指令
ARM 提供批量加载 / 存储指令实现现场保护:
1、保护现场(入栈)---stmfd
stmfd sp!, {r0-r12, lr} ; 将r0-r12和lr寄存器入栈,!表示自动更新sp
stmfd (store(存储) multiple(多个) full(满) decrease(减少))
STMFD<c> <Rn>{!}, <registers>
<Rn>:栈顶指针寄存器
{!},:入栈出栈后,栈顶指针寄存器自减
<registers>:入栈出栈的寄存器列表
stmfd
:store multiple full decrease(满减模式存储多个寄存器){r0-r12, lr}
:需要保护的寄存器列表(通用寄存器 + 返回地址)
2、恢复现场(出栈)---ldmfd
ldmfd sp!, {r0-r12, pc} ; 从栈中恢复寄存器,最后将lr值赋给pc实现返回
ldmfd (load(加载) multiple(多个) full(满) decrease(减少))
LDMFD<c> <Rn>{!}, <registers><Rn>:栈顶指针寄存器
{!},:入栈出栈后,栈顶指针寄存器自减
<registers>:入栈出栈的寄存器列表
ldmfd
:load multiple full decrease(满减模式加载多个寄存器)- 用
pc
替代lr
可直接实现函数返回,简化指令
2.5 嵌套函数调用的现场保护示例
b main ; 程序入口func1: ; 二级函数mov r0, #10mov r1, #20cmp r0, r1 ; 比较r0和r1movge r2, r0 ; 若r0 >= r1,r2 = r0movlt r2, r1 ; 若r0 < r1,r2 = r1bx lr ; 返回func0: ; 一级函数mov r0, #1mov r1, #2add r3, r0, r1 ; 计算r3 = 3stmfd sp!, {r0-r12, lr} ; 保护现场bl func1 ; 调用func1ldmfd sp!, {r0-r12, pc} ; 恢复现场并返回main: ; 主函数ldr sp, =0x40001000 ; 初始化栈指针mov r0, #100mov r1, #200stmfd sp!, {r0-r12, lr} ; 保护主函数现场bl func0 ; 调用func0ldmfd sp!, {r0-r12, lr} ; 恢复主函数现场mov r3, #300finish:b finish ; 程序结束循环
end
三、汇编与 C 语言的混合编程
在实际开发中,往往需要汇编与 C 语言混合编程:汇编负责底层硬件操作,C 负责上层逻辑实现。
---- 汇编c语言混合编程--配置
魔术棒 -> Debug -> Use Simulator->Run to main(取消)
魔术棒 -> Linker -> Use Memory Layout from Taget Dialog(勾选)
魔术棒 -> Taget -> ROM1 -> Start: 0x0 Size:0x2000
3.1 汇编中调用 C 函数
在汇编中调用c语言编写的函数
设有c语言定义的函数void func_c(void)
;在汇编代码中调用该函数,只需用import声明函数名即可,之后就可以使用bl指令调用该函数,注意,既然是调函数,就一定要保护现场
步骤详解:
创建 C 函数文件(main.c):
void c_add(int a, int b, int c, int d, int e) {int result = a + b + c + d + e;// 函数实现 }
extren声明外部函数:
导入 import c_add; (keil当中要求)
extern void c_add(void); // 声明C函数 import c_add; // Keil环境下需导入
调用流程(含现场保护):
; 保护现场 stmfd sp!, {r0-r12, lr}; 准备参数(ARM函数调用约定) ; r0-r3传递前4个参数,其余参数入栈 mov r0, #1 ; 第1个参数 mov r1, #2 ; 第2个参数 mov r2, #3 ; 第3个参数 mov r3, #4 ; 第4个参数 mov r4, #5 ; 第5个参数 stmfd sp!, {r4} ; 第5个参数入栈; 调用C函数 bl c_add; 清理栈上的参数 ldmfd sp!, {r4}; 恢复现场 ldmfd sp!, {r0-r12, lr}
解决栈对齐问题:
编译时可能出现错误:Error: L6238E: 无效的调用,因栈对齐问题
解决方案:添加栈对齐伪指令
preserve8 用于确保函数调用时栈指针保持 8 字节对齐
preserve8 ; 确保栈指针保持8字节对齐
工程配置注意事项:
- 魔术棒 -> Debug -> Use Simulator,取消 "Run to main"
- 魔术棒 -> Linker -> 勾选 "Use Memory Layout from Target Dialog"
- 魔术棒 -> Target -> ROM1:Start=0x0,Size=0x2000
- 若启动代码冲突:重建工程、删除.sct 文件、重新添加 start.s 和 main.c
-------------向c函数传参
向c函数传参的方法很简单,如果参数个数小于等于4个,就直接用r0~r3传参,
c函数返回值通过r0寄存器返回:
设有c函数:
int add_c(int a, int b, int c, int d)
{
return a + b + c + d;
}
如果参数个数大于4个,从第五个参数开始就需要通过栈来传参(从右向左入栈,即最后一个参数先入栈)
在c语言中 ---- 调用汇编编写的函数------ 类似,
不过在汇编中用export声明函数,同时需要在c语言中
用extern声明函数,按照标准,调用者负责保护现场和恢复现场
传参方法于此类似
3.2 C 语言中调用汇编函数
步骤详解:
汇编中导出函数: export func1;
; 汇编函数实现 func1:add r0, r0, r1 ; r0 = a + b(r0、r1为参数)bx lr ; 返回结果(通过r0传递)export func1 ; 导出函数,供C调用
C 中声明并调用汇编函数: extern int func1(int a, int b);
// 声明汇编函数 extern int func1(int a, int b);int main() {int result = func1(3, 5); // 调用汇编函数return 0; }
参数与返回值约定:
- 参数传递:r0-r3 依次传递第 1-4 个参数,超过 4 个的参数通过栈传递
- 返回值:通过 r0 寄存器返回(32 位),64 位返回值用 r0-r1
运行结果:
四、ARM 工作模式切换
- 切换arm内核的工作模式
切换工作方式的思路很简单,由于内核的工作模式是由cpsr寄存器的低5位来设置的,
那么就可以先把cpsr读出来,
更改低5位之后再设置进去。
这里读取cpsr使用 mrs指令,
写cpsr寄存器用 msr指令,
需要注意的是在keil环境下写cpsr需要写成: msr cpsr_c r0; 将r0的值写入到cpsr寄存器
ARM 处理器有 7 种工作模式,通过 CPSR(当前程序状态寄存器)的 M 域(bit [4:0])控制:
模式 | M 域值 | 说明 |
---|---|---|
用户模式 (usr) | 0x10 | 正常程序执行模式 |
快中断模式 (fiq) | 0x11 | 快速中断处理 |
中断模式 (irq) | 0x12 | 普通中断处理 |
管理模式 (svc) | 0x13 | 系统复位和 SWI 指令进入 |
MRS (read): MRS<c> <Rd>, <spec_reg>
MSR (writ): MSR<c> <spec_reg>, #<const>
MSR<c> <spec_reg>, <Rn>
模式切换代码示例
; 切换到管理模式(svc)
mrs r0, cpsr ; 读取CPSR到r0
bic r0, r0, #0x1F ; 清除M域(bit[4:0])
orr r0, r0, #0x13 ; 设置M域为管理模式(0x13)
msr cpsr, r0 ; 将修改后的值写回CPSR
mrs
:读取特殊寄存器(move to register from special register)msr
:写入特殊寄存器(move to special register from register)bic
:位清除指令(bit clear)orr
:位或指令(bit or)
五、异常向量表与软中断
异常向量表是 ARM 处理异常的核心机制,在内存起始地址(0x00000000)处预留 8 个异常入口,每个入口 4 字节:
地址 | 异常类型 | 说明 |
---|---|---|
0x00 | 复位 (Reset) | 系统上电或复位 |
0x04 | 未定义指令 | 执行未定义指令时 |
0x08 | 软件中断 (SWI) | 执行 swi 指令时 |
0x0C | 指令预取中止 | 指令读取错误 |
0x10 | 数据中止 | 数据访问错误 |
0x14 | 保留 | 未使用 |
0x18 | irq 中断 | 外部中断请求 |
0x1C | fiq 中断 | 快速中断请求 |
软中断 (SWI) 的使用
软中断用于用户模式下调用系统服务,通过指令swi #立即数
触发:
; 软中断示例mov r0, #100 ; 设置参数swi #7 ; 触发软中断,#7为功能号; 中断返回后继续执行
软中断处理流程:
- 处理器自动切换到管理模式
- 保存当前 PC 到管理模式的 LR(lr_svc)
- 自动跳转到 0x08 处执行异常处理程序
- 处理完成后通过
movs pc, lr
返回
六、异常向量表启动代码
1、arm汇编调用c语言函数以及c语言函数调用汇编编写的函数,函数的参数和返回值如何处理?
1. 汇编调用c语言:需在.s中用import声明函数名--导入,之后用bl指令调用该函数。
传参:如果参数个数小于等于4个,就用r0~r3传参,如果大于四个,从第五个参数开始就要通过栈来传参
返回值:通过r0寄存器返回
2 .c语言调用汇编:需在.s中用export声明函数--导出,函数结束用bx lr回到调用处,同时在c语言中用extern声明函数
传参:汇编函数从 r0~r3 读取前 4 个参数,超过 4 个的参数从栈中读取(栈顶为第 5 个参数)。
返回值:通过r0寄存器返回给c语言函数
2、arm内核中有几种异常,分别是什么,会使内核切换到那种工作模式?
ARM 内核定义了7 种异常,当异常发生时,处理器会自动切换到对应的特权工作模式,并跳转到向量表中对应异常的固定地址,再通过该地址指向的指令(如
B
或LDR PC,=handler
)跳转到具体的异常处理逻辑。具体如下:
异常类型 触发原因 对应工作模式 异常向量表地址 复位(Reset) 系统上电、复位引脚触发或 watchdog 超时 管理模式(Supervisor) 0x00000000 未定义指令(Undefined Instruction) 执行未被 ARM 架构定义的指令 未定义模式(Undefined) 0x00000004 软件中断(SWI/SVC) 触发swi时,执行 svc
指令时(主动请求系统服务)管理模式(Supervisor) 0x00000008 指令预取中止(Prefetch Abort) 指令读取时地址无效(如未映射内存) 中止模式(Abort) 0x0000000C 数据中止(Data Abort) 数据访问时地址无效或权限不足 中止模式(Abort) 0x00000010 保留(Reserved) 未使用(ARM 架构预留) 无(未定义) 0x00000014 IRQ(中断请求) 外部设备触发的普通中断(如 UART、定时器) IRQ 模式(IRQ) 0x00000018 FIQ(快速中断请求) 高优先级外部中断(如紧急硬件错误) FIQ 模式(FIQ) 0x0000001C