☰
ARM汇编基础:寄存器、寻址与启动代码实战指南
2026/10/15 23:15:29 网站建设 项目流程

简介:ARM汇编基础教程1面向ARM平台开发和逆向工程学习者,以系统化方式讲解汇编语言入门核心内容。资料为docx格式文档,共1个文件,压缩包大小约2.65MB,便于随时查阅。已有209人学习浏览,内容具有较高参考价值。教程详细梳理了ARM处理器版本与指令集对应关系,演示了从.s源文件经过as汇编、ld链接生成可执行程序的完整流程,并深入解析有符号与无符号的字、半字、字节数据类型及大小端字节序控制。寄存器部分重点说明R0-R12通用寄存器、R13堆栈指针、R14链接寄存器、R15程序计数器和CPSR状态寄存器在运算、函数调用和程序控制中的作用,同时介绍前四个参数经R0-R3传递等调用约定。配合内容预览中的章节规划和拓展阅读,可帮助读者为后续ARM指令集、内存读写、条件分支及栈和函数等专题打下扎实基础。

1. ARM汇编还在被需要的场景:启动代码与底层调试绕不开的基本功

很多人第一次接触 ARM 汇编,是在嵌入式开发的启动文件里。芯片上电后 C 语言的 main 函数还没准备好运行时环境,要靠一小段汇编代码把栈指针初始化好、把数据段搬到位、再把控制权交给 C 入口。没有这段汇编,整个工程根本跑不起来。学 ARM 汇编不是为了手写整个应用,而是为了看懂这关键的一小段代码,并且在遇到问题时能自己改、自己调。对于做嵌入式、做系统底层、做性能分析的技术人员来说,这是一项绕不开的基本功。如果你发现自己翻开启动文件时全是陌生的伪指令和寄存器名,这篇教程就是让你从零建立起 ARM 汇编的完整图景,读完能照着例子动手跑起来。

2. 先把机器模型立住:寄存器、内存与寻址方式怎么串起来

2.1 寄存器命名与分工:看懂 R0 到 R15 各自的角色

ARM 处理器的寄存器一共 16 个通用寄存器,分别叫 R0 到 R15。其中 R13 被用作栈指针,R14 保存函数返回地址,R15 是指令计数器,也就是程序当前执行到哪一条指令。剩下 R0 到 R12 都是通用寄存器,函数调用约定里规定 R0 到 R3 用来传前四个参数,R4 到 R11 用于保存局部变量和中间结果。这些约定是编译器遵循的,自己手写汇编时最好也统一使用,这样汇编代码和 C 代码互相调用时才不会乱套。

@ R0: 第一个参数 @ R1: 第二个参数 @ R13 (SP): 栈指针,压栈和出栈操作都靠它 @ R14 (LR): 链接寄存器,BL 指令会自动把返回地址放进这里 @ R15 (PC): 程序计数器,PC 的值决定下一条要执行的指令地址

这里要注意的是,不要把寄存器当成一条条孤立的数据存储位置。ARM 指令本身就是对寄存器做操作,内存里的数据要先用 load 指令搬到寄存器里,计算完再用 store 指令写回去。这种设计叫 load/store 架构,和 x86 那种可以直接对内存做加减法的方式完全不同。初学者最容易犯的错误,就是下意识地想写一条类似“内存里加一”的指令,这在 ARM 里根本不存在。

参数说明里最需要注意的是 LR 寄存器的行为。执行 BL(带链接跳转)指令时,处理器会自动把当前 PC 的下一条指令地址存到 R14,然后跳转到目标地址。函数返回时把 R14 搬回 PC 就行。如果函数里还有嵌套调用,就必须先把 LR 压栈保存,否则内层调用会覆盖掉外层还没有执行的返回地址。这是最常见的汇编 bug 来源。

2.2 三种寻址方式:为什么偏移寻址最容易写错

