代码反汇编:IDA Pro与Soft ICE之应用(4)

 边看边翻译,其中敲字错误或表述有误再所难免。如果您发现错误,而且也有时间,敬请留言告之,以便更正。 请尊重作者及译者工作之艰辛,若转摘,请务必注明出处如下:

------------------------------------------------------------------

《代码反汇编:IDA Pro与Soft ICE之应用》

英文名称:Disassembling Code IDA Pro and Soft ICE

作者:Vlad Pirogov

译者:罗祥勇 <E-mail:solo_lxy@126.com>
出自:CSDN Blog <背你走天涯>专栏
------------------------------------------------------------------  

       1.4 Intel微处理器的指令格式

                   1.4.1 一般要考虑的事项

当你研究Intel Pentium微处理器的时候,你可能会问,这些指令是如何在内存中存储的呢?指令MOV EAX, EBXMOV EAXEDI的区别在哪儿呢?本节的目的就来解释编码Intel处理器指令的一般模式。如果你对分析指令格式感兴趣的话,当你挖掘可执行代码的时候,本节的内容将在相当程度上给你于很大的帮助。

1.5显示了一段代码的内存区域。是使用OllyDbg调试器转储地,OllyDbg将在以后的章节中予以介绍。为了解译出这段字节,使其变为机器指令(更准确地说是,变成汇编指令),有必要去了解以下这些指令的格式。现在,我请你将注意力集中到这个方面上来。

 

1.5 程序代码的内存转储

首先,有必要指出指令的长度可以在1~10字节之间,或者更多。图1.6显示了Intel处理器指令的一般格式。正如你看到的,指令的结构可能非常复杂。但幸运的是,我们是可以理解这些结构的,因为处理器能正确的解释并执行它。很幸运,想理解他们并不是没有希望。

 

1.6  Intel处理器的指令格式

做为开始,首先研究前缀。正如你看到的,前缀是可选的。图1.6显示的所有前缀我们从逻辑上可以假设它们必须严格地对应定义好的代码以确保前缀和指令不会迷惑解译过程。有4种类型的前缀:

    指令前缀可以有以下几种值:

         F3H, 重复前缀REPE/REPZ

         F2H, 重复前缀REPNE/REPNZ

         F0H, 总线阻塞前缀LOCK

 

    地址大小指令(可用大小代替)前缀使用值67H

    操作数大小指令(可用大小代替)前缀使用值66H

    段替换前缀使用以下值:

         2EH – CS寄存器

         36H – SS寄存器

         3EH – DS寄存器

         26H – ES寄存器

         64H – FS寄存器

         65H – GS寄存器

有一点值得重点提出,同一条指令中没有多于两种的相同前缀。出现这样的指令将导致处理器错误。

因此,知道了前缀代码,就可以知道指令是以什么部件做为开始的:前缀还是直接就是代码。注意,这仅仅在你知道知道代码是从什么地方开始的时候才有用。否则,反汇编将从某个指令的中间开始,其结果将导致反汇编出来的代码与事实不符。

                   1.4.2 指令代码

现在,考虑1.17提供的处理器指令代码(一段小且简单的汇编语言片段)。

列表1.17 学习Intel处理器汇编指令格式的一段小的汇编程序

PUSH      EAX
PUSH      EBX
PUSH      ECX
POP       ECX
POP       EBX
POP       EAX
RET

正如你看到的那样,将三个寄存器的内容都保存到堆栈上。存储到堆栈的值又弹出到对应的寄存器中。完成上面的指令后,程序退出该过程。因此,如果研究其在内存的表示,你会注意到如下的字节序列:

50 53 51 59 5b 58 C3

