ARM -02-汇编指令

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

一、ARM 常用指令集速查

ARM 汇编指令格式:  指令{S}{条件码}  目标寄存器, 操作数1, 操作数2

加 S 后缀 → 运算完成后更新 CPSR 的条件标志位(N/Z/C/V)

1.1 数据处理指令

指令

操作

示例

MOV

Rd = Operand2

MOV R0, #5    ; R0 = 5

MVN

Rd = NOT Operand2(取反后存入)

MVN R0, #0   ; R0 = 0xFFFFFFFF

ADD

Rd = Rn + Op2

ADD R0, R1, R2  ; R0 = R1+R2

ADDS

Rd = Rn + Op2(更新CPSR)

ADDS R0, R0, R1

ADC

Rd = Rn + Op2 + C(使用进位C)

ADC R1, R1, R3  ; 64位加法高32位

SUB

Rd = Rn - Op2

SUB R0, R1, #1

AND

Rd = Rn & Op2

AND R0, R0, #0xF0

ORR

Rd = Rn | Op2

ORR R0, R0, #0x01

EOR

Rd = Rn XOR Op2

EOR R0, R1, #0xFF

BIC

Rd = Rn & (~Op2)

BIC R0, R0, #0x1F

CMP

执行 Rn - Op2,仅更新CPSR,不保存结果

CMP R0, #10

CMN

执行 Rn + Op2,仅更新CPSR

CMN R0, R1

TST

执行 Rn & Op2,仅更新CPSR

TST R0, #0x01

TEQ

执行 Rn XOR Op2,仅更新CPSR

TEQ R0, R1

1.2 EOR(按位异或)详解

用途

说明

示例

翻转特定位

Op2对应为1的位会被翻转,为0的位保持不变

EOR R0, R1, #0xFF  → R1低8位全部取反

判断两值是否相等

EOR后若结果为0,说明两寄存器完全相同

EOR R0, R1, R2 → R0=0表示R1==R2

加密/校验

CRC校验和XOR加密常用

1.3 BIC(位清除)详解

操作:Rd = Rn AND (NOT Op2)  —— 先将 Op2 取反,再与 Rn 做按位与

效果:Op2 中为1的位 → 强制将 Rn 对应位清0;Op2 中为0的位 → 保留 Rn 原值

BIC R0, R1, #0x0F    ; 将 R1 低4位清零,其余位不变,结果存 R0

BIC R0, R0, #0x1F    ; 常用于清除 CPSR 的工作模式位(低5位)

; 经典用法(改工作模式):先BIC清零目标位,再ORR写入新值

BIC R0, R0, #0x1F    ; 清除低5位(模式位)

ORR R0, R0, #0x10    ; 写入 User 模式 (10000)

二、立即数限制与 LDR 伪指令

ARM 指令固定32位,既要编码操作码和寄存器,留给立即数的只有12位(8位数值 + 4位移位量)。

2.1 MOV 立即数限制规则

  • 合法立即数:8位常数经过偶数次循环右移能得到的值
  • 合法:0xFF、0x100(0x01<<8)、0xFF00、0x1FC(0x7F<<1)
  • 非法:0x40001000、0x123456 等不能由8位循环移位得到的32位数

MOV R0, #0xFF          ; 合法,直接编码

MOV R0, #0x100         ; 合法(1 左移8位)

MOV R0, #0x40001000    ; 非法!汇编器会报错

2.2 LDR 伪指令 —— 加载任意32位立即数

