Arm架构
参考链接
Documentation for binutils 2.40 (sourceware.org)
GDB Documentation (sourceware.org)
[Documentation – Arm Developer](https://developer.arm.com/documentation#tab=all&cf-navigationhierarchiesproducts=Architectures,CPU Architecture,M-Profile,Armv6-M&numberOfResults=48)
中断向量表
向量表内容
-
初始堆栈指针 (SP):
- 位置: 向量表的第一个条目(偏移量0)。
- 内容: 初始化堆栈指针的地址。系统复位时,处理器会将这个值加载到主堆栈指针(MSP)寄存器中。
-
复位处理程序地址 (PC):
- 位置: 向量表的第二个条目(偏移量4)。
- 内容: 复位处理程序的入口地址。系统复位时,处理器会跳转到这个地址开始执行。
-
异常和中断处理程序地址:
- 位置: 从向量表的第三个条目开始(偏移量8),依次存放各个异常和中断的处理程序地址。
- 内容: 包括各种异常(如NMI、硬故障)和外部中断的处理程序地址。每个处理程序地址占用4个字节。
向量表排列示例
假设向量表的起始地址为 0x08000000,以下是一个示例排列:
| 偏移量 | 内容 | 描述 |
|---|---|---|
| 0x00 | 初始SP地址 | 初始堆栈指针 |
| 0x04 | 复位处理程序地址 | 复位处理程序(入口点) |
| 0x08 | NMI处理程序地址 | 非屏蔽中断处理程序 |
| 0x0C | 硬故障处理程序地址 | 硬故障处理程序 |
| 0x10 | 内存管理故障处理程序地址 | 内存管理故障处理程序 |
| 0x14 | 总线故障处理程序地址 | 总线故障处理程序 |
| 0x18 | 用法故障处理程序地址 | 用法故障处理程序 |
| … | … | … |
| 0xXX | 外部中断处理程序地址 | 外部中断处理程序(如EXTI) |
下面展示一个芯片的中断向量表:
1 | __vector_table |
注意事项
- 对齐要求: 向量表的起始地址通常需要对齐到特定的边界(如128字节或256字节),具体取决于处理器的实现。
- 可重定位: 在某些情况下,向量表可以被重定位到RAM中,以支持动态修改中断向量。
- 安全性: 确保向量表的内容在系统运行期间不会被意外修改,以防止中断处理程序被篡改。
寄存器
Arm-M系列的CPU寄存器有
-
通用寄存器(General Purpose Registers):R0 ~ R12:13个低位通用寄存器(Low Registers)
- 均可用于数据运算、地址计算。
- R0~R3 常用于函数参数传递和返回值(ABI规定)。
- R4~R11 常用于保存局部变量。
- R12:IP(Intra-Procedure-call scratch register),过程间调用临时寄存器。
-
特殊功能寄存器(Special Registers)
- R13 (SP):栈指针(Stack Pointer)
- 有两个:Main SP (MSP) 和 Process SP (PSP),用于操作系统线程切换。
- R14 (LR):链接寄存器(Link Register)
- 保存函数返回地址,调用子函数时自动保存。
- R15 (PC):程序计数器(Program Counter)
- 指向当前执行指令地址,可读可写(用于跳转)。
- R13 (SP):栈指针(Stack Pointer)
-
其他重要系统寄存器(通过MRS/MSR指令访问)
寄存器 名称 功能说明 CONTROL 控制寄存器 选择栈指针(MSP/PSP)、特权级别、FPU使能等 PRIMASK 中断屏蔽寄存器 禁止所有可屏蔽中断(类似关全局中断) FAULTMASK 异常屏蔽寄存器 禁止所有异常(除NMI和HardFault) BASEPRI 基础优先级寄存器 设置中断优先级阈值,屏蔽低优先级中断 IPSR 中断程序状态寄存器 当前正在执行的异常/中断编号 EPSR 执行程序状态寄存器 包含Thumb状态标志(T位)、条件标志等 APSR 应用程序状态寄存器 包含NZCVQ标志位(负、零、进位、溢出等) xPSR 组合程序状态寄存器 IPSR + EPSR + APSR 的组合(常用) -
FPU寄存器(仅Cortex-M4/M7/M33带FPU时存在)
- S0 ~ S31:32个单精度浮点寄存器(可配对成16个双精度D0~D15)
函数调用过程
ARM Cortex-M系列(Cortex-M0/M3/M4/M7等)芯片使用Thumb/Thumb-2指令集(几乎所有嵌入式应用都强制Thumb模式),GCC(arm-none-eabi-gcc)生成的函数调用遵循AAPCS(ARM Architecture Procedure Call Standard)调用约定。
调用约定(AAPCS)
调用约定(AAPCS)核心规则(Cortex-M):
- 参数传递:
- 前4个整数参数(int、指针等)用 R0 ~ R3 传递。
- 第5个及以后参数压入栈(push)。
- 返回值用 R0(或R0+R1用于64位)。
- 寄存器分类:
- Caller-saved(调用者负责保存):R0~R3、R12(可被被调用函数破坏)。
- Callee-saved(被调用者负责保存):R4~R11(如果函数使用,必须保存)。
- LR (R14):保存返回地址,调用函数时硬件自动保存到LR。
- SP (R13):栈指针。
- 栈对齐:SP必须8字节对齐(AAPCS要求)。
调用侧(Caller)
-
把前4个参数放入 R0~R3。
-
调用函数c语言视角为:
1
Key_IO_Init(gatIoKeyTable, IO_KEY_NUM);
-
调用函数汇编视角为:
1
2
3LDR R3, =gatIoKeyTable ; [PC, #12] [0x000294D0] =0x2000018C
MOVS R1, #6
MOVS R0, R3[!note]
这里看着是先将gatIoKeyTable参数的地址加载到R3寄存器中,之后又将R3的数据复制到R0中,看到汇编指令可能会有疑问,为什么不直接将gatIoKeyTable的地址存到R3中?
寄存器分配器的决定:GCC 的寄存器分配器(register allocator)在生成代码时,决定使用哪个寄存器来持有这个地址。
- 它选择了 R3 作为临时寄存器来加载地址(因为 R3 是 caller-saved,低寄存器,适合临时值)。
- 但后续代码需要这个地址在 R0 中(可能是作为函数参数、指针使用,或优化后 R0 更合适)。
因此,需要额外一条 MOVS R0, R3(或等价的 mov r0, r3)来复制值。这在优化级别下很常见(-O1/-O2/-O3),GCC 会优先使用低寄存器(R0~R3)作为临时,但最终根据使用位置调整。
-
-
如果有更多参数:
push {寄存器}或sub sp, #N+str存储。-
调用函数c语言视角为:
1
(void)AesEncryptUpdate(ECB_Zero, NULL, 16, Zero, 16, dataout, 16);
-
调用函数汇编视角为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14MOVS R3, #40
ADDS R2, R7, R3
MOVS R3, #16
STR R3, [SP, #8] ;这里的STR指令就是在压栈,从上往下压栈
MOVS R4, #24
ADDS R3, R7, R4
STR R3, [SP, #4]
MOVS R3, #16
STR R3, [SP, #0]
MOVS R3, R2
MOVS R2, #16
MOVS R1, #0
MOVS R0, #2
BL AesEncryptUpdate[!note]
这里可能会有疑问,栈的生长方向明明是向下的,为什么压栈的时候是将SP向上偏移压栈?
实际上,每个函数都有自己的栈帧,也就是每个函数都有自己的函数栈,在开始的时候,会首先分配栈空间,也就是sub指令,将栈向下生长,如以下函数序言:
1
2
3
4
5
6
7
8; test函数
PUSH {R4-R5, R7, LR}
SUB SP, SP, #16 ;这里的函数分配了16个字节大小的栈空间
ADD R7, SP, #0
STR R0, [R7, #12]
STR R1, [R7, #8]
STR R2, [R7, #4]
STR R3, [R7]如果调用这个函数,那么栈空间的分配为:
1
2
3
4
5
6
7
8高地址
+-------------------+ ← 起始地址
| 数据 | ← 外面调用test函数的栈数据
+-------------------+ ← 调用test函数前的SP
| 将要存放test函数中 |
| 产生的数据 |
+-------------------+ ← test序言结束之后的SP,分配了16个字节大小的数据
低地址序言结束后,函数内部的一些操作就可以安心的使用栈了,比如将很多参数(如果栈空间足够大)都压入栈中:
1
2
3
4
5
6STR R5, [SP, #20] ; 第10个参数(假设)
STR R4, [SP, #16] ; 第9个参数
STR R0, [SP, #12] ; 第8个参数
STR R1, [SP, #8] ; 第7个参数
STR R2, [SP, #4] ; 第6个参数
STR R3, [SP, #0] ; 第5个参数
-
-
调用指令:bl < function >(Branch with Link)
-
把返回地址保存到 LR。
-
跳转到被调用函数。
1
BL Key_IO_Init ; 这里的BL指令会跳转到Key_IO_Init,同时将PC + 4的地址保存到LR中
-
-
返回后:返回值在 R0,调用者可继续使用 R0~R3(因为它们可能被破坏)。
-
在被调用者的结尾可以看到将数据移入R0寄存器的操作:
-
c语言视角为:
1
return u8CanShouldBeClose; //函数体执行完成
汇编视角为:
1
2LDR R3, =u8CanShouldBeClose ; [PC, #8] [0x000297A8] =0x2000172E
LDRB R3, [R3] ;此时将计算出来的数据存放在R3中 -
代码视角:
1
} // 函数结束
汇编视角:
1
2
3
4
5MOVS R0, R3 ; 这个时候就会将R3中的数据移至R0中
MOV SP, R7
POP {R7, PC}
NOP
PUSH {R4, R7, LR}
-
-
被调用侧(Callee)
被调用侧除函数体外还有函数序言(prologue)和结尾(epilogue)
从C语言的角度来看:序言就是函数之前要执行的指令,结尾就是函数之后要执行的指令。💯
1 | void test() |
序言(prologue)
函数序言通常需要完成以下几件事(顺序可能略有差异):
| 步骤 | 目的 | 典型指令 | 说明 |
|---|---|---|---|
| 1 | 保存链接寄存器 LR(返回地址) | PUSH {…, LR} 或包含在 push 列表中 | 如果函数会调用其他函数(非叶子函数),必须保存 LR,否则子函数的 BL 会覆盖它。 |
| 2 | 保存被调用者需要保存的寄存器(callee-saved) | PUSH {R4-R11, …} | 根据 AAPCS,R4~R11、R13(SP)、R14(LR) 如果函数中使用,必须由当前函数(被调用者)负责保存和恢复。 |
| 3 | 为局部变量分配栈空间 | SUB SP, SP, #N | N 是局部变量总大小(包括对齐,通常 8 字节对齐)。如果没有局部变量,可能省略。 |
| 4 | (可选)建立帧指针(Frame Pointer) | MOV R7, SP 或 ADD R7, SP, #0 | 方便通过 [R7 + offset] 访问局部变量和参数,常用于带调试信息或中等优化级别。 |
| 5 | (可选)保存传入的参数到栈 | STR R0, [R7, #offset] 等 | 当函数需要长期使用参数、或准备调用 varargs 函数、或需要调试信息时,会把 R0~R3 的参数备份到栈上。 |
典型序言模式为:
-
模式1:带 frame pointer 的标准序言(最常见于 -O1/-O2 + 调试信息)
1
2
3
4PUSH {R4-R7, LR} ; 保存用到的 callee-saved 寄存器 + 旧 R7 + LR
SUB SP, SP, #32 ; 为局部变量分配空间(例如 32 字节)
ADD R7, SP, #0 ; 建立 frame pointer R7 = 当前 SP
; 可选:STR R0~R3 到 [R7 + offset] 保存参数 -
模式2:不带 frame pointer 的优化序言(加 -fomit-frame-pointer,-O2/-O3 常见)
1
2
3PUSH {R4-R7, LR} ; 只保存实际用到的寄存器 + LR
SUB SP, SP, #24 ; 分配局部变量空间(所有访问用 SP + offset)
; 不再有 MOV R7, SP -
模式3:叶子函数序言(函数内部不调用其他函数,所以不需要将LR压栈)
1
2
3PUSH {R4-R6} ; 只保存用到的寄存器,不需要保存 LR(因为不会被覆盖)
SUB SP, SP, #16 ; 分配局部变量
; 返回时用 BX LR(LR 仍是调用者写入的) -
模式4:极简序言(无局部变量、无需保存寄存器)
1
2; 什么都不做,直接进入函数体
; 返回用 BX LR
[!tip]
为什么序言必须做这些事?
- 保证正确返回:保存 LR(返回地址),否则子函数调用会破坏它,导致返回到错误位置。
- 遵守调用约定(AAPCS):callee-saved 寄存器(R4~R11)必须由当前函数保存和恢复,不能破坏调用者的值。
- 为局部变量提供空间:栈向下增长,sub sp 预留空间。
- 便于访问变量:用 frame pointer(R7)或 SP + offset 统一访问局部变量和参数备份。
- 支持调试和异常栈回溯:带 frame pointer 的序言能让调试器(gdb)或异常处理程序准确展开调用栈。
结尾(Epilogue)
函数结尾(epilogue)的主要任务是逆序撤销序言(prologue)所做的一切,恢复调用者的寄存器环境、释放栈空间,并正确返回到调用者。
==结尾必须严格与序言对应==,否则会导致栈失衡、寄存器破坏或返回地址错误。
| 步骤 | 目的 | 典型指令 | 说明 |
|---|---|---|---|
| 1 | 释放局部变量栈空间 | ADD SP, SP, #N 或直接融入 POP |
把序言中 SUB SP, SP, #N 分配的空间回收,使 SP 回到保存寄存器前的位置。 |
| 2 | 恢复 callee-saved 寄存器和 LR | POP {R4-R11, … , LR} 或 POP {…, PC} |
把序言中 PUSH 保存的寄存器值恢复回来。特别重要的是恢复 LR(返回地址)。 |
| 3 | 返回到调用者 | POP 到 PC 或 BX LR |
将 LR 中的返回地址写入 PC,实现跳转回调用者 BL 指令的下一条指令。 |
常见结尾模式(与序言对应)
-
模式1:带 frame pointer 的标准结尾(最常见,与你序言匹配)
序言示例:
1
2
3PUSH {R4-R5, R7, LR}
SUB SP, SP, #16
ADD R7, SP, #0对应结尾通常是:
1
2ADD SP, SP, #16 ; 释放局部变量空间(恢复到 push 后的 SP)
POP {R4-R5, R7, PC} ; 一次性恢复 R4、R5、旧 R7,并把保存的 LR 直接 pop 到 PC 返回或者等价的两步形式(较少见):
1
2
3ADD SP, SP, #16
POP {R4-R5, R7, LR}
BX LR关键点:Thumb 模式下允许
POP {..., PC},硬件会特殊处理把 LR 值写入 PC 并自动切换到 Thumb 状态。 -
模式2:不带 frame pointer 的优化结尾
序言:
1
2PUSH {R4-R7, LR}
SUB SP, SP, #24结尾:
1
2ADD SP, SP, #24
POP {R4-R7, PC} ; 直接返回 -
模式3:叶子函数结尾(序言没保存 LR)
序言只 PUSH 了部分寄存器,没动 LR:
1
2PUSH {R4-R6}
SUB SP, SP, #16结尾:
1
2
3ADD SP, SP, #16
POP {R4-R6}
BX LR ; LR 仍是调用者 BL 时写入的原始返回地址
[!important]
为什么结尾可以直接 POP 到 PC?
- Thumb/Thumb-2 指令集特意设计了
POP {..., PC}和LDMIA SP!, {..., PC}支持这种高效返回。- 硬件检测到写 PC 时,会自动清除 LR 最低位的 Thumb 状态位(因为它本来就是置 1 的),确保返回后仍在 Thumb 模式执行。
结尾的核心任务
- 释放局部变量空间(ADD SP 或融入 POP)。
- 恢复所有保存的寄存器(包括旧 R7 如果用了 frame pointer)。
- 恢复返回地址到 PC(POP PC 或 BX LR)。
只要结尾严格逆序对应序言,就能保证:
- 栈平衡(不泄漏内存)。
- 寄存器环境不变(调用者看不到变化)。
- 正确返回。
这也是为什么阅读反汇编时,看到结尾基本就是序言的“镜像反操作”。
总结:函数序言的核心任务就是**“保存必要寄存器 + 分配栈空间 + 建立访问基址”**,确保函数体能安全执行,并在退出时正确恢复现场并返回。GCC 会根据优化级别、是否需要调试信息、函数是否为叶子函数等自动选择最合适的序言形式。
CPU错误状态
当CPU出现问题时,我们可以通过CPU的一些寄存器可以看出问题的来源:
-
首要查看:故障状态寄存器(Fault Status Registers)
这些寄存器位于 System Control Block (SCB) 中,专门记录当前是否发生了故障以及故障类型。
寄存器名称 地址(相对于 SCB) 含义 SCB->CFSR (Configurable Fault Status Register) 0xE000ED28 最重要!分为三部分:UFSR / BFSR / MMFSR,记录 UsageFault、BusFault、MemManageFault 的具体原因 SCB->HFSR (HardFault Status Register) 0xE000ED2C 记录是否发生了 HardFault,以及是否由其他故障升级而来 SCB->DFSR (Debug Fault Status Register) 0xE000ED30 调试相关故障(一般不关心) SCB->MMFAR (MemManage Fault Address Register) 0xE000ED34 MemManage 故障时的无效地址 SCB->BFAR (Bus Fault Address Register) 0xE000ED38 BusFault 时的无效地址 推荐做法:在 HardFault_Handler 或其他故障处理函数里第一时间读取并保存 SCB->HFSR 和 SCB->CFSR 的值。
-
CFSR 寄存器详解(最有诊断价值)
CFSR 是 32 位寄存器,分成三个子寄存器(位段):
位段 位位置 常见标志位及含义 UFSR (Usage Fault Status) [31:16] UNDEFINSTR:执行了未定义指令
DIVBYZERO:除以 0(需使能)
UNALIGNED:非对齐访问(需使能)
NOCP:访问了不存在的协处理器
INVPC:无效的 PC 加载(异常返回错误)
INVSTATE:无效的 EPSR 状态(通常 Thumb 位错误)BFSR (Bus Fault Status) [15:8] IBUSERR:指令预取总线错误
PRECISERR:精确数据总线错误(有有效地址在 BFAR)
IMPRECISERR:非精确数据总线错误(地址可能不准)
UNSTKERR:异常返回时出栈错误
STKERR:异常入栈错误MMFSR (MemManage Fault Status) [7:0] IACCVIOL:指令访问违反 MPU 或缺页
DACCVIOL:数据访问违反 MPU 或缺页
MUNSTKERR:异常返回时 MPU 违反
MSTKERR:异常入栈时 MPU 违反
MLSPERR:浮点懒惰状态保存错误(M7)只要 CFSR 不为 0,就说明发生了可配置故障。
-
其他有助于诊断的寄存器
寄存器 含义 SCB->ICSR 包含当前正在执行的异常号(VectActive)和挂起的异常 SCB->SHCSR 使能了哪些故障(MemManage、BusFault、UsageFault) LR(进入异常时) 进入异常前的值通常是特殊 EXC_RETURN 值(如 0xFFFFFFF1/9/D/E),可以判断是从线程还是 Handler 模式进入、是否用了 FPU 等 栈上保存的上下文 异常发生时硬件自动压入栈的 8 个寄存器(R0-R3,R12,LR,PC,xPSR),其中栈上的 PC 就是故障指令地址!
[!tip]
需要注意的是,有些芯片是没有不支持这些寄存器的,如M0的核就不支持这些寄存器。
在写代码的时候可以在 HardFault_Handler 中加入以下代码(C 语言):
1 | void HardFault_Handler(void) |
用 J-Link / ST-Link / DAPLink 连接后,程序停在 HardFault 时,直接在调试器里查看 cfsr、hfsr、stacked_pc,就能快速定位:
- 是非法指令?
- 是非法地址访问?
- 是除零?
- 是栈溢出导致入栈失败?
- 还是返回时出栈错误?
thumb指令集
数据处理
| 指令 | 格式 | 功能 | 示例 |
|---|---|---|---|
| MOV | MOV Rd, Rm |
复制 Rm 到 Rd |
MOV R1, R2 |
| MVN | MVN Rd, Rm |
Rd = ~Rm(按位取反) |
MVN R1, R2 |
| ADD | ADD Rd, Rn, Rm |
Rd = Rn + Rm |
ADD R1, R2, R3 |
ADD Rd, Rn, #imm3 |
Rd = Rn + 3-bit 立即数 |
ADD R1, R2, #1 |
|
| SUB | SUB Rd, Rn, Rm |
Rd = Rn - Rm |
SUB R1, R2, R3 |
SUB Rd, Rn, #imm3 |
Rd = Rn - 3-bit 立即数 |
SUB R1, R2, #1 |
|
| AND | AND Rd, Rm |
Rd = Rd & Rm(按位与) |
AND R1, R2 |
| ORR | ORR Rd, Rm |
`Rd = Rd | Rm`(按位或) |
| EOR | EOR Rd, Rm |
Rd = Rd ^ Rm(按位异或) |
EOR R1, R2 |
| NEG | NEG Rd, Rm |
Rd = -Rm(取负) |
NEG R1, R2 |
| CMP | CMP Rn, Rm |
比较 Rn 和 Rm,设置标志位 |
CMP R1, R2 |
CMP Rn, #imm8 |
比较 Rn 和 8-bit 立即数 |
CMP R1, #10 |
|
| TST | TST Rn, Rm |
测试 Rn & Rm,设置标志位 |
TST R1, R2 |
移位操作
| 指令 | 格式 | 功能 | 示例 |
|---|---|---|---|
| LSL | LSL Rd, Rm, #imm5 |
逻辑左移 Rm(最多 31 位) |
LSL R1, R2, #4 |
| LSR | LSR Rd, Rm, #imm5 |
逻辑右移 Rm(最多 31 位) |
LSR R1, R2, #4 |
| ASR | ASR Rd, Rm, #imm5 |
算术右移 Rm(符号位扩展) |
ASR R1, R2, #4 |
| ROR | ROR Rd, Rm, #imm5 |
循环右移 Rm |
ROR R1, R2, #4 |
分支与控制
| 指令 | 格式 | 功能 | 示例 |
|---|---|---|---|
| B | B label |
无条件跳转到 label |
B loop |
| BL | BL label |
跳转到 label,保存返回地址到 LR |
BL func |
| BX | BX Rm |
跳转到 Rm,可切换 ARM/Thumb 模式 |
BX R1 |
| BEQ/BNE | B{cond} label |
条件跳转(基于 CPSR 标志位) |
BEQ loop(Z=1 时跳转) |
加载和存储
| 指令 | 格式 | 功能 | 示例 |
|---|---|---|---|
| LDR | LDR Rd, [Rn, #imm5] |
从 [Rn + imm5] 加载 32 位数据 |
LDR R1, [R2, #4] |
| STR | STR Rd, [Rn, #imm5] |
存储 Rd 到 [Rn + imm5] |
STR R1, [R2, #4] |
| LDRB | LDRB Rd, [Rn, #imm5] |
从 [Rn + imm5] 加载 8 位数据 |
LDRB R1, [R2, #4] |
| STRB | STRB Rd, [Rn, #imm5] |
存储 Rd 的低 8 位 |
STRB R1, [R2, #4] |
| LDRH | LDRH Rd, [Rn, #imm5] |
从 [Rn + imm5] 加载 16 位数据 |
LDRH R1, [R2, #4] |
| STRH | STRH Rd, [Rn, #imm5] |
存储 Rd 的低 16 位 |
STRH R1, [R2, #4] |
| LDMIA | LDMIA Rn!, {reg-list} |
从 Rn 加载多个寄存器(递增) |
LDMIA R1!, {R2, R3} |
| STMIA | STMIA Rn!, {reg-list} |
存储多个寄存器到 Rn(递增) |
STMIA R1!, {R2, R3} |
栈操作
| 指令 | 格式 | 功能 | 示例 |
|---|---|---|---|
| PUSH | PUSH {reg-list} |
压入寄存器到栈(SP 递减) |
PUSH {R1, R2} |
| POP | POP {reg-list} |
从栈弹出数据到寄存器(SP 递增) |
POP {R1, R2} |
软件中断
| 指令 | 格式 | 功能 | 示例 |
|---|---|---|---|
| SWI | SWI #imm8 |
触发软件中断(imm8 为参数) |
SWI #0 |
其他
| 指令 | 格式 | 功能 | 示例 |
|---|---|---|---|
| ADC | ADC Rd, Rm |
带进位加法 Rd = Rd + Rm + C |
ADC R1, R2 |
| SBC | SBC Rd, Rm |
带借位减法 Rd = Rd - Rm - !C |
SBC R1, R2 |
| MUL | MUL Rd, Rm |
乘法 Rd = Rd * Rm(低 32 位) |
MUL R1, R2 |
| RSB | RSB Rd, Rn, #0 |
反向减法 Rd = 0 - Rn |
RSB R1, R2, #0 |
| CMN | CMN Rn, Rm |
比较 Rn + Rm,设置标志位 |
CMN R1, R2 |
| ADR | ADR Rd, label |
加载 label 的地址到 Rd |
ADR R1, data |