ARM 的寻址方式看起来很多,实际工作中常用的就三种。第一种叫立即数寻址,直接把常数值放到寄存器里,比如 MOV R0, #0x100。第二种叫寄存器寻址,操作数本身就是另一个寄存器的值,比如 MOV R0, R1。第三种是偏移寻址,地址是一个基址寄存器加上一个偏移量,用于访问内存中的结构体或数组元素。

LDR R0, [R1] @ 读取 R1 指向的内存地址处的值到 R0 LDR R0, [R1, #4] @ 读取 R1+4 地址处的值到 R0 LDR R0, [R1, R2] @ 读取 R1+R2 地址处的值到 R0 LDR R0, [R1, #4]! @ 先更新 R1 = R1+4,再读取新地址的值 LDR R0, [R1], #4 @ 先读取 R1 地址的值,再更新 R1 = R1+4

后两条指令前面带个感叹号和后面跟逗号的写法,是实际项目里最容易踩坑的地方。带感叹号的叫前索引,表示先改变基址寄存器的值再访问内存,常用于栈操作。不带感叹号、偏移量写在括号后面的叫后索引,先访问内存再改变基址,常用于连续遍历缓冲区。很多人在手写汇编时把这两种写法搞混,结果地址计算完全错误。区分方法很简单:感叹号在括号里面,基址先更新后使用;偏移量在后面且无感叹号,基址后更新。

2.3 初次上手:用模拟器建立一个最简汇编工程

在还没有开发板的时候,用模拟器起步是最快的方式。常见做法是安装 GNU 工具链,用 QEMU 的 ARM 模拟模式来跑编译出来的二进制文件。先把工具链装好,然后建立一个最简单的汇编源文件,不依赖任何开发板。

# 安装 GNU 交叉工具链和 ARM 模拟器 sudo apt install gcc-arm-none-eabi qemu-system-arm

新建一个 start.S 文件,内容如下:

.syntax unified .arch armv7-a .global _start .section .text _start: MOV R0, #1 MOV R1, #2 ADD R2, R0, R1 B . .section .data

这段代码做的事情很简单,给 R0 赋值为 1,给 R1 赋值为 2,相加结果放到 R2 中,然后死循环。最后一行 B . 表示跳转到当前地址,也就是原地打转,这是裸机程序的典型结束方式,因为没有操作系统帮你退出程序。

arm-none-eabi-as -o start.o start.S arm-none-eabi-ld -Ttext=0x8000 -o start.elf start.o arm-none-eabi-objcopy -O binary start.elf start.bin qemu-system-arm -M vexpress-a9 -sd start.bin -nographic

这几条命令依次完成编译、链接、生成纯二进制、用模拟器运行。链接时用 -Ttext=0x8000 指定代码段的加载地址,这是模拟器默认的启动地址。参数说明:如果换了别的开发板型号,启动地址要跟着变,否则程序根本执行不到。运行后你看到的是一片空白,没有任何输出,因为这段代码没做任何输出动作,但程序确实在跑。要验证执行结果,借助调试器单步执行才看得到寄存器变化。

3. 动手写第一个程序:常用指令、条件标志与参数怎么设

3.1 数据搬运与算术指令:从 MOV 到 ADD、SUB、MUL

汇编程序的逻辑很简单,先把数搬到寄存器里,做运算,再把结果搬走。数据搬运用 MOV 和 LDR。MOV 只能搬立即数或寄存器值,LDR 才能从内存取数,这两个概念经常被混淆。下面代码演示常见的算术操作组合:

MOV R0, #10 @ R0 = 10 MOV R1, #3 @ R1 = 3 ADD R2, R0, R1 @ R2 = R0 + R1 = 13 SUB R3, R0, R1 @ R3 = R0 - R1 = 7 MUL R4, R0, R1 @ R4 = R0 * R1 = 30 AND R5, R0, R1 @ R5 = R0 & R1 = 2 ORR R6, R0, R1 @ R6 = R0 | R1 = 11 EOR R7, R0, R1 @ R7 = R0 ^ R1 = 9

这些指令的方向值得强调:ARM 的指令把目标寄存器放在最前面,后面才是操作数。写成 ADD R2, R0, R1,含义是把 R0 和 R1 相加的结果存到 R2。这和 x86 汇编的写法正好相反,x86 里目标操作数通常写在最后面。习惯 x86 的人头一个星期都会因为写反操作数位置而频繁报错,编译器会直接说寄存器不存在或者操作数类型不匹配。

MUL 指令是老版本 ARM 中比较特别的,它有一点限制,目标寄存器不能和第一个操作数相同,而且只能两个寄存器相乘,不能直接拿立即数做乘法。如果想要 R0 = R0 * 5,就得先 MOV R1, #5 再 MUL R0, R0, R1。这条指令的写法在较新的 ARMv8 里改成了更好的形式,但你在维护老工程时还是会遇到旧写法。

3.2 条件标志位与分支指令:看懂 B、BEQ、BNE 的跳转逻辑

裸机程序里最常用的控制流就是条件分支。ARM 设计了一个很有意思的机制,每条指令都可以带上条件码,只有满足条件才真正执行,不满足就相当于空操作。默认情况下条件码是 AL,也就是无条件执行。下面的代码演示了比较和分支的组合:

_start: MOV R0, #5 MOV R1, #8 CMP R0, R1 @ 比较 R0 和 R1 BGT greater @ 如果 R0 > R1 则跳转 MOV R2, #0 @ 否则 R2 = 0 B end greater: MOV R2, #1 @ R2 = 1 end: B end

CMP 指令不保存结果,只修改条件标志位,后续的 BGT、BEQ、BNE 等分支指令根据标志位决定是否跳转。G 代表无符号数大于,T 代表测试结果。这里的坑在于有符号数和无符号数的判断用的是不同条件码,用 BGT 判断两个用半字表示的数还够用,一旦涉及负数就要改用 BGE 或 BLT,并且要明确数是有符号类型。

条件分支是汇编调试时最让人头疼的部分。处理器不会告诉你它是怎么走到这一步的,你只能靠单步执行时观察标志位的变化。建议每次都把那几个关键标志位单独列出来看,比较指令之后紧跟着的分支指令执行结果是否符合预期,别在脑子里模拟,直接看调试器显示的值。实践中很多逻辑错误都能靠这一招快速定位。

3.3 栈操作与函数调用:PUSH 和 POP 背后的地址变化

在嵌入式 C 语言和汇编混编时,栈操作是必然要掌握的。PUSH 和 POP 指令在旧版本 ARM 中分别写作 STMDB 和 LDMIA,现在的统一语法里可以直接用 PUSH 和 POP 这两个更直观的助记符。

PUSH {R4, LR} @ 压栈保存 R4 和返回地址 MOV R4, #100 BL some_function @ 调用子函数,返回地址自动存入 LR POP {R4, PC} @ 恢复 R4,同时把保存的 LR 值恢复到 PC

这段代码是函数调用的标准开头和结尾。开头的 PUSH 保存了当前函数的返回地址,因为下面马上就要用 BL 调用其他函数,LR 会被覆盖。结尾的 POP 把旧返回地址赋给 PC,程序就能正确回到上一级调用者。这里的巧门是 POP 到 PC 而不是先 POP 到 LR 再 MOV 到 PC,可以省一条指令,同时也减少了出错机会。

栈指针的方向也容易出错。ARM 的栈是向下增长的,PUSH 操作会让 SP 地址减小,POP 让 SP 地址增大。假如你在初始化代码里把 SP 设在一个比较低的位置,一压栈就会写到无效内存区。所以栈空间一般放在内存比较高的区域,给向下增长留足余地。这个方向感一定要在脑子里立住,否则排查内存访问异常时会找半天都找不到原因。

@ 错误示范:SP 指到内存最低有效位置 @ PUSH 会把数据写到地址更低的区域,如果那边不存在就触发异常 @ 正确定栈方式: LDR SP, =0x10000 @ 把栈顶设在内存比较高的合法区域

3.4 伪指令与常量池:LDR Rd, =value 和 MOV 的区别

初学者经常被一个写法搞迷糊,LDR R0, =0x12345678。继续用编程语言思考的话,MOV R0, #0x12345678 好像也能做到。问题是 ARM 指令长度是固定的,立即数空间有限,不能容纳任意 32 位数值。MOV 指令只能构造一部分立即数,其余情况就要用伪指令 LDR Rd, =value。

LDR R0, =0x12345678 @ 汇编器会在常量池中存放这个值 @ 编译后变成 LDR R0, [PC, #offset] MOV R1, #0xFF @ 这个值简单,可以用 MOV 直接构造

参数说明:LDR 伪指令会被汇编器翻译成一条从文字池(literal pool)加载数据的指令。文字池是编译时自动生成的一块数据区域,放在代码段的某个位置。因为 PC 是当前指令地址加 8,这段 LDR 的偏移量会在链接时自动计算。使用伪指令的好处是不用关心立即数能不能编码,缺点是多了一次内存访问,如果这段代码放在性能关键路径上,尽量换成 MOV 或者用多个逻辑指令拼接数值。

4. 用工具链跑通一次完整编译:从启动文件到反汇编验证

4.1 开发环境选型:GNU 工具链还是厂商 IDE

写 ARM 汇编最先要解决的是用什么工具链。独立汇编和调试推荐 GNU 工具链,因为它不绑定特定芯片厂商,支持所有 ARM 型号,而且反汇编、调试、链接脚本全面可查。厂商 IDE 一般在开发板调试时更方便,自带烧录和调试界面,但抽象程度高,很多细节被隐藏。对于学习汇编来说,强烈建议先用 GNU 工具链把完整流程跑通,搞清楚每一步做了什么,再去用厂商 IDE 会顺手很多。

我一般会建议学习阶段多建几个不同的小工程,每个工程只验证一条指令或一种寻址方式。这样比写一个很大的汇编程序更利于定位认知盲区。工具链安装本身很简单,就是装一个交叉编译器,需要注意区分系统架构和目标架构。如果本机是 x86 环境,就要装 arm-none-eabi 版本而不是 arm-linux-gnueabi 版本,前者面向裸机开发,后者面向带操作系统的环境,两者的库和启动方式不一样。

4.2 链接脚本的作用:为什么不能只靠编译命令

汇编这一步只要语法正确就能生成目标文件,但链接这一步就复杂了。默认的链接方式对裸机程序有时并不适用,比如代码段和数据段要放到特定地址,中断向量表必须放在最前面。这时候就需要一个链接脚本(linker script),它告诉链接器各种段的摆放位置。

arm-none-eabi-ld -Ttext=0x8000 -o start.elf start.o

这条命令只用 -Ttext=0x8000 指定代码段位置,对复杂工程不够用。一个实际的链接脚本至少包含四段内容:

SECTIONS { . = 0x8000; .text : { *(.text*) } .data : { *(.data*) } .bss : { *(.bss*) } }

脚本做的第一件事是把当前地址计数器设为 0x8000,之后的代码段从该地址开始放置。.text 放所有目标文件的指令代码,.data 放已初始化的全局变量,.bss 放未初始化的全局变量。BSS 段不占用二进制文件空间,运行时才被清零,这是很多初学者看不懂二进制文件大小的原因。

链接脚本里还有一个容易忽略的点是启动文件的入口。链接器默认的入口是 _start,这个符号必须存在且在 .text 段最前面。如果入口被放在靠后的位置,处理器上电后执行的就是莫名其妙的指令。检查办法是把生成的 ELF 文件用 readelf 命令看一下入口地址,和链接脚本设置的地址一致才算正确。

4.3 反汇编验证:用 objdump 确认指令编码与跳转目标

写完汇编最重要的一步是回头确认机器码。编译器的优化和汇编器的重定位有时会改变你写的形式,但这些变化都对应着相同的机器指令。使用 arm-none-eabi-objdump 可以让你看清最终生成的机器码:

arm-none-eabi-objdump -d start.elf

输出结果会列出指令地址、十六进制机器码和反汇编文本。对照检查时重点关注跳转指令的目标地址,因为链接时重定位会把相对偏移算好,若有符号链接问题,显示的目标地址会和你想跳的位置不同。比如你写 B 到某个标签,反汇编显示跳转到一条不相关的指令那里,就说明标签定义或链接脚本有误。

另一个有用的选项是显示重定位信息:

arm-none-eabi-objdump -r start.o

对未链接的目标文件执行这个命令,能看到哪些位置在最终链接时会被修改。看到 R_ARM_CALL 这类条目就说明有一条函数调用等着重定位。实际操作中,有时链接后地址错乱并不是代码问题,而是启动文件里少了向量表或向量表没放在起始地址。用 objdump 查看 ELF 头部信息,确认第一条指令是否正确位于入口地址即可快速排除这类问题。

4.4 最小可运行程序:从启动文件到内存布局验证

把前面的内容串成一个完整的最小工程。一个典型的裸机汇编工程由三部分组成:向量表、启动代码、主逻辑。向量表是给处理器用的,异常发生时处理器会从向量表里取出对应处理函数的地址。启动代码负责设置栈、搬运数据段、清零 BSS 段。主逻辑就是你真正要执行的业务代码。

.syntax unified .arch armv7-a .section .isr_vector .global _start _start: LDR PC, reset_handler LDR PC, undefined_handler LDR PC, swi_handler B . reset_handler: .word _reset undefined_handler: .word _undefined swi_handler: .word _swi .section .text _reset: LDR SP, =0x10000 @ 设置栈指针 BL main @ 跳入主逻辑 B . _undefined: B . _swi: B . .section .data main: MOV R0, #10 MOV R1, #20 ADD R2, R0, R1 B .

向量表的作用经常被忽略。如果省掉这几位,处理器一上电取值地址就是 0x00000000,第一条指令执行结果和预期完全无关。向量表每个表项占 4 字节,对应不同异常类型。 这里的 LDR PC, reset_handler 是从后面的标签处读取一个字到 PC,标签本质上是一个内存地址,里面存的才是真正的处理函数地址。这种方法比直接跳转灵活,因为向量表位置和函数位置可以自由布局。

链接时需要额外指定向量表的起始地址:

arm-none-eabi-as -o startup.o startup.S arm-none-eabi-ld -Ttext=0x0 -o demo.elf startup.o

把所有段都放在起始地址 0x0 开始的位置,因为处理器复位后从 0x0 取得第一条指令。链接后建议用 objdump 检查一下,确保 0x0 处确实是向量表的第一项,别让链接器把其他段放在这个地址上。对于不使用操作系统的裸机程序来说,中断向量表的布局决定了整个软件是否正常启动,值得花十分钟耐心验证。

5. 汇编调试避坑:最常见的 5 个反直觉错误与排查路径

5.1 指令条件码未写上,程序跳过关键逻辑

现象:一段代码明明从逻辑上应该执行某个分支,但单步运行后发现它直接跳过去了,标志位的值又看起来是对的。排查时发现,目标分支语句之前的那条比较指令使用了无符号数的跳转条件 BHI,而参与比较的数实际是有符号的负数。CMP 指令执行后表示小于零的负标志位被置位了,BHI 条件根本不成立,于是程序悄悄走另一条路。

原因:指令的条件码要和参与运算的数据类型严格匹配。无符号数用 HI、LS,有符号数用 GT、LT、GE、LE,混用就翻车。负数在寄存器里以补码形式存储,无符号比较会把负数当成很大的正数。

解决:所有参与比较的数据先确认是有符号还是无符号,选对应条件码。另外,调试时把 CPSR 里的标志位全部展开看,不要只看简单的零标志或进位标志,负标志的状态变化才是判断有符号比较的关键。

5.2 内存对齐问题引发硬件异常

现象:程序运行到某条 LDR 指令时直接进入未定义指令异常或数据访问异常,死循环停在异常向量表里。检查代码发现,访问的是一个结构体里的某个字段,按偏移量取出来之后在寄存器里做计算,奇怪的是之前读同一块内存的值都正常,偏偏读某个特定偏移时崩了。

原因:有些 ARM 平台对未对齐的内存访问是直接抛出异常的,LDR 读取的地址必须是 4 的倍数。如果结构体里混了字节、半字、字类型的字段,编译器不自动做对齐处理,你手算偏移时很容易踩到非对齐地址。

解决:要么把结构体字段对齐成 4 字节,要么使用专门的未对齐加载指令。ARMv7 架构里 LDR 本身支持未对齐访问,但需要确保系统配置允许。稳妥做法:检查硬编码偏移量是否为 4 的倍数,确有必要切换到支持任意对齐的变种指令。 排查这类问题的最快路径是查看异常向量表里保存的返回地址,指向哪条指令,再看那一条访问了什么地址,基本一击即中。

5.3 函数返回地址丢失,程序神秘跳飞

现象:主程序调用子函数后,代码执行乱套了。有时回到错误地址,有时跑进异常,有时重复执行同一段代码。调试器里看到 LR 寄存器的值在子函数没执行完时就已经变了,但主程序跳转调用写的并没有问题。

原因:子函数里又调用了另一个子函数,第二次调用把新的返回地址写进了 LR,覆盖了第一次调用的返回地址。嵌套深度只要超过一层,这种情况必然会出问题。单独看每一层调用代码都没毛病,但组合起来就丢数据了。

解决:每个函数入口处先把 LR 压栈,出口处恢复。最简单也最可靠的方式:

myfunc: PUSH {LR} BL another_func POP {PC}

这条规则是绝对的,只要函数内部还有函数调用,就必须保存 LR。听起来像常识,但实际问题中大量的莫名跳飞都是因为它。如果代码体积有限,有些短小函数也可以改成不调用其他函数的形式,用寄存器传递返回地址,但这只适用于底层工具函数,常规代码不要这么干。

5.4 常量池位置导致 PC 相对加载取错数

现象:用 LDR R0, =0x12345678 加载一个常数,程序结果却完全不对。反汇编后看到这条指令对应的机器码是从 PC 加偏移的位置加载数据,但那个位置的 4 个字节内容并不是期望的常数,而是某条别的指令的编码。

原因:汇编器把文字池放在了离指令比较远的位置,而链接完成后 PC 相对偏移超出了合法范围,或者文字池被放在了别的段里被数据覆盖了。最常见的是汇编器把文字池放在了代码段末尾,但代码段后来又追加了一段数据,恰好改变了布局。

解决:使用 .ltorg 指令强制文字池立即生成在当前位置。这段指令放在一个段的中途,可以避免文字池跑太远。

LDR R0, =0x12345678 .ltorg

这样安排的优点是文字池紧挨着访问它的指令,链接不会把它挪到很远的地方。对于跳转偏移超界的问题,加 .ltorg 也是一剂良药。反汇编确认的时候,重点看指令后面的地址和文字池中的数据,两边对应上就没有问题。

5.5 分支跳转范围超限,链接器静默修改

现象:工程代码量变大后突然出现链接错误,提示分支目标越界,或者干脆没有提示但运行行为错误。汇编器在生成 B 指令时用的是相对偏移,这个偏移是有范围限制的,常见条件下大概是正负 32MB。代码段超过这个范围,B 指令够不着目标地址了。

原因:对于大体积固件,代码段跨越的范围很大,短跳转指令无法覆盖。链接器有时会自动把指令转换成其他形式,有时直接报错。

解决:合理拆分代码段,让需要互相跳转的函数放得离得近一些。或者改用跳转偏移更大的指令形式,比如先加载地址再跳转。实际操作中先按错误提示查是哪个符号越界,把那个函数移到链接脚本里更合适的位置是保留代码结构下最快的办法。

@ 用 LDR + BX 实现远程跳转 LDR R0, =target_func BX R0

BX 指令可以跳到任意 32 位地址,和 LDR 配合就没有偏移范围的烦恼。代价是多了若干条额外指令和一次内存访问。对性能要求不高的初始化阶段,用这种方式跳转到远处的函数完全可以接受。

6. 进阶:条件执行与 IT 指令块,让分支代码更紧凑

ARM 汇编里有一项 x86 汇编完全没有的能力,条件执行。任何一条数据或算术指令都可以附带条件码,满足条件就执行,不满足就相当于一条 NOP。这意味着一些简单的 if 逻辑根本不需要分支跳转,直接写成条件指令就能完成。典型一个小例子,用条件执行实现二值选择:

CMP R0, R1 MOVLT R2, #1 @ 如果 R0 < R1,则 R2 = 1 MOVGE R2, #0 @ 如果 R0 >= R1,则 R2 = 0

这段代码没有一条跳转指令,处理器在执行 MOVGE 时如果条件为假就跳过,不会造成流水线清空和重新填充的开销。条件执行是 ARM 性能优化的一大支柱,也是读懂汇编代码时最容易忽略的部分。反汇编时经常会看到同一地址附近刷出一片 MOVNE、ADDNE、LDREQ 之类的指令,它们在逻辑上其实是一个紧凑的 if-else 块。

对于纯条件指令还是必须要用分支的取舍,一般阈值是逻辑复杂度。三五个条件的组合用条件执行还能接受,嵌套层次更深的就老老实实写分支,否则一段指令报错时排查的功夫比省下来的性能还贵。

IT 指令块是条件执行的扩展。在支持 Thumb-2 指令集的 ARM 处理器上,IT 指令可以控制接下来一段指令中哪些条件执行,哪些条件反执行。形式说明:IT 后面跟 T 和 E 两种字母序列,T 表示条件成立时执行,E 表示条件不成立时执行。

ITTT EQ MOVEQ R0, #1 MOVEQ R1, #1 MOVEQ R2, #1

这条指令的效果是让接下来三条 MOV 指令都只在 EQ 条件成立时执行。每个字母代表一条指令,ITTT 就是三真,后续指令集齐三条,最后一条本来也应是 EQ 条件的,可以省略不写。IT 指令配合条件指令可以像正常条件执行一样构造完整的逻辑块,还省了前缀。

写 IT 块最怕的是条件前缀和 IT 字母序列对不上。比如写成了 ITTE,但后面三条指令全部只带 EQ 前后缀,汇编器会直接报错。每次改动 IT 块里的指令时都要连带检查条件和字母序列。调试技巧层面,建议把 IT 块短时间控制在四条以内,超过四条放弃条件执行,用普通分支更清晰。

我在实际项目里积累的一个个人习惯是:每次写完一段汇编,都强制自己用 objdump 把反汇编结果看一遍,对照源代码逐条核对条件码类型和跳转目标,这比在调试器里单步漫无目的地点击可靠得多。有一次一个与中断向量表相关的异常问题翻了很久,最后发现就是向量表里少了一个 B 指令,后面所有表项都偏移了 4 字节,所有中断处理函数全都错了位。这种问题在反汇编输出里一眼就能看出来——向量表每 8 字节应是跳转指令加保留位,结果变成了连续两个地址值。让反汇编输出成为你日常工具链的一部分,汇编调试就不可能成为黑匣子。这篇 ARM 汇编基础到这里,希望你带着自己的开发板或模拟器亲手跑一遍上面的例子,遇到字符不认识的时候翻回到寄存器一节重新对照,慢慢你会发现启动文件不再是天书。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询