思维的第一反应就是,上面的每条指令占用一个字节。你看到的是典型的8位的指令。例如,C3H就是RET指令(更准确的说是RETN)。因此,开始的6字节更让人感兴趣。首先,我们研究一下PUSH指令。这些指令的二进制表示为:01010000B(PUSB EAX),01010011B(PUSH EBX),01010001B(PUSH ECX)。注意这些指令只是字节低位的位不同。因此,可以得出以下不证自明地结论:这样的指令编码成指令代码;也就是说,指令代码指定给定的操作要进行什么样的动作和要操作哪个寄存器。为了确定我们的假设,考虑下面三个POP指令代码:01011001B(POP ECX),01011011B(POP EBX),01011000(EAX)。这样情况就清晰了。例如,比较PUSH EBXPOP EBX。注意开始的2个位是匹配的。但是,其它指令对的这两个位也是相等的,如PUSH EAX/POP EAXPUSH ECX/POP ECX。开始的3个位都是匹配的。另一方面说,对于所有的PUSH指令,开始的5个位都是匹配的(01010B)POP指令也是一样(01011B)。刚才发现的一般模式并不是随机地。事实上,这些指令PUSH reg/POP reg不但把操作编码进了指令,而且把寄存器也编码进去了。寄存器的代码是通用的。这些代码不单会出现在指令代码中,而且还会出现在R/M字段。这个问题以下章节再做讨论。

现在,考虑如下的32为工作寄存器:

    EAX – 000B

    EDI – 111B

    EBX – 011B

    ESI – 110B

    ECX – 001B

    ESP – 100B

    EDX – 010B

    EBP – 101B

第一眼看上去,相当直接,因为我们发现了一般的模式。但是,情况并非如此简单。还有16位寄存器,8位寄存器。更让人失望的是,PUSHPOP操作除了将以上列出的工作寄存器或内存单位作为操作数外,其他寄存器也可以作为其操作数,这些代码是不同的。所有这些问题我们会依次解决。

注意,PUSHPOP指令并使用8为寄存器。因此,寻址8位寄存器的问题对PUSHPOP就不会出现。但还有16位寄存器。可能很出人意料,16位和32位寄存器编码方式是一样。例如,AX的代码为110B。将16位的寄存器数据压入堆栈会发生什么情况呢?答案很直接:操作数大小替换前缀用在指令代码之前,如66H。因此,PUSH AX指令使用两个字节表示: 66 50POP EAX指令使用66 58表示。一旦遇到这样的前缀,处理器就知道需要将当前指令的32位操作数替换成16位。从而你可以得到如下结论:使用32位的寄存器比使用16位的积存器更高效。

不幸地是,PUSHPOP指令关于寄存器操作数的指令代码的一般模式还是对部分代码存在限制。以下是使用在PUSHPOP指令中的段寄存器代码:

    PUSH CS -  OEH

    PUSH DS – 1EH

PUSH SS – 16H

PUSH ES – 06H

PUSH FS – 0FA0H

PUSH GS – 0FA8H

POP DS – 1FH

POP SS – 17H

POP ES – 07H

POP FS – 0FA1H

POP GS – 0FA9H

通过以上指令,我们仅发现这些指令对(PUSH DS / POP DS)相差为1。同时,有必要注意,使用段寄存器FSGS的指令有两个字节。因为这些寄存器是在新的Intel处理器家族带入的,这些处理器中没有1个字节的代码。一般情况下,处理器开发者经常受限于可选的解决方案;因此,对于这些方案,你就不要期望任何一切都应该有规可循了。

继续我们的探索过程,回答下面的问题:除表示指令和寄存器的代码之外,所有地方都使用指令吗?

考虑条件跳转指令。首先,有必要看看近跳转指令(也就是跳转距离在256字节范围内的)是如何编码的。看一个小的汇编语言片段(列表1.18)。注意,尽管这个例子没有实际意义,但它可以让你发现一些有意思的模式。

列表1.18 学习跳转汇编指令格式的一段小的汇编程序

JZ  _LAB

        JNZ _LAB

        JB  _LAB

        JNB _LAB

        JG  _LAB

        JNG _LAB

_LAB:

看看调试器给出的如下代码序列:

74 0A 75 08 72 06 73 04 7F 02 7E 00

可以很清晰的发现,每条指令占用两个字节,而第二个字节定义地址(假设已经处理了条件)。在仔细研究了第一和最后一个指令后,可很容易的发现,这些都是相对于指令结束位置的位移(参见图1.6)因此,事情就变得清晰了-至少现在上清晰了。

现在,仔细看看每条指令的第一个字节,可以发现如下一般的模式:

    JZ – 01110100B

    JNZ – 01110101B

    JB – 01110010B

    JNB – 01110011B

    JG – 01111111B

    JNG – 01111110B

