对于 Cortex-A 芯片来讲,大部分芯片在上电以后 C 语言环境还没准备好,所以第一行程序 肯定是汇编的,至于要写多少汇编程序,那就看你能在哪一步把 C 语言环境准备好。所谓的C语言环境就是保证 C 语言能够正常运行。C 语言中的函数调用涉及到出栈入栈,出栈入栈就要对堆栈进行操作,所谓的堆栈其实就是一段内存,这段内存比较特殊,由 SP 指针访问,SP 指针指向栈顶。芯片一上电 SP 指针还没有初始化,所以 C 语言没法运行,对于有些芯片还需要初始化 DDR,因为芯片本身没有 RAM,或者内部 RAM 不开放给用户使用,用户代码需要在DDR 中运行,因此一开始要用汇编来初始化 DDR 控制器。后面学习 Uboot 和 Linux 内核的时候汇编是必须要会的,是不是觉得好难啊?还要会汇编!前面都说了只是在芯片上电以后用汇编来初始化一些外设,不会涉及到复杂的代码,而且使用到的指令都是很简单的,用到的就那么十几个指令。所以,不要看到汇编就觉得复杂,打击学习信心。
GNU汇编和ARM汇编
注意,我们这里学的ARM汇编指的都是GNU ARM汇编,即GNU汇编,比较通用,并且支持ARM处理器。
GNU 汇编(GNU Assembler,简称
gas
)和 “ARM 汇编” 并不是完全对立的概念,它们的区别主要体现在语法规范和工具链归属上,具体可以从以下角度理解:1. 核心区别:语法规范与工具链
ARM 汇编(通常指 ARM 官方语法)
是 ARM 公司定义的汇编语法规范,主要用于 ARM 官方工具链(如armasm
汇编器)。其语法有明确的官方标准,例如:
- 指令后缀区分条件执行(如
MOVNE R0, #1
表示 “不等时移动”);- 伪指令格式更严谨(如
AREA
、EXPORT
、IMPORT
等用于段定义和符号导出);- 注释使用
;
开头。示例(ARM 官方语法):
AREA MyCode, CODE, READONLY EXPORT main main MOV R0, #5 ; 给R0赋值5 BX LR ; 返回 END
GNU 汇编(GAS 语法)
是 GNU 工具链(binutils
中的as
汇编器)采用的语法规范,支持多种架构(包括 ARM、x86 等)。它对 ARM 汇编的语法做了一些调整,更贴近 GNU 工具链的统一风格,例如:
- 条件执行通过前缀
.
表示(如movne R0, #1
,注意指令小写,条件后缀前无N
);- 伪指令使用
.
开头(如.section
、.global
、.extern
对应 ARM 官方的AREA
、EXPORT
、IMPORT
);- 注释支持
/* */
(多行)和//
(单行)。示例(GNU ARM 语法):
.section .text, "ax" .global main main: mov r0, #5 // 给r0赋值5 bx lr
2. 能否混合使用?
一般不建议直接混合使用,原因如下:
- 两者的汇编器(
armasm
和gas
)只识别各自的语法,例如 GNU 汇编的.global
在 ARM 官方汇编中会报错,反之 ARM 官方的EXPORT
在 GAS 中也不被识别。- 指令的条件执行、伪指令格式、符号处理等细节存在差异,混合编写会导致汇编失败。
3. 实际开发中的选择
- 如果使用 ARM 官方工具链(如 Keil MDK),需遵循ARM 官方语法;
- 如果使用 GNU 工具链(如 GCC 交叉编译环境,常见于 Linux 内核、嵌入式 Linux 开发),需使用GNU ARM 语法;
- 两者的核心指令集(如
MOV
、ADD
、LDR
等)功能一致,差异主要在伪指令和语法细节,熟悉一种后切换成本较低。简言之,GNU 汇编是 GNU 工具链的通用语法规范(支持 ARM),而 “ARM 汇编” 通常指 ARM 官方语法,两者因工具链不同而有语法差异,不能直接混合编写,但可根据工具链选择对应语法。
为什么gas汇编跟arm汇编语法不一致,gas汇编还能在arm上正确执行?
GAS(GNU Assembler)汇编与 ARM 官方汇编(如 armasm)的语法不一致,但仍能在 ARM 处理器上正确执行,核心原因是两者最终都会被转换为相同的 ARM 机器码,语法差异仅存在于 “人类可读的汇编层面”,不影响最终生成的二进制指令。具体可从以下角度理解:
1. 语法是 “表象”,机器码是 “本质”
ARM 处理器执行的是二进制机器码(由 0 和 1 组成的指令),而汇编语言只是机器码的 “人类可读表示”。无论是 GAS 语法还是 ARM 官方语法,最终目的都是通过汇编器(
gas
或armasm
)将汇编代码转换为 ARM 处理器能识别的机器码。例如,“将寄存器 R0 赋值为 5” 这个操作:
- ARM 官方语法:
MOV R0, #5
- GAS 语法:
mov r0, #5
两种写法的语法细节(大小写、寄存器命名风格)不同,但汇编器都会将其转换为相同的机器码(如
0xE3A00005
),ARM 处理器执行时只认这串二进制指令,完全不关心原始汇编代码的语法风格。2. 语法差异是 “工具链规范” 导致的,与处理器无关
GAS 和 ARM 官方汇编的语法差异,本质是不同工具链(GNU 工具链 vs ARM 官方工具链)的设计规范不同,而非针对 ARM 处理器的底层差异:
- GAS 作为 GNU 工具链的一部分,需要支持 x86、ARM、MIPS 等多种架构,因此采用了一套相对统一的语法风格(如指令小写、伪指令带
.
前缀,如.section
、.global
),方便开发者跨架构使用。- ARM 官方汇编(armasm)是 ARM 公司为自家处理器定制的工具,语法更贴近 ARM 架构的原生设计(如指令大写、伪指令无
.
前缀,如AREA
、EXPORT
),更强调与 ARM 架构特性的绑定。这些语法规则是工具链自己定义的 “翻译规则”,只要汇编器能正确将其翻译成 ARM 机器码,处理器就可以正常执行。
3. 汇编器的 “翻译适配” 作用
汇编器(
gas
或armasm
)的核心功能就是解析特定语法的汇编代码,生成对应的机器码。GAS 针对 ARM 架构做了专门适配:
- 它能识别 ARM 的所有指令集(ARM、Thumb、Thumb-2),无论用户用 GAS 语法写
mov r0, r1
还是add r2, r3, #4
,都能正确转换为对应的 ARM 机器码。- 对于伪指令(如段定义、符号导出),GAS 会将自己的语法(如
.section .text
、.global main
)转换为链接器需要的信息,最终生成符合 ARM 处理器执行要求的可执行文件(如 ELF 格式)。4. 类比:不同语言描述同一个动作
可以把这种现象类比为 “用不同语言说同一句话”:
- 中文 “请把那本书递给我” 和英文 “Please pass me that book”,表达方式不同,但传递的信息和最终效果(对方递书)是一样的。
- 同样,GAS 语法和 ARM 官方语法只是 “描述 ARM 指令的不同语言”,最终都指向相同的机器码执行效果。
总结
GAS 汇编与 ARM 官方汇编的语法差异是工具链设计规范导致的,属于 “表层语法” 的不同。只要汇编器(GAS)能正确将自己的语法转换为 ARM 处理器可识别的机器码,生成的程序就可以在 ARM 上正常执行。语法只是开发者与工具链交互的 “接口”,而处理器只关心最终生成的机器码 —— 这正是两者能兼容的核心原因。
常用指令列表
先提供下ARM汇编常用指令列表
以下是ARM架构(特别是ARMv7/ARMv8)中最常用的汇编指令分类列表:
1. 数据传输指令
指令 说明 示例 MOV
移动数据 MOV R0, R1
MVN
移动取反数据 MVN R0, #0xFF
LDR
从内存加载字 LDR R0, [R1]
STR
存储字到内存 STR R0, [R1]
LDRB
加载字节 LDRB R0, [R1]
STRB
存储字节 STRB R0, [R1]
LDM
批量加载 LDMIA R0!, {R1-R3}
STM
批量存储 STMIA R0!, {R1-R3}
2. 算术运算指令
指令 说明 示例 ADD
加法 ADD R0, R1, R2
ADC
带进位加法 ADC R0, R1, R2
SUB
减法 SUB R0, R1, #10
SBC
带借位减法 SBC R0, R1, R2
MUL
乘法 MUL R0, R1, R2
MLA
乘加 MLA R0, R1, R2, R3
UMULL
无符号长乘法 UMULL R0, R1, R2, R3
SMULL
有符号长乘法 SMULL R0, R1, R2, R3
3. 逻辑运算指令
指令 说明 示例 AND
逻辑与 AND R0, R1, R2
ORR
逻辑或 ORR R0, R1, R2
EOR
逻辑异或 EOR R0, R1, R2
BIC
位清除 BIC R0, R1, #0xFF
LSL
逻辑左移 LSL R0, R1, #2
LSR
逻辑右移 LSR R0, R1, #3
ASR
算术右移 ASR R0, R1, #4
ROR
循环右移 ROR R0, R1, #5
4. 比较与测试指令
指令 说明 示例 CMP
比较 CMP R0, R1
CMN
负数比较 CMN R0, #1
TST
位测试 TST R0, #0x80
TEQ
相等测试 TEQ R0, R1
5. 分支指令
指令 说明 示例 B
无条件分支 B label
BL
带链接分支(函数调用) BL func
BX
分支交换 BX LR
BLX
带链接分支交换 BLX R0
6. 条件执行指令
所有ARM指令都可以条件执行,条件码加在指令后:
条件码 含义 标志位 EQ
相等 Z=1 NE
不等 Z=0 CS/HS
进位/无符号大于等于 C=1 CC/LO
无进位/无符号小于 C=0 MI
负数 N=1 PL
正数或零 N=0 VS
溢出 V=1 VC
无溢出 V=0 HI
无符号大于 C=1且Z=0 LS
无符号小于等于 C=0或Z=1 GE
有符号大于等于 N=V LT
有符号小于 N≠V GT
有符号大于 Z=0且N=V LE
有符号小于等于 Z=1或N≠V 示例:
ADDEQ R0, R1, R2
(仅在Z=1时执行)7. 特殊指令
指令 说明 示例 SWI/SVC
软件中断/超级用户调用 SVC #0
MRS
读特殊寄存器 MRS R0, CPSR
MSR
写特殊寄存器 MSR CPSR, R0
NOP
空操作 NOP
DMB
数据内存屏障 DMB
DSB
数据同步屏障 DSB
ISB
指令同步屏障 ISB
8. Thumb/Thumb-2特有指令
指令 说明 IT
If-Then块(条件执行) CBZ
比较为零跳转 CBNZ
比较非零跳转 TBB
表分支字节 TBH
表分支半字 9. ARMv8/AArch64新增重要指令
指令 说明 LDP
加载寄存器对 STP
存储寄存器对 CSEL
条件选择 CSET
条件设置 RET
函数返回 接下来具体学习吧
基本语句格式
如果大家使用过 STM32 的话就会知道 MDK 和 IAR 下的启动文件 startup_stm32f10x_hd.s其中的汇编语法是有所不同的,将 MDK 下的汇编文件直接复制到 IAR 下去编译就会出错,因为 MDK 和 IAR 的编译器不同,因此对于汇编的语法就有一些小区别。我们要编写的是 ARM汇编,编译使用的 GCC 交叉编译器,所以我们的汇编代码要符合 GNU 语法。
GNU 汇编语法适用于所有的架构,并不是 ARM 独享的,GNU 汇编由一系列的语句组成,
每行一条语句,每条语句有三个可选部分,如下:
label:instruction @ comment
label 即标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。注意 label 后面的“:”,任何以“:”结尾的标识符都会被识别为一个标号。
instruction 即指令,也就是汇编指令或伪指令。
@符号,表示后面的是注释,就跟 C 语言里面的“/*”和“*/”一样,其实在 GNU 汇编文件中我们也可以使用“/*”和“*/”来注释。comment 就是注释内容。
比如如下代码:
add: MOVS R0, #0X12 @设置 R0=0X12
标号一般顶格写,指令部分前面四个空格。
上面代码中“add:”就是标号,“MOVS R0, #0X12”就是指令,最后的“@设置 R0=0X12”就是注释。
注意!ARM 中的指令、伪指令、伪操作、寄存器名等可以全部使用大写,也可以全部使用小写,但是不能大小写混用。
两种不同风格的ARM指令:
ARM官方的ARM汇编风格:
指令一般用大写、Windows中IDE开发环境(如ADS、MDK等)常用。如: LDR R0, [R1]
GNU风格的ARM汇编:
指令一般用小写字母、linux中常用。如:ldr r0, [r1]
:::GNU操作系统是一种由自由软件构成的类 Unix 操作系统,该系统基于 Linux 内核。
在 ARM 汇编中,标号(Label) 是用于标识程序中特定位置的名称,主要用于跳转、调用子程序或访问数据,是汇编程序中组织代码和数据的重要方式。
标号的基本特性
定义方式
标号通常放在指令或数据的前面,以冒号:
结尾(某些也可省略,取决于汇编器),例如:start: ; 代码标号 MOV R0, #0 ; 指令 data_buffer: ; 数据标号 .word 0x12345678 ; 数据定义
本质
标号在汇编时会被转换为对应位置的内存地址,因此可以直接作为跳转或数据访问的目标。标号的主要用途
程序跳转
用于B
(分支)、BL
(带链接分支)等指令,指定跳转目标:loop: ; 循环体代码 SUB R1, R1, #1 ; R1减1 BNE loop ; 若R1≠0,跳回loop继续循环
子程序调用
配合BL
指令调用子程序,并通过标号返回:main: BL func ; 调用func子程序 ; 主程序后续代码 func: ; 子程序代码 MOV PC, LR ; 从子程序返回(LR存返回地址)
数据访问
标识数据段中的变量或缓冲区,便于通过标号直接访问:.data msg: .asciz "Hello, ARM!" ; 字符串标号 .text main: LDR R0, =msg ; 将msg的地址加载到R0
标号的作用域
默认情况下,标号仅在当前文件或代码块中有效,无需额外声明。
如果想要被其他文件访问,就要使用.global来声明成全局标号。
.global reset_handler ; 声明为全局标号 reset_handler: ; 复位处理代码
特殊标号
.
(当前地址):表示当前指令或数据的地址,常用于计算相对偏移:.word . ; 存储当前地址的值
标号的相对引用:汇编器会自动计算标号之间的相对地址,尤其适合位置无关代码(PIC)。
注意事项
- 标号名称需符合汇编器的命名规则(通常以字母开头,可包含数字、下划线)。
- 同一作用域内不能有重复的标号,否则会导致汇编错误。
- 标号的实际地址由链接器最终确定,汇编阶段仅确定相对偏移。
通过合理使用标号,可以使汇编代码结构清晰,便于维护和跳转控制。
有时候我们会看到有些地方用分号“;”来注释
在 ARM 汇编中,分号(
;
)是否能作为注释符,取决于你使用的汇编器和具体环境:GNU 汇编器(
as
,如arm-linux-gnueabihf-as
)
通常不支持分号作为注释符,主要使用@
作为注释标记。如果使用分号,可能会被视为语法错误或无效字符。示例(不推荐):
mov r0, #0 ; 这种写法在 GNU as 中可能报错,应改用 @ mov r1, #1 @ 正确的注释方式
ARM 官方汇编器(
armasm
)
支持分号作为注释符,这是其推荐的注释方式之一,与@
作用相同。示例:
MOV R0, #0 ; 这是有效的注释(在 armasm 中) MOV R1, #1 @ 也支持 @ 作为注释符
其他环境
在某些嵌入式开发工具链(如 Keil、IAR)中,分号通常被支持作为注释符,兼容 ARM 汇编语法。总结:
- 如果你使用的是 GNU 工具链(
as
),建议优先使用@
作为注释符,避免使用分号。- 如果你使用的是 ARM 官方工具链或嵌入式 IDE,分号通常是合法的注释符。
为了保证代码的可移植性,尤其是在交叉编译场景下,统一使用
@
作为注释符是更安全的选择。
常见符号
gnu汇编中的一些符号:
@ 用来做注释。可以在行首也可以在代码后面同一行直接跟,和C语言中//类似。当然,也可以直接用C语言中的//和/* */
:以冒号结尾的是标号,标号类似于C语言中的函数名,在汇编中,以_start标号作为程序的入口。
. 点号在gnu汇编中表示当前指令的地址
# 立即数前面要加#或$,表示这是个立即数
注意,点.符号
伪指令前面的点(
.
)不表示当前地址,它是 ARM 汇编中伪指令的语法标识,用于区分伪指令和机器指令。具体来说:
- 带点(
.
)的指令(如.word
、.byte
、.text
)是伪指令,由汇编器解析,用于指导汇编过程(如定义数据、划分段、设置对齐等),不会直接生成机器码。- 不带点的指令(如
MOV
、ADD
、B
)是机器指令,会被汇编器翻译成 CPU 可执行的二进制指令。而表示当前地址的是单独的
.
(一个点),它是一个特殊的符号,例如:current_addr: .word . ; 存储当前地址(即 current_addr 标号的地址)
这里的
.
才代表当前汇编位置的地址,与伪指令前的点(如.word
中的.
)含义完全不同。简单总结:
- 伪指令前的
.
是语法标记(区分伪指令与机器指令)- 单独的
.
才表示当前地址
汇编指令和伪指令
汇编指令是CPU机器指令的助记符,经过编译后会得到一串1、0组成的机器码,可以由CPU读取执行。
汇编伪指令本质上不是指令(只是和指令一起写在代码中),它是编译器环境提供的,目的是用来指导编译过程,经过编译后伪指令最终不会生成机器码。
不说人话的定义呀,所以。。。指令和伪指令到底有啥区别?
伪指令可以看做是汇编器的语法格式,告诉汇编器要如何将汇编指令编译成机器码。
参考:
我们写的指令,由“编译软件”翻译成机器码的,称为指令语句;有一些是写给“编译软件”看的,不翻译成机器码,这样的就是“伪指令”。
伪指令不直接控制运行时刻的机器,但是控制翻译程序如何生成机器指令代码,也就是只为编译服务,编译完成后,伪指令的作用也就消失了。
常见伪指令直接参考:
汇编入口
汇编程序的默认入口标号是_start,不过我们也可以在链接脚本中使用 ENTRY 来指明其它 的入口点,下面的代码就是使用_start 作为入口标号:
.global _start _start: ldr r0, =0x12 @r0=0x12
上面代码中.global 是伪操作,表示_start 是一个全局标号,类似 C 语言里面的全局变量。
我们也可以在链接时通过
-e
参数指定其他入口:#arm-linux-gnueabihf-ld -o program program.o # 默认以_start为入口 # 若入口标签为其他名称(如main),需显式指定: arm-linux-gnueabihf-ld -e main -o program program.o
其实就是把入口标签处的指令,作为程序的开头,然后再将程序烧录到入口地址处。
如果指定了多个入口呢?一般会报错或默认使用
_start
。
汇编语言可以不要入口吗?
在汇编语言中,程序通常需要一个明确的入口点,但具体是否 “必须” 取决于运行环境和程序类型。
1. 大多数场景下需要入口点
对于可独立运行的程序(如操作系统内核、应用程序),入口点是必不可少的,它告诉加载器或操作系统 “从哪里开始执行第一条指令”。
裸机环境(如嵌入式系统):
入口点通常由硬件或启动程序指定(如 ARM 的复位向量表),例如:.section .vector reset: B main ; 复位后跳转到main(入口点) .text main: ; 程序入口 MOV R0, #0 ...
操作系统环境(如 Linux):
编译器 / 链接器会默认寻找_start
作为入口点(或通过链接脚本指定),若缺失会导致程序无法启动。2. 特殊情况可以 “没有显式入口”
作为库文件被调用:
若汇编代码仅作为函数库(供其他程序调用),无需独立入口,只需暴露函数标号即可。嵌入式固件的特定场景:
某些固定地址执行的程序(如 Bootloader),可能直接通过硬件复位地址(如 0x00000000)作为隐含入口,无需显式声明标号。总结
- 独立运行的程序必须有入口点,否则加载器无法确定执行起点。
- 作为组件被调用的代码(如库函数)可以没有入口点,只需提供可被外部引用的标号。
入口点的本质是 “程序执行的起始标记”,是否需要显式定义,取决于程序的运行方式和所处环境。
代码段
用户可以使用.section 伪操作来定义一个段,汇编系统预定义了一些段名:
.text 表示代码段。
.data 初始化的数据段。
.bss 未初始化的数据段。
.rodata 只读数据段。
我们当然可以自己使用.section 来定义一个段,每个段以段名开始,以下一段名或者文件结 尾结束,比如:
.section .testsection @定义一个 testsetcion 段
更多待补充。
为什么汇编段名前面要加个点?
在汇编语言中,段名(如
.text
、.data
)前面的点(.
)是一种语法约定,主要用于区分段名与普通标号(Label),并标识这些名称是汇编器内置的预定义段(Predefined Sections)。这种约定的具体作用和来源如下:
1. 区分预定义段与用户自定义标号
汇编中,用户定义的标号(如
main
、loop
)通常不带点,而带点的名称(如.text
)被汇编器识别为系统预定义的特殊段。例如:.text ; 预定义代码段(带点) main: ; 用户自定义标号(不带点) MOV R0, #0
通过点的有无,汇编器能快速区分 “系统保留的段名” 和 “用户定义的标识”,避免命名冲突。
2. 遵循汇编器的语法规范
这种约定源于早期汇编器(如 GNU 汇编器
gas
)的设计,逐渐成为行业通用标准。带点的名称被赋予特殊含义:
- 表示该名称是汇编器或链接器的内置关键字,而非普通符号。
- 用于标识程序的结构性元素(段、伪指令等),与执行逻辑(指令、标号)区分开。
3. 与伪指令的命名规则保持一致
汇编中的伪指令(如
.word
、.align
)也以点开头,段名的命名方式与其统一,形成了 “带点的名称用于辅助汇编过程” 的逻辑:
- 点(
.
)作为一种 “元标记”,表明这些名称是给汇编器看的 “指令”,而非给 CPU 执行的机器指令。总结
段名前的点是语法层面的区分标记,核心目的是:
- 明确标识该名称是汇编器预定义的段(而非用户自定义标号);
- 避免命名冲突,确保汇编器能正确解析程序结构;
- 与伪指令等辅助性语法保持风格一致,增强代码可读性。
这种约定在多数汇编器(如 ARM 的
armasm
、x86 的gas
)中通用,是汇编语言发展中形成的行业规范。
汇编中的语句必须要定义在某个段下面吗?
在汇编语言中,语句通常需要定义在某个段(Section)下面,但具体取决于汇编器和目标架构的要求。大多数现代汇编器(如 ARM 的
armasm
、x86 的nasm
或gas
)都采用 “段式” 组织方式,要求代码和数据必须属于特定的段,以确保程序被正确加载到内存中执行。为什么需要段?
段(Section)的核心作用是告诉汇编器和链接器:这段内容的用途是什么(代码 / 数据 / 只读数据等),以及应该被加载到内存的哪个区域。例如:
- 代码段(
.text
):存放可执行指令,通常加载到内存的可执行区域;- 数据段(
.data
):存放初始化的数据,通常加载到可读写区域;- 只读数据段(
.rodata
):存放常量,通常加载到只读区域。操作系统和加载器依赖这些段信息来正确分配内存、设置权限(如代码段不可写、只读数据段不可修改),否则程序可能无法正常执行(甚至崩溃)。
特殊情况:默认段的隐式定义
有些汇编器允许在不显式声明段的情况下编写代码,此时汇编器会将语句默认放入某个段中(通常是代码段
.text
)。例如:; 未显式声明段,汇编器默认放入.text段 start: MOV R0, #0 MOV PC, LR
这里虽然没有写
.text
,但汇编器会自动将代码归为.text
段。不定义段的风险
如果完全脱离段结构编写汇编语句(且汇编器不支持默认段),会导致:
- 汇编器报错:无法确定语句的用途和内存分配方式;
- 链接器失败:无法生成可执行文件(缺少段信息);
- 程序加载错误:即使生成文件,操作系统也无法正确加载到内存。
总结
- 现代汇编器通常要求语句属于某个段,显式声明段(如
.text
、.data
)是规范的做法;- 部分汇编器支持隐式默认段(如默认将代码放入
.text
),但不建议依赖此特性,显式声明段可使代码更清晰、可移植性更好;- 段的本质是对程序内容进行分类,确保内存分配和权限设置正确,是汇编程序能够正常执行的基础。
Cortex-A7常用汇编指令
指令参考
注意,ARM架构的那些寄存器都可以用对应的名字来访问,比如:
R0~R15,SP、LR、PC、CPSR、SPSR;
ARM采用RISC架构,CPU处理数据时,需要先将内存中内容加载入CPU中通用寄存器中才能被CPU处理。(CISC结构可以直接处理内存数据)
也就是说,ARM 不能直接访问内存中的数据。
处理器内部数据传输指令
内部指的是寄存器之间的数据传递。
使用处理器做的最多事情就是在处理器内部来回的传递数据,常见的操作有:
①、将数据从一个寄存器传递到另外一个寄存器。
②、将数据从一个寄存器传递到特殊寄存器,如 CPSR 和 SPSR 寄存器。
③、将立即数传递到寄存器。
数据传输常用的指令有三个:MOV、MRS 和 MSR,这三个指令的用法如表 7.2.1.1 所示:
![]()
分别来详细的介绍一下如何使用这三个指令:
1、MOV 指令
MOV 指令用于将数据从一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄存器里面,使用示例如下:
MOV R0,R1 @将寄存器 R1 中的数据传递给 R0,即 R0=R1 MOV R0, #0X12 @将立即数 0X12 传递给 R0 寄存器,即 R0=0X12
2、MRS 指令
MRS 指令用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器,要读取特殊 寄存器的数据只能使用 MRS 指令!使用示例如下:
MRS R0, CPSR @将特殊寄存器 CPSR 里面的数据传递给 R0,即 R0=CPSR
3、MSR 指令
MSR 指令和 MRS 刚好相反,MSR 指令用来将普通寄存器的数据传递给特殊寄存器,也就 是写特殊寄存器,写特殊寄存器只能使用 MSR,使用示例如下:
MSR CPSR, R0 @将 R0 中的数据复制到 CPSR 中,即 CPSR=R0
助记:
MRS,MOV R S,表示是从SPSR或者CPSR到寄存器R;
MSR,MOV S R,表示是从寄存器R到SPSR或者CPSR;
注意:
语句的参数和参数之间,需要用逗号隔开;
mvn指令
mvn和mov用法一样,区别是mov是原封不动的传递,而mvn是按位取反后传递
按位取反的含义:
譬如r1 = 0x000000ff,然后mov r0, r1 后,r0 = 0xff
但是我mvn r0, r1后,r0=0xffffff00
立即数
上面提到了一个立即数,是个啥?
参考:
在ARM汇编的数据处理指令中经常会使用到常数,而ARM汇编中规定使用的常数必须是立即数。为啥呢?
这是由于所有的ARM指令是精简指令集,指令长度固定都是32位,对于ARM数据处理指令自然也是一样。数据处理指令大致可包含3类,数据传送指令、数据算术逻辑运算指令和数据比较指令。在一条ARM数据处理指令中,除了要包含处理的数据值外,还要标识ARM命令名称,控制位,寄存器等其他信息。这样在一条ARM数据处理指令中,能用于表示要处理的数据值的位数只能小于32位;
具体过程查看上述参考文章,一个结论就是:
一个8bit常数循环右移(Y*2 = {0,2,4,6,8, ...,26, 28, 30})就得到一个立即数了。
问题来了,那岂不是所有的立即数都是偶数?岂不是立即数没法处理所有的常数了?如果我们想要给某个寄存器赋一个非立即数的常数,岂不是做不到了?
显然,这是极其不合理的。
参考:
合法的立即数的判断 - Zackary丶Liu - 博客园 (cnblogs.com)
关于立即数的基本使用可参考:
那么,汇编中如何使用非法立即数呢?
对于无法表示的32位数, 只有通过逻辑或算术运算等其它途径获得了。比如0xffffff00, 可以通过0x000000ff按位取反得到。
不过,实际编程中使用时,其实没必要一个一个的算,只要利用LDR伪指令就可以了。
ARM伪指令与伪操作 - 又要起名字呀 - 博客园 (cnblogs.com)
LDR 伪指令用于加载 32 位的立即数或一个地址值到指定寄存器 。形式如:
LDR{cond} register,=[expr | label_expr]
与 ARM 指令的 LDR 相比 , 伪指令的 LDR 的参数有“ =” 号 。应用之一就是加载立即数:
如 LDR R2, =0xFF0 其等同于MOV R2, #0xFF0 但需要注意的是LDR指令加载常量可以是合法的立即数也可以不是,但是MOV加载数时必须为合法的立即数。
存储器访问指令
ARM 不能直接访问存储器,我们用汇编来配置寄存器的时候需要借助存储器访问指令。
常用的存储器访问指令有两种:LDR 和 STR,用法如表 7.2.1.2 所示:
分别来详细的介绍一下如何使用这两个指令:1、LDR 指令
LDR 主要用于从存储加载数据到寄存器 Rx 中,LDR 也可以将一个立即数加载到寄存器 Rx
中,LDR 加载立即数的时候要使用“=”,而不是“#”。在嵌入式开发中,LDR 最常用的就是读
取 CPU 的寄存器值,比如 I.MX6UL 有个寄存器 GPIO1_GDIR,其地址为 0X0209C004,我们现在要读取这个寄存器中的数据,示例代码如下:
示例代码 LDR 指令使用
LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004 LDR R1, [R0] @读取地址 0X0209C004 中的数据到 R1 寄存器中
上述代码就是读取寄存器 GPIO1_GDIR 中的值,读取到的寄存器值保存在 R1 寄存器中,
上面代码中 offset 是 0,也就是没有用到 offset。
注意:::直接操作的仍然是寄存器,而不是内存,因为根本就没法直接操作内存,而是把内存地址存入到一个寄存器中,然后再操作寄存器。
2、STR 指令
LDR 是从存储器读取数据,STR 就是将数据写入到存储器中,同样以 I.MX6UL 寄存器
GPIO1_GDIR 为例,现在我们要配置寄存器 GPIO1_GDIR 的值为 0X20000002,示例代码如下:
示例代码 STR 指令使用
LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004 LDR R1, =0X20000002 @R1 保存要写入到寄存器的值,即 R1=0X20000002 STR R1, [R0] @将 R1 中的值写入到 R0 中所保存的地址中
不管是ldr还是str,第一个参数都是寄存器,第二个参数都是存储了内存地址的寄存器。只不过,数据传递的方向是相反的。
LDR 和 STR 都是按照字进行读取和写入的,也就是操作的 32 位数据,如果要按照字节、
半字进行操作的话可以在指令“LDR”后面加上 B 或 H,比如按字节操作的指令就是 LDRB 和
STRB,按半字操作的指令就是 LDRH 和 STRH。
注意:
ARM汇编中,LDR指令和LDR伪指令的指令助记符是一样的,所以在阅读汇编代码时,如何确定一条LDR是指令还是伪指令呢?
由于LDR指令和伪指令的助记符相同,所以我们必须从指令操作数的格式来区分。
LDR既是指令又是伪指令。只有加载立即数的时候,也就是后面有=的时候是伪指令,否则就是正常的指令。
LDR伪指令的形式是“LDR Rn,=expr”
可参考:
嵌入式汇编—— LDR 指令和 LDR 伪指令的区别_嵌入式ldr是什么意思-CSDN博客
ARM汇编- LDR加载指令,LDR伪指令 - 黑暗里的影子 - 博客园 (cnblogs.com)
感觉这个设计很容易让人误解。
offset
指令中的偏移offset,是以地址为单位的,如果是32位的数据,就要偏移4个地址,才能访问到下一个数据,示例如下:
.data format: .asciz "Register value: %d\n" mydata: .word 100, 200 .text .global main .extern printf main: push {lr} ldr r4, =mydata ldr r5, [r4, #4] ldr r0, =format mov r1, r5 bl printf mov r0, #0 pop {pc}
这个数据打印出来的就是200
C语言里指针之所以是+1就能访问下一个元素,那是因为C语言内部已经做过处理了。
批量内存读写
ldr/str每周期只能访问4字节内存,写入内存时太慢。
如果需要批量读取,解决方案是ldm/stm
ldm(load register mutiple)
stm(store register mutiple)
常用后缀:
- ia 每次传送后地址加1(这里是逻辑1,实际是+4个字节,下同);
- ib 每次传送前地址加1;
- da 每次传送后地址减1;
- db 每次传送前地址减1;
- fd 满递减堆栈;
- ed 空递减堆栈;
- fa 满递增堆栈;
- ea 空递增堆栈;
这些后缀分为两类:
通用多寄存器加载/存储(
IA
/IB
/DA
/DB
)
适用于普通内存操作(如数组、缓冲区处理)。
后缀含义:
I
= Increment(递增地址)
D
= Decrement(递减地址)
A
= After(操作后更新指针)
B
= Before(操作前更新指针)栈操作(
FD
/ED
/FA
/EA
)
- 上面的是通用后缀,关注的是具体操作,下面的几个后缀是对通用指令的封装,让我们可以只关注使用的栈类型即可。
专门用于栈操作(如
PUSH
/POP
),ARM推荐使用FD
(Full Descending)作为标准栈模型。后缀含义:
F
= Full(栈指针指向最后一个有效数据)
E
= Empty(栈指针指向下一个空闲位置)
D
= Descending(栈向低地址增长)
A
= Ascending(栈向高地址增长)总结:
栈操作:优先用
FD
(STMFD
/LDMFD
)。普通内存操作:用
IA
/DA
等通用后缀。反向操作:
STMDA
/LDMDB
等。理解这些后缀的关键是区分栈操作和普通内存操作,并记住ARM的标准栈模型是
FD
。
使用stmdb是不是可以用于入栈?
STMDB
可以用于入栈操作,并且它的行为与标准的STMFD
完全一致。实际上,在 ARM 汇编中,STMFD
就是STMDB
的别名,两者生成的机器码相同。1.
STMDB
和STMFD
的关系
STMFD
(Store Multiple Full Descending)
是 ARM 推荐的栈操作指令(标准满递减栈)。
行为:先递减栈指针(
SP
),再存储数据(对应PUSH
)。
STMDB
(Store Multiple Decrement Before)
通用多存储指令,行为与
STMFD
完全相同。在汇编器中,
STMFD
通常会被直接转换成STMDB
。示例对比
STMFD SP!, {R1-R3} ; 标准入栈(推荐写法)
等价于:
STMDB SP!, {R1-R3} ; 完全相同的机器码
两者的区别仅仅是助记符不同,实际执行的操作完全一致。
2. 为什么
STMDB
可以用于入栈?
STMDB
的行为是 "先递减指针,再存储数据",而STMFD
也是同样的逻辑:
SP
先递减(SP -= 4 * N
,N
是寄存器数量)。按顺序存储数据(
[SP] = R1
,[SP+4] = R2
,[SP+8] = R3
...)。因此,
STMDB
完全符合 Full Descending 栈模型(ARM 标准栈)。3.
STMDB
vsSTMFD
:该用哪个?
指令 用途 推荐程度 STMFD
标准栈操作( PUSH
)✅ 推荐 STMDB
通用存储(也可入栈) ⚠️ 可用但不推荐
推荐使用
STMFD
:
语义更清晰,明确表示栈操作(
FD
= Full Descending)。代码可读性更好,其他开发者更容易理解。
STMDB
也可用:
如果你在写底层代码或编译器,可能会看到
STMDB
用于栈操作。但手动编写汇编时,建议优先用
STMFD
。4. 其他栈操作指令对比
指令 等效指令 栈模型 行为 STMFD
STMDB
Full Descending 先减 SP
,再存数据LDMFD
LDMIA
Full Descending 先取数据,再增 SP
STMED
STMDA
Empty Descending 先存数据,再减 SP
LDMEA
LDMIB
Empty Descending 先增 SP
,再取数据ARM 标准栈模型是
FD
(Full Descending),所以STMFD
/LDMFD
是最常用的。5. 总结
STMDB
可以用于入栈,因为它和STMFD
是相同的指令。但推荐使用
STMFD
,因为:
更符合 ARM 标准栈语义(
FD
= Full Descending)。代码可读性更好,减少歧义。
其他栈操作:
出栈用
LDMFD
(等效LDMIA
)。非标准栈(如
ED
)才需要考虑STMDA
/LDMIB
等。最终建议:在栈操作中坚持使用
STMFD
和LDMFD
,避免混淆!
ldm和stm使用时容易和ldr以及str搞混。
因为ldm和ldr,在寄存器以及指示内存地址的寄存器方向上是相反的。
ldm中,第一个参数是用来指示内存地址的,而在ldr中,第二个参数才是用来只是内存地址的。一定要十分注意。
LDM(或STM) 指令的格式为:
LDM(或STM) {条件}{类型} 基址寄存器{!}, 寄存器列表{^}
寄存器列表
:用大括号{}
包含的寄存器集合,如{r0, r1, r2}
或连续寄存器{r0-r3}
。- 基址寄存器不允许为R15,寄存器列表可以为R0~R15的任意组合。
LDM(或STM)指令用于从由基址寄存器所指示的一片连续存储器到寄存器列表所指示的多个寄存器之间传送数据,该指令的常见用途是将多个寄存器的内容入栈或出栈。
示例:
STMFD R13!, {R0, R4-R12, LR} ;将寄存器列表中的寄存器(R0,R4到R12,LR)存入堆栈。 LDMFD R13!, {R0, R4-R12, PC} ;将堆栈内容恢复到寄存器(R0,R4到R12,LR)。 其中R13也可以直接写成SP STMFD SP!, {R0, R4-R12, LR} ;将寄存器列表中的寄存器(R0,R4到R12,LR)存入堆栈。 LDMFD SP!, {R0, R4-R12, PC} ;将堆栈内容恢复到寄存器(R0,R4到R12,LR)。
注意,堆栈是在内存中的,SP中存的本来就是内存中的堆栈地址。
四种栈:
空栈:栈指针指向空位,每次存入时可以直接存入然后栈指针移动一格;而取出时需要先移动一格才能取出
满栈:栈指针指向栈中最后一格数据,每次存入时需要先移动栈指针一格再存入;取出时可以直接取出,然后再移动栈指针
增栈:栈指针移动时向地址增加的方向移动的栈
减栈:栈指针移动时向地址减小的方向移动的栈
!的作用
{!}为可选后缀,若选用该后缀,则当数据 传送完毕之后,将最后的地址写入基址寄存器,否则基址寄存器的内容不改变。
ldmia r0, {r2 - r3}
ldmia r0!, {r2 - r3}
^的作用
ldmfd sp!, {r0 - r6, pc}
ldmfd sp!, {r0 - r6, pc}^
^的作用:当命令为ldm,且在寄存器中列表中有pc时,会同时将spsr写入到cpsr,一般用于从异常模式返回。同时,该后缀还表示传入或传出的是用户模式下的寄存器,而不是当前模式下的寄存器。
总结:
批量读取或写入内存时要用ldm/stm指令,各种后缀以理解为主,不需记忆,最常见的是stmia和stmfd
谨记:操作栈时使用相同的后缀就不会出错,不管是满栈还是空栈、增栈还是减栈。
更多待补充。
用法示例
1. 栈操作(最常见场景)
函数调用时保存 / 恢复寄存器现场
func: push {r0-r3, lr} ; 等价于 stmdb sp!, {r0-r3, lr} ; 1. sp = sp - 20(5个寄存器×4字节=20字节) ; 2. 将r0、r1、r2、r3、lr依次存入[sp]、[sp+4]...[sp+16] ; 函数逻辑... pop {r0-r3, pc} ; 等价于 ldmia sp!, {r0-r3, pc} ; 1. 从[sp]、[sp+4]...加载数据到r0、r1、r2、r3、pc ; 2. sp = sp + 20(恢复栈指针)
2. 数据块批量传输
将连续内存数据加载到多个寄存器,或反之
.data arr: .word 1, 2, 3, 4 ; 内存中连续存储4个32位整数 .text ldr r0, =arr ; r0 = 数组首地址 ldmia r0!, {r1-r4} ; 从arr加载数据到r1-r4,地址后增 ; 执行后:r1=1, r2=2, r3=3, r4=4,r0=arr+16(原地址+4×4) ; 修改寄存器值 add r1, r1, #10 add r2, r2, #10 add r3, r3, #10 add r4, r4, #10 ldr r0, =arr stmia r0!, {r1-r4} ; 将r1-r4存回arr,地址后增 ; 执行后:arr变为11, 12, 13, 14
3. 上下文保存与恢复
中断或异常处理时,保存所有寄存器状态
; 保存上下文(r0-r12, lr, spsr) save_context: mrs r1, spsr ; 将状态寄存器spsr读入r1 stmdb sp!, {r0-r12, lr, r1} ; 全部存入栈中 mov pc, lr ; 恢复上下文 restore_context: ldmia sp!, {r0-r12, lr, r1} ; 从栈中恢复 msr spsr_cxsf, r1 ; 将r1的值写回spsr mov pc, lr
关键注意事项
- 地址对齐:
ldm
/stm
操作的内存地址必须是 4 字节对齐(32 位 ARM),否则会触发对齐异常。- 寄存器列表顺序:寄存器在内存中的存储顺序与列表一致,例如
{r0, r1}
会将 r0 存在低地址,r1 存在高地址。- 基址更新
!
:若不加!
,基址寄存器的值不会改变;加!
则自动调整(常用于循环批量操作)。- 与栈的配合:使用
stmdb
/ldmia
(即push
/pop
)时,栈指针sp
始终指向最后一个入栈的元素(满栈特性)。
ldm
和stm
是 ARM 汇编中提升效率的核心指令,尤其在函数调用、中断处理等需要批量操作寄存器的场景中不可或缺,理解其地址增长方式和栈操作逻辑是掌握 ARM 汇编的关键。
压栈和出栈指令
我们通常会在 A 函数中调用 B 函数,当 B 函数执行完以后再回到 A 函数继续执行。要想再跳回 A 函数以后代码能够接着正常运行,那就必须在跳到 B 函数之前将当前处理器状态保存
起来(就是保存 R0~R15 这些寄存器值),当 B 函数执行完成以后再用前面保存的寄存器值恢复R0~R15 即可。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。在进行现场保护的时候需要进行压栈(入栈)操作,恢复现场就要进行出栈操作。压栈的指令为 PUSH,出栈的指令为 POP,PUSH 和 POP 是一种多存储和多加载指令,即可以一次操作多个寄存器数据,他们利用当前的栈指针 SP 来生成地址,PUSH 和 POP 的用法如表 7.2.3.1所示:
假如我们现在要将 R0~R3 和 R12 这 5 个寄存器压栈,当前的 SP 指针指向 0X80000000,处理器的堆栈是向下增长的,使用的汇编代码如下:
PUSH {R0~R3, R12} @将 R0~R3 和 R12 压栈
压栈完成以后的堆栈如图 7.2.3.1 所示:
图7.2.3.1就是对R0~R3,R12进行压栈以后的堆栈示意图,此时的SP指向了0X7FFFFFEC,
假如我们现在要再将 LR 进行压栈,汇编代码如下:
PUSH {LR} @将 LR 进行压栈
对 LR 进行压栈完成以后的堆栈模型如图 7.2.3.2 所示:
图 7.2.3.2 就是分两步对 R0~R3,R12 和 LR 进行压栈以后的堆栈模型,如果我们要出栈的话
就是使用如下代码:
POP {LR}@先恢复 LR POP {R0~R3,R12} @再恢复 R0~R3,R12
出栈的就是就是 SP 当前执行的位置开始,地址依次增大来提取堆栈中的数据到要恢复的寄存器列表中。
PUSH 和 POP 的另外一种写法是“STMFD SP!”和“LDMFD SP!”,
因此上面的汇编代码可以改为:
示例代码 7.2.3.1 STMFD 和 LDMFD 指令
STMFD SP!,{R0~R3, R12} @R0~R3,R12 入栈 STMFD SP!,{LR} @LR 入栈 LDMFD SP!, {LR} @先恢复 LR LDMFD SP!, {R0~R3, R12} @再恢复 R0~R3, R12
STMFD 可以分为两部分:STM 和 FD,同理,LDMFD 也可以分为 LDM 和 FD。看到 STM
和 LDM 有没有觉得似曾相识(不是 STM32 啊啊啊啊),前面我们讲了 LDR 和 STR,这两个是数据加载和存储指令,但是每次只能读写存储器中的一个数据。STM 和 LDM 就是多存储和多加载,可以连续的读写存储器中的多个连续数据。
FD 是 Full Descending 的缩写,即满递减的意思。根据 ATPCS 规则,ARM 使用的 FD 类型
的堆栈,SP 指向最后一个入栈的数值,堆栈是由高地址向下增长的,也就是前面说的向下增长的堆栈,因此最常用的指令就是 STMFD 和 LDMFD。STM 和 LDM 的指令寄存器列表中编号小的对应低地址,编号高的对应高地址。
算术运算指令
汇编中也可以进行算术运算, 比如加减乘除,常用的运算指令用法如表 7.2.5.1 所示:
在嵌入式开发中最常会用的就是加减指令,乘除基本用不到。
逻辑运算指令
我们用 C 语言进行 CPU 寄存器配置的时候常常需要用到逻辑运算符号,比如“&”、“|”等,逻辑运算符。使用汇编语言的时候也可以使用逻辑运算指令,常用的运算指令用法如表 7.2.6.1所示:
逻辑运算指令都很好理解,后面时候汇编配置 I.MX6UL 的外设寄存器的时候可能会用到。
AND直接不就能清除某些位嘛,为什么还弄个BIC指令?
简单来说,举个例子,我有个数0x6666,我想让第1位,第6位,第9位清零,其他都保留不变,当然,我可以构造出一个数,让其第1、6、9这三位为0,其他位为1,这个数就是如下的这个:0b1111111011011110,我可以直接与上这个数,但说实话,不太直观,更直观的是,我直接让我想其清零的位为1,这样0b0000000100100001,就很直观了,直接与上这个数的取反结果。
所以,是为了啥呢?我觉得是直观性,不过貌似还有些其他的原因。
在某些情况下可以使用
AND
指令来实现类似BIC
的功能,但BIC
指令的存在有几个重要的原因和优势:1. 逻辑直观性
BIC
指令的语义是"清除这些位",而AND
的语义是"保留这些位"当你想清除某些位时:
使用
BIC
:BIC R0, R0, #0xFF
(直接表达"清除低8位")使用
AND
:AND R0, R0, #~0xFF
(需要计算掩码的反码)后者需要程序员手动计算位掩码的反码(
~
),增加了认知负担和出错可能性。2. 指令效率
在某些架构(如ARM)中,
BIC
是单指令完成操作如果使用
AND
实现同样功能:
需要先加载/计算掩码的反码
然后再执行
AND
可能需要多条指令
例如在ARM中:
BIC R0, R0, #0xFF // 单指令
等效的
AND
实现:MOV R1, #0xFFFFFF00 // 需要额外寄存器 AND R0, R0, R1 // 两指令
3. 立即数处理优势
在RISC架构(如ARM)中,立即数有特殊限制。
BIC
可以直接使用某些立即数模式,而用AND
实现时可能需要更复杂的指令序列。4. 与
ORR
/EOR
的对称性ARM设计了一组对称的位操作指令:
ORR
(位设置)
AND
(位保留)
EOR
(位取反)
BIC
(位清除)这样形成了完整的位操作指令集,编程更一致。
5. Thumb指令集优化
在Thumb/Thumb-2指令集中,
BIC
有更紧凑的编码形式,比等效的AND
实现更节省代码空间。实际对比示例
假设要清除
R0
的第3位和第7位:用 BIC:
BIC R0, R0, #0x88 // 直接清除 (0x88 = 0b10001000)
用 AND:
AND R0, R0, #0xFFFFFF77 // 需要计算 ~0x88
后者不仅更难阅读,而且在某些架构中可能无法作为单条指令实现(因为大立即数可能需要先加载到寄存器)。
总结
方面 BIC 等效AND实现 可读性 直接表达"清除位" 需要反向思维计算掩码 指令效率 通常单指令完成 可能需要多条指令 立即数支持 可直接使用更多立即数模式 大立即数可能需要额外处理 代码密度 Thumb模式下更紧凑 通常需要更多字节编码 所以
BIC
不是冗余指令,而是为了提供更高效、更直观的位清除操作而专门设计的。
跳转指令
有多种跳转操作,比如:
①、直接使用跳转指令 B、BL、BX 等。
②、直接向 PC 寄存器里面写入数据。
上述两种方法都可以完成跳转操作,但是一般常用的还是 B、BL 或 BX,用法如表 7.2.4.1:
我们重点来看一下 B 和 BL 指令,因为这两个是我们用的最多的,如果要在汇编中进行函
数调用使用的就是 B 和 BL 指令:
B 指令
这是最简单的跳转指令,B 指令会将 PC 寄存器的值设置为跳转目标地址, 一旦执行 B 指
令,ARM 处理器就会立即跳转到指定的目标地址。如果要调用的函数不会再返回到原来的执行
处,那就可以用 B 指令,如下示例:
_start: ldr sp,=0X80200000 @设置栈指针 4 b main @跳转到 main 函数
上述代码就是典型的在汇编中初始化 C 运行环境,然后跳转到 C 文件的 main 函数中运行,上述代码只是初始化了 SP 指针,有些处理器还需要做其他的初始化,比如初始化 DDR 等等。
因为跳转到 C 文件以后再也不会回到汇编了,所以在第 4 行使用了 B 指令来完成跳转。
BL 指令
BL 指令相比 B 指令,在跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以
通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,这是子程序调用一个基本但常用的手段。比如 Cortex-A 处理器的 irq 中断服务函数都是汇编写的,主要用汇编来实现现场的保护和恢复、获取中断号等。但是具体的中断处理过程都是 C 函数,所以就会存在汇编中调用 C 函数的问题。而且当 C 语言版本的中断处理函数执行完成以后是需要返回到irq 汇编中断服务函数,因为还要处理其他的工作,一般是恢复现场。这个时候就不能直接使用B 指令了,因为 B 指令一旦跳转就再也不会回来了,这个时候要使用 BL 指令,示例代码如下:
push {r0, r1} @保存 r0,r1 cps #0x13@进入 SVC 模式,允许其他中断再次进去 bl system_irqhandler@加载 C 语言中断处理函数到 r2 寄存器中 cps #0x12 @进入 IRQ 模式 pop {r0, r1} str r0, [r1, #0X10] @中断执行完成,写 EOIR
上述代码中bl system_irqhandler就是执行 C 语言版的中断处理函数,当处理完成以后是需要返回来继续执行下面的程序,所以使用了 BL 指令。
b bl bx具体啥区别?
在 ARM 汇编中,
b
、bl
、bx
都是跳转指令,但它们的核心功能、使用场景和特性有明显区别,主要体现在是否保存返回地址、是否切换指令集和跳转方式上。以下是详细对比:1.
b
(Branch,单纯跳转)
核心功能:无条件或条件跳转,仅改变程序执行流程,不保存返回地址,不涉及指令集切换。
跳转方式:直接跳转到指令中指定的标签地址(如
b label
)。跳转范围:属于 “短跳转”,目标地址需在当前指令的 ±32MB 范围内(具体取决于 ARM 架构版本)。
典型用途:
- 流程分支(如
beq
、bne
等条件跳转)。- 循环(如
b loop
跳回循环起点)。- 不需要返回的跳转(如程序退出流程)。
示例:
start: cmp r0, #0 ; 比较r0和0 beq exit ; 若相等(r0=0),跳转到exit(条件跳转) b process ; 否则跳转到process(无条件跳转) process: ; 处理逻辑 b start ; 跳回start,形成循环 exit: ; 退出逻辑
2.
bl
(Branch with Link,带链接的跳转)
核心功能:跳转的同时,自动将返回地址保存到
lr
(链接寄存器),但不切换指令集。跳转方式:直接跳转到指定标签地址(如
bl func
)。跳转范围:与
b
相同,±32MB 内的短跳转。典型用途:函数调用(需要在子函数执行后返回原位置)。
示例:
main: bl add_func ; 跳转到add_func,同时将下一条指令地址存入lr mov r0, #0 ; add_func执行完后,从这里继续执行 bx lr ; 程序结束 add_func: ; 加法逻辑(如 r0 = r1 + r2) bx lr ; 从lr中取返回地址,跳回main
关键点:
bl
保存的返回地址是 “当前bl
指令的下一条指令地址”,子函数通过bx lr
即可返回。3.
bx
(Branch and Exchange,跳转并切换指令集)
核心功能:跳转的同时,根据目标地址的最低位切换指令集(0=ARM 状态,1=Thumb 状态),不保存返回地址。
跳转方式:间接跳转,通过寄存器中的地址跳转(如
bx r0
)。跳转范围:无固定范围,可跳转到寄存器指向的任意地址(受地址空间限制)。
典型用途:
- 函数返回(如
bx lr
,利用bl
保存的返回地址)。- 在 ARM 和 Thumb 指令集之间切换执行(如从 ARM 状态跳转到 Thumb 函数)。
- 动态跳转(目标地址在运行时计算,存于寄存器中)。
示例:
; 从ARM状态跳转到Thumb函数 ldr r0, =thumb_func ; thumb_func地址最低位为1(表示Thumb状态) bx r0 ; 跳转并切换到Thumb状态执行 ; 子函数返回 sub_func: ; 函数逻辑 bx lr ; 从lr取地址返回,自动匹配原指令集(ARM/Thumb)
核心区别总结
指令 是否保存返回地址 是否切换指令集 跳转方式 典型用途 b
否 否 直接跳转标签 流程分支、循环 bl
是(保存到 lr
)否 直接跳转标签 函数调用(需返回) bx
否 是 间接跳转(寄存器) 函数返回、指令集切换、动态跳转 补充说明
bl
与bx
常配合使用:bl
负责调用函数并保存返回地址,bx lr
负责读取lr
并返回。- 若需要 “带链接且切换指令集” 的跳转,可使用
blx
指令(结合了bl
和bx
的功能)。- 条件跳转仅
b
支持(如beq
、bne
),bl
和bx
无条件版本。理解这三条指令的区别,是掌握 ARM 汇编函数调用、流程控制的基础。
为什么函数返回不直接使用b来跳转而要用bx ?
在 ARM 汇编中,函数返回通常使用
bx lr
而非b
指令,核心原因是bx
能处理 指令集切换 问题,而b
无法做到。具体原因如下:1.
b
指令的局限性
b
是单纯的跳转指令,它只能:
- 跳转到固定标签地址(无法直接使用寄存器中的地址,如
lr
寄存器)。- 不支持指令集切换(只能在当前指令集内跳转)。
如果用
b
实现返回,需要先将lr
中的地址写入一个标签,这在动态执行中不可行(lr
的值是运行时确定的,而非编译期的固定标签)。此外,若函数调用跨越了 ARM 和 Thumb 两种指令集,b
会因无法切换指令集而导致执行错误。2.
bx
指令的关键作用
bx
设计用于 间接跳转(通过寄存器) 和 指令集切换,完美适配函数返回需求:
- 支持寄存器间接跳转:
bx lr
可直接使用lr
寄存器中保存的返回地址(该地址由bl
/blx
指令在调用时自动存入),无需依赖固定标签。- 自动切换指令集:
lr
寄存器中保存的返回地址,其最低位会被设置为0
(ARM 状态)或1
(Thumb 状态)。bx
会检测这一位,自动将处理器切换到对应的指令集,确保返回后能正确执行后续指令。示例:跨指令集调用的返回
假设场景:ARM 状态的主程序调用 Thumb 状态的子函数:
若此处用; ARM 状态代码(32位指令) main: blx thumb_func ; 调用 Thumb 函数,lr 最低位被设为 1(标记返回后需切换回 ARM 状态) ; ... 后续为 ARM 指令 ; Thumb 状态代码(16/32位指令) thumb_func: ; ... 函数逻辑 bx lr ; lr 最低位为 1,返回时自动切换回 ARM 状态,正确执行 main 后续指令
b lr
会失败:b
不支持寄存器跳转,且无法识别lr
中的指令集标记,会导致处理器在错误的指令集下执行,触发异常。总结
函数返回必须解决两个问题:使用动态返回地址(
lr
寄存器) 和 可能的指令集切换。bx
同时满足这两个需求,而b
既不支持寄存器间接跳转,也无法处理指令集切换,因此无法用于函数返回。这一设计体现了 ARM 架构对多指令集(ARM/Thumb)兼容的支持,确保了跨指令集调用的正确性。
比较指令
ARM比较与测试指令详解
1. CMP (Compare) 比较指令
功能:比较两个操作数并设置标志位(相当于SUBS但不存储结果)
语法:
CMP Rn, Operand2
操作:
计算 Rn - Operand2,根据结果设置标志位: N = 结果的第31位 Z = 1(如果结果为0) C = 无借位时置1(无符号数比较时使用) V = 有符号溢出时置1
示例:
CMP R0, R1 @ 比较R0和R1 CMP R2, #100 @ 比较R2和立即数100
典型用途:
条件分支前的比较
循环计数器比较
数值范围检查
2. CMN (Compare Negative) 负数比较指令
功能:比较一个操作数与另一个操作数的负数(相当于ADDS但不存储结果)
语法:
CMN Rn, Operand2
操作:
计算 Rn + Operand2,根据结果设置标志位 (标志位设置方式与CMP相同)
示例:
CMN R0, R1 @ 相当于比较R0和-R1 CMN R2, #1 @ 相当于比较R2和-1
典型用途:
检查寄存器是否为特定负数的补码
快速比较与-1(CMN Rx, #1)
3. TST (Test) 测试指令
功能:执行按位与操作并设置标志位(相当于ANDS但不存储结果)
语法:
TST Rn, Operand2
操作:
计算 Rn AND Operand2,根据结果设置标志位: Z = 1(如果按位与结果为0) N = 结果的第31位 C和V标志不受影响
示例:
TST R0, #0x80 @ 测试R0的第7位是否为1 TST R1, R2 @ 测试R1和R2是否有共同置位位
典型用途:
测试特定位是否置位
检查寄存器是否为0的快速方法(TST Rx, Rx)
4. TEQ (Test Equivalence) 测试等价指令
功能:执行按位异或操作并设置标志位(相当于EORS但不存储结果)
语法:
TEQ Rn, Operand2
操作:
计算 Rn EOR Operand2,根据结果设置标志位: Z = 1(如果两个操作数相等) N = 结果的第31位 C和V标志不受影响
示例:
TEQ R0, R1 @ 测试R0和R1是否相等(所有位) TEQ R2, #0xFF @ 测试R2是否等于0xFF
典型用途:
比较两个值是否完全相等(比CMP更严格,比较所有位)
快速测试寄存器是否等于特定值
比较总结表
指令 实际运算 主要用途 影响标志位 CMP Rn - Op2 一般比较 N,Z,C,V CMN Rn + Op2 比较负数 N,Z,C,V TST Rn & Op2 位测试 N,Z TEQ Rn ^ Op2 位相等测试 N,Z 使用场景示例
1. 条件分支
CMP R0, #10 BGE loop_end @ 如果R0 >= 10则跳转
2. 位测试
TST R0, #0x04 BNE bit_set @ 如果第2位为1则跳转
3. 快速相等检查
TEQ R0, R1 BEQ values_equal
4. 负数检查
CMN R0, #1 BEQ is_minus_one @ 如果R0 == -1则跳转
重要说明
这些指令都不会修改任何通用寄存器,只影响CPSR中的标志位
所有指令都可以条件执行(如CMPEQ)
在Thumb-2模式下,这些指令的形式可能略有不同
标志位的变化是后续条件指令(如Bcond)执行的基础
mrc和mcr指令
在 ARM 汇编中,
MRC
(Move from Coprocessor to ARM Register)和MCR
(Move from ARM Register to Coprocessor)是用于ARM 核心与协处理器之间数据传输的专用指令,是处理器与协处理器通信的核心方式。二者功能互补,分别负责 “协处理器→ARM 寄存器” 和 “ARM 寄存器→协处理器” 的数据传输。一、指令基本格式
两条指令的语法结构相似,仅方向不同:
; MRC:从协处理器读取数据到ARM寄存器 MRC{<cond>} <coproc>, <opcode1>, <Rd>, <Crn>, <Crm>{, <opcode2>} ; MCR:从ARM寄存器写入数据到协处理器 MCR{<cond>} <coproc>, <opcode1>, <Rd>, <Crn>, <Crm>{, <opcode2>}
各参数含义:
<cond>
:可选条件码(如EQ
、NE
等),指定指令执行的条件(默认无条件执行)。<coproc>
:协处理器编号(0~15,如p15
表示系统控制协处理器 CP15)。<opcode1>
:协处理器操作码(0~7),由目标协处理器定义,指定具体操作类型。<Rd>
:ARM 核心的通用寄存器(MRC
中用于接收数据,MCR
中用于提供数据)。<Crn>
:协处理器的主寄存器(32 位,0~15),标识操作的核心寄存器组。<Crm>
:协处理器的辅助寄存器(32 位,0~15),用于扩展地址,配合Crn
定位具体寄存器。<opcode2>
:可选第二操作码(0~7),进一步细化操作(部分场景需省略)。二、核心功能与区别
指令 方向 核心作用 典型场景 MRC
协处理器 → ARM 寄存器 读取协处理器寄存器的值到 ARM 核心寄存器 获取处理器 ID、缓存状态、故障信息等 MCR
ARM 寄存器 → 协处理器 将 ARM 寄存器的值写入协处理器的目标寄存器 配置 MMU、启用缓存、设置页表基地址等 三、典型应用示例(以 CP15 协处理器为例)
CP15(系统控制协处理器)是
MRC
/MCR
最常用的场景,以下是几个典型操作:1. 读取处理器 ID(
MRC
); 从CP15的c0寄存器读取处理器ID到r0 MRC p15, 0, r0, c0, c0, 0 ; r0中包含处理器厂商、架构、型号等信息(如ARM Cortex-A9的ID编码)
2. 启用 MMU(
MCR
配合MRC
); 步骤1:读取系统控制寄存器(SCTLR,CP15的c1寄存器)到r0 MRC p15, 0, r0, c1, c0, 0 ; 步骤2:修改r0,置位bit0(MMU使能位) ORR r0, r0, #0x1 ; 步骤3:将修改后的值写回SCTLR,启用MMU MCR p15, 0, r0, c1, c0, 0
3. 刷新数据缓存(
MCR
); 将r0的值(通常为0)写入CP15的c7寄存器,触发数据缓存刷新 MOV r0, #0 MCR p15, 0, r0, c7, c14, 0 ; c7+c14+opcode2=0 表示“刷新整个数据缓存”
4. 设置页表基地址(
MCR
); 将页表物理基地址(存储在r0中)写入CP15的c2寄存器(TTBR0) LDR r0, =0x80000000 ; 假设页表基地址为0x80000000 MCR p15, 0, r0, c2, c0, 0 ; c2+c0+opcode2=0 对应“页表基地址寄存器0(TTBR0)”
四、注意事项
协处理器依赖性:指令功能完全由目标协处理器(
<coproc>
)定义,不同协处理器(如p10
浮点协处理器、p15
系统协处理器)的Crn
、opcode
含义不同,需参考对应协处理器的手册。特权级限制:多数与系统控制相关的操作(如配置 CP15 的 MMU、缓存)需要在特权模式(如 SVC 模式、EL1 异常级)下执行,用户模式下访问会触发 “未定义指令异常”。
架构兼容性:
- ARMv7 及更早架构(AArch32)中,
MRC
/MCR
是访问协处理器的主要方式。- ARMv8 AArch64 架构中,部分协处理器功能(如 CP15 的系统控制)被
MSR
/MRS
指令替代,但仍保留对部分协处理器的MRC
/MCR
支持。错误处理:若协处理器不支持指令中的
opcode
或寄存器组合,会触发 “未定义指令异常”,需通过异常处理程序捕获。总结
MRC
和MCR
是 ARM 核心与协处理器通信的桥梁,前者负责 “读取”,后者负责 “写入”。在系统级编程中(如内核初始化、驱动开发),这两条指令是配置硬件(如内存管理、缓存、处理器状态)的核心工具,尤其在与 CP15 协处理器交互时不可或缺。理解它们的用法是掌握 ARM 底层硬件控制的基础。
指令后缀
在 ARM 汇编中,指令后缀用于明确指定操作的数据宽度、条件或特殊功能,不同类型的指令支持的后缀有所不同
条件执行后缀
看个简单的例子
mov r0, r1 @ 相当于C语言中的r0 = r1;
上句代码没有执行条件,如果加上条件执行后缀,如下:
moveq r0, r1 @ 表示如果eq后缀成立,则直接执行mov r0, r1;如果eq不成立则本句代码直接作废,相当于没有,类似于C语言中 if (eq){r0 = r1;}
条件后缀执行注意2点:
1、条件后缀是否成立,不是取决于本句代码,而是取决于这句代码之前的代码运行后的结果。这里就要看上表中的标志这一列,moveq r0, r1,如果上一句代码执行后,标志位Z=1,那么就会执行本条语句,否则不执行。
2、条件后缀决定了本句代码是否被执行,而不会影响上一句和下一句代码是否被执行。常用的就是EQ、NE、GE、LE、LT、GT
宽度后缀
以下是常见的数据宽度后缀
B(byte)功能不变,操作长度变为8位
H(half word)功能不变,长度变为16位
注意:ARM是32位指令,默认处理的是32位数据。
S(signed)功能不变,操作数变为有符号
如 ldr ldrb ldrh ldrsb ldrsh
注意,上面的几个后缀常用于ldr/str中
在 ARM 汇编中,
mov
指令没有b
(字节)、h
(半字)这类长度后缀。比如用的话就会报错:
mov
指令的设计特性:
- 始终操作完整的 32 位寄存器(即使你只想处理其中的部分位)
- 不支持通过后缀限定操作宽度(这一点与内存访问指令不同)
与之对比的是内存访问指令(
ldr
/str
系列):
- 它们确实支持
b
(字节)、h
(半字)后缀,例如ldrb
(加载字节)、strh
(存储半字)- 用于明确指定内存操作的数据宽度
为什么
mov
不需要宽度后缀?因为
mov
是寄存器之间的数据传递,而 ARM 寄存器本身是 32 位的。即使你只想操作低 8 位或 16 位,也需要通过额外指令(如and
掩码)来处理,而不是通过mov
的后缀。示例
; 正确:传递32位数据 mov r0, r1 ; 正确:获取r1的低8位(通过and掩码) mov r0, r1 and r0, r0, #0xFF ; 保留低8位,高24位清零 ; 错误:ARM中不存在movb指令 movb r0, r1 ; 会报"bad instruction"错误
总结:
mov
指令始终处理 32 位寄存器数据,通过后缀限定宽度的方式仅适用于内存访问指令(ldr
/str
等)。
状态标志位后缀
S(S标志)功能不变,影响CPSR标志位
如 mov和movs movs r0, #0,此时r0中的值为0,cpsr中的状态位Z就会置1。
为什么会使得Z置1?
指令执行后会根据运算结果(此处是赋值的结果)更新 CPSR 中的
Z
位(零标志位):
若
r0
的值为0
,此时Z
位会被设置为1
(表示结果为零)。若
r0
的值不为0
,则Z
位会被设置为0
(表示结果非零)。
条件语句和循环语句
在 ARM 汇编中,条件语句和循环语句的实现主要依赖于标志位(CPSR 寄存器中的条件标志) 和条件分支指令。ARM 架构提供了丰富的条件执行机制,使得汇编代码可以灵活地实现分支和循环逻辑。
一、条件语句的实现
条件语句(如
if-else
)的核心是通过比较指令设置标志位,然后根据标志位执行条件分支或条件指令。1. 关键基础
- 标志位:CPSR 寄存器中的 N(负数)、Z(零)、C(进位)、V(溢出)标志位,由比较指令(
CMP
)或运算指令(如SUB
、ADD
等)自动设置。- 条件后缀:ARM 指令可以添加条件后缀(如
EQ
、NE
、GT
等),使得指令仅在满足条件时执行。- 条件分支指令:
B<cond>
(如BEQ
、BNE
),仅在满足条件时跳转到指定标签。2. 示例:实现
if (a == b) { ... } else { ... }
假设
a
和b
分别存储在r0
和r1
中,实现如下逻辑:if (a == b) { result = 1; // 若相等,结果为1 } else { result = 0; // 若不等,结果为0 }
对应的 ARM 汇编代码:
CMP r0, r1 ; 比较r0(a)和r1(b),设置Z标志位(相等则Z=1) BEQ equal ; 若Z=1(相等),跳转到equal标签 MOV r2, #0 ; 不等时执行:r2(result)=0 B end ; 跳转到结束(跳过else部分) equal: MOV r2, #1 ; 相等时执行:r2(result)=1 end: ; 后续代码
3. 条件执行指令(更简洁的方式)
ARM 允许直接在指令后加条件后缀,无需显式分支,适合简单逻辑:
CMP r0, r1 ; 比较a和b MOVEQ r2, #1 ; 若相等(EQ),r2=1(仅当Z=1时执行) MOVNE r2, #0 ; 若不等(NE),r2=0(仅当Z=0时执行)
二、循环语句的实现
循环语句(如
for
、while
)的核心是计数器和条件跳转:通过修改计数器,每次循环后检查条件,满足则继续循环,否则退出。1. 示例 1:实现
for (i=0; i<5; i++) { ... }
用
r0
作为计数器i
,循环执行 5 次MOV r0, #0 ; 初始化i=0 loop_start: CMP r0, #5 ; 比较i和5 BGE loop_end ; 若i>=5(GE),跳转到循环结束 ; 循环体:此处添加需要重复执行的代码 ; ... ADD r0, r0, #1 ; i++(计数器自增) B loop_start ; 无条件跳回循环开始 loop_end: ; 循环结束后的代码
2. 示例 2:实现
while (a > 0) { a--; }
用
r0
存储a
,当a>0
时循环递减loop_start: CMP r0, #0 ; 比较a和0 BLE loop_end ; 若a<=0(LE),跳转到循环结束 ; 循环体:a-- SUB r0, r0, #1 ; a = a - 1 B loop_start ; 跳回循环开始 loop_end: ; 循环结束后的代码
3. 优化:使用
SUBS
指令合并运算和标志位设置
SUBS
(带标志位的减法)可以在修改计数器的同时更新标志位,减少指令数MOV r0, #5 ; 初始化计数器i=5(循环5次) loop_start: CMP r0, #0 ; 检查i是否为0 BEQ loop_end ; 若i=0,退出循环 ; 循环体代码 ; ... SUBS r0, r0, #1 ; i--,同时更新Z标志位(i=0时Z=1) BNE loop_start ; 若i≠0(NE),继续循环 loop_end:
总结
- 条件语句:通过
CMP
设置标志位,配合B<cond>
分支指令或带条件后缀的指令(如MOVEQ
)实现。- 循环语句:通过计数器控制,每次循环后更新计数器并检查条件(用
CMP
或SUBS
),满足则用B<cond>
跳转回循环开始。掌握标志位和条件分支的使用,是实现复杂控制流的基础。
ARM的8种寻址方式
参考:
万字长文带你由浅入深夯实ARM汇编基础——汇编指令及寻址方式最全梳理(附示例)!-CSDN博客
首先,到底什么是寻址?
寻址(Addressing)是指计算机在进行内存读取或写入操作时,通过指定内存单元的地址来访问内存数据的过程。在计算机中,所有的数据都保存在内存中,并且每个内存单元都有一个唯一的地址,这个地址可以被用来访问这个内存单元中的数据。在程序执行过程中,CPU通过指令中的地址来访问内存中的数据,根据不同的寻址方式可以实现不同的内存访问方式,如寄存器寻址、立即寻址、寄存器偏移寻址、寄存器间接寻址、基址寻址、多寄存器寻址、相对寻址等。
寄存器寻址 mov r1, r2,
寄存器之间的数据交换,把r2里的数据运送到r1中,寄存器是通过名字来识别的。
类似于C语言中的r1 = r2;
立即寻址 mov r0, #0xFF00
就是把一个数直接给到寄存器中,这里是将立即数0xFF00赋值到r0中。
通常,我们会使用伪代码ldr来操作常数。
寄存器移位寻址 mov r0, r1, lsl #3
这里表示将r1的值向左移动3位后,再赋值给r0。
lsl是个左移指令。左移1位相当于乘以2。
寄存器间接寻址 ldr r1, [r2]
汇编中,中括号[ ]表示要访问的是内存,[ ]里放的是内存地址。汇编中,如果要表示内存地址,通常都是用这种间接寻址的方式。
这句命令表示,将r2中的数所指向的那个内存地址中的数给r1,此时r2相当于是指针。
实际上,C语言中的指针寻址,就是通过这种原理来实现的。
基址变址寻址 ldr r1, [r2, #4]
将r2中的地址数加上4之后所指向的那个内存中的数据给r1。
多寄存器寻址 ldmia r1!, {r2-r7, r12}
通常,只要涉及到ld的,都是将内存中的数据加载到寄存器中。
这里表示,将r1中的地址数作为你内存首地址,依次加1的地址中的数依次放入后面的几个寄存器中,这里的r1!相当于是一个指针数组,而r1表示数组首元素。
堆栈寻址 stmfd sp!, {r2-r7, lr}
和多寄存器寻址类似,只不过多寄存器是操作普通寄存器,而stmfd操作的是堆栈寄存器。
相对寻址 beq flag
flag:
类似于跳转功能,跳到flag标号处执行。
汇编文件的后缀
ARM汇编文件的常见后缀包括.s(小写)和.S(大写),其中.S文件在编译时会进行预处理,而.s文件则不会。
ARM汇编文件的后缀类型
ARM汇编文件通常使用以下两种后缀:
.s(小写)。
在Linux环境下,小写后缀的.s文件编译时不进行预处理,因此不能包含宏定义等预处理指令。
适用于纯汇编代码,无需预处理操作。
.S(大写)。
大写后缀的.S文件在编译时会执行预处理,支持宏定义、条件编译等预处理指令。
更常用于混合C与汇编的开发场景,例如启动代码或需要预处理的复杂逻辑。
其他相关说明
Windows与Linux的区别:Windows系统不区分文件后缀大小写,因此.s和.S编译行为一致;而Linux严格区分大小写,两者处理方式不同。
历史与扩展名变体:部分工具链可能支持.asm后缀,但.s和.S是ARM架构下的主流标准。
实际应用建议:若需兼容GNU工具链或嵌入式开发,优先使用.S后缀以保留预处理灵活性。
汇编的主要应用场景
汇编是不是主要面向硬件和架构编程,几乎不会涉及到业务逻辑?
这种说法有一定道理,但并不完全准确。汇编语言的特性确实使其更贴近硬件和架构,但 “几乎不会涉及业务逻辑” 需要结合具体场景来看。以下从几个角度详细分析:
一、汇编语言的核心特性:与硬件紧密绑定
汇编语言的指令直接对应 CPU 的操作(如寄存器读写、内存访问、算术运算等),且依赖特定架构(如 ARM、x86 的指令集差异极大)。因此:
- 天然面向硬件和架构:汇编的核心用途是控制硬件细节(如外设寄存器配置、中断处理、内存映射 I/O 等),或针对架构特性进行极致优化(如利用 ARM 的 NEON 指令加速多媒体计算)。
- 脱离硬件则失去意义:汇编代码无法跨架构通用,甚至同架构的不同版本(如 ARMv7 与 ARMv8)也可能不兼容,这与高级语言(如 C、Python)的平台无关性形成鲜明对比。
二、汇编是否涉及业务逻辑?视场景而定
业务逻辑通常指 “实现产品功能的逻辑流程”(如用户登录验证、订单计算、数据过滤等)。汇编是否处理这些逻辑,取决于开发需求:
多数情况下不直接处理业务逻辑:
- 高级语言(C/C++、Java 等)已能清晰表达业务逻辑,且开发效率远高于汇编。
- 现代编译器优化能力极强,多数场景下,高级语言编译后的机器码效率接近手写汇编,无需为业务逻辑编写汇编。
少数场景下会涉及业务逻辑:
- 极致性能需求:如高频交易系统的核心计算模块、嵌入式设备的实时数据处理(需在微秒级完成逻辑判断),可能用汇编优化关键业务逻辑(如复杂的条件分支、循环计算)。
- 逆向工程:分析二进制程序(如恶意软件)时,反汇编得到的代码必然包含业务逻辑(如病毒的传播判断逻辑、加密算法的流程),此时汇编是理解业务逻辑的唯一途径。
- 资源极度受限的环境:如某些单片机(无操作系统、内存仅 KB 级),可能全程用汇编实现简单业务逻辑(如 “检测传感器值> 阈值则点亮 LED”)。
三、总结
- 汇编的核心场景是硬件控制和架构级优化,而非直接实现业务逻辑。
- 业务逻辑通常由高级语言处理,但在极端性能需求或逆向分析中,汇编会间接涉及业务逻辑的表达。
简单说:汇编是 “工具”,硬件控制是其 “主要舞台”,但必要时也能用来 “表演” 业务逻辑 —— 只是性价比通常不高。
问题总结
01
end of file not at end of a line; newline inserted ------------------as编译.s文件时.
英文的意思就是说文末没有换行符。Unix文档的回车换行符是一个字符\n,Windows的是分别的两个\n\r,
解决方法:在文件结尾回车一下.
02
语句错误,每次都忘了在参数之间用逗号隔开。
03
表示立即数需要一个前缀#
在汇编中,要考虑合法立即数和非法立即数的问题,很多指令不能处理非法立即数,比如mov指令和ldr指令都不能处理非法立即数,会提示错误:
为了解决这个问题,我们常用ldr伪指令来操作立即数,此时就不用考虑立即数是合法的还是非法的。
04
make时出现警告:
解决“arm-linux-ld: warning: cannot find entry symbol _start; defaulting to 00000000”问题_史达芬林的博客-CSDN博客
我一开始还以为说的是,直接用.global_start:
后面发现还是有这个问题,但其实是写错位置了,应该先声明,再定义:
类似于C语言中的先声明,再定义。
05
汇编中的立即数问题:
06
有时候会在汇编程序的主程序结束处写一个死循环,为什么呢?
这是在裸机开发下的常用方式。
因为裸机程序是直接在CPU上运行的,CPU会逐行运行裸机程序直到CPU断电关机。如果我们的程序所有的代码都执行完了CPU就会跑飞,跑飞以后是未定义的,所以千万不能让CPU跑飞,不让CPU跑飞的办法就是在我们整个程序执行完后添加死循环。
比如:
07
汇编中能直接用C语言中的宏定义#define
补充
✔
为什么说汇编语言的执行效率高?为什么用汇编能提高代码执行效率?最终不都会翻译成机器语言吗?运行时不都一样嘛?
所有语言编写的代码最终要运行,都要转化成机器码。但是,由于这个“转化”所采用的方法不同,其所需要消耗的时间也使不同的。 举个简单的例子来说,比如把一个变量的值自加1,并执行100次,也就是下面这条语句: for(i=0;i<100;){i++;}那么对于一个没有充分优化的C语言编译器而言,你需要每次寻址内存找到变量,然后把变量值拷贝到寄存器,然后对寄存器自加1,然后把寄存器值写回到内存,整个过程需要反复执行100次。 但是如果你写汇编代码,那就没这么麻烦了,你只需要寻址内存一次,把变量读入寄存器,然后对寄存器自加100次,最后写回内存即可。你可以想见,这个汇编代码的执行速度要比C语言快得多,但它们所执行的功能是一样的。 当然,我前面这个例子只是用来说明问题,并不具有实践价值。实践中有很多因素影响程序的效率,例如编译方式、优化程度等等。而这些与程序员的素质也有关系,一个差的汇编程序很可能不如一个好的C语言程序执行效率高。
汇编语言编写的程序,可以直接翻译为机器代码。而高级语言的程序,由于其翻译为机器,代码的翻译程序不可能具有活人那么高的智能,会插入许多多余代码,这些多余代码会浪费机器的执行时间。
汇编语言是一种低级的计算机语言,它直接操作计算机的硬件资源,包括寄存器、内存和输入输出设备等。与高级语言相比,汇编语言更加接近于机器语言,更加底层。
汇编语言的作用在于优化程序的执行速度和效率。通过直接操作底层硬件资源,汇编语言可以实现对程序代码的精细控制,充分发挥计算机硬件的性能,从而提高程序的执行速度和效率。
✔
优化程序的重要性和目标
程序执行速度的优化在现代计算机应用中具有重要意义。优化程序可以提高用户体验,加快计算速度,节省计算资源,在某些高性能领域中甚至可以决定产品的竞争力。
优化程序的目标通常包括以下几个方面:
减小程序的运行时间,提高程序的响应速度。
减少程序的内存占用,提高计算资源的利用率。
优化算法和数据结构,改进程序的整体性能。
程序优化不仅仅是通过改进代码,还可能涉及硬件平台的选择和优化,算法的优化,以及对程序运行过程中的性能瓶颈进行分析和解决。
✔
程序执行速度优化的基本概念
程序执行速度的优化是提高计算机程序运行效率的一种重要手段。在现代计算机系统中,程序执行速度的快慢往往直接影响到用户体验和系统性能。因此,对于需要高效运行的程序,进行优化是非常必要的。
✔
程序执行速度的影响因素
程序执行速度受多个因素的影响,以下是常见的影响因素:
算法复杂度:算法的复杂度直接决定了程序需要执行多少次基本操作。一般来说,复杂度高的算法执行速度比复杂度低的算法更慢。
数据结构:不同的数据结构对于程序执行的效率有很大的影响。选择合适的数据结构能够减少计算和访问时间,从而提高程序的执行速度。
编译器优化:编译器在将高级语言编译为机器语言的过程中,会进行一系列的优化操作,如代码优化、循环展开、指令调度等,来提高程序的执行效率。
并行计算:利用多核处理器进行并行计算可以加快程序的执行速度。通过合理地将任务分配到不同的核心上,可以实现并行计算,从而提高程序的运行效率。
等等。
✔
常见的程序优化方法
针对不同的程序,可以采取不同的优化方法来提高其执行速度。以下是常见的程序优化方法:
算法优化:通过改进算法,减少计算次数或者采用更高效的算法,来提高程序的执行速度。
数据结构优化:选择合适的数据结构,能够减少程序对数据的访问时间,从而提高程序的执行效率。
编译器优化:通过合理地设置编译器选项,调整编译器的优化策略,可以提高程序的执行效率。
并行计算优化:通过将程序适配到多核处理器上,合理地进行任务划分和并行计算,可以提高程序的执行速度。
内存优化:合理地使用内存,减少内存读写次数,优化内存访问模式,可以大幅提高程序的执行效率。
等等。
在优化程序执行速度时,需要综合考虑各种因素,通过不同的优化手段综合提升程序的执行效率。
参考:
https://worktile.com/kb/ask/46375.html
同样需要编译器,而汇编语言比高级语言快,其原因在于两种语言在编译后生成的机器代码不同。汇编语言具有更小的指令集、更好的控制和更少的开销的三大优势,而相比之下,高级语言有更大的指令集、更多的开销两大劣势。
✔
汇编语言的优势
汇编语言是一种面向机器的语言,与机器语言一一对应,因此汇编语言可以更直接地控制机器,比高级语言更接近底层硬件。
更小的指令集:汇编语言的指令集比高级语言的指令集小得多,这使得汇编语言生成的机器代码更加紧凑,执行效率更高。
更好的控制:汇编语言允许程序员更好地控制程序的执行过程,包括直接访问内存、寄存器等底层硬件资源,从而更好地满足特定的应用场景。
更少的开销:汇编语言生成的机器代码不需要解释器或虚拟机等中间层的支持,直接在底层硬件上执行,因此执行效率更高,消耗的资源更少。
✔
高级语言的劣势
高级语言是一种抽象层次更高的语言,提供了更多的语法结构和语言特性,比汇编语言更容易学习和编写,但是生成的机器代码效率较低。
更大的指令集:高级语言提供了更多的语法结构和语言特性,导致生成的机器代码更为复杂,执行效率较低。
更多的开销:高级语言生成的机器代码需要解释器或虚拟机等中间层的支持,会消耗更多的资源,导致执行效率较低。
总之,汇编语言与高级语言各有优劣,汇编语言更适合对程序执行效率有极高要求的应用场景,如操作系统、嵌入式系统等,而高级语言则更适合开发复杂应用程序,提高开发效率。在实际应用中,程序员需要根据具体的需求选择合适的语言和工具,以达到优异的效果。
汇编语言特点理解
我们知道,CPU是执行指令和计算的中枢大脑,存储器只是用来存储数据的,并不能进行运算,CPU里有控制器和运算器,里面有一些移位寄存器和加法器等数字电路,可以把数据从内存里取来,然后放到运算器里进行运算。
以ARM架构Cortex-A系列芯片为例,其CPU里设计了一些寄存器R0-R15,这些就是CPU用来实现程序执行和数据计算的一些寄存器,其中包括通用寄存器R0-R12、SP、LR、PC,还有CPSR、SPSR。
汇编语言的一大特点,就是可以直接操作这些CPU内部的寄存器,从而干涉CPU的执行过程,相对C语言来说,更加接近硬件,更加底层,但同时也更容易出错,更难以编写大型软件程序,因为它偏向于硬件语言,而非自然语言,C语言是在汇编语言之上的又一层封装,符合人类的思维和使用习惯,向使用者屏蔽了操作CPU内部寄存器的细节。
所以,在现代语言设计中,汇编语言一般都只在应用初始化时用来搭好C语言的运行环境,然后就跳转到C语言里去执行了,或者在一些效率要求很高的部分,用汇编来编写代码。所谓的C语言运行环境,其实就是用来设置必要的外设,比如内存,以及设置寄存器SP的值,也就是栈的起始地址,等等。
汇编语言的特点总结
汇编语言是一种低级编程语言,与计算机的硬件架构紧密相关。它具有以下几个主要特点:
1. 直接控制硬件
汇编语言允许程序员直接控制处理器、寄存器、内存和其他硬件资源。这使得它非常适合用于系统编程、嵌入式开发和性能优化等场景。
2. 指令集依赖性
汇编语言与具体的指令集架构(ISA)密切相关。不同的处理器架构(如x86、ARM、MIPS等)有不同的指令集,因此为一种处理器编写的汇编代码通常不能直接在另一种处理器上运行。
3. 低抽象级别
汇编语言提供了对计算机资源的细粒度控制,但同时也意味着程序员需要处理更多的底层细节,如寄存器分配、内存管理、中断处理等。
4. 高效性
由于汇编语言能够直接操作硬件,它可以生成非常高效的机器代码。这对于性能要求极高的应用(如实时系统、操作系统内核等)非常重要。
5. 可移植性差
由于汇编语言依赖于特定的硬件架构,其可移植性较差。同一个程序在不同架构上的汇编代码可能需要完全重写。
6. 难以维护和调试
汇编语言代码通常比高级语言更难阅读和维护。此外,调试汇编语言代码也需要更多的专业知识和经验。
7. 灵活性
尽管汇编语言复杂且难以掌握,但它也提供了极大的灵活性。程序员可以精确地控制程序的行为,实现一些高级语言难以实现的功能。
8. 工具链支持
现代的汇编语言通常有丰富的工具链支持,包括编译器、调试器、反汇编器等。这些工具可以帮助开发人员更有效地编写和调试汇编代码。
9. 学习曲线陡峭
学习和掌握汇编语言需要深入了解计算机体系结构、操作系统原理以及硬件工作原理。这对初学者来说可能是一个挑战。
10. 历史背景
汇编语言是最早的编程语言之一,早在20世纪50年代就已经存在。它是计算机科学发展史上的一个重要里程碑。
总的来说,汇编语言是一种功能强大但复杂的编程语言,适用于需要高性能和精细控制的应用场景。然而,对于大多数应用程序开发而言,高级语言(如C、C++、Python等)通常更为合适,因为它们提供了更高的抽象级别和更好的可读性。
注意,汇编中语句不用以分号结尾