在嵌入式开发中,ARM 处理器的启动代码是连接硬件与操作系统或应用程序的桥梁。
学习arm汇编的主要目的是为了编写arm启动代码,启动代码启动以后,引导程序到c语言环境下运行。换句话说启动代码的目的是为了在处理器复位以后搭建c语言最基本的需求。因此启动代码的主要任务有:
一、启动代码的核心任务
启动代码的主要目的是在处理器复位后,为 C 语言程序运行搭建最基本的环境,具体包括:
- 初始化异常向量表
- 初始化各工作模式的栈指针寄存器
- 开启 ARM 内核中断允许
- 将工作模式设置为 User 模式
- 引导程序进入 C 语言主函数执行
接下来,我们将学习实现这些任务所需的 ARM 汇编基础和关键指令。
汇编
二、ARM 汇编基础
1. 汇编程序结构
一个基本的 ARM 汇编程序结构如下:
area reset, code, readonly
code32
entry
; 代码主体
end
2. 关键- 伪操作: 它们不是 ARM 处理器实际的指令(如 MOV, ADD 等),而是写给汇编器看的命令,用于指导汇编器如何工作
area:定义一个段,是最重要的伪操作。程序、数据、堆栈等都需要组织在不同的段中
reset: 这是你为这个段起的名字。名字 reset 具有很强的暗示性,通常用于表示复位向量段,即CPU上电或复位后首先执行的第一段代码所在的位置。
code: 指定该段的属性为代码,意味着这个段包含可执行的指令。
readonly: 指定该段的属性为只读。对于代码段来说,这通常是默认且必须的。
code32:表示后续指令使用 32 位的 ARM 指令集
thumb:表示后续指令使用 16 位的 Thumb 指令集
entry:指定程序入口点
end:标记汇编程序的结束
三、常用指令详解
1. 数据传送指令 MOV
MOV 指令用于在寄存器之间或寄存器与立即数之间传送数据,基本格式:
MOV{S}<c> <Rd>, #<const> ; 立即数传送到寄存器
MOV{S}<c> <Rd>, <Rm> ; 寄存器传送到寄存器
MOV 指令还可以配合移位操作:
; 移位操作的规范形式
ASR{S} <Rd>, <Rm>, #<n> ; 算术右移
LSL{S} <Rd>, <Rm>, #<n> ; 逻辑左移
LSR{S} <Rd>, <Rm>, #<n> ; 逻辑右移
ROR{S} <Rd>, <Rm>, #<n> ; 循环右移
RRX{S} <Rd>, <Rm> ; 带扩展的循环右移
算数右移
为了进行除法运算
示例:
mov r0, #0x8 ; 将立即数0x8传送到r0
mov r1, r0 ; 将r0的值传送到r1
mov r3, #31 ; 将立即数31传送到r3
mov r0, #1
mov r6, r0, lsl #31 ; 将r0的值左移31位后传送到r6
mov r7, r0, lsl r3 ; 将r0的值左移r3中指定的位数(31位)后传送到r7
注意:
- 移位量#<n>或寄存器<Rs>的取值范围是 0-31
- RRX{S}:扩展右移 (不需要移位量)
- 计算机中数据以二进制形式存在,没有符号、浮点等概念,这些是编程时的解读方式
- 与C语言中的赋值运算对比(左值/右值),利于加深理解
2. 加法指令 ADD
ADD 指令用于执行加法操作,基本格式:
; 立即数作为第二操作数
ADD{S}<c> <Rd>, <Rn>, #<const>
; 寄存器作为第二操作数
ADD{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
; 寄存器作为第二操作数移位量
ADD{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
示例:
mov r0, #0x0F
mov r1, #0xF0
mov r2, #1
add r6, r0, #0xF0 ; r6 = r0 + 0xF0
add r7, r0, r1 ; r7 = r0 + r1
add r7, r0, r1, lsl #1 ; r7 = r0 + (r1 << 1)
add r8, r0, r1, lsl r2 ; r8 = r0 + (r1 << r2)
注意:
- (1){, <shift>} 其中{}代表可选择,“,”表示在使用时需要在Rm后添加“,” shift 移位量(立即数)
- (2) add r0, #3, #2 :为什么没有这种形式,C语言int a = 1 + 2; 编译阶段计算, 不需要在机器指令中体现
3. 减法指令 SUB
SUB 指令用于执行减法操作,基本格式:
; 立即数作为第二操作数
SUB{S}<c> <Rd>, <Rn>, #<const>
; 寄存器作为第二操作数
SUB{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
; 寄存器作为第二操作数移位量
SUB{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
示例:
mov r0, #0xFF
mov r1, #0xF0
mov r2, #1
sub r6, r0, #0xF0 ; r6 = r0 - 0xF0
sub r7, r0, r1 ; r7 = r0 - r1
sub r7, r0, r1, lsl #1 ; r7 = r0 - (r1 << 1)
sub r8, r0, r1, lsl r2 ; r8 = r0 - (r1 << r2)
4. 立即数的概念
在 ARM 指令中,立即数特指 12 位立即数(imm12)。判断一个数是否为有效的 12 位立即数的标准是:
将该数展开为二进制形式后,必须存在一种偶数位的循环右移方式,使得移位后高 24 位全为 0,低 8 位为有效的 imm8。
这是因为 ARM 指令中,立即数的编码方式是:
8 位数据(imm8)加上 4 位循环右移量(rotate),实际值为 imm8 循环右移 (2×rotate) 位。
5. 加载与存储指令 LDR/STR
LDR(加载)和 STR(存储)指令用于在内存和寄存器之间传送数据:
LDR<c> <Rt>, <label> ; 从label指向的内存地址加载数据到Rt寄存器
STR<c> <Rt>, <label> ; 将Rt寄存器的数据存储到label指向的内存地址
这些指令在初始化内存、设置异常向量表等操作中非常重要。
6. 按位取反 移动指令 MVN
MVN 指令将源操作数按位取反后传送到目标寄存器:
MVN{S}<c> <Rd>, #<const> ; 立即数取反后传送到Rd
MVN{S}<c> <Rd>, <Rm>{, <shift>} ; 寄存器值取反后传送到Rd
MVN{S}<c> <Rd>, <Rm>, <type> <Rs> ; 带移位的寄存器值取反后传送到Rd
MVN 指令常用于生成特定的掩码,特别是需要设置某些位为 0 而其他位为 1 的情况。
7. 位清除指令 BIC
BIC 指令用于将目标寄存器中指定的位清 0:
BIC{S}<c> <Rd>, <Rn>, #<const> ; 用立即数掩码清除Rn中的位,结果存到Rd
BIC{S}<c> <Rd>, <Rn>, <Rm>{, <shift>} ; 用寄存器掩码清除Rn中的位,结果存到Rd
BIC{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs> ; 用带移位的寄存器掩码清除Rn中的位
示例:
mov r0, #0xFFFFFFFF ; r0 = 0xFFFFFFFF
mov r1, #1 ; r1 = 1
bic r2, r0, r1, lsl #31 ; 清除r0的第31位,结果存到r2
bic r3, r0, #(1 << 31) ; 同上,使用立即数掩码
8. 按位或指令 ORR
ORR 指令用于执行按位或操作,常用于设置寄存器中的特定位:
ORR{S}<c> <Rd>, <Rn>, #<const> ; 寄存器与立即数按位或
ORR{S}<c> <Rd>, <Rn>, <Rm>{, <shift>} ; 寄存器与寄存器按位或
ORR{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs> ; 寄存器与带移位的寄存器按位或
示例:
mov r0, #0x00 ; r0 = 0x00
mov r1, #1 ; r1 = 1
mov r2, #31 ; r2 = 31
; 以下指令均为设置r0的第31位为1
orrs r6, r0, #0x80000000
orrs r7, r0, #(1 << 31)
orrs r8, r0, r1, lsl #31
orrs r9, r0, r1, lsl r2
在启动代码中,ORR 指令常用于设置 CPSR 寄存器中的特定标志位,如开启中断。
9.带s指令
四、条件判断与标志位NZCV
1、CPSR 寄存器中的条件标志位
ARM 处理器的当前程序状态寄存器(CPSR)中包含四个条件标志位,用于表示指令执行结果的状态:
- N(Negative):符号标志位。当指令执行结果的最高位(bit31)为 1 时,N=1,表示结果作为有符号数时为负值。
- Z(Zero):零值标志位:上条指令执行结果为0(即bit0 - bit31 均为0),则 Z = 1;
- C(Carry):进位标志位。进行无符号解读,如果在加法过程中进位或者减法时没有借位,则为 C = 1,否则 C = 0
- V(Overflow):进行有符号解读,是否发生溢出 -2^31 - 2^31-1(两个正数加得负数,两个负数加得正数),对于有符号运算,当运算结果超出有符号数的表示范围时,V=1。
2、常用条件码
eq
:等于(Z=1)ne
:不等于(Z=0)ge
:大于或等于(N=V)gt
:大于(Z=0 且 N=V)le
:小于或等于(Z=1 或 N≠V)lt
:小于(N≠V)al
:无条件执行(默认)
3、比较指令 CMP
CMP 指令用于比较两个操作数,本质上是执行减法操作但不保存结果,只更新标志位:
CMP<c> <Rn>, #<const> ; 比较寄存器与立即数
CMP<c> <Rn>, <Rm>{, <shift>} ; 比较两个寄存器
CMP<c> <Rn>, <Rm>, <type> <Rs> ; 比较寄存器与带移位的寄存器
cmp r0, r1
等价于subs r0, r1
(带 S 后缀的减法指令,会更新标志位但不保存结果)。
示例:比较获取三个数中的最大值
; 假设r0, r1, r2中存放三个待比较的数,结果存放在r3中
mov r3, r0 ; 先假设r0是最大值
cmp r3, r1 ; 比较r3和r1
blt update_max1 ; 如果r3 < r1,则更新最大值
b check_r2 ; 否则直接比较r2
update_max1:
mov r3, r1 ; 将r1设为当前最大值
check_r2:
cmp r3, r2 ; 比较当前最大值和r2
blt update_max2 ; 如果当前最大值 < r2,则更新最大值
b end_compare ; 否则结束比较
update_max2:
mov r3, r2 ; 将r2设为最大值
end_compare:
; 此时r3中存放的是三个数中的最大值
4、跳转指令
ARM 汇编提供了多种跳转指令,用于控制程序流程:
- B(Branch):无条件跳转
B<c> <label> ; 跳转到label处执行
b fun ; 等价于 ldr pc, =fun
- BL(Branch with Link):带返回地址的跳转,常用于函数调用
BL<c> <label> ; 跳转到label处执行,并将返回地址保存到lr寄存器
bl fun ; 调用fun函数,返回地址存于lr
- BX(Branch and Exchange):跳转并切换指令集
BX<c> <Rm> ; 跳转到Rm寄存器指定的地址,可切换ARM/Thumb模式
bx lr ; 从子程序返回,等价于 mov pc, lr
在启动代码中,这些跳转指令用于实现异常处理、函数调用和程序流程控制。
五、分支及循环结构
循环是程序设计中最基本的控制结构之一,无论是 C 语言还是汇编语言,都需要围绕 "循环三要素" 来实现:
- 循环结束条件
- 推动循环趋向终结的语句
- 循环体(重复执行的代码块)
1.分支结构
1. do...while 循环的实现
C 语言中的 do...while 循环先执行循环体,再判断循环条件:
int i = 0;
int sum = 0;
do{
sum += i; // 循环体:累加
i++; // 推动循环终结:计数器递增
}while(i <= 100); // 循环结束条件
对应的 ARM 汇编实现:
mov r0, #0 ; 初始化i=0,使用r0寄存器存储i
mov r1, #0 ; 初始化sum=0,使用r1寄存器存储sum
loop: ; 循环标签(相当于循环体开始)
add r1, r1, r0 ; 循环体:sum += i (r1 = r1 + r0)
add r0, r0, #1 ; 推动循环终结:i++ (r0自增1)
cmp r0, #100 ; 比较i与100(设置条件码)
ble loop ; 若i <= 100 (BLE: Branch if Less than or Equal),跳回loop继续循环
汇编实现中,使用cmp
指令进行条件比较,ble
条件跳转指令控制循环是否继续,这是汇编实现循环的核心机制。
2. while 与 for 循环的实现
C 语言中的 while 循环先判断条件,再执行循环体:
int i = 0;
int sum = 0;
while(i <= 100) // 先判断循环条件
{
sum += i; // 循环体
i++; // 推动循环终结
}
for 循环是 while 循环的,将初始化、条件判断和迭代语句整合在一起:
int sum = 0;
for(int i = 0; i <= 100; i++) // 初始化;条件;迭代
{
sum += i; // 循环体
}
对应的 ARM 汇编实现(while 和 for 在汇编层面实现相同):
mov r0, #0 ; 初始化i=0
mov r1, #0 ; 初始化sum=0
loop: ; 循环标签
cmp r0, #100 ; 先判断条件:比较i与100
bgt finish ; 若i > 100 (BGT: Branch if Greater Than),跳转到循环结束
add r1, r1, r0 ; 循环体:sum += i
add r0, r0, #1 ; 迭代:i++
b loop ; 无条件跳回loop继续循环
finish: ; 循环结束标签
b finish ; 死循环(通常在嵌入式中表示程序结束)
与 do...while 的汇编实现相比,while/for 的实现将条件判断移到了循环体之前,这正是两种循环结构的本质区别。
六、函数定义及调用机制
函数是代码复用和模块化的基础,ARM 汇编通过特殊寄存器实现函数调用与返回,理解这一机制对掌握汇编编程至关重要。
1. 函数的基本结构
C 语言中函数的定义格式:
返回值类型 函数名(形参列表) {
// 函数体
代码块;
return 返回值;
}
在 ARM 汇编中,函数的实现依赖两个关键寄存器:
- PC(Program Counter):程序计数器,存储下一条要执行的指令地址
- LR(Link Register):链接寄存器,存储函数调用后的返回地址
函数调用的核心机制是:
- 调用函数时,将当前 PC 值(返回地址)保存到 LR
- 函数执行完毕后,将 LR 的值恢复到 PC,实现返回
2. ARM 汇编函数调用示例
下面通过一个简单的函数调用来理解这一机制:
; 函数定义:计算两个数的和
func:
mov r0, #1 ; 函数体:设置第一个参数
mov r1, #2 ; 函数体:设置第二个参数
add r3, r0, r1 ; 函数体:计算和,结果存放在r3
bx lr ; 函数返回:将LR的值赋给PC,回到调用处
; 主程序
main:
mov r0, #100 ; 主程序逻辑:设置一些初始值
mov r1, #200 ; 主程序逻辑:设置一些初始值
bl func ; 调用func函数(BL: Branch with Link)
; BL指令会自动将下一条指令地址存入LR
mov r3, #300 ; func返回后继续执行的指令
b main ; 主程序循环
函数调用过程解析
- 执行
bl func
指令时,硬件自动完成:- 将下一条指令(
mov r3, #300
)的地址存入 LR 寄存器 - 将 PC 设置为 func 函数的入口地址,开始执行函数
- 将下一条指令(
- 函数执行到
bx lr
时:- 将 LR 中保存的返回地址(
mov r3, #300
的地址)赋给 PC - 程序跳回主程序继续执行
- 将 LR 中保存的返回地址(
在 ARM 汇编中,BL
(Branch with Link)指令是实现函数调用的关键,它自动完成了返回地址的保存;而BX LR
(Branch and eXchange)指令则实现了函数返回。
作业:
1、ARM 内核工作模式有哪些,分别是在什么情况下被切换?ARM 基本工作模式(7 种)
1、User(用户模式):非特权模式,大部分任务执行在此模式。
普通程序运行模式,权限最低,无法直接访问硬件资源或切换模式。
切换场景:程序正常执行时默认处于该模式,仅能通过异常进入其他模式。
2、IRQ(普通中断模式):低优先级(normal)中断产生时进入此模式。
特权模式,用于处理通用中断请求。
切换场景:外部设备触发 IRQ 中断时自动进入。
3、FIQ(快速中断模式):高优先级(fast)中断产生时进入此模式。
特权模式,用于处理高优先级、低延迟的中断(如定时器、高速外设)。
切换场景:外部设备触发 FIQ 中断时自动进入(优先级高于 IRQ)。
4、Supervisor(管理模式):当复位或软中断指令执行时将会进入这种模式
特权模式,用于操作系统内核管理,是复位后的默认模式。
切换场景:复位(Reset)、执行SWI
(软件中断)指令时进入5、Abort(中止模式):存取异常时进入此模式。
特权模式,用于处理内存访问错误(如非法地址、权限不足)。
切换场景:发生预取指中止(指令读取失败)或数据中止(数据读写失败)时进入。6、Undef(未定义模式):执行未定义指令时进入此模式。
特权模式,用于处理未识别的指令。
切换场景:CPU 执行未定义指令时自动进入。7、System(系统模式):使用与 User 模式相同寄存器集的特权模式。
特权模式,使用用户模式的寄存器集,用于运行操作系统核心代码。
切换场景:仅通过软件修改 CPSR 的模式位(M [4:0])主动切换。Cortex - A 特有模式
Monitor(监控模式):为安全扩展而来,用于执行安全监控代码;属于特权模式。
2、异常向量表是什么?
异常向量表是 ARM 内核 内存中一组固定地址的集合,每个地址对应一种异常(如复位、中断等),用于存储该异常的处理程序入口地址(或跳转指令)。
当异常发生时,CPU 会自动跳转到向量表中对应异常的固定地址,再通过该地址指向的指令(如
B
或LDR PC,=handler
)跳转到具体的异常处理逻辑。以 ARM 小端模式为例,典型异常向量表地址分布:
- 0x00000000:复位(Reset)
- 0x00000004:未定义指令
- 0x00000008:软件中断(SWI)
- 0x0000000C:预取指中止
- 0x00000010:数据中止
- 0x00000014:保留
- 0x00000018:IRQ 中断
- 0x0000001C:FIQ 中断
3、什么是立即数?如何判断某数是非法是12位立即数?实现压缩数据
1、立即数:指在指令中直接包含的常数(无需从内存读取),ARM 指令中立即数通过 12 位编码 表示(8 位数值 + 4 位旋转值)。
编码规则:立即数 = 8 位数值(imm8)经过 4 位旋转值(rotate)右移(rotate×2)位得到,即immediate = imm8 >> (rotate×2)
(或左移补 0,等效于循环右移)。2、判断非法 12 位立即数:把某个数展开成2进制,该数必须存在一种循环右移(偶数位),使得移位后高24位全0,低8位即为有效imm8;
4、b,bl,bx指令的区别是什么?1、b:无条件跳转指令,仅修改 PC 寄存器(程序计数器),不保存返回地址。
用途:用于普通跳转(如循环、条件分支),无返回需求场景。
示例:b loop
(跳转到 loop 标签)。2、bl:带链接的跳转指令,跳转时自动将下一条指令地址存入 LR(链接寄存器),再修改 PC。
用途:用于函数调用(需返回调用处),函数返回时通过bx lr
恢复执行。
示例:bl func
(调用 func 函数,返回地址存入 LR)。3、bx:跳转并切换指令集(ARM ↔ Thumb),跳转地址的最低位(bit0)决定目标指令集:
- 若 bit0 = 1:切换到 Thumb 模式(16 位指令集);
- 若 bit0 = 0:保持 ARM 模式(32 位指令集)。
用途:在 ARM 和 Thumb 指令集间切换执行,常配合 LR 实现函数返回(bx lr
)。
5、ARM内核采用的栈是哪种栈?ARM 内核默认采用 满递减栈,其特点:
满栈:栈指针(SP)指向最后一个已压入栈的元素(而非下一个空闲位置)。
递减栈:入栈时 SP 减小(向低地址方向增长),出栈时 SP 增大(向高地址方向移动)。