结论不证自明。条件跳转的代码就是简单的70H,低4位为表示跳转条件。同时,很明显,最低的那个为表示条件反转:如JZ –> 0, JNZ -> 1等。凭直觉还有一些规则也很明显:等于0的话本位为0,大于意味着本位为1。位1~3定义了条件。因为3位可以表示8个不同的条件,因此可以总结表1.10的结论,进一步的结论可参见表1.26

列表1.17 条件跳转码

指令

代码

JB/JNAE/JC

001

JBE/JNA

011

JE/JZ

010

JL/JNGE

110

JLE/JNG

111

JO

000

JP/JPE

101

JS

100

很自然地会有以外疑问:对于跳转语句,在32位的段的地址如何表示呢?为了研究这个问题,修改列表1.18中的代码。修改版本见列表1.19

列表1.19 学习跳转汇编指令格式的一段小的汇编程序

JZ  _LAB

        JNZ _LAB

        JB  _LAB

        JNB _LAB

        JG  _LAB

        JNG _LAB

        DB   1000H DUP(0)

_LAB:

通过在JNB指令后插入数据指令,会迫使汇编器生成32位的跳转偏移。生成结构见列表1.20

列表1.20 列表1.19的内存转储

OF 84 1E  10 00 00

OF 85 18  10 00 00

OF 82 12  10 00 00

OF 83 0C  10 00 00

OF 8F 06  10 00 00

OF 8E 00  10 00 00

前面的列表表示成一个表格,每行表示和其相关的指令。正如你看到的,指令代码现在是2个字节。每行的第一个字节都是0FH。第二个字节的结构值得研究。操作码为80H,其后跟着指令代码和条件反转位。和地址相关的部分看起来很奇怪。这里的地址(更准确的说是偏移),就是一个一般的32位数字,同时这个原则:重要位在高地址。因此,1E 10 00 00就是00 00 10 1E,它就是指令JNZ _LABRET之间以字节为单位的精确距离。所有跳转语句都是一样的。

                   1.4.3 MOD R/M字节域

考虑一个看起来相当简单的操作:MOV EAX EBX。它由两个字节的8B C3组成。因为在寄存器间有很多的变体,很自然的就可以推测EAXEBX都被编码了。同时,还可以假设第一个字节表示操作码,而寄存器被编码至第二个字节。而C3的二进制表示为:11000011B。为了做可比性分析,看看MOV EBX EAX指令。这条指令的的代码为:8B D8。顺便说一下,这里假设第一个字节为操作码,而D8的二进制表示为:11011000B。将它和C8的二进制表示进行比较。很自然,这些字节下面的三个为彼此之间是不同的:000B011B。这里是先前提过的EAXEBX寄存器。很好!MOV指令操作于两个32位的寄存器之上。你遇到过MOD R/M字节,它的结构我们接下来做细致的研究(参见图1.6)。

MOD R/M字节有以下三个域:

MOD连同R/M域一起,这个域形成32种不同的值:8个寄存器和24中索引模式。在前面提供的例子中,这个域的值为11,这意味着R/M域表示的是寄存器。

REG/Code本域表示要么表示寄存器代码要么表示3位的操作代码。

R/M本域即可表示操作数所处的寄存器,也可也MOD域一起编码地址模式。

一个合理地疑问就出来了:MOV EAXEBXMOV EBXEAX指令之间的差别在哪里呢?你可能已经这样猜想了。后面的指令以一个前缀开始-附加的66H前面已经提过。

当遇到8位的寄存器时该如何呢?因为处理器开发者只剩下3个位了,可以推测命令的代码会变。这种推测证明是正确的。例如。MOV BLAL指令编码为2个字节:8A D8。注意这些是八个8位的寄存器;结果,他们是可以3为编码出来的:

    AL – 000B

    BL – 011B

    CL – 001B

    DL – 010B

    AH – 100B

    BH – 111B

    CH – 101B

    DH – 110B

现在,可以很容易的发现D8H对应于ALBL寄存器。同时,可以猜测出带有一个寄存器和一个立即数的MOV指令,不使用MOD R/M字节。这种情况下,有必要只编码一个操作数。例如,MOV EBX1234H指令的代码为:BB 34 12 00 00MOV ECX 1234H指令的代码为:B9 34 12 00 00。尽量自己去研究这个问题。你可以很容易地看到,指令代码对应于B8H,开始的3位定义寄存器,其中保存了立即操作数。但是,当分析MOV EAX 1234H指令的时候,你就会惊奇。这个指令的代码为:B8 34 12 00 00。之所以这样,是开发者意识到使用EAX寄存器(累加器)比使用其他寄存器移动数据使用的更多。因此,这个指令的代码要短些。

