嵌入式 - ARM(3)从基础调用到 C / 汇编互调

发布于:2025-09-14 ⋅ 阅读:(20) ⋅ 点赞:(0)

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    ; 初始化r0
    mov r1, #2    ; 初始化r1
    add r3, r0, r1 ; 计算r3 = r0 + r1
    bx lr         ; 函数返回,跳回LR指向的地址

main:            ; 主函数
    mov r0, #100  ; 初始化r0
    mov r1, #200  ; 初始化r1
    bl func       ; 调用func,自动将下条指令地址存入LR
    mov r3, #300  ; func返回后执行此指令

执行流程解析:

  1. bl func执行时,自动将mov r3, #300的地址存入 LR
  2. 执行 func 函数体
  3. 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(如stmfdldmfd

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>}:可选条件码,用于条件执行(如 eqnegt 等)。例如 streq 表示 “相等时才执行存储”。
  • {<type>}:可选数据类型,指定存储数据的长度及符号性:
    • 无后缀:默认存储 32 位字(word)
    • b:存储 8 位字节(byte)
    • h:存储 16 位半字(halfword)
    • sb:存储 8 位有符号字节(带符号扩展)
    • sh:存储 16 位有符号半字(带符号扩展)
  • <src>:源寄存器,即要存储到内存的数据所在的寄存器(如 r0r1 等)。
  • [<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, #10
    mov r1, #20
    cmp r0, r1    ; 比较r0和r1
    movge r2, r0  ; 若r0 >= r1,r2 = r0
    movlt r2, r1  ; 若r0 < r1,r2 = r1
    bx lr         ; 返回

func0:           ; 一级函数
    mov r0, #1
    mov r1, #2
    add r3, r0, r1 ; 计算r3 = 3
    stmfd sp!, {r0-r12, lr}  ; 保护现场
    bl func1     ; 调用func1
    ldmfd sp!, {r0-r12, pc}  ; 恢复现场并返回

main:            ; 主函数
    ldr sp, =0x40001000  ; 初始化栈指针
    mov r0, #100
    mov r1, #200
    stmfd sp!, {r0-r12, lr}  ; 保护主函数现场
    bl func0     ; 调用func0
    ldmfd sp!, {r0-r12, lr}  ; 恢复主函数现场
    mov r3, #300

finish:
    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指令调用该函数,注意,既然是调函数,就一定要保护现场

步骤详解

  1. 创建 C 函数文件(main.c)

    void c_add(int a, int b, int c, int d, int e) {
        int result = a + b + c + d + e;
        // 函数实现
    }
    
  2. extren声明外部函数

  3.  导入 import c_add; (keil当中要求)

    extern void c_add(void);  // 声明C函数
    import c_add;            // Keil环境下需导入
    
  4. 调用流程(含现场保护)

    ; 保护现场
    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}
    
  5. 解决栈对齐问题
    编译时可能出现错误:

    Error: L6238E: 无效的调用,因栈对齐问题
    
     

    解决方案:添加栈对齐伪指令

        preserve8 用于确保函数调用时栈指针保持 8 字节对齐

preserve8  ; 确保栈指针保持8字节对齐
  1. 工程配置注意事项

    • 魔术棒 -> 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 语言中调用汇编函数

步骤详解

  1. 汇编中导出函数:        export func1;

    ; 汇编函数实现
    func1:
        add r0, r0, r1  ; r0 = a + b(r0、r1为参数)
        bx lr           ; 返回结果(通过r0传递)
    
    export func1  ; 导出函数,供C调用
    
  2. 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 工作模式切换

  1. 切换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为功能号
    ; 中断返回后继续执行

软中断处理流程:

  1. 处理器自动切换到管理模式
  2. 保存当前 PC 到管理模式的 LR(lr_svc)
  3. 自动跳转到 0x08 处执行异常处理程序
  4. 处理完成后通过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