尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

# [特殊字符]️ 嵌入式调试从入门到进阶 —— ARM 架构(二)

# [特殊字符]️ 嵌入式调试从入门到进阶 —— ARM 架构(二)
📅 发布时间:2026/7/30 12:45:06

🛠️ 嵌入式调试从入门到进阶 —— 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(内存)变量/数据❌ 丢失
inta=1;// "a = 1" 这条指令 → 保存到 Flash// 执行结果(内存中 a 的值=1)→ 写在 RAM

过程:

  1. a = 1;这条指令编译成机器码,保存在Flash中
  2. CPU 上电后从 Flash 读取指令
  3. 执行指令,结果写入RAM

代码(指令)保存在 Flash,变量(数据)保存在 RAM。运行时代码从 Flash 读取,数据写入 RAM


2.2 内存映射与片选

CPU 发出地址和数据,但这些信息要去哪个设备(Flash、RAM、GPIO、UART)呢?

答案:内存控制器 + 片选信号

CPU 发出地址 ↓ 内存控制器(MCU 厂家实现,各不相同) ↓ 根据地址范围判断 → 选中对应外设的片选引脚(CS) ↓ 被选中的外设:可以收发数据 没被选中的外设:相当于不存在,不影响总线

同一时间只能有一个片选有效,否则总线冲突。

CPU 发出的地址经过内存控制器,根据地址范围激活对应外设的片选引脚


2.3 CPU 眼中的世界——地址和数据

在 CPU 眼里,它只知道两件事:

  1. 发出去一个地址
  2. 从这个地址读数据,或者往这个地址写数据

它不关心这个地址对应的是 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 的三步执行

a=a+1;

这条语句不是一步完成的。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 --output=uart_poll\uart_poll.dis uart_poll\uart_poll.axf

这条命令把 .axf(ELF 格式)反汇编成可读的 .dis 文件

反汇编文件内容,展示了 C 语句对应的汇编指令

以a = 1为例,看反汇编中的两条指令:

汇编指令含义对应 C 的哪部分
MOV R0, #1把立即数 1 加载到 R0a = 1中的1
STR R0, [R1]把 R0 的值写入 R1 指向的内存地址a =(赋值操作)

完整过程:

  1. MOV R0, #1—— 从Flash读取这条指令 → CPU 执行 → R0 变为 1
  2. STR 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 == R1(Equal),跳转到 XXX

EQ(相等)、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 PROC

PROC= Procedure(子程序开始)。意思是:我来定义一个叫rw_ram的汇编函数了。

②EXPORT rw_ram [WEAK]

EXPORT把这个函数导出,C 代码才能调用它。后面的[WEAK]是弱属性——如果你在 C 文件里也写了一个同名的rw_ram函数,C 版本优先,汇编这个就自动作废。这么设计是为了避免冲突。

③mov r1, #0x20000000

mov:赋值指令,把0x20000000这个数放进 R1。
0x20000000就是 ARM 芯片内部 RAM 的起始地址(也就是片内 SRAM 的首地址)。

执行完这句,R1 里就存了 RAM 的基地址。

④LDR r0, [r1]

LDR:从内存读数。[R1]的意思是:取出 R1 里存的地址,去那个地址指向的内存拿数据。

翻译成人话:把地址 0x20000000 上存的值读出来,放到 R0 里。

⑤ENDP

End Procedure——函数到此结束。

⑥END

整个汇编文件的句号。汇编器读到这里就收工下班了。

这六行串起来,一句话总结就是:

定义一个可被 C 调用的弱函数:把 RAM 首地址存入 R1,读该地址的值,放到 R0 后返回。


调试里走一趟:眼见才为实

代码看完了,咱们进调试器实际跑一遍,亲眼看看这些指令到底干了什么。

在main.c里找到rw_ram();这一行,下个断点,然后全速运行。程序跑到这个断点就停住了。这时候Step Into(单步跟进去),进到rw_ram函数体里面。

看到 R1 没有?执行mov r1, #0x20000000之前它还是个上电没初始化的乱值,但执行完这条指令后,R1 稳稳地变成了0x20000000!

这就是mov的作用——把立即数送进寄存器,立竿见影。

再点一次单步,执行LDR r0, [r1]。盯住 R0:

R0 从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 相反——先弹出编号小的,从低地址往高地址取。


先认识几个寄存器的别名

写汇编的时候,寄存器叫法有些约定俗成的规矩:

寄存器别名作用
R13SP(Stack Pointer)栈指针,指向当前栈顶位置
R14LR(Link Register)链接寄存器,存函数返回地址
R15PC(Program Counter)程序计数器,指向当前执行的指令

记住:R13(SP)就是栈的"光标"——它指到哪,PUSH 就往那写,POP 就从那读。


举个具体的例子

假设现在 RAM 的范围是0x20000000~0x2001C000,SP 初始指向0x20001000。

PUSH {R0, R1, R2, R3}

执行 PUSH 时,CPU 会从高地址往低地址依次存:

  1. 存 R3→ SP 指向的位置写入 R3 的值,SP -= 4
  2. 存 R2→ 新位置写入 R2 的值,SP -= 4
  3. 存 R1→ 新位置写入 R1 的值,SP -= 4
  4. 存 R0→ 新位置写入 R0 的值,SP -= 4

压完 4 个寄存器,SP 一共减了 16 字节(4 × 4),现在指向0x20000FF0。

POP {R4, R7, R8, R10}

执行 POP 时,从低地址往高地址依次取:

  1. 弹到 R4→ 从 SP 位置读数据 → 存入 R4,SP += 4
  2. 弹到 R7→ 新位置读数据 → 存入 R7,SP += 4
  3. 弹到 R8→ 新位置读数据 → 存入 R8,SP += 4
  4. 弹到 R10→ 新位置读数据 → 存入 R10,SP += 4

结果:R4 = 原来 R0 的值,R7 = 原来 R1 的值,R8 = 原来 R3 的值,R10 = 原来 R4 的值(注意 R10 拿到的是原来第 4 个压进去的,不是 R4)

弹完 4 个,SP 回到最初的0x20001000。

简单说:PUSH 是"存了往后挪",POP 是"取了往前赶"。


调试里看 SP 的变化

理论讲完了,进调试器亲眼看看。