考虑列表1.21代码片段。

列表1.21 学习MOV指令格式的测试程序

MOV EAX, DATA1

MOV EBX, DATA1

MOV ECX, DATA1

MOV EDX, DATA1

MOV EDI, DATA1

MOV ESI, DATA1

这里,DATA1是某个32位的变量。通过反汇编这段程序,可以得到列表1.22的结果。

列表1.22  列表1.21中程序的反汇编代码

Al   00104000

851D 00104000

850D 00104000

8B15 00104000

853D 00104000

8B35 00104000

可以看到EAX和其他寄存器明显不同。从内存加载数据到寄存器有其专有的代码。和其他指令一样,MOD R/M字段也存在。将十六进制代码转换为二进制代码,你可以看到所有指令的MOD域都为00BREG域编码寄存器,R/M字段为101B。可以推测MODR/M域定义了某种寻址模式,和前面的指令一样,EAX是个例外,这里也是一样。当前的模式假设有效地址只通过一个数字表示-32位的偏移。顺便问一下,如果你改变了这些指令中间的操作数会发生什么呢?只有指令代码会改变。其他的都不会改变,因为地址模式和寄存器都没改变。

在来看看指令MOV [EBX]ECX。就像你看到的,这条指令使用EBX间接寻址。本条指令中的这类操作的代码为89HMOD R/M自己包含寄存器和寻址模式-0B。很容易就可发现MOD域包含00REGR/M域包含ECXEBX的代码。现在,加大复杂度:研究MOV [EBX + 10] ECX指令。对于这条指令,反汇编出来的代码为以下字节序列:89 45 0A。正如你看到的,本指令代码还是一样。很明显,最后一个字节就是偏移。MOD R/M字节为:010010011B。作为结果,和MOV [EBX]ECX指令比较,只有MOD域改变了。原因很简单:地址改变了。我希望你能利用表1.27解释MOD R/M字节的意义。

    1.27  32位地址的MOD R/M字节结构

有效地址

MOD

R/M

[EAX]

00

000

[EBX]

00

011

[ECX]

00

001

[EDX]

00

010

[ESI]

00

110

[EDI]

00

111

Offset32

00

101

[]

00

100

Offset8 [EAX]

01

000

Offset8 [EBX]

01

011

Offset8 [ECX]

01

001

Offset8 [EDX]

01

010

Offset8 [ESI]

01

110

Offset8 [EDI]

01

111

Offset8 [EBP]

01

101

Offset8 [ESP]

01

100

Offset32 [EAX]

10

000

Offset32 [EBX]

10

011

Offset32 [ECX]

10

001

Offset32 [EDX]

10

010

Offset32 [ESI]

10

110

Offset32 [EDI]

10

111

Offset32 [EBP]

10

101

Offset32 [...]

10

100

EAX/AX/AL

11

000

EBX/BX/BL

11

011

ECX/CX/CL

11

001

EDX/DX/DL

11

010

ESP/SP/AH

11

100

EBP/BP/CH

11

101

ESI/SI/DH

11

110

EDI/DI/BH

11

111

注意:表1.27中,offset8表示1字节的偏移,offset32表示32位的偏移,[...]字符串表示SID字节后紧接MOD和R/M域。

仔细研究表1.27。正如你看到的,MOD R/M字节不允许你定义相对地址寻址属性的重要属性,相关系数。另外一个字节,SIB用于此目的。R/M字段设置为100B表示SIB字节必须出现。

                   1.4.4 SIB字节域

最后,是时间研究SIB字节了。它表示scale index base。因此,本字节有以下三个域(参见图1.6):

    7~6,索引比例域,指定比例系数。

    5~3,索引域,指定索引寄存器。

    2~0,定义基址寄存器。

考虑列表1.23使用汇编语言所写代码片段。

列表1.23 学习MOV指令格式的测试程序

MOV [EAX*4][EBX+5], EAX

MOV [EBX*4][EAX+5], EAX

MOV [ECX*8][EDX+5], EAX

