ARM 汇编学习笔记(二)启动代码、指令、函数调用与 C 混合编程

本文根据 ARM 汇编课程笔记整理,主要围绕 编写 ARM 启动代码 展开。启动代码的目标是在处理器复位后,搭建 C 语言运行所需的最基本环境,最终引导程序进入 main 函数。

目录

  • [1. 环境准备](#1. 环境准备)
  • [2. 启动代码的主要任务](#2. 启动代码的主要任务)
  • [3. 汇编格式与伪操作](#3. 汇编格式与伪操作)
  • [4. 常用 ARM 指令](#4. 常用 ARM 指令)
    • [4.1 MOV 传送指令](#4.1 MOV 传送指令)
    • [4.2 ADD / SUB 加减法指令](#4.2 ADD / SUB 加减法指令)
    • [4.3 什么是 12 位立即数](#4.3 什么是 12 位立即数)
    • [4.4 LDR / STR 加载与存储](#4.4 LDR / STR 加载与存储)
    • [4.5 MVN / BIC / ORR 位操作](#4.5 MVN / BIC / ORR 位操作)
    • [4.6 条件标志 NZCV 与 CMP](#4.6 条件标志 NZCV 与 CMP)
    • [4.7 跳转指令 B / BL / BX](#4.7 跳转指令 B / BL / BX)
  • [5. 循环结构](#5. 循环结构)
  • [6. 函数定义与调用](#6. 函数定义与调用)
    • [6.1 PC 与 LR](#6.1 PC 与 LR)
    • [6.2 栈与保护现场](#6.2 栈与保护现场)
    • [6.3 STMFD / LDMFD](#6.3 STMFD / LDMFD)
    • [6.4 汇编调用 C 函数](#6.4 汇编调用 C 函数)
    • [6.5 C 语言调用汇编函数](#6.5 C 语言调用汇编函数)
  • [7. 修改工作模式](#7. 修改工作模式)
  • [8. 异常向量表](#8. 异常向量表)
  • [9. 作业与参考答案](#9. 作业与参考答案)
  • [10. 总结](#10. 总结)

1. 环境准备

  • 开发工具:Keil 4
  • 参考文档:ARMv7A_V7R_Architecture_Reference_Manual

学习 ARM 汇编的主要目的不是为了写纯汇编程序,而是为了编写启动代码。启动代码运行在 C 语言环境之前,负责初始化硬件和运行环境。


2. 启动代码的主要任务

处理器复位后,启动代码通常需要完成以下工作:

  1. 初始化异常向量表
  2. 初始化各工作模式的栈指针寄存器
  3. 开启 ARM 内核中断允许
  4. 将工作模式设置为 User 模式
  5. 引导程序进入 C 语言主函数执行

因此,汇编指令的学习主要围绕这几个目的展开。


3. 汇编格式与伪操作

伪操作不是 ARM 处理器实际执行的指令,而是写给汇编器的命令,用于指导汇编器如何工作。

asm 复制代码
    AREA reset, CODE, READONLY
    CODE32
    ENTRY

    END
  • AREA:定义一个段。程序、数据、堆栈等都需要组织在不同的段中。
  • reset:段名,通常表示复位向量段,即 CPU 上电后首先执行的第一段代码。
  • CODE:指定该段属性为代码,包含可执行指令。
  • READONLY:只读属性,代码段通常默认且必须只读。
  • CODE32:后续指令使用 32 位 ARM 指令集。
  • THUMB:后续指令使用 16 位 Thumb 指令集。
  • ENTRY:指定程序入口。
  • END:汇编文件结束。

4. 常用 ARM 指令

4.1 MOV 传送指令

asm 复制代码
MOV{S}<c> <Rd>, #<const>
MOV{S}<c> <Rd>, <Rm>

MOV 也可以带移位操作:

MOV 形式 等价规范形式
MOV{S} <Rd>, <Rm>, ASR #<n> ASR{S} <Rd>, <Rm>, #<n>
MOV{S} <Rd>, <Rm>, LSL #<n> LSL{S} <Rd>, <Rm>, #<n>
MOV{S} <Rd>, <Rm>, LSR #<n> LSR{S} <Rd>, <Rm>, #<n>
MOV{S} <Rd>, <Rm>, ROR #<n> ROR{S} <Rd>, <Rm>, #<n>
MOV{S} <Rd>, <Rm>, ASR <Rs> ASR{S} <Rd>, <Rm>, <Rs>
MOV{S} <Rd>, <Rm>, LSL <Rs> LSL{S} <Rd>, <Rm>, <Rs>
MOV{S} <Rd>, <Rm>, LSR <Rs> LSR{S} <Rd>, <Rm>, <Rs>
MOV{S} <Rd>, <Rm>, ROR <Rs> ROR{S} <Rd>, <Rm>, <Rs>
MOV{S} <Rd>, <Rm>, RRX RRX{S} <Rd>, <Rm>

注意:

  1. 与 C 语言赋值运算对比(左值/右值),便于理解。
  2. #<n> / <Rs> 取值范围一般为 0 - 31
  3. RRX 是扩展右移,不需要移位量。
  4. 计算机只识别二进制数据,没有有无符号、浮点等概念,这些是解释方式。

示例:

asm 复制代码
    MOV r0, #0x8
    MOV r1, r0
    MOV r3, #31

    MOV r0, #1
    MOV r6, r0, LSL #31
    MOV r7, r0, LSL r3

4.2 ADD / SUB 加减法指令

ADD 指令格式:

asm 复制代码
ADD{S}<c> <Rd>, <Rn>, #<const>
ADD{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
ADD{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>

SUB 指令格式类似:

asm 复制代码
SUB{S}<c> <Rd>, <Rn>, #<const>
SUB{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
SUB{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>

示例:

asm 复制代码
    MOV r0, #0x0F
    MOV r1, #0xF0
    MOV r2, #1

    ADD r6, r0, #0xF0
    ADD r7, r0, r1
    ADD r7, r0, r1, LSL #1
    ADD r8, r0, r1, LSL r2

    SUB r6, r0, #0xF0
    SUB r7, r0, r1
    SUB r7, r0, r1, LSL #1
    SUB r8, r0, r1, LSL r2

注意:

  • {, <shift>}{} 表示可选,, 表示使用时需要在 Rm 后添加逗号和移位量。
  • 为什么没有 ADD r0, #3, #2?因为 C 语言中 int a = 1 + 2; 会在编译阶段计算,不需要在机器指令中体现。

4.3 什么是 12 位立即数

ARM 数据处理指令中的立即数并不是任意 32 位数,而是 12 位立即数 imm12

判断标准:

把某个数展开成二进制,该数必须存在一种 循环右移偶数位 的方式,使得移位后高 24 位全 0,低 8 位即为有效 imm8。

编码格式:

复制代码
0000 (rotate) 0000 0000 (imm8)
N(0-15)        1000 0001
2N(0-30)

也就是说,一个 32 位数如果能由一个 8 位立即数循环右移偶数位得到,它就是合法立即数。

例如:

asm 复制代码
    MOV r0, #0x80000000   ; 合法,1 循环右移 2 位?
    LDR sp, =0x40001000   ; 非法立即数,需要用 LDR 伪指令加载

4.4 LDR / STR 加载与存储

asm 复制代码
LDR<c> <Rt>, <label>
STR<c> <Rt>, <label>

笔记中写的 sdr 应为 STR

LDR 除了加载内存数据,还常用作伪指令加载 32 位立即数:

asm 复制代码
    LDR sp, =0x40001000

因为 0x40001000 不是合法 12 位立即数,不能直接 MOV sp, #0x40001000


4.5 MVN / BIC / ORR 位操作

MVN:按位取反传送

asm 复制代码
MVN{S}<c> <Rd>, #<const>{, <shift>}
MVN{S}<c> <Rd>, <Rm>
MVN{S}<c> <Rd>, <Rm>, <type> <Rs>

BIC:位清除,指定位置 0

asm 复制代码
BIC{S}<c> <Rd>, <Rn>, #<const>
BIC{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
BIC{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>

示例:

asm 复制代码
    MOV r0, #0xFFFFFFFF
    MOV r1, #1
    BIC r2, r0, r1, LSL #31
    BIC r3, r0, #(1 << 31)

ORR:位置 1

asm 复制代码
ORR{S}<c> <Rd>, <Rn>, #<const>
ORR{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
ORR{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>

示例:

asm 复制代码
    MOV r0, #0x00
    MOV r1, #1
    MOV r2, #31
    ORRS r6, r0, #0x80000000
    ORRS r7, r0, #(1 << 31)
    ORRS r8, r0, r1, LSL #31
    ORRS r9, r0, r1, LSL r2

4.6 条件标志 NZCV 与 CMP

CPSR 寄存器中条件判断标志位:

  • N:符号标志位。上条指令执行结果最高位 bit31 为 1,则 N = 1,作为有符号解释时为负值。
  • Z:零值标志位。结果为 0,则 Z = 1。
  • C:进位标志位。无符号解读,加法进位或减法无借位时 C = 1。
  • V:溢出标志位。有符号解读,发生溢出时 V = 1(两个正数加得负数,两个负数加得正数)。

常见条件码:

条件码 含义
EQ 相等
NE 不相等
GE 有符号大于等于
GT 有符号大于
LE 有符号小于等于
LT 有符号小于
AL 无条件执行

CMP 比较指令

asm 复制代码
CMP<c> <Rn>, #<const>
CMP<c> <Rn>, <Rm>{, <shift>}
CMP<c> <Rn>, <Rm>, <type> <Rs>

CMP r0, r1 等价于 SUBS r0, r1,只更新标志位,不保存结果。

练习:比较获取三个数中最大值。


4.7 跳转指令 B / BL / BX

指令 说明 是否保存返回地址 目标
B 普通分支 标签
BL 带链接分支,函数调用 是,保存到 LR 标签
BX 分支并交换指令集 寄存器
asm 复制代码
B<c> <label>
b fun          ; 等价于 ldr pc, =fun

BL<c> <label>
bl fun

BX<c> <Rm>
bx lr          ; 等价于 mov pc, lr

5. 循环结构

循环三要素:

  1. 循环结束条件
  2. 推动循环趋向终结的语句
  3. 循环体

do...while 的汇编实现

C 语言:

c 复制代码
int i = 0;
int sum = 0;
do {
    sum += i;
    i++;
} while (i <= 100);

汇编:

asm 复制代码
    MOV r0, #0      ; i
    MOV r1, #0      ; sum
loop
    ADD r1, r1, r0
    ADD r0, r0, #1
    CMP r0, #100
    BLE loop

while / for 的汇编实现

C 语言:

c 复制代码
int i = 0;
int sum = 0;
while (i <= 100) {
    sum += i;
    i++;
}

汇编:

asm 复制代码
    MOV r0, #0
    MOV r1, #0
loop
    CMP r0, #100
    BGT finish
    ADD r1, r1, r0
    ADD r0, r0, #1
    B loop
finish
    B finish

6. 函数定义与调用

6.1 PC 与 LR

  • PC 是程序计数器。
  • LR(R14)保存子程序返回地址。
  • 调用函数:BL func 会把下一条指令地址保存到 LR,然后跳转。
  • 返回:BX LRMOV PC, LR

示例:

asm 复制代码
    B main

func
    MOV r0, #1
    MOV r1, #2
    ADD r3, r0, r1
    BX lr

main
    MOV r0, #100
    MOV r1, #200
    BL func
    MOV r3, #300

6.2 栈与保护现场

问题:

  1. 函数被调可能修改主调寄存器。
  2. 函数嵌套时,LR 只有一个,无法正确返回。

解决:压栈保护现场,弹栈恢复现场

栈类型:

  • 空栈:*sp = xxxxsp++
  • 满栈:sp++*sp = xxxx
  • 增栈:sp += 4
  • 减栈:sp -= 4

ARM 2440 通常采用 满减栈

栈顶指针初始化:

asm 复制代码
    LDR sp, =0x40001000

Keil 中可在魔术棒 -> Target -> IRAM1 设置:#0x40000000 size:0x1000

6.3 STMFD / LDMFD

保护现场:

asm 复制代码
STMDB<c> <Rn>{!}, <registers>

恢复现场:

asm 复制代码
LDMFD<c> <Rn>{!}, <registers>

示例:

asm 复制代码
    B main

func1
    MOV r0, #10
    MOV r1, #20
    CMP r0, r1
    MOVGE r2, r0
    MOVLT r2, r1
    BX lr

func0
    MOV r0, #1
    MOV r1, #2
    ADD r3, r0, r1
    STMFD sp!, {r0-r12, lr}
    BL func1
    LDMFD sp!, {r0-r12, pc}
    ; BX lr

main
    LDR sp, =0x40001000
    MOV r0, #100
    MOV r1, #200
    STMFD sp!, {r0-r12, lr}
    BL func0
    LDMFD sp!, {r0-r12, lr}
    MOV r3, #300

finish
    B finish
    END

GNU 体系中保护现场、恢复现场通常由主调完成。

6.4 汇编调用 C 函数

步骤:

  1. 创建 main.c

  2. 声明 extern void c_add(void);

  3. 导入 IMPORT c_add(Keil 要求)

  4. 保护现场,BL 调用,恢复现场

  5. 解决编译报错:

    asm.axf: Error: L6238E: start.o(reset) contains invalid call from '~PRES8 ...' function to 'REQ8 ...' function c_add.

解决办法:栈对齐伪指令 PRESERVE8,确保函数调用时栈指针保持 8 字节对齐。

  1. 创建工程自动添加启动代码报错时:
    • 重建覆盖工程
    • 删除 .sct 文件
    • 添加 start.smain.c
    • 重设软件配置
  2. 魔术棒 -> Debug -> Use Simulator -> Run to main(取消)
  3. 魔术棒 -> Linker -> Use Memory Layout from Target Dialog(勾选)
  4. 魔术棒 -> Target -> ROM1 -> Start:0x0 Size:0x2000
  5. 函数传参:
asm 复制代码
    STMFD sp!, {r0-r12, lr}
    MOV r0, #1
    MOV r1, #2
    MOV r2, #3
    MOV r3, #4
    MOV r4, #5
    STMFD sp!, {r4}
    BL c_add
    LDMFD sp!, {r4}
    LDMFD sp!, {r0-r12, lr}

6.5 C 语言调用汇编函数

  1. 导出:EXPORT func1
  2. 声明:extern int func1(int a, int b);

7. 修改工作模式

CPSR 的 M 域控制工作模式。

  • MRS:读状态寄存器

    asm 复制代码
    MRS<c> <Rd>, <spec_reg>
  • MSR:写状态寄存器

    asm 复制代码
    MSR<c> <spec_reg>, #<const>
    MSR<c> <spec_reg>, <Rn>

示例:切换到 User 模式

asm 复制代码
    MRS r0, cpsr
    BIC r0, r0, #(0x1FF << 0)
    ORR r0, r0, #(0x10 << 0)
    MSR cpsr, r0

8. 异常向量表

异常向量表通常放在启动代码最前面。

软中断异常:

asm 复制代码
    SWI #7

通过 SWI 指令可以触发软件中断异常,进入对应的异常处理程序。


9. 作业与参考答案

1. 什么是立即数?如何判断某数是否是非法 12 位立即数?

立即数是在指令中直接给出的常量。ARM 数据处理指令支持 12 位立即数,编码为 8 位有效数循环右移偶数位。若一个 32 位数无法通过这种方式得到,就是非法 12 位立即数,需要用 LDR 伪指令加载。

2. B、BL、BX 指令的区别是什么?

  • B:普通跳转,不保存返回地址。
  • BL:带链接跳转,保存下一条指令地址到 LR,用于函数调用。
  • BX:跳转到寄存器地址,并根据寄存器 bit0 切换 ARM/Thumb 状态,常用于函数返回。

3. ARM 内核采用的栈是哪种栈?

ARM 通常采用 满减栈 (Full Descending),即栈顶指针先减后存,STMDB / LDMFD 配合使用。

4. CPSR 中条件标志位分别在什么情况下被置位?

  • N:结果最高位为 1。
  • Z:结果为 0。
  • C:加法进位或减法无借位。
  • V:有符号运算溢出。

5. ARM 汇编调用 C 语言函数以及 C 语言调用汇编函数,参数和返回值如何处理?

  • 汇编调用 C:前 4 个参数放入 r0-r3,更多参数压栈;返回值通常放在 r0。调用前保护现场,调用后恢复现场,注意 8 字节栈对齐。
  • C 调用汇编:汇编函数用 EXPORT 导出,C 中用 extern 声明。参数同样通过 r0-r3 传递,返回值放在 r0

10. 总结

本文整理了 ARM 汇编启动代码相关的核心知识:

  • 启动代码的任务与伪操作
  • 常用数据处理指令:MOV、ADD、SUB、MVN、BIC、ORR
  • 立即数判断
  • 条件标志与 CMP
  • 跳转指令 B、BL、BX
  • 循环结构
  • 函数调用、栈保护、STMFD/LDMFD
  • 汇编与 C 混合编程
  • 修改工作模式与异常向量表

掌握这些内容后,就可以编写一个基本的 ARM 启动代码,并顺利进入 C 语言环境运行。

相关推荐
学习FPGA的电气小兴兴1 小时前
基于FPGA的CORDIC向量模式实现arctan运算,Verilog HDL实现
嵌入式硬件·fpga开发·数字信号处理·verilog·fpga·cordic·arctan
xxy222412 小时前
ARM裸机开发(4)GPIO与LED/按键驱动详解——引脚复用与电气特性配置
arm开发·gpio
国科安芯2 小时前
商业航天星载数据管理单元的存储容错与接口集成方案研究
嵌入式硬件·架构·ecc·商业航天·抗辐射
天空'之城2 小时前
单片机基础核心知识点汇总(三十八)
单片机·嵌入式硬件
Awh-2 小时前
ARM裸机开发 Day01 学习笔记
arm开发·arm·arm裸机
wuminyu2 小时前
Markword在紧凑对象头上的实现原理剖析
java·linux·c语言·jvm·c++
myy-learn2 小时前
51单片机-中断-晶体管
51单片机
潘潘的嵌入式日记3 小时前
位操作怎么写才不出错?置位、清位、翻转、取位一次讲清
c语言·嵌入式·寄存器·位操作
崇山峻岭之间3 小时前
HC32F460PETB的PH2的EVENTOUT
单片机