购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.2 x86指令

x86汇编语言包括数百种指令。其中最常用的包括以下几种:

●算术指令:

■ add。

■ sub。

■ mul。

■ inc。

■ dec。

●位操作指令:

■ and。

■ or。

■ xor。

■ not。

●栈指令:

■ call。

■ return。

■ push。

■ pop。

●数据移动指令:

■ mov。

●执行流程指令:

■ jmp。

■ 条件跳跃指令。

●比较指令:

■ test。

■ cmp。

●其他指令:

■ lea。

■ nop。

虽然这看起来很多,但请考虑一下编程语言中常用的操作符(+、-、*、/、%、&&、||、&、|、^、!、~、<、>、<=、>=、==、.、->等)和主要关键词(if、else、switch、while、do、case、break、continue、for等)。用汇编语言实现这些行为需要很强的能力。

说实话,没有人能记住所有的x86指令,也没有必要这样做。x86指令的完整列表可以在http://ref.x86asm.net/coder32.html上找到,若有需要,可以在这里查阅任何指令的详细信息。

然而,要想成为一名成功的逆向工程师,理解最常用的x86指令的工作原理是非常必要的。如果你熟悉这部分关键的x86指令,你就能阅读并理解大多数x86程序。

3.2.1 mov

正如它的名字所暗示的,mov指令将数据从一个位置移动到另一个位置,例如,在寄存器和内存位置之间复制数据,或者在特定位置放置一个立即数。需要注意的是,尽管它的名字叫作“移动”,但它其实是在复制数据,并不是在移动数据(数据并未从源头被移除,而是被从源头复制到目标位置)。

mov指令的语法是move destination, source。例如,mov eax, 5这条指令会将值5放进寄存器eax。同样,mov eax,[1]这条指令会将地址为0x1的值移动到eax。

在处理mov指令和类似指令时,需要记住变量名的使用会影响被移动值的长度。例如,指令mov eax,[0x100]会将一个32位数值移动到eax,而指令mov dx,[0x100]会将一个16位数值移动到dx。

注意: 在x86指令中,可以使用寄存器值来标识内存地址。例如,指令mov[eax], ebx将ebx中存储的值移动到以eax为地址的内存位置。如果eax的值为0x7777,内存地址0x7777就是ebx的值被存储的地方。

mov是一种多功能的操作符,是助记符与机器码能力的绝佳示例。如图3.1所示,mov可以以各种方式被使用,而每一种方式都会根据使用的两个操作数转化为不同的机器码。所有这些不同的变体在助记符级别都被表示为mov。将助记符转化为正确机器码的任务就是由汇编器来完成的。

实操示例

假设变量i位于地址100,而j位于地址200,那么下面的伪代码应该如何用汇编语言编写呢?

这一行伪代码可以被转化为三条x86指令:

图3.1 mov指令

注意,寄存器eax用于存储从内存地址100复制到内存地址200的值。这样做的原因是单条指令无法执行两次内存访问。必须使用像eax这样的寄存器进行临时存储。

当我们查看代码时,可能会觉得直接将立即数42装入内存地址200,而不是用两次操作从内存地址100将之加载到内存地址200,似乎更有意义。然而,编译器不会,也不应该这么做。

这样做的原因是潜在的多线程应用程序。如果系统上运行着另一个线程,那么在将42放入内存地址100和将内存地址100处的值复制到内存地址200的步骤之间,内存地址100处的值可能已经被更新了。从内存地址100处复制值而不是使用立即数,有助于确保存储在内存地址200处的变量j获取存储在i中的值的最新版本。

3.2.2 inc、dec

x86指令inc和dec分别用来将指定的值增加或减少1。这与传统代码中的i++或i--指令是等价的。

这些指令只需要一个操作数,操作数可以是寄存器或内存地址。例如,指令inc eax将eax中储存的数值增加1,而dec[0x12345678]将内存地址0x12345678中储存的数值减少1。

3.2.3 add、sub