MOV [EDX*8][ECX+5], EAX

下面是反汇编代码(列表1.24)。

列表1.24 列表1.23程序对应的机器码

89 44 83  05

89 44 98  05

89 44 CA  05

89 44 Dl  05

所有指令的操作码都为89H44H表示MOD R/M字节。转换为二进制格式为:44H = 01000100B。因此,MOD = 01。这就以为着,本指令中存在偏移。最后的字节5表示偏移。REG域为000B,表示数据从EAX寄存器拷贝。R/M域为100B,这个值是个例外(见表1.27),表示接下来会出现SIB字段。顺便说以下,先前遇到指令的这个字节都不同。从第一条指令开始研究。对于此指令,指令为83H = 10000011B。比例系数域为10B。索引位为000B,表示索引寄存器,使用EAXEAX寄存器用做保存运算结果。基址寄存器域为001B,表示使用EBX。因此,本字节所有字段都很清晰了。现在可以研究SIB字段的使用方法了(1.28)

    1.28  SIB的字节结构

索引比例

比例字段值

索引字段值

[EAX]

00

000

[EBX]

00

011

[ECX]

00

001

[EDX]

00

010

[EBP]

00

101

[ESI]

00

110

[EDI]

00

111

Unused

00

100

[EAX * 2]

01

000

[EBX * 2]

01

011

[ECX * 2]

01

001

[EDX * 2]

01

010

[EBP * 2]

01

101

[ESI * 2]

01

110

[EDI * 2]

01

111

Unused

01

100

[EAX * 4]

10

000

[EBX * 4]

10

011

[ECX * 4]

10

001

[EDX * 4]

10

010

[EBP * 4]

10

101

[ESI * 4]

10

110

[EDI * 4]

10

111

Unused

10

100

[EAX * 8]

11

000

[EBX * 8]

11

011

[ECX * 8]

11

001

[EDX * 8]

11

010

[EBP * 8]

11

101

[ESI * 8]

11

110

[EDI * 8]

11

111

Unused

11

100

MOV [EAX * 8][EBX + 10],ECX和MOV [EAX][EBX * 8 + 10]之间的区别就清晰了。第一条指令中,EAX比例索引表示,第二条指令中,本职责由EBX担当,因此基地址寄存器的职责也刚好相反。同时,通过分析我们也清楚了指令MOV [EAX * 4][EBX * 2]在技术上是不可能的。

                   1.4.5 一个手工反汇编的简单例子

现在,在获得了必须的经验后,你就可以尽力反汇编图1.5给出的代码了。55H代表PUSH EBP指令。很容易发现PUSH指令的代码是50HEBP的代码等于5101B)。接下来的代码为8DH。很清楚它不是一个前缀,因为前缀是什么值大家都知道。原则上说,可以查看手册或使用调式器。结果显示为LEA指令。因为该指令有两个操作数,很显然指令必须要有MOD R/M字节。下来字节是ECH。通过其二进制表示,可以得到如下结果:ECH = 11101100B。如果一切都是正确的,则开始的两位定义寄存器寻址,数据直接保存在寄存器中(参见表1.27)。这种情况下,接下来的3位为(REG   定义寄存器,表示数据将要被加载哪个寄存器,最后的位定义获取数据的寄存器。这里源寄存器使用了ESP(代码为100B)。

LEA指令通常用语获取一些变量的地址。这种情况下,它的代码是怎样的呢?情况很清晰而且也很直接。假设你遇到了以下指令:LEA EBPDATA1。它的反汇编代码为:8D 2D 00 10 40 00。很清楚,后面的四个字节表示变量的地址。MOD R/M字节是如何出现的呢? MOD R/M2DH = 00101101B。注意最后的3位,参考表1.27MOD = 00)。最后的三位指定的数字为101B,表示有效地址为一个段中的偏移 换句话说就是。变量的直接地址。因此,第二个字节后跟的就是偏移。

现在回想以下图1.3。接下来的字节为53H。表示PUSH EBX指令(3 = 011B,代表的是EBX寄存器)。下来字节为C7H。是MOV指令的代码,其目的操作数可以是寄存器或内存单元(DWORD数据类型),源操作数为立即数。很清楚,接下来的字节表示MOD R/M字节。其值为05H = 00000101B。因此,可以知道立即操作数被传送至内存单元。接下来的4个字节必须是那个内存地址 。这里为D0 86 40 00。现在,知道请求的地址为004086D0H。最后,这条只的最后一个字节为32H。因此,可以得到以下结论:我们要解码的是MOV DWORD PTR [004086D0H]32指令。现在,看看我们这次反汇编的成果(列表1.25)。

