# [特殊字符]️ 嵌入式调试从入门到进阶 —— ARM 架构(二)
️ 嵌入式调试从入门到进阶 —— ARM 架构二三分写七分调。调试是嵌入式开发中最核心、最不可替代的能力。 目录一、前言二、从代码到硬件——一条语句的执行之旅2.1 Flash 存指令RAM 存数据2.2 内存映射与片选2.3 CPU 眼中的世界——地址和数据2.4 CPU 内部寄存器2.5 a a 1 的三步执行2.6 反汇编解读——从 C 到机器码一、前言学习 ARM 架构不是让你天天写汇编。而是让你理解 C 代码在硬件上到底是怎么执行的——这样当你调试时看反汇编、看寄存器、看栈回溯才知道它们在说什么。本章围绕一个最简单的问题展开“a 1;” 这条语句在单片机里到底发生了什么二、单片机硬件系统_2.1 Flash 存指令RAM 存数据回到最基础的问题“为什么我的板子上电的时候可以使得内存的某一块区域为 1 呢”关键区分存储介质存什么断电后Flash ROM 类程序指令代码✅ 不丢失RAM内存变量/数据❌ 丢失inta1;// a 1 这条指令 → 保存到 Flash// 执行结果内存中 a 的值1→ 写在 RAM过程a 1;这条指令编译成机器码保存在Flash中CPU 上电后从 Flash 读取指令执行指令结果写入RAM代码指令保存在 Flash变量数据保存在 RAM。运行时代码从 Flash 读取数据写入 RAM2.2 内存映射与片选CPU 发出地址和数据但这些信息要去哪个设备Flash、RAM、GPIO、UART呢答案内存控制器 片选信号CPU 发出地址 ↓ 内存控制器MCU 厂家实现各不相同 ↓ 根据地址范围判断 → 选中对应外设的片选引脚CS ↓ 被选中的外设可以收发数据 没被选中的外设相当于不存在不影响总线同一时间只能有一个片选有效否则总线冲突。CPU 发出的地址经过内存控制器根据地址范围激活对应外设的片选引脚2.3 CPU 眼中的世界——地址和数据在 CPU 眼里它只知道两件事发出去一个地址从这个地址读数据或者往这个地址写数据它不关心这个地址对应的是 Flash、RAM 还是 GPIO。它只做读写操作。CPU 看到的是统一编址的地址空间——它不管背后是什么设备只管发地址、读写数据2.4 CPU 内部寄存器CPU 读到的数据放在哪里写数据的数据从哪里来答案CPU 内部有寄存器R0, R1, R2, …读取外设 → 地址总线 → CPU → 存入 R0举例 写入R1举例 → CPU → 地址总线 → 外设举个例子// 外设某个寄存器的值// 可能被 CPU 读到 R0 或 R1// 也可能把 R2 的值写入某个外设CPU 内部有多个寄存器R0~R15读到的数据暂存在寄存器中要写的数据也从寄存器发出2.5 a a 1 的三步执行aa1;这条语句不是一步完成的。CPU 内部执行时拆成了三步操作步骤操作说明① 读a的值 → 读到 R0从 RAM 读取变量 a 到寄存器② 加R0 R0 1CPU 内部 ALU 执行加法CPU 不是只能读和写它内部还有计算单元③ 写R0 的新值 → 写回 RAM把计算结果写回 a 的内存地址重点CPU 内部一定有加法器/ALU算术逻辑单元否则没法做加减法。CPU 执行a a 1的完整过程读 → 加 → 写扩展到循环执行for (int i 0; i 100; i) { AA(); // 反复调用 test_num; }每次循环CPU 都要反复经历去Flash取指令 → 执行指令 → 去RAM读写数据。CPU 反复从 Flash 取指令、执行、从 RAM 读写。红线表示取指令路径蓝线表示数据读写路径2.6 反汇编解读——从 C 到机器码fromelf --text -a -c --outputuart_poll\uart_poll.dis uart_poll\uart_poll.axf这条命令把 .axfELF 格式反汇编成可读的 .dis 文件反汇编文件内容展示了 C 语句对应的汇编指令以a 1为例看反汇编中的两条指令汇编指令含义对应 C 的哪部分MOV R0, #1把立即数 1 加载到 R0a 1中的1STR R0, [R1]把 R0 的值写入 R1 指向的内存地址a 赋值操作完整过程MOV R0, #1—— 从Flash读取这条指令 → CPU 执行 → R0 变为 1STR R0, [R1]—— 将 R0 的值写入RAM中 a 的地址这就解释了最初的问题a 1这个数值保存在 Flash 上执行 MOV 和 STR 两条指令使得 RAM 里面某一位变为 1。总结看似简单的一句a 1;背后涉及 Flash 存储、CPU 取指译码执行、寄存器暂存、内存控制器片选、RAM 写入这一整套流程。把这些搞懂后面的 ARM 汇编、中断、栈回溯才能真的理解。三、汇编指令——读写内存LDR / STR说句实话汇编指令不用全背。我干了这么多年也没记住多少条。遇到看不懂的汇编指令怎么办就两个字——百度一搜就明白了真不用硬记。那写不出复杂的汇编程序咋整——没必要写多复杂啊。你把栈初始化好剩下的直接用 C 写函数就完事了。我们平时打交道最多的翻来覆去就这几类指令指令类别常用指令读写内存LDR、STR、PUSH、POP算术运算ADD、SUB位运算AND、ORR、TST移位运算左移、右移跳转B、BL、BX、BLX比较CMP比较指令后面还能跟条件后缀只有条件成立才执行。比如CMP R0, R1 比较 R0 和 R1 BEQ XXX 如果 R0 R1Equal跳转到 XXXEQ相等、NE不相等这些后缀贴在指令后面就成了条件执行指令。数据传输三要素源、目的、长度拿LDR来举例。每次用LDR脑子里要过三个东西目的读到的数放哪——必须放进 CPU 内部的某个寄存器比如R0。CPU 读到的数据一定要先进寄存器没别的路。源从哪读——一个内存地址。比如[R1, #val]意思是以 R1 的值加上偏移 val算出最终地址去那里取数据。长度读几个字节LDR默认 4 字节读 2 字节用LDRH读 1 字节用LDRB。记住LDR 只能在 CPU 寄存器和内存之间搬数据。你想访问外设的地址也得先把地址存到寄存器里比如 R1 里放个地址值再用LDR R0, [R1]去读。CPU 执行LDR R0, [R1, #val]时它会发出地址R1 val通过数据总线去内存里把那个值取回来放到 R0 里。上手实操rw_ram 函数逐行拆解光说理论不过瘾直接上能跑的代码。下面这个rw_ram函数干的事很简单——读写 RAM 的第一个地址rw_ram PROC EXPORT rw_ram [WEAK] mov r1, #0x20000000 LDR r0, [r1] ENDP END一行一行拆开看①rw_ram PROCPROC Procedure子程序开始。意思是我来定义一个叫rw_ram的汇编函数了。②EXPORT rw_ram [WEAK]EXPORT把这个函数导出C 代码才能调用它。后面的[WEAK]是弱属性——如果你在 C 文件里也写了一个同名的rw_ram函数C 版本优先汇编这个就自动作废。这么设计是为了避免冲突。③mov r1, #0x20000000mov赋值指令把0x20000000这个数放进 R1。0x20000000就是 ARM 芯片内部 RAM 的起始地址也就是片内 SRAM 的首地址。执行完这句R1 里就存了 RAM 的基地址。④LDR r0, [r1]LDR从内存读数。[R1]的意思是取出 R1 里存的地址去那个地址指向的内存拿数据。翻译成人话把地址 0x20000000 上存的值读出来放到 R0 里。⑤ENDPEnd Procedure——函数到此结束。⑥END整个汇编文件的句号。汇编器读到这里就收工下班了。这六行串起来一句话总结就是定义一个可被 C 调用的弱函数把 RAM 首地址存入 R1读该地址的值放到 R0 后返回。调试里走一趟眼见才为实代码看完了咱们进调试器实际跑一遍亲眼看看这些指令到底干了什么。在main.c里找到rw_ram();这一行下个断点然后全速运行。程序跑到这个断点就停住了。这时候Step Into单步跟进去进到rw_ram函数体里面。看到 R1 没有执行mov r1, #0x20000000之前它还是个上电没初始化的乱值但执行完这条指令后R1 稳稳地变成了0x20000000这就是mov的作用——把立即数送进寄存器立竿见影。再点一次单步执行LDR r0, [r1]。盯住 R0R0 从0x00000000一下子变成了0x12345678这个值就是 RAM 地址0x20000000上原来存的数据——前面代码里定义了一个全局变量g_val 0x12345678它就被放在 RAM 的开头位置。继续往下单步走每一步寄存器都跟着变——你亲眼能看到LDR 把内存里的值读到了寄存器里这个过程。这种体验比光看书上写一百遍LDR 是读内存指令都来得直接。这就是调试的魅力一行反汇编一个寄存器变化比背十遍指令格式都好使。四、汇编指令——PUSH / POP读写栈PUSH 的本质就是写内存别把 PUSH 想得多玄乎它的本质就是一条 STR 指令——往 SP 指向的地址写数据。源要压栈的寄存器比如R0、R1、LR目的SP栈指针当前指向的位置操作把寄存器的值写到 SP 指向的内存PUSH 一次能压多个寄存器压的顺序也有规矩——编号大的先压先存到高地址再依次往低地址存。POP 的本质就是读内存POP 反过来本质就是一条 LDR 指令——从 SP 指向的地址读数据弹回寄存器。源SP 指向的栈内存目的要弹出的寄存器操作从 SP 指向的位置读数据存入寄存器POP 的顺序和 PUSH 相反——先弹出编号小的从低地址往高地址取。先认识几个寄存器的别名写汇编的时候寄存器叫法有些约定俗成的规矩寄存器别名作用R13SPStack Pointer栈指针指向当前栈顶位置R14LRLink Register链接寄存器存函数返回地址R15PCProgram Counter程序计数器指向当前执行的指令记住R13SP就是栈的光标——它指到哪PUSH 就往那写POP 就从那读。举个具体的例子假设现在 RAM 的范围是0x20000000~0x2001C000SP 初始指向0x20001000。PUSH {R0, R1, R2, R3}执行 PUSH 时CPU 会从高地址往低地址依次存存 R3→ SP 指向的位置写入 R3 的值SP - 4存 R2→ 新位置写入 R2 的值SP - 4存 R1→ 新位置写入 R1 的值SP - 4存 R0→ 新位置写入 R0 的值SP - 4压完 4 个寄存器SP 一共减了 16 字节4 × 4现在指向0x20000FF0。POP {R4, R7, R8, R10}执行 POP 时从低地址往高地址依次取弹到 R4→ 从 SP 位置读数据 → 存入 R4SP 4弹到 R7→ 新位置读数据 → 存入 R7SP 4弹到 R8→ 新位置读数据 → 存入 R8SP 4弹到 R10→ 新位置读数据 → 存入 R10SP 4结果R4 原来 R0 的值R7 原来 R1 的值R8 原来 R3 的值R10 原来 R4 的值注意 R10 拿到的是原来第 4 个压进去的不是 R4弹完 4 个SP 回到最初的0x20001000。简单说PUSH 是存了往后挪POP 是取了往前赶。调试里看 SP 的变化理论讲完了进调试器亲眼看看。这张图里ABC函数刚被调用还没执行第一条指令。看寄存器面板SPR130x20000538LRR140x08001F37返回地址PCR150x08000314指向PUSH {r3, lr}这条指令反汇编窗口显示的就是ABC函数的完整指令序列PUSH {r3,lr} 保存 R3 和返回地址到栈上 MOVS r0,#0x02 a 2 STR r0,[sp,#0] 把 a 的值存到栈上局部变量位置 POP {r3,pc} 恢复 R3把 LR 弹回 PC——返回单步执行完 POP 后再看 SPSP 从0x20000538变成了0x20000530——这就是 POP 执行后 SP 回退了 8 字节因为 PUSH 时压了 R3 和 LR 两个寄存器内存窗口中也能看到栈上存的返回地址0x08001F37PUSH 时 SP 减少栈生长方向是向下的POP 时 SP 增加恢复回去——SP 就是栈的水位线PUSH 涨潮POP 退潮。延伸函数调用时寄存器的保存规则PUSH/POP 学完了下一个自然的问题是函数调用时哪些寄存器要压栈保存谁负责保ARM 有个标准约定叫AAPCS把寄存器的保存责任分得清清楚楚。咱们从 main 调 Add 函数看起C 代码大概是这样的Add(100,1);BB(100,A);intvalCC(1,2,3,4);编译后调用 Add 之前先设置参数MOVS r1, #1 第二个参数 1 MOVS r0, #0x64 第一个参数 100 BL Add 调用 Add 函数函数的参数通过 R0~R3 传递返回值通过 R0 传递。那问题来了——寄存器在函数调用过程中被修改了怎么办ARM 把寄存器分成了两类寄存器分类谁负责保存说明R0~R3调用者保存调用者Caller传参数/返回值被调函数可能随意改R4~R11被调用者保存被调函数Callee要借用的话必须先存再恢复R12临时寄存器调用者也叫 IP随意用R13SP栈指针—栈指针谁都不能乱动R14LR链接寄存器—存返回地址BL 自动设置R15PC程序计数器—指向当前指令用人话说R0~R3函数 A 调函数 BB 改不改它不管。如果 A 觉得 R0~R3 很重要A 自己负责在调用前保存。R4~R11B 想借用的话必须先把原值压栈返回前恢复。A 可以放心调完 B 后 R4~R11 还是原来的值。Add 函数的栈帧结构PUSH {r0, r1, lr} 保存参数和返回地址到栈上 SUB sp, sp, #4 为局部变量腾空间 LDRD r0, r1, [sp, #4] 从栈上加载参数到寄存器 ADD r0, r0, r1 r0 a b核心计算 STR r0, [sp, #0] 把结果存回栈上局部变量 POP {r1-r3, pc} 恢复寄存器返回再看 BB 函数的反汇编PUSH {r0, r1, lr} 保存现场 SUB sp, sp, #0x68 分配局部变量空间 ... 函数体干活 ADD sp, sp, #0x70 回收栈空间 POP {pc} 返回一句话总结R0~R3 是用完不管的调用者自己备份R4~R11 是借用必还的被调函数负责保存恢复。五、汇编指令——ADD / SUB算术运算CPU 内部有个专门干苦力活的模块——ALU算术逻辑单元加减法都是它来算。ADD 和 SUB 的语法格式很灵活总结下来就这几种ADD 指令的写法ADD R0, #1 R0 R0 1 ADD R0, R0, #1 R0 R0 1同上完整写法 ADD R0, R1, #1 R0 R1 1 ADD R0, R1, R2 R0 R1 R2注意看规律第一个是目的地后面的都是参与者。不管是加立即数、加寄存器、还是寄存器加寄存器格式都一样。SUB 指令——看个实例光列语法太干直接上代码和反汇编对照着看。C 代码voidABC(intv){volatileinta1;av-a;}经编译器优化后v - a也就是v - 1变成了下面的汇编ABC 0x08000314: b508 PUSH {r3,lr} 保存现场 0x08000316: 1e40 SUBS r0,r0,#1 r0 v - 1 0x08000318: 9000 STR r0,[sp,#0] 存到局部变量 a 0x0800031a: bd08 POP {r3,pc} 恢复现场返回关键指令SUBS r0, r0, #1这里有几个细节值得说SUBS的后缀S表示这次减法会更新标志位比如结果是不是 0、有没有借位。如果写成SUB不带 S就不影响标志位。大多数时候我们写的是SUBS。操作数顺序SUBS r0, r0, #1相当于r0 r0 - 1。ARM 的语法是目的寄存器放最前面这一点跟 ADD 完全一样。所以整段汇编翻译成人话就是把 R3 和返回地址LR压栈→ 保存现场r0 v - 1→ 算出差值把结果存到栈上变量 a 的位置 → 写回内存恢复 R3返回到调用者调试里走一趟假设你在 Keil 里调用了ABC(100)在a v - a这一行下个断点。命中断点后打开反汇编窗口看到的正是上面那 4 条指令。此时R0 100参数 v 的值。单步执行SUBS r0, r0, #1后盯住 R0 寄存器R0 从 0x64100变成了 0x6399——减 1 操作完成了再把这一条指令和后面的STR r0, [sp, #0]连起来看就明白了SUBS 只改了寄存器里的值STR 才把这个值写回内存中变量 a 的地址。这就是寄存器操作和内存操作的分工。一句话总结ADD 和 SUB 的格式完全一样区别只在于一个是加一个是减。S 后缀表示会更新标志位写不写看你的需求。六、汇编指令——AND / ORR / TST位运算位运算也是 ALU 的拿手好戏。AND按位与和 ORR按位或的写法和 ADD 一模一样AND R0, #1 R0 R0 1 AND R0, R1, #1 R0 R1 1 AND R0, R1, R2 R0 R1 R2 ORR R0, R1, R2 R0 R1 | R2AND 常用于屏蔽某些位——你想知道一个数的某一位是 0 还是 1就和对应位为 1 的掩码做 AND。举个例子voidABC(intv){volatileinta;//if (v 1)if(v0x30)/* 检查 v 的 bit4 或 bit5 是否为 1 */a1;elsea2;}v 0x30的意思是只关心 v 的第 4 位和第 5 位0x30 0011 0000其他位全屏蔽掉。只要这两位的任意一个是 1条件就成立。TST——位测试指令编译成汇编后if (v 0x30)会变成这样TST R0, #0x30 测试 R0 的 bit4 或 bit5 是否为 1 BNE XXX 如果非零有一位是1跳转TST 本质上就是 AND但它的特殊之处在于计算结果不要了只更新 Z 标志位。TST 结果Z 标志含义所有测试位都是 0Z 1条件不成立 → 走 else至少有一位是 1Z 0条件成立 → 走 if记住AND 是又算又存TST 是只算不存。你想检查某些位但不关心具体值就用 TST。调试里看 Z 标志变化在if (v 0x30)这一行下断点假设 v 0x35二进制 0011 0101。v 的 bit4 1因为 0x35 的 bit4 是 1所以v 0x30的结果非零。单步执行 TST 后看 xPSR 寄存器Z 标志位 0——因为非零条件成立。再把 v 改成 0x0F二进制 0000 1111bit4 和 bit5 都是 0重新跑一次Z 标志位变成 1——AND 结果为 0条件不成立走 else。画个对应关系条件v 0x30 结果Z 标志执行分支v 0x35有 bit 置 1非零Z 0a 1ifv 0x0F没有 bit 置 1零Z 1a 2else一句话总结AND 和 ORR 做位运算TST 做位测试。它们都是 ALU 的基本操作区别在于要不要保存结果。七、汇编指令——CMP比较与标志位CMP——比较两个数CMP R0, R1 计算 R0 - R1只更新标志位CMP 本质上就是做一次减法减完的结果不要了但标志位N、Z、C、V会记录下来ZZero 1→ 说明 R0 R1相减为 0NNegative 1→ 说明 R0 R1结果为负CCarry 1→ 说明 R0 R1无借位然后根据标志位来决定后续指令是否执行CMP R0, R1 BEQ Label 如果 Z1相等跳转 BNE Label 如果 Z0不相等跳转 BGT Label 如果 N0 Z0大于跳转 BLT Label 如果 N1小于跳转动手看一下标志位在调试器里单步执行随时可以查看 xPSR 寄存器的标志位状态上图中N 0结果为正Z 1结果为零这一位被红框标出说明上一条指令的结果是 0C 1有进位/无借位V 0无溢出执行MOVS R0, #0后Z 标志位变为 1因为 R0 变成了 0。这就是带 S 后缀的指令更新标志位的直接证据。再看这两条指令MOVS R0, #0 R0 0更新标志位 → Z 1 MOVS R0, #1 R0 1更新标志位 → Z 0第一次 MOVS 把 0 赋给 R0结果为零Z 自动置 1。第二次 MOVS 把 1 赋给 R0结果非零Z 自动清 0。这一切都是 CPU 自动完成的不需要你手动干预。一句话总结CMP 和 TST 只改标志位不留结果。标志位N/Z/C/V就是 CPU 的便签纸后面的条件跳转指令全靠它做决策。八、汇编指令——移位运算左移 / 右移移位操作也是 ALU 的基本功。ARM 里有三条移位指令指令全称行为适用场景LSLLogical Shift Left左移低位补 0无符号数 ×2ⁿLSRLogical Shift Right右移高位补 0无符号数 ÷2ⁿASRArithmetic Shift Right右移高位补符号位有符号数 ÷2ⁿ基本写法LSL R0, R0, #2 R0 R0 2左移 2 位 LSL R0, R1, #2 R0 R1 2 LSR R0, R0, #1 R0 R0 1右移 1 位 ASR R0, R0, #1 R0 R0 1高位补符号位语法和 ADD 完全一样第一个是目的地第二个是源第三个是移位数。左移一位 乘以 2MOVS R0, #3 R0 3 LSL R0, R0, #1 R0 3 1 63 的二进制是...0000 0011左移一位变成...0000 0110 6。左移 n 位等于乘以 2ⁿ。反过来MOVS R0, #8 R0 8 LSR R0, R0, #2 R0 8 2 28 的二进制...0000 1000右移两位变成...0000 0010 2。右移 n 位等于除以 2ⁿ对无符号数。算术右移 vs 逻辑右移——有什么区别假设 R0 -8有符号数二进制补码表示为...1111 1000LSR R0, R0, #1 逻辑右移高位补 0 → 结果变成 0x7FFFFFFC正数不对了 ASR R0, R0, #1 算术右移高位补符号位 1 → 结果是 -4正确对有符号数做右移必须用 ASR算术右移它保留了符号位。对无符号数做右移用 LSR高位补 0 就行。一句话总结左移 乘 2ⁿ右移 除 2ⁿ无符号用 LSR有符号用 ASR。移位比乘除法快得多编译器经常用移位优化乘除法。九、汇编指令——跳转B / BL / BX / BLX程序默认是一条一条往下执行的。跳转指令就是用来岔路的——要么跳到别的代码段要么调用函数。B——无条件跳转B Label 直接跳转到 Label最简单的跳转不带返回值。相当于 C 里的goto。BL——跳转并保存返回地址BL Add 调用 Add 函数BL 比 B 多干了一件事跳转之前先把下一条指令的地址返回地址存到LRR14里。被调函数执行完用BX LR就能回来。这就是函数调用的底层原理。BX——跳转并返回BX LR 返回到调用者把 LR 的值赋给 PC程序就跳回去了。这是 ARM 里函数返回的标准写法。BLX——调用切换指令集BLX R0 调用R0指向的函数自动切换ARM/Thumb模式BLX 是 BL BX 的结合体——既保存返回地址又跳转还能在 ARM 和 Thumb 指令集之间自动切换。条件跳转——配合 CMP 使用跳转指令后面可以加条件后缀决定满不满足条件才跳CMP R0, R1 BEQ Equal R0 R1 时跳转Z 1 BNE NotEqual R0 ! R1 时跳转Z 0 BGT Greater R0 R1 时跳转N 0 Z 0 BLT Less R0 R1 时跳转N 1 BGE Geq R0 R1 时跳转 BLE Leq R0 R1 时跳转调试里看 BL 的执行上图是调试中执行OLED_Clear()时的状态当前正在执行的指令是BL.W OLED_ClearPC 0x08001F5E执行这条 BL 指令时CPU 会把下一条指令的地址存入 LR跳转到OLED_Clear函数的入口地址函数执行完后遇到BX LR就回到原来的位置继续再看一个循环调用的例子for (m 0; m 100; m) { AA(); test_num; }的循环中每次循环都会执行BL.W AA来调用 AA 函数可以看到反汇编窗口中的BL.W 0x080002E4 AA指令AA 函数内部有PUSH {r3,pc}和POP {r3,pc}来保存和恢复现场寄存器传参回顾结合第五节补充的 AAPCS 规则函数调用的完整流程是参数通过 R0~R3 传递跳转BL FuncName— 把返回地址存到 LR跳到函数入口被调函数PUSH {寄存器, lr}保存现场 → 执行函数体 →POP {寄存器, pc}恢复并返回返回POP {pc}把之前存的 LR 值弹给 PC程序回到调用处一句话总结B 是 jumpBL 是 call有去有回BX LR 是 return。配合 CMP 和条件后缀EQ/NE/GT/LT就能实现 C 语言里的 if/else/for/while 所有控制流。 完整指令索引本文涉及的所有汇编指令一张表总结分类指令作用读内存LDR/LDRH/LDRB读 4/2/1 字节到寄存器写内存STR/STRH/STRB将寄存器值写入内存入栈PUSH {regs}将寄存器压栈写 SP 指向的内存出栈POP {regs}从栈弹出到寄存器读 SP 指向的内存加法ADD/ADDS加法运算带 S 更新标志位减法SUB/SUBS减法运算带 S 更新标志位按位与AND位与运算按位或ORR位或运算位测试TST位与测试只改 Z 标志左移LSL左移 n 位×2ⁿ右移LSR/ASR逻辑/算术右移 n 位÷2ⁿ比较CMP减法比较只改 N/Z/C/V 标志无条件跳转B label直接跳转goto调用函数BL func跳转 存返回地址到 LR函数返回BX LR从 LR 恢复 PC调用切换BLX Rn调用 自动切换指令集条件后缀EQ/NE/GT/LT/GE/LE贴在指令后面条件成立才执行