add和sub指令分别用于对特定值进行加法或减法计算。这些指令接受两个操作数。例如,add指令会采用add destination, value形式。

在add指令中,目标位置(destination)可以是寄存器或内存位置,而值(value)可以是寄存器、内存位置或立即数。这个操作会进行destination+value计算,并将结果存储在目标位置。这意味着目标位置的原始值与该数学表达式是相关的,但在保存结果之后会被覆盖。注意,这两个操作数的大小必须相同。例如,add eax, ebx是一条有效的指令(32位值与32位值相加),而sub eax, bx是无效的(32位值减16位值)。

在使用add和sub指令时,我们需要考虑所操作数值的大小。例如,sub ecx,[100]这条指令的目标位置是ecx,这就意味着我们在操控一个32位的数值。然而,add dword[edx], 100这条指令需要大小说明符dword,因为32位数值edx表明了这个内存地址的长度是32位的,但并没有指出那个位置上的数据大小。

3.2.4 mul

mul操作执行无符号整数乘法运算。然而,它有些不寻常,因为它只接受一个操作数,但隐式地使用了两个额外的寄存器。mul操作的语法是mul operand,其中operand可以是寄存器或内存地址。该操作将eax中存储的值与operand指定的值相乘。

mul操作的结果会保存在edx:eax中,其中edx存储了结果的高32位。即使结果小于32位且不需要edx,edx和eax中的值也会被mul改变。有意思的是,mul在进行32位算术运算时,可以得到一个64位的输出(edx:eax)。

mul操作的一个例子是mul eax,它对存储在eax中的32位值进行平方运算。当操作数包含一个内存地址时,该值的长度可以变化。例如,mul dword[0x555]将eax与存储在0x555地址上的32位值相乘,而mul byte[0x123]则使用存储在0x123地址上的8位值与eax相乘。

3.2.5 div

div操作执行无符号除法运算。就像mul操作一样,它只接受一个操作数,但隐式地修改eax和edx寄存器。在这种情况下,商储存在eax中,余数储存在edx中。例如,5除以2的商是2,余数是1。

div操作同时使用eax和edx作为其输入,并以与mul输出相同的方式对其进行格式化,将高32位放在edx中。就像mul一样,即使不需要edx(也就是余数是零),输出依然会修改eax和edx。

div eax是div操作的一个例子。这相当于计算eax, edx=edx:eax / eax。在这种情况下,操作数是一个32位寄存器,但使用内存地址可以指示并使用不同长度的除数。

实操示例

假设你想计算123除以4的余数。这可以通过四条汇编指令完成:

最后,商被存储在eax中,余数则被存储在edx中。

3.2.6 and、or、xor

x86标准支持几种不同的布尔操作。与(and)、或(or)和异或(xor)操作都需要两个操作数。下面展示了这三种操作的真值表。输入选项显示在表格的顶部和左边。

这三种操作都使用相同的语法mnemonic destination, source。例如,and操作的语法是and destination, source。与add操作类似,destination必须是一个寄存器或内存地址,而source可以是寄存器、内存地址或者立即数。此外,就像add操作一样,destination会在计算中被使用,但是也会被重写以保存结果。

布尔操作可以用于各种目的。例如,or eax, 0xffffffff就是一个快速将eax值设为全1的方法。操作and dword[0xdeadbeef], 0x1可以掩盖0xdeadbeef位置32位值除低位之外的所有位。操作xor eax,eax是清零eax值的常用方法。

3.2.7 not

not操作计算值的补码。如果你不熟悉“补码”这个术语,那么可以把它理解为把所有的0变成1,把所有的1变成0。它反转了数字。它采用not operand语法格式,只接受一个操作数。

not操作可以作用于各种长度的值。例如,操作not ch计算8位寄存器ch的补码,not dword[2020]计算位于地址2020的32位值的补码。

3.2.8 shr、shl

shr和shl是x86中可用的两种移位操作,其中shr代表向右移位,shl代表向左移位。这两项操作都需要两个操作数:需要移位的值的位置以及移位的位数,例如shr register, immediate。