列表1.25 手工反汇编的指令

PUSH EBP

LEA EBP, ESP

PUSH EBX

MOV DWORD PTR [4086D0H], 32

手工反汇编是件很乏味的工作,不是吗?但是,一旦掌握了这节描述的技术和实战经验后,你会发现没什么可难得到你的事了。

事实表明,有些微处理器的代码至少可以用两套指令集表示。这里有一个这种情况的典型例子。MASM32翻译器将指令MOV EBX, 34H转换为以下的代码序列:bb 34 00 00 00。这种情况下,EBX寄存器被编码到指令代码的开始的3位上(011B)。但是,从更通用的观点来看,还有编码此指令的另外的可能-也就是,使用MOD R/M字节。当使用这种表示法时,指令的代码序列如下:C7 C3 34 00 00 00。就像你看到的,本指令的第二种变体长了一个字节。

                   1.4.6 反汇编的难处

一个普遍的观点认为汇编语言和机器语言实际上是一样的。显然,这样就可以的出以下明显的结论,任何汇编代码都可以毫不含糊地使用机器码重建。但是,尽管存在这种观点,情况也并非如此简单。本节就讨论一些其中的困难。

第一个困难就是针对数据结构的重建。确定数据结构的唯一可能性就是分析使用他们的指令。这里问题就出现了。数据可以通过多种途径被使用。例如,考虑一个一眼看起来可以很容易反汇编的指令-MOV DWORD PTR [4086D0H],32。这条指令显示了某个数据保存在4086D0H地址处。使用直接寻址模式,所以情况很清晰。但是,MOV EAX[EBX]指令是干什么的你知道吗?为了找出EBX寄存器中存储的是什么,有必要分析程序代码。如果被分析的代码想列表1.26一样,你就很幸运了。

列表1.26 表明某个32位的变量保存早4176A8H地址处的指令序列

MOV EAX, 4176A0H

ADD EAX, 8

MOV EBX, EAX

基于这段代码,我们就清楚32位的变量保存在4176A8H地址处。但是,实际的地址经常因使用指令的不同形成相当多指令形式。对该地址的操作通常很诡异。如果是这种情况,就只只能通过一步一步的跟踪代码的执行了,也就是说,使用调试机制。

然而,获取变量的地址经常并不够;还有必要知道其大小。例如,当处理数组的时候,确定其有多少元素并不轻松。即使知道下一个变量的地址也帮不上忙,因为其中可能存在两个变量之间的字节对齐。

在可使用两种不同的方法获取内存中某个对象的地址时,这种情况会更糟。一般来说,LEA指令用来获取指定变量的地址,例如:LEA EAXa1。因此,遇到这样的字节序列:8D 05 08 10 40 00,你可以立即发现地址为401008H被加载至EAX。(8DH边式LEA指令,05H表示MOD R/M字节,这样的分析我们已经列举了很多了。)但是,汇编语言中还有指令做同样的事情。也就是指令MOV reg32, offset varoffset关键字指示汇编器用变量的地址代替变量值。因此,如果你分析代码,想知道他是一个立即数还是地址就很困难。注意,有时做这样的分析可能也很困难。

另外一个问题就是确定跳转地址和过程地址。不但通过CALL指令可以将控制交个过程,还可以使用JMPRET指令。列表1.27显示四种调用过程的方法。后面三个调用过程的方法将加大反汇编的复杂度,因为在某些情况下他们会阻止研究者认为他们是从其他地方开始的函数调用。

列表1.27 显示不同方式调用程序的测试程序

.586P

.MODEL FLAT, STDCALL

TEXT SEGMENT

START:

; Explicit call

        CALL PR1

        LEA  EAX, PR1

; Implicit call

        CALL EAX

        PUSH OFFSET L1

; Return address in the stack

        JMP EAX

L1:

        PUSH OFFSET L2

        PUSH EAX

; Now the stack top contains the procedure address.

