9. ARM汇编基础.md 17 KB

9 ARM汇编基础

9.1 完整概念讲解

9.1.1 为什么需要汇编

Cortex-A 系列芯片上电后,SP 指针未初始化,C 语言运行环境尚未建立,无法直接执行 C 代码。必须先用汇编完成以下工作:

  1. 初始化 DDR 控制器(芯片内部 RAM 不开放给用户代码时)
  2. 设置 SP 指针(栈顶地址)
  3. 初始化其他必要外设

当汇编把 C 环境设置好后,才跳转到 C 语言的 main 函数。这与 STM32 的启动文件 startup_stm32f10x_hd.s 原理相同,只是 STM32 的启动文件由 ST 写好,开发者无需修改。

参考文档

  • 《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition.pdf》— A4 章讲解全部指令
  • 《ARM Cortex-A(armV7)编程手册 V4.0.pdf》— 编程模型与寄存器说明

9.1.2 GNU 汇编语法

MDK 和 IAR 的汇编语法不同,不能直接移植。本教程使用 GCC 交叉编译器,汇编代码必须符合 GNU 语法。

基本格式

label:instruction @comment
  • label:标号,表示地址位置,以 : 结尾
  • instruction:汇编指令或伪操作
  • @:注释符号(也可用 /* */

大小写规则:指令、伪操作、寄存器名可全大写或全小写,但不能混用。

段定义伪操作

伪操作 含义
.text 代码段
.data 已初始化数据段
.bss 未初始化数据段
.rodata 只读数据段
.section 自定义段
.section .testsection  @ 定义自定义段

常用伪操作

伪操作 用法示例 含义
.byte .byte 0x12 定义单字节数据
.short .short 0x1234 定义双字节数据
.long .long 0x12345678 定义 4 字节数据
.equ .equ num, 0x12 赋值(num = 0x12)
.align .align 4 4 字节对齐
.end .end 源文件结束
.global .global _start 定义全局符号

函数格式

函数名:
    函数体
    返回语句       @ 非必须

示例——Cortex-A7 中断服务函数:

/* 未定义中断 */
Undefined_Handler:
    ldr r0, =Undefined_Handler
    bx  r0

/* SVC 中断 */
SVC_Handler:
    ldr r0, =SVC_Handler
    bx  r0

/* 预取终止中断 */
PrefAbort_Handler:
    ldr r0, =PrefAbort_Handler
    bx  r0

9.1.3 数据传输指令

处理器内部数据传输的三种场景:

场景 指令
寄存器 → 寄存器 MOV
特殊寄存器(CPSR/SPSR)→ 通用寄存器 MRS
通用寄存器 → 特殊寄存器 MSR

MOV 指令

将数据从一个寄存器拷贝到另一个寄存器,或加载立即数到寄存器。

MOV R0, R1          @ R0 = R1
MOV R0, #0x12       @ R0 = 0x12

MRS 指令

读取特殊寄存器(CPSR / SPSR)的值到通用寄存器。读特殊寄存器只能用 MRS

MRS R0, CPSR        @ R0 = CPSR

MSR 指令

将通用寄存器的值写入特殊寄存器。写特殊寄存器只能用 MSR

MSR CPSR, R0        @ CPSR = R0

9.1.4 存储加载指令

ARM 不能直接访问存储器(包括寄存器映射的 RAM),需要通过 LDR/STR 指令间接访问。

指令 描述
LDR Rd, [Rn, #offset] 从存储器 Rn+offset 读取数据到 Rd
STR Rd, [Rn, #offset] Rd 的值写入存储器 Rn+offset
LDRB Rd, [Rn, #offset] 按字节读取
STRB Rd, [Rn, #offset] 按字节写入
LDRH Rd, [Rn, #offset] 按半字(16 位)读取
STRH Rd, [Rn, #offset] 按半字写入

LDR 指令详解

LDR 有两种用法:

@ 用法1:从存储器加载数据到寄存器
LDR R0, =0x0209C004      @ 将地址 0x0209C004 加载到 R0(立即数用 =)
LDR R1, [R0]             @ 从地址 0x0209C004 读取数据到 R1

@ 用法2:加载立即数到寄存器
LDR R0, =0x12345678      @ R0 = 0x12345678

关键区别:加载立即数用 =,加载地址值也用 =;加载寄存器中存储的地址指向的数据用 []

STR 指令详解

@ 配置 GPIO1_GDIR 寄存器(地址 0x0209C004)的值为 0x20000002
LDR R0, =0x0209C004      @ R0 = 寄存器地址
LDR R1, =0x20000002      @ R1 = 要写入的值
STR R1, [R0]             @ 将 R1 写入 R0 指向的地址

9.1.5 压栈出栈指令

函数调用时需要保存/恢复寄存器状态(现场保护/恢复现场),使用 PUSH 和 POP 指令。

指令 描述
PUSH <reg list> 将寄存器列表压入栈中
POP <reg list> 从栈中恢复寄存器列表
STMFD SP!, <reg list> PUSH 的另一种写法
LDMFD SP!, <reg list> POP 的另一种写法

压栈示例

@ 将 R0~R3 和 R12 压栈
PUSH {R0~R3, R12}

@ 将 LR 压栈
PUSH {LR}

堆栈变化示意(SP 初始值 0x80000000,向下增长):

高地址
0x80000000  ┌─────────┐
            │  (空)    │ ← 压栈前 SP
0x7FFFFFEC  ├─────────┤
            │   R12    │
            │   R3     │
            │   R2     │
            │   R1     │
            │   R0     │ ← PUSH {R0~R3,R12} 后 SP
0x7FFFFFE8  ├─────────┤
            │   LR     │ ← PUSH {LR} 后 SP
低地址

出栈示例

出栈顺序必须与压栈相反:

POP {LR}                @ 先恢复 LR
POP {R0~R3, R12}        @ 再恢复 R0~R3, R12

STMFD / LDMFD 写法

PUSHPOPSTMFD / LDMFD 的简化写法:

STMFD SP!, {R0~R3, R12}    @ 等价于 PUSH {R0~R3, R12}
STMFD SP!, {LR}

LDMFD SP!, {LR}            @ 等价于 POP {LR}
LDMFD SP!, {R0~R3, R12}

FD = Full Descending(满递减):ARM 使用 FD 类型堆栈,SP 指向最后一个入栈值,堆栈由高地址向低地址增长。STM / LDM 是多存储/多加载指令,可一次操作多个连续数据。

9.1.6 跳转指令

指令 描述
B <label> 跳转到 label(范围 ±2KB,用 B.W 可扩大到 32 位)
BX <Rm> 间接跳转到 Rm 中的地址,并切换指令集
BL <label> 跳转到 label,并将返回地址保存到 LR
BLX <Rm> 跳转到 Rm 指定地址,保存返回地址到 LR,并切换指令集

B 指令——不保存返回地址

用于不需要返回的跳转,例如汇编初始化完成后跳转到 C 的 main 函数:

_start:
    ldr sp, =0x80200000     @ 设置栈指针
    b main                   @ 跳转到 main,不再返回

BL 指令——保存返回地址

用于函数调用,跳转前自动将当前 PC 值保存到 LR(R14):

push {r0, r1}               @ 保存 r0, r1
cps #0x13                   @ 进入 SVC 模式
bl  system_irqhandler       @ 调用 C 语言中断处理函数
cps #0x12                   @ 进入 IRQ 模式
pop {r0, r1}
str r0, [r1, #0x10]         @ 写 EOIR,中断处理完成

选择原则:需要返回原处用 BL,跳转后不返回用 B

9.1.7 算术运算指令

指令 公式 说明
ADD Rd, Rn, Rm Rd = Rn + Rm 加法
ADD Rd, Rn, #immed Rd = Rn + #immed 立即数加法
ADC Rd, Rn, Rm Rd = Rn + Rm + C 带进位加法
SUB Rd, Rn, Rm Rd = Rn - Rm 减法
SUB Rd, Rn, #immed Rd = Rn - #immed 立即数减法
SBC Rd, Rn, Rm Rd = Rn - Rm - !C 带借位减法
MUL Rd, Rn, Rm Rd = Rn * Rm 乘法(32位)
UDIV Rd, Rn, Rm Rd = Rn / Rm 无符号除法
SDIV Rd, Rn, Rm Rd = Rn / Rm 有符号除法

嵌入式开发中最常用的是加减指令,乘除基本用不到。

9.1.8 逻辑运算指令

指令 公式 说明
AND Rd, Rn, Rm Rd = Rn & Rm 按位与
AND Rd, Rn, #immed Rd = Rn & #immed 立即数与
ORR Rd, Rn, Rm `Rd = Rn Rm`
ORR Rd, Rn, #immed `Rd = Rn #immed`
BIC Rd, Rn, Rm Rd = Rn & (~Rm) 位清除
BIC Rd, Rn, #immed Rd = Rn & (~#immed) 立即数位清除
ORN Rd, Rn, #immed `Rd = Rn (~#immed)`
EOR Rd, Rn, Rm Rd = Rn ^ Rm 按位异或
EOR Rd, Rn, #immed Rd = Rn ^ #immed 立即数异或

逻辑运算指令常用于配置 I.MX6UL 外设寄存器时的位操作。


9.2 核心API/语法

寄存器说明

Cortex-A7 有 16 个 32 位通用寄存器(R0-R15)和 2 个特殊状态寄存器:

寄存器 别名 用途
R0-R3 参数传递 / 返回值(AAPCS 调用约定)
R4-R10 通用变量,被调用者保存
R11 FP 帧指针(可选)
R12 IP 过程间调用暂存
R13 SP 栈指针,指向当前栈顶
R14 LR 链接寄存器,保存子程序返回地址
R15 PC 程序计数器,当前正在执行的指令地址
CPSR 当前程序状态寄存器(标志位、中断使能、处理器模式)
SPSR 保存的程序状态寄存器(异常返回时恢复 CPSR)

CPSR 各位含义

31  30  29  28  ...  7  6  5  4  3  2  1  0
 N   Z   C   V       I  F  T  M4 M3 M2 M1 M0
                                 └──处理器模式──┘
名称 含义
N 负数 运算结果为负置 1
Z 运算结果为零置 1
C 进位 加法进位 / 减法借位
V 溢出 有符号运算溢出置 1
I IRQ 1 = 禁止 IRQ 中断
F FIQ 1 = 禁止 FIQ 中断
T Thumb 0 = ARM 状态,1 = Thumb 状态
M[4:0] 模式 处理器模式(SVC=10011, IRQ=10010 等)

指令速查表

类别 指令 语法
数据传输 MOV / MRS / MSR MOV Rd, Operand2
存储加载 LDR / STR / LDRB / STRB LDR Rd, [Rn, #offset]
压栈出栈 PUSH / POP / STMFD / LDMFD PUSH {reg list}
跳转 B / BL / BX / BLX BL label
算术 ADD / ADC / SUB / SBC ADD Rd, Rn, Rm
乘除 MUL / UDIV / SDIV MUL Rd, Rn, Rm
逻辑 AND / ORR / BIC / EOR AND Rd, Rn, Rm

9.3 代码示例

完整的 Cortex-A7 启动汇编框架

@ Cortex-A7 启动汇编示例(适用于 I.MX6ULL)
@ 文件:start.S

.global _start

_start:
    @ ============================================
    @ 第一阶段:汇编初始化
    @ ============================================

    @ 1. 关闭 IRQ 和 FIQ
    mrs r0, cpsr
    orr r0, r0, #0xc0       @ 设置 I=1, F=1,禁止中断
    msr cpsr, r0

    @ 2. 设置 SVC 模式
    mrs r0, cpsr
    bic r0, r0, #0x1f       @ 清除 M[4:0] 位
    orr r0, r0, #0x13       @ 设置为 SVC 模式(10011)
    msr cpsr, r0

    @ 3. 设置栈指针(SVC 模式栈)
    ldr sp, =0x80200000      @ DDR 起始地址 + 2MB 作为栈顶

    @ 4. 清零 .bss 段
    ldr r0, =__bss_start     @ .bss 起始地址(链接脚本定义)
    ldr r1, =__bss_end       @ .bss 结束地址
    mov r2, #0
bss_loop:
    cmp r0, r1               @ 比较当前地址与结束地址
    bge bss_done             @ 如果 >= 结束地址,跳转完成
    str r2, [r0]             @ 写零
    add r0, r0, #4           @ 地址 +4
    b bss_loop               @ 继续循环
bss_done:

    @ 5. 跳转到 C 语言 main 函数
    bl main                   @ 调用 main,保存返回地址到 LR

    @ main 返回后(理论上不会到这里)
hang:
    b hang                   @ 死循环

中断向量表与跳转

@ 中断向量表(放在代码段最前面)
.section .text.startup, "ax"

.global _vector_table
_vector_table:
    ldr pc, =_start           @ 复位
    ldr pc, =Undefined_Handler
    ldr pc, =SWI_Handler
    ldr pc, =PrefAbort_Handler
    ldr pc, =DataAbort_Handler
    nop                       @ 保留
    ldr pc, =IRQ_Handler
    ldr pc, =FIQ_Handler

@ IRQ 中断服务函数示例
IRQ_Handler:
    push {r0, r1}             @ 保存 r0, r1
    cps #0x13                 @ 切换到 SVC 模式
    bl  system_irqhandler     @ 调用 C 语言中断处理函数
    cps #0x12                 @ 切换回 IRQ 模式
    pop {r0, r1}
    str r0, [r1, #0x10]       @ 写 EOIR 清中断
    subs pc, lr, #4           @ 返回到中断前的下一条指令

函数调用与现场保护

@ 汇编函数示例:调用 C 函数并保存寄存器
my_function:
    push {r4-r11, lr}        @ 保存 callee-saved 寄存器和返回地址

    @ 调用 C 函数
    mov r0, #10               @ 第一个参数
    mov r1, #20               @ 第二个参数
    bl  c_add_func            @ 调用 C 函数,返回值在 r0

    pop {r4-r11, pc}          @ 恢复寄存器并返回

9.4 注意事项与易错点

= 与 # 的区别

LDR R0, =0x12345678    @ 加载立即数到 R0(伪指令,编译器生成文字池)
LDR R0, [R1]           @ 从 R1 指向的地址读取数据
MOV R0, #0x12          @ 加载小立即数(8 位以内)

= 用于加载任意 32 位立即数(编译器自动处理),# 用于直接立即数(有限范围)。

PUSH/POP 顺序必须匹配

@ 错误!出栈顺序与压栈相反
PUSH {r0, r1, r2}
POP  {r2, r1, r0}      @ 正确但不推荐

@ 推荐写法:POP 按原顺序写,硬件自动按正确顺序恢复
PUSH {r0, r1, r2}
POP  {r0, r1, r2}      @ 推荐!

B vs BL 的选择

场景 使用 原因
初始化完成后跳 main B 不需要返回汇编
调用 C 函数 BL 需要返回继续执行
中断处理调用 C 函数 BL 必须返回完成中断恢复

函数返回

  • bx lr:返回到 LR 保存的地址
  • pop {pc}:从栈中恢复 PC(等价于返回)
  • mov pc, lr:简单返回(不切换指令集)

指令大小写

@ 以下两种写法等价
MOV R0, R1
mov r0, r1

@ 但不能混用
Mov r0, R1              @ 错误!

9.5 面试要点

常见问题

Q:为什么 Cortex-A 芯片上电后必须先执行汇编?

A:上电时 SP 指针未初始化,C 语言的函数调用依赖栈(出栈入栈),没有栈 C 语言无法运行。汇编不依赖栈,可以先初始化 DDR、设置 SP,建立 C 运行环境后再跳转到 C 代码。

Q:MOV 和 LDR 加载立即数有什么区别?

A:MOV 只能加载 8 位立即数(通过循环右移偶数位得到的值),LDR 伪指令可以加载任意 32 位立即数(编译器会将值放在代码段附近的文字池中,用 PC 相对寻址加载)。

Q:B 和 BL 指令的区别?

A:B 只做跳转,不保存返回地址;BL 跳转前自动将当前 PC+4(下一条指令地址)保存到 LR 寄存器,支持子程序调用后返回。

Q:PUSH/POP 与 STMFD/LDMFD 的关系?

A:PUSH 等价于 STMFD SP!POP 等价于 LDMFD SP!。FD 表示 Full Descending(满递减堆栈),ARM 处理器默认使用这种堆栈模型。

Q:什么是现场保护?为什么需要它?

A:函数调用或中断发生时,需要先保存当前寄存器值(R0-R15),称为现场保护;函数返回或中断处理完成后,再恢复寄存器值,称为恢复现场。这是保证程序正确执行的基础。

Q:CPSR 寄存器的作用?

A:CPSR(Current Program Status Register)保存当前处理器的状态信息,包括:条件标志位(N/Z/C/V)、中断禁止位(I/F)、处理器模式位(M[4:0])、Thumb 状态位(T)等。通过 MRS/MSR 指令读写。