反编译是进行逆向工程的简单方法,因为它能让我们回到更高级的语言和逻辑结构。然而,这个简单的方法并不总是可行的。对于编译到机器码的语言,我们需要更深入地理解计算机架构以及机器码和汇编代码的运作方式。
一般来说,我们认为普通程序员不需要深入了解计算机的工作原理。当用过程式语言编写程序时,操作系统会处理所有的低级操作。程序显示为一个进程,该进程在需要处理器、内存和文件系统时随时可以访问它们。进程似乎有自己连续的内存空间,文件只是一系列可读写的字节。
然而,实际上这些都不是真的,操作系统一直在为你抽象这些真实情况(以便使编程更简单)。深入理解计算机架构实际运作方式对于逆向工程师来说至关重要。图1.3展示了构成计算机的主要组件,包括中央处理器、桥接器、内存和外设。
图1.3 计算机架构
中央处理器(Central Processing Unit,CPU)是计算机进行处理的地方。CPU内部包含以下组件:
●算术逻辑单元(Arithmetic Logic Unit,ALU):ALU负责在计算机中执行数学运算,比如加法和乘法。
●寄存器:寄存器负责进行临时数据存储,并被用作x86指令的主要输入和输出。寄存器提供对单字数据的极速访问,并通常通过名称进行访问。
●控制单元:控制单元负责执行代码。这包括读取指令和协调计算机内其他元件的操作。
CPU通过系统总线(bus)连接到桥接器(bridge)。桥接器的主要目的是将CPU与系统的其他组件(包括内存和I/O总线)连接起来,I/O总线是外设(如键盘、鼠标和扬声器)与系统相连接的地方。当信息在总线上流动时,桥接器负责控制这种信息流并确保流入一个总线的流量被正确地路由到适当的总线上。
外设通过I/O总线连接,使得计算机能够与外部世界进行通信。这包括从显卡、键盘、鼠标、扬声器和其他系统发送和接收数据。
顾名思义,内存是计算机上存储数据的地方。数据以线性字节序列的形式存储,可以通过它们的地址访问。这种设计允许系统对存储的数据以相对较快的速度进行访问。
当程序想要访问内存中的数据时,CPU会通过总线发送一个请求给桥接器,然后桥接器会将这个请求转发给内存,在那里,指定地址的数据会被访问。然后,请求的数据需要沿着原路返回到CPU,才能被程序使用。相比之下,寄存器位于CPU内部,这使得它更易于访问。
寄存器是位于CPU内部的存储设备,不同于内存,它并不是线性字节序列。寄存器有特定的名称,并与每个寄存器有一定的大小关联。
寄存器和内存有同样的功能:它们都用来存储数据。然而,它们各有所长(就质量和数量而言)。寄存器数量稀少且昂贵,但数据访问速度极快。内存便宜且大量存在,但访问速度较慢。
程序关联的大部分数据(包括代码本身和其数据)将存储在内存中。在程序运行期间,会将小块的数据复制到寄存器进行处理。
计算机运行的是二进制的数字逻辑。所有的东西要么是打开的(1),要么是关闭的(0)。这也包括在计算机上运行的程序。所有高级语言最终都会被转换成一系列称为机器码(machine code)的二进制比特(bit)。机器码定义了计算机为了完成期望功能所要执行的一系列指令。
每个程序员都从“Hello World”程序开始学习编程语言。在x86中,“Hello World”的机器码如下:
为了便于阅读,这段机器码是以十六进制编写的,但它真正的值是一个由1和0组成的二进制字符串。这个二进制字符串包含了很多指令:翻转晶体管以计算信息、从内存中提取数据、通过系统总线发送信号、与显卡交互,以及输出“Hello World”文本。如果你觉得这串字符似乎有点短,无法完成所有这些工作,那是因为这些指令会触发操作系统(在这个例子中是Linux)来协助其完成。
机器码可以非常精细地控制处理器。机器码能完成的功能包括:
●数据的内存读取与写入。
●向寄存器中传输数据和从寄存器中读取数据。
●控制系统总线。
●控制算术逻辑单元(ALU)、控制单元和其他组件。
这种低级别的控制意味着用机器码编写的应用程序可以非常强大和高效。然而,虽然记住并输入各种比特序列来执行特定任务很炫酷,但这种方式效率低下且容易出错。
在机器码中,一系列的比特代表特定的操作。例如,0x81或10000001是一个指令,它将两个值相加并将结果存储在特定的位置。
汇编代码是对于人类而言可读的机器码。程序员可以使用add,而不是必须记住像0x81或10000001这样的十六进制或二进制字符串。add助记符已被映射到0x81,所以这个简略写法使得编程变得更容易,同时也不会失去使用机器码编程的任何优点。
将机器码翻译成汇编代码会使其更易于理解。例如,前面的“Hello World”示例机器码可以被转化为一系列易于理解的指令。
如果你对机器码有所了解,那么直接用它来编程可能很有趣,而且它有自己特定的适用场合。但在大部分时间里,这种做法既不高效也不实际。相比之下,使用汇编语言编程不仅能带来与直接使用机器码同等的好处,更重要的是,它更加实用。
一旦代码用汇编语言写好了,就能通过一个称为“汇编”的过程由汇编器转化为机器码。而已经是机器码的程序则可以通过反汇编器转换回汇编代码。
许多程序员并不直接使用机器码或汇编语言编写程序。相反,他们更喜欢使用更高级别的语言,这些语言能隐藏更多的细节。例如,以下伪代码就类似于许多高级过程式语言代码。
在编译过程中,这些高级语言会被转化成类似于下面的汇编代码:
然后,我们可以使用汇编器将汇编代码转换成计算机可以使用的机器码:
“计算机”这个词覆盖了广泛的系统。智能手表和台式计算机在工作方式上有许多相似之处。然而,它们的内部组件可能有很大的不同。
指令集架构(Instruction Set Architecture,ISA)描述的是运行程序的生态系统。ISA定义的因素包括:
●寄存器:ISA规定了处理器是拥有单个寄存器还是拥有上百个寄存器。它还定义了这些寄存器的大小,即它们是包含8位还是128位。
●地址和数据格式:ISA规定了用于访问内存中数据的地址格式。它还定义了系统一次可以从内存中获取多少字节的数据。
●机器指令:不同的ISA可能支持不同的指令集合。它还定义了是否支持加法、减法、等于、停止等指令。
通过定义物理系统的功能,ISA也间接地定义了汇编语言。ISA规定了哪些低级指令可用,以及这些指令的功能。
微架构(microarchitecture)描述了特定的ISA如何在处理器上实现。图1.4给出了Intel Core 2架构的一个示例。
ISA和微架构共同定义了计算机架构。成千上万的ISA和成千上万的微架构意味着也存在成千上万的计算机架构。
定义
指令集架构(ISA)定义了寄存器、地址、数据格式和机器指令的工作方式。微架构则负责在处理器上实现ISA。ISA和微架构共同定义了计算机架构。
虽然存在成千上万的计算机架构,但它们大体上可以分为两大类。精简指令集计算(Reduced Instruction Set Computing,RISC)架构定义了一小部分比较简洁的指令。一般来说,RISC架构更便宜、更容易创建,而且硬件体积更小,功耗更小。
图1.4 Intel Core 2架构
相对而言,复杂指令集计算(Complex Instruction Set Computing,CISC)架构定义了更多的强大指令。CISC处理器的造价更高,创造难度更大,一般体积更大,功耗也更大。
虽然从客观角度来看,CISC架构似乎比RISC架构要差,但它的主要优势在于编程的简便性和高效性。让我们来看一个假想的例子:一个程序希望在RISC和CISC系统中将一个值乘以5。
在这个例子中,如果CISC处理器有一个能从内存中加载值并对其执行乘法运算,然后将结果存储在相同内存位置的乘法操作,那么它可以通过一条指令完成计算。但是,因为乘法运算太复杂,RISC处理器可能没有直接的乘法操作。相反,RISC可以从内存中加载值,将它和自身相加四次,然后将结果存储在同一内存位置。
RISC和CISC架构各有优点、缺点和使用场景。例如,一个CISC操作一条指令能够执行的任务,一个RISC操作可能需要100条指令才能达成。然而,一个CISC操作可能需要100倍的时间,或者需要100倍的功率。
现今,RISC和CISC架构均被广泛使用。常见的RISC架构实例包括:
●ARM(用于手机和平板计算机)。
●MIPS(用于嵌入式系统和网络设备)。
●PowerPC(用于原始Mac和Xbox360)。
在本书中,我们专注于研究x86汇编语言,这是一种CISC架构。目前,所有现代个人计算机以及服务器都在使用这种架构,并且它得到了所有主流操作系统(如Windows、Mac以及Linux)甚至游戏系统(比如Xbox One)的支持,这使其成为软件破解学习中最有力的一种。