; The next stack element contains the return address from the procedure.

        RETN           ; The call using the RET command

L2:

        RETN

PR1 PROC

        RETN

PR1 ENDP

TEXT ENDS

END START

列表1.27显示了使用不同方法实施函数调用的程序代码。

最重要的问题就是查找正确的地址,它决定了代码块从哪儿开始。如果过程不能通过交叉引用(corss-references)标示,则你可能希望至少可以通过正确的解码找出过程所处的位置。哎,即使这样的目的也未尝能达到,过程的地址还是不清楚。假设你已经找到了第一个直接调用过程的地址。再假设你也找到了这个过程的结束。不幸运地是,没有人可以保证另外一个过程就在其后面。任何NOP指令都可以分割两个过程。例如,MASM32可以通过使用ALIGN插入这样的操作符。

更多关于数据、过程和其他程序结构的识别将在第三章介绍。

                   1.4.7 x87 FPU指令

你可能知道算术协处理器。FPU指令和Intel Pentium处理器指令之间有什么主要的不同吗?多往前走几步,我会告诉你没有什么主要的不同。但是,有其专有特征。FPU指令最小的长度为2字节。指令的第一个字节,在处理妻指令中经常被称之为操作码,但是对FPU不是这样(查看本章后面的内容),它总是将最高的5位设置为11011B。这意味着FPU指令的最高的半个字节总为DH。这就允许代码挖掘者很容易就可以在主存中发现协处理器指令。

除了第一个字节,FPU指令还可能包含MOD R/M字节,用来指出操作数的内存位置。例如,考察指令:FLD QWORD PTR [20814000H],它从内存地址(通过操作数或地址指定)中往协处理器的堆栈上压入一个长浮点数。这条指令的代码序列为:DD 05 20814000。第一个字节的二进制表示为:11011101B。开始的5个位已经说过了,而低位的3个位值得关注。这条指令是FPU指令的一部分,用于操作主存的数据。如果这个字节的最低位1,表示指令从协处理器的堆栈中传送数据到内存,或从内存中拷贝数据。其他的指令将该为设置为0。例如,算术或比较指令。指令的第21位决定内存格式(MF)。有以下可能的取值:

       00 – 短浮点数(32位)

       01 – 短整数,二进制(32位)

       10 – 长浮点数(64位)

       11 – 10字节数(32位)

上面的情况中,该位置为10,也就是说表示长浮点数。因此,我们明白了FPU指令的第一个字节不再表示为操作码。

现在,考察MOD R/M字节:05H = 00000101B。因此,MOD = 00B reg = 000B, R/M = 101B。查询表1.27,结果就很显然了-也就是说,地址被直接定义(通过R/M值)。因此,中间的3位(REG)不是别的就是操作码。

看看另外一条指令:FADD ST1), ST0)(参见表1.21)。本指令将保存在寄存器ST0)和ST1)相加,结果保存到ST(1)中。指令代码为DC C1。而二进制表示为:11011100 11000001。先看第一个字节。位0设置0,是有协处理器寄存器参与的算术和比较指令中的操作码。位1表示操作完成后是否弹出堆栈。本指令中,堆栈不弹出,因为位10。位2表示结果是否保存到栈顶(0)或其他寄存器(1)。在这个例子中,结果保存到ST(1)。现在,我们来看看第二字节。MOD域被设置为11B,表示操作作用于存储于堆栈上的结果上。R/M域位001B,定义第二寄存器参与操作(ST(1))。第一个字节总用于ST0)。结果,操作代码就为0000B

现在,看看FSQRT指令,本指令用于计算栈顶元素的平方根。对诸如此类的指令,包括(除高阶函数之外)常量加载和一些算术操作,典型情况下只使用一个堆栈寄存器-ST0)。本指令的代码位D9 FA,二进制格式为:11011001 11111010。对这样的指令,所有的指令都是常量,除了第二个字节开始的四个位(1010B),它们定义了要执行的操作。

最后,还有另外一种操作FPU的操作类型。这些操作不接受任何操作数。如FINIT指令(见表1.23),用于在启动时初始化协处理器。本指令的代码位:DB E311011011 11100011B。对这些指令,和先前的情况一样,只有第二个字节的前4个位是有意义的,它们定义要执行的操作。

到此,本节关于Pentium微处理器的指令格式就介绍完了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值