这张图里,ABC函数刚被调用,还没执行第一条指令。看寄存器面板:

  • SP(R13)=0x20000538
  • LR(R14)=0x08001F37(返回地址)
  • PC(R15)=0x08000314(指向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 后,再看 SP:

  • SP 从0x20000538变成了0x20000530——这就是 POP 执行后 SP 回退了 8 字节(因为 PUSH 时压了 R3 和 LR 两个寄存器)
  • 内存窗口中也能看到栈上存的返回地址0x08001F37

PUSH 时 SP 减少(栈生长方向是向下的),POP 时 SP 增加(恢复回去)——SP 就是栈的"水位线",PUSH 涨潮,POP 退潮。


延伸:函数调用时寄存器的保存规则

PUSH/POP 学完了,下一个自然的问题是:函数调用时,哪些寄存器要压栈保存?谁负责保?

ARM 有个标准约定叫AAPCS,把寄存器的保存责任分得清清楚楚。

咱们从 main 调 Add 函数看起:

C 代码大概是这样的:

Add(100,1);BB(100,'A');intval=CC(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,随意用
R13(SP)栈指针—栈指针,谁都不能乱动
R14(LR)链接寄存器—存返回地址,BL 自动设置
R15(PC)程序计数器—指向当前指令

用人话说:

  • R0~R3:函数 A 调函数 B,B 改不改它不管。如果 A 觉得 R0~R3 很重要,A 自己负责在调用前保存。
  • R4~R11:B 想借用的话,必须先把原值压栈,返回前恢复。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){volatileinta=1;a=v-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 完全一样。

所以整段汇编翻译成人话就是:

  1. 把 R3 和返回地址(LR)压栈→ 保存现场
  2. r0 = v - 1→ 算出差值
  3. 把结果存到栈上变量 a 的位置 → 写回内存
  4. 恢复 R3,返回到调用者

调试里走一趟

假设你在 Keil 里调用了ABC(100),在a = v - a这一行下个断点。

命中断点后,打开反汇编窗口,看到的正是上面那 4 条指令。此时R0 = 100(参数 v 的值)。

单步执行SUBS r0, r0, #1后,盯住 R0 寄存器:

R0 从 0x64(100)变成了 0x63(99)——减 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 | R2

AND 常用于"屏蔽"某些位——你想知道一个数的某一位是 0 还是 1,就和对应位为 1 的掩码做 AND。

举个例子:

voidABC(intv){volatileinta;//if (v == 1)if(v&0x30)/* 检查 v 的 bit4 或 bit5 是否为 1 */a=1;elsea=2;}

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 1111,bit4 和 bit5 都是 0),重新跑一次:

Z 标志位变成 1——AND 结果为 0,条件不成立,走 else。

画个对应关系:

条件v & 0x30 结果Z 标志执行分支
v = 0x35(有 bit 置 1)非零Z = 0a = 1(if)
v = 0x0F(没有 bit 置 1)零Z = 1a = 2(else)

一句话总结:AND 和 ORR 做位运算,TST 做位测试。它们都是 ALU 的基本操作,区别在于要不要保存结果。


七、汇编指令——CMP:比较与标志位

CMP——比较两个数

CMP R0, R1 @ 计算 R0 - R1,只更新标志位

CMP 本质上就是做一次减法,减完的结果不要了,但标志位(N、Z、C、V)会记录下来:

  • Z(Zero)= 1→ 说明 R0 == R1(相减为 0)
  • N(Negative)= 1→ 说明 R0 < R1(结果为负)
  • C(Carry)= 1→ 说明 R0 >= R1(无借位)

然后根据标志位来决定后续指令是否执行:

CMP R0, R1 BEQ Label @ 如果 Z=1(相等),跳转 BNE Label @ 如果 Z=0(不相等),跳转 BGT Label @ 如果 N=0 & Z=0(大于),跳转 BLT Label @ 如果 N=1(小于),跳转

动手看一下标志位

在调试器里单步执行,随时可以查看 xPSR 寄存器的标志位状态:

上图中:

  • N = 0:结果为正
  • Z = 1:结果为零(这一位被红框标出,说明上一条指令的结果是 0)
  • C = 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 完全一样:第一个是目的地,第二个是源,第三个是移位数。

左移一位 = 乘以 2

MOVS R0, #3 @ R0 = 3 LSL R0, R0, #1 @ R0 = 3 << 1 = 6

3 的二进制是...0000 0011,左移一位变成...0000 0110= 6。左移 n 位等于乘以 2ⁿ。

反过来:

MOVS R0, #8 @ R0 = 8 LSR R0, R0, #2 @ R0 = 8 >> 2 = 2

8 的二进制...0000 1000,右移两位变成...0000 0010= 2。右移 n 位等于除以 2ⁿ(对无符号数)。

算术右移 vs 逻辑右移——有什么区别?

假设 R0 = -8(有符号数,二进制补码表示为...1111 1000):

LSR 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 多干了一件事:跳转之前,先把下一条指令的地址(返回地址)存到LR(R14)里。被调函数执行完,用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_Clear(PC = 0x08001F5E)
  • 执行这条 BL 指令时,CPU 会:
    1. 把下一条指令的地址存入 LR
    2. 跳转到OLED_Clear函数的入口地址
    3. 函数执行完后,遇到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 规则,函数调用的完整流程是:

  1. 参数:通过 R0~R3 传递
  2. 跳转:BL FuncName— 把返回地址存到 LR,跳到函数入口
  3. 被调函数:PUSH {寄存器, lr}保存现场 → 执行函数体 →POP {寄存器, pc}恢复并返回
  4. 返回:POP {pc}把之前存的 LR 值弹给 PC,程序回到调用处

一句话总结:B 是 jump,BL 是 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贴在指令后面,条件成立才执行

相关新闻

  • Perlin Noise柏林噪声:从梯度噪声原理到程序化生成实践
  • 广州欧亚康体:商用泳池水处理一站式方案 - 城刊速递
  • 焊接符号讲解

最新新闻

  • Windows用户如何轻松读取Linux分区:Ext2Read完整使用指南
  • 义乌靠谱音响店,2026年亲测汽车音响首推义乌繁声 - GrowthUME
  • 快速处理SEGY地震数据的终极指南:SegyIO库的完整教程
  • 终极指南:如何用VRRTest快速检测显示器可变刷新率功能
  • 清单来了:2026最新一键生成论文工具测评与推荐大全
  • 软考架构师-嵌入式系统

日新闻

  • 终极TeamSpeak3音乐机器人搭建指南:5分钟实现语音聊天室音频播放
  • 广州海珠区内搬家攻略,平价靠谱搬家服务商推荐,专业打包搬运省心避坑全流程指南 - 厚道搬家
  • 大语言模型入门指南:从零到精通掌握AI核心技术的5大步骤

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号