3. 8051指令集基础:数据传送、算术运算、逻辑运算与控制转移
指令集,说白了就是单片机的“母语”。你要逆向分析固件,就得先听懂它在说什么。8051的指令集不算复杂,总共111条指令,但真正高频出现的,也就四类:数据传送、算术运算、逻辑运算、控制转移。我个人习惯把这四类称为“固件逆向的四块基石”。
今天我们就把这四块基石一块一块掰开来看。别急,我会结合项目里踩过的坑来讲,保证你听完就能用。
3.1 数据传送指令——固件里的“搬运工”
数据传送指令是8051里出现频率最高的指令,没有之一。说白了,就是把数据从一个地方搬到另一个地方。搬来搬去,控制逻辑就出来了。
3.1.1 核心指令速览
| 指令 | 功能 | 字节数 | 周期 |
|---|---|---|---|
MOV A, #data |
立即数送累加器 | 2 | 1 |
MOV A, Rn |
寄存器送累加器 | 1 | 1 |
MOV Rn, A |
累加器送寄存器 | 1 | 1 |
MOV direct, A |
累加器送直接地址 | 2 | 1 |
MOV A, @Ri |
间接寻址送累加器 | 1 | 2 |
MOVX A, @DPTR |
外部RAM读入累加器 | 1 | 2 |
MOVC A, @A+DPTR |
查表读入累加器 | 1 | 2 |
PUSH direct |
压栈 | 2 | 2 |
POP direct |
出栈 | 2 | 2 |
MOV A, #data 后面紧跟着 MOV direct, A,基本就是在初始化某个寄存器或端口。我曾经逆向一个空调控制板,发现它用这种方式连续写了20多个字节——嗯,那是在初始化LCD的显示缓冲区。
3.1.2 寻址方式与反汇编中的识别
8051有7种寻址方式,但反汇编时你只需要重点关注三种:
- 立即寻址:操作数直接写在指令里,比如
MOV A, #0x55。反汇编里一眼就能看出来,数值是固定的。 - 直接寻址:操作数是地址,比如
MOV A, 0x90。0x90是P1口的地址,看到这个你就知道它在读端口。 - 间接寻址:通过R0或R1指向地址,比如
MOV A, @R0。这种常用于循环操作,比如内存拷贝。
MOVX A, @DPTR 指令,以为它在频繁读写外部RAM。后来才发现,它其实是在访问一个映射到外部地址空间的硬件寄存器。如果你不查硬件手册,很容易误判。
3.2 算术运算指令——加减乘除里的玄机
8051的算术运算指令其实挺“朴素”的,只有加、减、乘、除,而且乘除只能用在累加器A和寄存器B之间。但你别小看这几条指令,控制算法、校验计算、甚至加密解密都靠它们。
3.2.1 核心指令速览
| 指令 | 功能 | 影响标志位 |
|---|---|---|
ADD A, Rn |
A + Rn → A | C, AC, OV |
ADDC A, Rn |
A + Rn + CY → A | C, AC, OV |
SUBB A, Rn |
A - Rn - CY → A | C, AC, OV |
INC Rn |
Rn + 1 → Rn | 无 |
DEC Rn |
Rn - 1 → Rn | 无 |
MUL AB |
A × B → B:A | OV, CY=0 |
DIV AB |
A ÷ B → A余B | OV, CY=0 |
DA A |
十进制调整 | C |
3.2.2 反汇编中的算术逻辑还原
你想想看,在反汇编里看到 ADD A, #0x01 和 INC A,效果是一样的,但编译器选哪个?我观察过,Keil C51编译器倾向于用 INC A,因为它只占1个字节。而手工汇编的固件,经常混用。
还有一个细节:DA A 指令。这玩意儿是BCD码调整用的。我在分析一个电表固件时,看到大量 ADD 后面紧跟 DA A,就知道它在做十进制运算。这种模式在计量类设备里非常常见。
ADD 或 SUBB 后面跟着 JC 或 JNC,那基本就是在做条件判断。这是还原控制逻辑的重要线索。
3.3 逻辑运算指令——位操作的利器
8051的逻辑运算指令,我个人觉得是它最强大的地方。尤其是位操作能力,很多现代32位单片机都比不了。你想想看,一个8位单片机,能直接操作每一位,这在控制领域简直是神器。
3.3.1 核心指令速览
| 指令 | 功能 | 典型用途 |
|---|---|---|
ANL A, Rn |
A & Rn → A | 位屏蔽 |
ORL A, Rn |
A | Rn → A | 位置位 |
XRL A, Rn |
A ^ Rn → A | 位翻转、加密 |
CLR A |
0 → A | 清零 |
CPL A |
~A → A | 取反 |
RL A |
A循环左移 | 乘2 |
RR A |
A循环右移 | 除2 |
SWAP A |
A高低半字节交换 | 字节拆分 |
3.3.2 位操作指令——8051的独门绝技
8051有一个专门的位寻址区(20h~2Fh),共128个可位寻址的位。对应的指令有:
SETB bit:置位CLR bit:清零CPL bit:取反JB bit, rel:位为1则跳转JNB bit, rel:位为0则跳转JBC bit, rel:位为1则跳转并清零
JB P1.0, LABEL,我第一反应就是去找P1.0接了什么硬件。可能是按键,可能是传感器,也可能是中断输入。我曾经分析一个遥控器固件,发现它用 JBC 指令来检测按键并自动清除标志位——这个细节帮我快速定位了按键扫描逻辑。
3.4 控制转移指令——程序的“方向盘”
控制转移指令决定了程序的流向。没有它们,程序就只能一条路走到黑。逆向分析时,控制转移指令是你还原程序流程图的关键。
3.4.1 无条件转移 vs 条件转移
| 类型 | 指令 | 跳转范围 | 字节数 |
|---|---|---|---|
| 无条件 | LJMP addr16 |
64KB | 3 |
| 无条件 | AJMP addr11 |
2KB | 2 |
| 无条件 | SJMP rel |
-128~+127 | 2 |
| 无条件 | JMP @A+DPTR |
64KB | 1 |
| 条件 | JZ rel |
-128~+127 | 2 |
| 条件 | JNZ rel |
-128~+127 | 2 |
| 条件 | JC rel |
-128~+127 | 2 |
| 条件 | JNC rel |
-128~+127 | 2 |
| 条件 | CJNE A, #data, rel |
-128~+127 | 3 |
| 条件 | DJNZ Rn, rel |
-128~+127 | 2 |
3.4.2 调用与返回指令
子程序调用是结构化编程的基础。8051提供了 LCALL(3字节,64KB范围)和 ACALL(2字节,2KB范围)两种调用指令。返回指令只有 RET 和 RETI(中断返回)。
RET 返回,但调用它的地方却是一个中断服务程序。这会导致中断系统混乱。后来发现,这是编译器优化后的结果——它把中断服务程序里的公共代码提取成了子程序。嗯,逆向时不能只看指令,还得结合上下文。
3.5 知识体系总览
下面这张图,是我自己总结的8051指令集知识体系。每次做逆向分析前,我都会在脑子里过一遍这个框架。
3.6 实战要点总结
好了,四类指令都讲完了。最后给你几个实战中一定会用到的要点:
- 数据传送指令看流向:谁传给谁,决定了数据从哪里来到哪里去。逆向时重点关注
MOVX和MOVC,它们往往涉及外部设备或查表数据。 - 算术运算指令看标志位:
ADD、SUBB后面的条件跳转,就是控制逻辑的分水岭。我一般会把这些跳转目标地址记下来,画成流程图。 - 逻辑运算指令看模式:
ANL用于屏蔽,ORL用于置位,XRL用于翻转。看到XRL频繁出现,十有八九是在做加密或校验。 - 控制转移指令看结构:
LCALL和RET成对出现,就是子程序。看到DJNZ就是循环。看到JMP @A+DPTR就是跳转表,常用于状态机或菜单切换。