第3章
汇编基础
3.1 CPU架构与指令集
CPU即中央处理单元(Central Processing Unit),有时也简称为处理器(processor),其作用是从内存中读取指令,然后解码和执行。CPU架构就是CPU的内部设计和结构,也叫作微架构(Microarchitecture),由一堆硬件电路组成,用于实现指令集所规定的操作或者运算。
指令集架构(Instruction Set Architecture,ISA)简称指令集,包含了一系列的操作码(opcode),以及由特定CPU执行的基本命令。指令集在CPU中的实现称为微架构,要想设计CPU,首先得决定使用什么样的指令集,然后才是设计硬件电路。根据指令集的特征,通常可分为CISC和RISC两大阵营。
由于指令集是一堆二进制数据,非常不利于阅读和理解,于是有人就发明了汇编语言(Assembly language),用类似人类语言的方式对指令集进行描述,每条汇编指令都有对应的指令。再往后,C/C++等高级语言的诞生更加方便了程序的编写,推动了信息化和互联网的普及。
3.1.1 指令集架构
最先诞生的是复杂指令集计算机(Complex Instruction Set Computer,CISC),典型代表就是x86处理器。从1978年Intel推出的第一款x86处理器8086开始,8088、80286等都被统称为x86处理器。1999年AMD又将x86架构从32位扩展到了64位,称为AMD64。在Linux发行版中,将x86-64称为amd64,而x86则称为i386。
1974年IBM提出了精简指令集计算机(Reduced Instruction Set Computer,RISC)的概念,旨在通过减少指令的数量和简化指令的格式来优化和提高CPU的指令执行效率。典型代表有ARM处理器、MIPS处理器和DEC Alpha处理器等。以ARM处理器为例,1985年Acorn推出了基于ARMv1指令集的第一代ARM1处理器,2011年推出的ARMv8将指令集扩展到64位,称为AArch64,继承自ARMv7的指令集则称为AArch32。在Linux发行版中,将AArch64称为aarch64,AArch32则称为arm。由于RISC较高的执行效率以及较低的资源消耗,当前包括iOS、Android在内的大多数移动操作系统和嵌入式系统都运行在这类处理器上。
长期以来,CISC和RISC都处于你追我赶的竞争当中,同时也在不断地相互借鉴对方的优点。从Intel P6系列处理器开始,CISC指令在解码阶段上向RISC指令转化,将后端流水线转换成类似RISC的形式,即等长的微操作(micro-ops),弥补了CISC流水线实现上的劣势。同期,ARMv4也引入了代码密度更高的Thumb指令集,允许混合使用16位指令和32位指令,力图提高指令缓存的效率。可以说,CISC和RISC在指令集架构层面上的差异已经越来越小。
3.1.2 CISC与RISC对比
我们选择x86和ARM处理器,分别从指令集、寄存器和寻址方式等方面来进行对比。大多数RISC的指令长度是固定的,对于32位的ARM处理器,所有指令都是4个字节,即32位;而CISC的指令长度是不固定的,通常在1到6个字节之间。固定长度的指令有利于解码和优化,可以实现流水线(pipeline),缺点则是平均代码长度更大,会占用更多的存储空间。
从逆向工程的角度来看,指令长度不固定会造成更大的麻烦:因为同一段操作码,从不同的地方开始反汇编,可能会出现不同的结果,即指令错位。在第12章第2节中,我们会详细分析如何利用指令错位,意外地获得一些有用的gadget。例如,5e和5f分别是指令pop rsi和pop rdi的操作码,通过指令错位,我们就得到了下面的gadget。
另外,基于80%的工作由其中20%的指令完成的原则,RISC设计的指令数量也相对较少,或者说更加简洁。CISC可能为某个特定的操作专门设计一条指令,而RISC则需要组合多条指令来完成该操作。例如,x86处理器拥有专门的进栈指令push和出栈指令pop,而ARM处理器没有这类指令,需要通过load/store以及add等多条指令才能完成。
对于寻址方式,由于ARM采用了load/store架构,处理器的运算指令在执行过程中只能处理立即数,或者寄存器中的数据,而不能访问内存。因此,存储器和寄存器之间的数据交互,由专门的load(加载)和store(回存)指令负责。相反,x86既能处理寄存器中的数据,也能处理存储器中的数据,因此寻址方式也更加多样,通常可分为立即寻址(例如mov eax,0)、寄存器寻址(mov eax,ebx)、直接寻址(mov eax,[0x200adb])和寄存器间接寻址(mov eax,[ebx])。
指令数量的限制使得RISC处理器需要更多的通用寄存器。ARM通常包含31个通用寄存器,而x86只有8个(EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP),x86-64则增加到16个(R8~R15)。寄存器数量的差异在函数调用的设计上尤为明显,RISC可以完全使用寄存器来传递参数,而CISC只能完全使用栈(x86),或者结合使用栈和部分寄存器(x86-64)。
对不同指令集架构以及汇编语言的理解是逆向工程的基础,本章的后续内容将分别对最常见的x86、ARM和MIPS汇编进行讲解。如果你是在x86处理器的平台上学习ARM和MIPS,那么可以通过QEMU进行模拟,请查看第5章分析环境搭建。