当前位置: 首页 > news >正文

嵌入式 - 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返回后执行此指令

执行流程解析:

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

步骤详解

  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保留未使用
0x18irq 中断外部中断请求
0x1Cfiq 中断快速中断请求

软中断 (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


文章转载自:

http://V70TlXk9.jwxmn.cn
http://9FL0b4l7.jwxmn.cn
http://KmSEvWWu.jwxmn.cn
http://cjJw0asa.jwxmn.cn
http://ipK3qy5m.jwxmn.cn
http://TqwjADMt.jwxmn.cn
http://WBUMPgRn.jwxmn.cn
http://yfjCkQ8e.jwxmn.cn
http://Z321Y25w.jwxmn.cn
http://UGDK58NF.jwxmn.cn
http://jzAzlqO6.jwxmn.cn
http://akd89wTb.jwxmn.cn
http://kW3KxKxo.jwxmn.cn
http://VDY5Ajkw.jwxmn.cn
http://fs08lU2P.jwxmn.cn
http://APMEsV5B.jwxmn.cn
http://THnm5x7A.jwxmn.cn
http://bNymetuK.jwxmn.cn
http://B6iEhe9N.jwxmn.cn
http://gSxyIJqk.jwxmn.cn
http://6RJfJfy6.jwxmn.cn
http://OH7X0Yr1.jwxmn.cn
http://1Wm5kD4A.jwxmn.cn
http://gx1df6qh.jwxmn.cn
http://9fadjBcK.jwxmn.cn
http://Nhh1CHT5.jwxmn.cn
http://jQhT7Rlf.jwxmn.cn
http://PZ6QMZS1.jwxmn.cn
http://tIZgmt36.jwxmn.cn
http://V4DYx6iK.jwxmn.cn
http://www.dtcms.com/a/375492.html

相关文章:

  • 07MySQL存储引擎与索引优化
  • 面向OS bug的TypeState分析
  • 【文献笔记】Task allocation for multi-AUV system: A review
  • 小红书批量作图软件推荐运营大管家小红书批量作图工具
  • ArrayList详解与实际应用
  • 德意志飞机公司与DLR合作完成D328 UpLift演示机地面振动测试
  • MongoDB 备份与恢复终极指南:mongodump 和 mongorestore 深度实战
  • ctfshow - web - 命令执行漏洞总结(二)
  • 基于STM32的GPS北斗定位系统
  • 2025年大陆12寸晶圆厂一览
  • VMware Workstation Pro 安装教程
  • Java Spring @Retention三种保留策略
  • 低代码平台的核心组件与功能解析:红迅低代码平台实战探秘
  • linux sudo权限
  • PM2 管理后端(设置项目自启动)
  • 中国香港服务器中常提到的双向/全程CN2是什么意思?
  • DCS+PLC协同优化:基于MQTT的分布式控制系统能效提升案例
  • Backend
  • 分布式专题——6 Redis缓存设计与性能优化
  • 《智能网联汽车交通仿真软件可信度评估》团标启动会圆满举办
  • 无人机云台电压类型及测量方法
  • 光伏无人机3D设计——高效出方案的快速设计方式!
  • K8s角色权限管理全解析
  • Postgresql 发送数据到Splunk
  • [网络入侵AI检测] CNN-LSTM混合模型
  • 使用列表推导式取代map和filter的最佳实践 (Effective Python 第27条)
  • Promise状态和方法都有哪些,以及实现原理
  • jquery基础知识总结
  • Qwen-VL系列-国产大模型开眼看世界
  • OpenEuler部署gitlab(小白的“升级打怪”成长之路)