LDR Rd, =constant  是一条伪指令,汇编器将其自动转换为:

  • 若值较小且合法 → 直接转换为 MOV 指令
  • 若值较大无法用 MOV 表示 → 在代码段附近生成一个「文字池(Literal Pool)」,将常数存入文字池,再用 LDR Rd, [PC, #offset] 从文字池读取

LDR SP, =0x40001000    ; 伪指令,加载任意32位地址到SP

LDR R0, =0x12345678    ; 汇编器自动放入文字池并替换为内存读取

💡 为什么PC步进是4

code32 模式下,每条指令固定32位(4字节),所以 PC 每步进一次加4。跳转指令(B/BL)会改变PC为目标地址,但跳转后继续顺序执行时仍每次加4。

三、64位加法 —— ADDS + ADC

ARM 是32位处理器,要计算64位数据的加法,需要两条指令配合进位标志位 C。

指令

说明

ADDS

加法并更新 CPSR 标志位(包括进位C),但自身不使用C

ADC

加法并使用 CPSR 中的进位C(用于高32位相加时加上低32位产生的进位)

// 计算 0x1FFFFFFFF + 0x4(64位加法)

// 低32位存 R0,高32位存 R1

MOV R0, #0xFFFFFFFF   ; 低32位

MOV R1, #0x1          ; 高32位(合起来是 0x1_FFFFFFFF)

MOV R2, #0x4          ; 加数

MOV R3, #0x0



ADDS R0, R0, R2       ; 低32位相加,若有进位则 CPSR.C = 1

ADC  R1, R1, R3       ; 高32位相加 + 进位C,得到正确结果



; 结果:R0 = 0x00000003,R1 = 0x00000002(即 0x2_00000003)

四、条件码与汇编中的判断语句

ARM 的独特设计:几乎所有指令都可以加条件后缀,CPU 根据 CPSR 的 N/Z/C/V 标志决定是否执行。

4.1 常用条件码

后缀

含义

判断条件(CPSR位)

EQ

Equal(相等)

Z = 1

NE

Not Equal(不等)

Z = 0

GT

Greater Than(大于,有符号)

Z = 0 且 N = V

GE

Greater or Equal(大于等于,有符号)

N = V

LT

Less Than(小于,有符号)

N ≠ V

LE

Less or Equal(小于等于,有符号)

Z = 1 或 N ≠ V

HI

Higher(高于,无符号)

C = 1 且 Z = 0

LS

Lower or Same(低于等于,无符号)

C = 0 或 Z = 1

CS/HS

Carry Set(进位)

C = 1

CC/LO

Carry Clear(无进位)

C = 0

AL

Always(总是执行,默认)

任何情况

4.2 用汇编实现 if-else

// 计算 0x1FFFFFFFF + 0x4(64位加法)

// 低32位存 R0,高32位存 R1

MOV R0, #0xFFFFFFFF   ; 低32位

MOV R1, #0x1          ; 高32位(合起来是 0x1_FFFFFFFF)

MOV R2, #0x4          ; 加数

MOV R3, #0x0


ADDS R0, R0, R2       ; 低32位相加,若有进位则 CPSR.C = 1

ADC  R1, R1, R3       ; 高32位相加 + 进位C,得到正确结果


; 结果:R0 = 0x00000003,R1 = 0x00000002(即 0x2_00000003)

4.3 用汇编实现 while 循环

// C语言:while (R0 != 0) { R0--; }



loop

    CMP  R0, #0         ; 判断 R0 是否为0

    BEQ  end_loop       ; 若 R0 == 0,跳出循环

    SUB  R0, R0, #1     ; R0--

    B    loop           ; 无条件跳回循环开始

end_loop



; B    无条件跳转

; BEQ  相等时跳转(Z=1)

; BNE  不相等时跳转(Z=0)

五、栈操作 —— STMFD / LDMFD

ARM 使用满递减栈(Full Descending,FD):SP 指向最后压入的数据,压栈时向低地址增长。

5.1 STMFD —— 入栈(保护现场)

要素

说明

语法

STMFD SP!, {寄存器列表}

动作

从寄存器列表中编号最高的开始,依次 SP-=4,将寄存器内容存入 SP 地址

感叹号 !

Write-Back,操作完成后将新 SP 值写回 SP 寄存器(必须有!)

效果

SP 向低地址移动 4×N 字节(N = 寄存器个数)

5.2 LDMFD —— 出栈(恢复现场)

要素

说明

语法

LDMFD SP!, {寄存器列表}

动作

从 SP 当前位置依次读取数据到寄存器,每次 SP+=4

感叹号 !

操作完成后将新 SP 值写回 SP 寄存器

效果

SP 向高地址移动(还原到压栈前的位置)

5.3 实际压栈/出栈过程(图解)

// 函数入口:保护现场

STMFD SP!, {R4-R12, LR}

// 作用:将 R4~R12 和 LR 共10个寄存器压栈

// SP 向低地址移动 10×4 = 40 字节



// 函数出口:恢复现场并返回

LDMFD SP!, {R4-R12, PC}    ; 注意:弹出到PC而不是LR!

// 将栈中LR的值直接弹出到PC,实现函数返回

// ^符号(用于异常返回时同时恢复CPSR):LDMFD SP!, {R4-R12, PC}^

没有 ! 会怎样

STMFD SP, {R0-R3}  → 数据压入栈,但 SP 不更新!

下次再压栈时会覆盖刚才的数据,造成栈破坏。

正确写法必须带 !:STMFD SP!, {R0-R3}

六、ARM 调用约定 ATPCS

ATPCS(ARM-Thumb Procedure Call Standard)规定了函数调用时寄存器的使用规则,保证C和汇编可以互相调用。

6.1 寄存器使用规则

寄存器

角色

规则

R0 ~ R3

参数寄存器 / 返回值

前4个整型参数通过 R0~R3 传递;函数返回值存入 R0(64位返回值用 R0+R1);被调用者可以随意修改

R4 ~ R11

保存寄存器

被调用的函数不能改变这些寄存器的值!若要使用,必须先压栈保存,返回前恢复

R12 (IP)

内部调用临时寄存器

编译器使用,不可信赖

R13 (SP)

栈指针

始终指向当前栈顶,调用前后必须保持一致(平衡栈)

R14 (LR)

链接寄存器

调用函数时由 BL 指令自动设为返回地址;若函数内再调用函数,需先压栈保存 LR

R15 (PC)

程序计数器

不能直接操作(只能通过跳转指令改变)

6.2 超过4个参数时的传递方式

前4个参数:用 R0~R3 传递(快速,不占栈空间)

第5个及以后的参数:调用者(Caller)负责在调用前将参数压栈,被调用者从栈中读取。

// C语言函数:int c_add(int a, int b, int c, int d, int e, int f)

// 对应汇编调用:

MOV  R0, #1          ; 参数 a → R0

MOV  R1, #2          ; 参数 b → R1

MOV  R2, #3          ; 参数 c → R2

MOV  R3, #4          ; 参数 d → R3

MOV  R4, #5          ; 参数 e(超过4个,放入R4临时存储)

MOV  R5, #6          ; 参数 f

STMFD SP!, {R4, R5}  ; 将 e 和 f 压栈!调用者负责压栈

BL   c_add           ; 调用函数

                     ; c_add 内部:R0~R3读前4个参数,再从SP读第5、6个

 为什么不直接用R4/R5传参

因为 R4~R11 是被保护寄存器,被调用函数内部会保存并恢复它们,如果用来传参被调用者会恢复原值,参数就丢失了。所以规定第5个以后的参数必须通过栈传递。

七、C 与汇编混合编程

7.1 基本规则

  • 头部必须加 PRESERVE8  ← 声明使用8字节对齐栈(AAPCS标准要求)
  • 汇编导出函数:EXPORT  函数名  ← 让C文件可以调用此汇编函数
  • 汇编导入C函数:IMPORT  函数名  ← 在汇编中调用C语言函数
  • C文件声明汇编函数:int asm_add(int x, int y);  ← 参数类型随便定义,功能由汇编实现

7.2 start.s 完整代码注解

    PRESERVE8              ; 声明8字节栈对齐

    AREA reset, CODE, READONLY  ; 定义只读代码段,段名reset

    CODE32                 ; ARM 32位指令集

    ENTRY                  ; 程序入口点



; ─── 异常向量表(必须放在代码最开头,对应 0x00~0x1C)───

    B  start      ; 0x00 Reset → 跳转到 start 初始化

    NOP           ; 0x04 Undef(未定义指令异常,暂不处理)

    B  deal_swi   ; 0x08 SWI(软件中断)→ 跳转到 deal_swi 处理

    NOP           ; 0x0C Prefetch Abort

    NOP           ; 0x10 Data Abort

    NOP           ; 0x14 Reserved

    NOP           ; 0x18 IRQ

    NOP           ; 0x1C FIQ



; ─── SWI 软中断服务函数 ───

deal_swi

    STMFD SP!, {R4-R12, LR}  ; 保护现场(把R4~R12和LR全部压栈)

    SUB   R0, LR, #4         ; LR指向SWI指令的下一条,减4得到SWI指令地址

    LDR   R1, [R0]           ; 读取 SWI 指令的机器码

    BIC   R0, R1, #(0xFF<<24); 清除高8位(opcode),留下低24位的SWI号

    IMPORT c_deal_swi

    BL    c_deal_swi         ; 调用C函数处理SWI号

    LDMFD SP!, {R4-R12, PC}^ ; 恢复现场,^ 表示同时恢复 CPSR



; ─── 系统初始化 start ───

start

    LDR SP, =0x40001000      ; 设置管理模式(SVC)的栈指针(伪指令)



    ; === 切换到 User 模式 ===

    MRS R0, CPSR             ; 读取当前 CPSR

    BIC R0, R0, #0x1F        ; 清除低5位(模式位)

    ORR R0, R0, #0x10        ; 写入 User 模式(10000)

    MSR CPSR_C, R0           ; 写入CPSR(只更新控制域)



    LDR SP, =0x40000C00      ; 设置 User 模式的栈指针(独立!)



    ; === 测试函数调用(6个参数)===

    MOV R0, #1  ; MOV R1, #2  ; MOV R2, #3  ; MOV R3, #4

    MOV R4, #5  ; MOV R5, #6

    STMFD SP!, {R4, R5}      ; 第5、6个参数压栈

    IMPORT c_add

    BL c_add                 ; 调用 C 函数 c_add



    SWI #7                   ; 触发软中断,SWI号为7

    NOP

    B start                  ; 死循环



; ─── 汇编实现的加法函数 asm_add ───

    EXPORT asm_add           ; 导出供 C 调用

asm_add

    STMFD SP!, {R4-R12, LR}  ; 保护 R4~R12 和 LR

    ADD   R0, R0, R1         ; R0 = R0 + R1(利用 ATPCS:参数在R0/R1)

    LDMFD SP!, {R4-R12, PC}  ; 恢复寄存器,PC = LR(函数返回)

    END

7.3 main.c 代码注解

// 声明汇编中实现的函数(必须在C中声明才能调用)

int asm_add(int x, int y);



// SWI 软中断的 C 处理函数

// 参数 num 就是 SWI 指令中的立即数(由汇编提取后传入)

void c_deal_swi(unsigned int num)

{

    switch(num)

    {

        case 7:              // SWI #7 → 执行case 7的操作

            break;

        default:

            break;

    }

}



// 6个参数的加法函数

// a,b,c,d 通过 R0~R3 传入;e,f 通过栈传入(由调用者压栈)

int c_add(int a, int b, int c, int d, int e, int f)

{

    int sum = a + b + c + d + e + f;

    return sum;   // 返回值放在 R0

}

八、工作模式切换(MSR / MRS)

ARM 处理器有7种工作模式(User/FIQ/IRQ/SVC/Abort/Undef/System),通过操作 CPSR 寄存器切换。

8.1 MRS 和 MSR 指令

指令

格式

作用

MRS

MRS Rd, CPSR

读取 CPSR 到通用寄存器 Rd(必须先读再改)

MSR

MSR CPSR_<域>, Operand

将 Operand 写入 CPSR 的指定域

8.2 MSR 的域(_fields)

代表

说明

CPSR_C

控制域(bit0~7)

包含:中断屏蔽位I/F,Thumb状态T,工作模式M[4:0]。切换模式用这个域

CPSR_F

标志域(bit24~31)

包含:条件码N/Z/C/V

CPSR_X

扩展域(bit8~15)

很少直接使用

CPSR_S

状态域(bit16~23)

通常不直接操作

8.3 切换到 User 模式(代码解析)

    MRS R0, CPSR         ; 第一步:将当前CPSR读入R0(不能直接写CPSR)

    BIC R0, R0, #0x1F   ; 第二步:清除低5位(当前模式位清零)

    ORR R0, R0, #0x10   ; 第三步:写入User模式编码 10000 = 0x10

    MSR CPSR_C, R0      ; 第四步:将R0写回CPSR控制域(生效!)

⚠️ 重要:各模式 SP 是独立的!

每种工作模式都有自己独立的 SP 寄存器(Banked Register)

切换模式后,SP 的值变为该模式自己的值(未初始化时为未知值)

所以切换到新模式后,必须立即重新设置该模式的 SP!

start.s 中:SVC模式 SP = 0x40001000,切换到User后 SP = 0x40000C00

九、SWI 软中断

9.1 SWI 指令原理

SWI(Software Interrupt,软件中断)是一条主动触发中断的指令,用于从用户模式进入管理模式(系统调用)。

要素

说明

语法

SWI #立即数  (立即数范围:0 ~ 0x00FFFFFF,24位)

触发效果

CPU 自动切换到 SVC 模式,PC 跳转到 0x08(异常向量表的SWI入口)

立即数的作用

编码在 SWI 指令机器码的低24位,用于区分不同的系统调用功能

Linux 系统调用

Linux 用 SWI #0 触发所有系统调用,用 R7 存放系统调用号

9.2 如何从 SWI 指令中提取号码

进入 deal_swi 时,LR 指向 SWI 指令的下一条指令,所以 SWI 指令地址 = LR - 4。

deal_swi

    STMFD SP!, {R4-R12, LR}    ; 保护现场


    SUB  R0, LR, #4            ; R0 = LR - 4 = SWI指令的地址

    LDR  R1, [R0]              ; R1 = 读取该地址的内容(SWI指令的机器码)

    BIC  R0, R1, #(0xFF<<24)   ; 清除高8位(操作码 0xEF),剩下低24位就是SWI号


    ; 此时 R0 = SWI号(如 SWI #7 → R0 = 7)

    BL   c_deal_swi            ; 将SWI号作为参数传给C函数处理


    LDMFD SP!, {R4-R12, PC}^   ; 恢复现场,^ 表示同时将SPSR恢复到CPSR

9.3 SWI 机器码格式

位段

bit31~28

bit27~24

bit23~0

内容

条件码(默认1110=AL)

0xF(SWI操作码标识)

SWI立即数(你写的数字)

示例 SWI #7

1110

1111

000000000000000000000111

💡 异常向量表在代码开头的原因

ARM 上电复位后 PC = 0x00000000,执行的第一条指令就是地址0处的内容

如果这里是普通代码,遇到SWI/中断时 CPU 会跳到 0x08,但那里可能是普通指令而不是处理函数

所以代码开头8条必须是异常向量表(B/NOP),真正的初始化代码(start)在向量表之后

十、C语言底层原理(汇编视角)

10.1 变量初始化

  • 全局变量:存储在 BSS 段(未初始化)或 DATA 段(已初始化),默认为0,无需手动初始化
  • 局部变量:存储在栈上,内容为随机值(上次使用的残留数据),必须手动初始化!

10.2 指针的本质

指针是变量,存储的内容是内存地址(一个整数)。指针本身是什么类型,只有在定义声明时才确定。

  • 同一个地址(如 0x20000000)可以解释为 int(4字节读取)、char(1字节读取)、二级指针* 或普通整数
  • 内存寻址:地址以字节为单位编号,地址0x00=第0字节,0x01=第1字节,依次递增

10.3 if / switch 的底层

  • if 条件判断:底层用 CMP 指令比较,结果更新 CPSR 标志位,再用条件跳转指令(BEQ/BNE/BGT等)实现分支
  • switch多个 case 对应多个条件判断,等价于嵌套的 if-else,底层同样是 CMP + 条件跳转
  • while 循环:底层是「CMP 判断条件 → BEQ 退出 → 执行循环体 → B 跳回判断」的循环结构

10.4 为什么要用汇编直接操作 SP/LR/PC

C 语言无法直接操作 SP(栈指针)、LR(返回地址)、PC(程序计数器)这些特殊寄存器,因为:

  • C 编译器不提供访问这些寄存器的语法
  • 初始化 SP 必须在任何 C 代码执行之前完成(SP 未设置时压栈会破坏内存)
  • 异常返回需要特殊的 PC 回写语法(LDMFD ... PC}^),只有汇编能表达
  • 这就是嵌入式系统的 start.s 必须用汇编写的原因

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值