shr和shl是逻辑移位操作符。这意味着,当按照给定的立即数将数值移位时,这两项操作会在左边或右边将这个数值进行零扩展。因此,任何因移位产生的新的位都会自动填充为0。

例如,操作shr al, 3将把存储在al中的值向右移动三位。如果al存储的值是00010000,那么操作后的结果将会是00000010。

提示: 零扩展向右移位的值会用零填满空位,这被称为逻辑移位。符号扩展向右移位的值则会用最高有效位的值填满空位,这被称为算术移位。

3.2.9 sar、sal

sar和sal是算术移位操作符。它们的语法与逻辑移位的语法相同,但实现方式不同。sar执行的是向右的算术移位,而sal执行的是向左的算术移位。

当执行左移操作时,sal指令与shl指令操作相同,都是对值进行零扩展。例如,当al中存储的值为00000100时,指令shl al, 3和sal al, 3都会产生值00100000。所有新的位都会用0来填充。

然而,sar操作会对值进行符号扩展,而shl操作会进行零扩展。符号扩展意味着它会复制最高有效位。例如,如果al寄存器包含的值是10000000,那么指令shr al, 3会产生值00010000,如下所示:

然而,指令sar al, 3会生成11110000。由于最高有效位是1,所以所有新的位都会复制为1。

3.2.10 nop

nop代表“无操作”(no operation)。它是一个一字节的操作符(0x90),并不执行任何操作。

虽然nop在技术上并无实际作用,但它在多种合法场景下都会被应用,包括以下几种:

●时间调整。

●内存对齐(Memory alignment)。

●风险防控。

●分支延迟槽(RISC架构)。

●稍后由未来补丁替换的占位符。

而在安全领域,它被用于以下情况:

●黑客攻击。

●破解。

3.2.11 lea

lea代表“加载有效地址”(load effective address)。它接受两个操作数,包括destination(寄存器或内存地址)和必须是内存地址的source。lea指令将计算出指定source操作数的地址,并将其放在destination处。对于熟悉指针的人来说,它类似于&操作符。

虽然lea用来处理地址,但它也常常被用于简单的数学运算。例如,操作lea eax,[ebx+ecx+5]是在询问ebx+ecx+5指向的地址是什么,并将那个地址存入eax。这实际上就是在计算ebx+ecx+5并将结果存入eax。而lea的一个更常规的用法是lea eax,[100],它会将值100存入eax。

虽然从表面上看,这可能会显得有点儿愚蠢或无意义,但是lea这个操作符实际上是很有用的,因为它能让我们在汇编中更加高效地处理数组。在数组中,各个值是存储在基址的特定偏移量处。(还记得基址加偏移量寻址模式吗?)有了lea,我们就能够有效地计算出数组中某个元素的地址。例如,假设eax中存储的是字符数组的基址,那么指令lea ebx,[eax+2]就会把数组第二个元素的地址放到ebx中。这条单一的指令比执行同样的操作需要的一系列指令mov ebx, eax和add ebx, 2更高效。

实操示例

如果将下面的伪代码翻译成汇编语言,应该怎么写?我们假设变量i存储在地址100处,j存储在地址200处,k存储在地址300处。

这段伪代码将会被汇编成如下的x86指令:

在这个例子中,需要注意ebx和bl的使用。本来要存入这个寄存器的值可以放在bl中。但是,在进行加法运算时,整个ebx寄存器都被用到了。这是因为类升级,如果向一个4字节的值加一个1字节的值,那么1字节的值会被升级为4字节,且额外的字节必须为0。在这种情况下,bl中原本为0x05的值被升级为ebx中的0x00000005。进行xor操作清除ebx是必要的,这样可以确保完全清除在ebx寄存器中存储的以前的值,不会影响加法运算的结果。 ks64pYdJyyux6yROTfbwqozFSaZZO3QP5wEiE0oKyZ4zR391mZrXs10pdFzgEMlQ

点击中间区域
呼出菜单
上一章
目录
下一章
×