4. Prefixes - Part II

I'll be back.

-- Arnold Schwarzenegger, The Movie - "Terminator"(1984)

Prefixes合集

在前一章中我们已经知道:

  1. 所有Prefixes的长度都是1个字节。
  2. 一个OpCode可能会有几个Prefixes。
  3. 如果有多个Prefixes,那么它们的顺序可以打乱。
  4. 如果Prefixes不能对随它之后的OpCode起作用,那么它就会被忽略。

现在我们将要学习剩下的几个Prefixes,它们可以被划分为5个集合,分别是:

  1. Change DEFAULT operand size. (66)
  2. Change DEFAULT address size. (67)
  3. Repeat prefixes. (F2, F3)
  4. Segment override prefixes(change DEFAULT segment). (2E, 36, 3E, 26, 64, 65)
  5. LOCK prefix. (F0)

Prefix 66

在前面我们已经学习过它了,而且也够详细的了,对吗?

Prefix 67

改变默认的地址大小。

请注意:6766的分别在于,66改变的是默认的操作数大小,而67则是地址的大小。两者有什么差异呢?

8A 00    MOV AL, [EAX]

现在把它的OpCode改成以67开头的:

67 8A 00    MOV AL, [BX+SI]

我们可以看到:

  1. 地址由原来的32位的[EAX]变成了16位的[BX+SI]。
  2. 疑问来了:为什么不是[AX],而是[BX+SI]呢?

第2个问题我们将会在以后的{ModR/M}和{SIB}的格式讲解中回答。现在我们可以暂时认为,在16位的地址模式中无法完全使用32位中的对应的地址模式,两种模式中的寄存器有着一定的区别。(看不明白?没关系,后面的章节中会详细解释)

强调一点:Prefix 67同样也是一个“触发器”,它起的作用是“切换”,而不是“指定”。

Repeat Prefixes (F2, F3)

Repeat Prefixes通常是与movsscascmps等串指令搭配使用的,它们有:

F2: REPNE
F3: REP / REPE

Repeat Prefixes作为一个串操作指令的前缀,它重复执行其后的串操作指令。每一次重复都先判断(E)CX是否为0,如为0就结束重复,否则(E)CX的值减1,然后再重复其后的串操作指令。所以当(E)CX的值为0时,就不再执行其后的操作指令。

它类似于LOOP指令,但LOOP指令是先把(E)CX的值减1,后再判断是否为0。

举例:

CLD
MOV ECX, 3
REP MOVSB

运行的结果是把DS:(E)SI的3个字节(byte)移动到ES:(E)DI去。

有两点规则:

  1. 你可以看到有3种Repeat Prefixes的助记符:rep/repe/repene,但是只有2个OpCode:F2F3
  2. 如果某些指令只使用前缀rep,那么这里的rep可以用repe或者repne来代替。

第2条规则比较难以理解,对吗?我们来举个例子:

REP LODSB
REPE LODSB
REPNE LODSB

这3条助记符的运行结果都是一样的:它会重复运行指令LODSB一共(E)CX次,而不管它的Repeat Prefixes是rep/repe[F3]还是repne[F2]。

但是请注意:第2条规则的适用范围仅仅是只使用rep”的指令,意即无论是F2还是F3,对指令的执行结果都无影响,而这样的指令非常的少!

从OpCode的角度来看rep/repe[F3]和repne[F2]的区别:

我们知道,重复串指令时可能会改变某些标志位(例如ZF),在这种情况下,有些指令与重复前缀搭配使用时,F2F3会把最后一位与标志位ZF进行比较,如果它们不相同,则重复串指令的操作将会结束。而有些指令不用进行这个比较的操作,因此标志位ZF对这些指令的运行结果无影响。

讲得不够清楚?呵呵,把F2F3转换成二进制就能明白了。

1111 0010  F2
1111 0011  F3

最后我们再来看看Repeat Prefixes的结束条件:

Repeat Prefiex的结束条件
Repeat Prefix结束条件1结束条件2
REPECX=0None
REPEECX=0ZF=0
REPNEECX=0ZF=1

从上表中可以看出:reperepne的结束必须同时满足两个结束条件,而rep只管ECX等不等于0。

看到这里,再结合上面的第2条规则,我们就能更清楚了:由于rep并不与标志位ZF进行比较,所以它可以被替换成repe或者repne,对执行结果无影响!

Segment Override Prefixes (2E, 36, 3E, 26, 64, 65)

我们先来看看这些Prefixes是什么:

Prefixes && Explanation
PrefixExplanation
2ECS segment override prefix
36SS segment override prefix
3EDS segment override prefix
26ES segment override prefix
64FS segment override prefix
65GS segment override prefix

再来看一个例子:

   8B 03    MOV EAX, [DWORD DS:EBX]
65 8B 03    MOV EAX, [DWORD GS:EBX]

65就是一个Segment override prefix,用来改变默认的段,从上表中我们可以看出:65代表的是段GS。注意!这里也是用默认的概念。

读者在这里也许会存在一个疑问:默认?我怎么知道当前默认的是哪个段呢?以及为什么要用默认的概念呢?

答案是这样的:在使用内存中的数据时,处理器必须首先知道它的段地址(Segment)和偏移量(Offset),但是如果在每个地方都要显式地直接指出段地址,那么在OpCode格式中就必须增加一个新的域,这将会比现有的OpCode体系多占用大量的字节,而且处理器也必须多花费额外的时钟周期来进行解码——无论在空间还是时间上,都不值得!

因此,为了解决这个问题,一个方案诞生了:

指令由不同的定义被划分为不同的组,每个组各自有一个默认的段:

CS: for EIP pointer
ES: 目的操作数是内存单元的串指令(movs, cmps等),在这里源操作数是储存在段DS里面。
SS: 堆栈操作(push, pop等)
DS: 剩下的数据操作指令。

有了这个规则,处理器识别当前应该用哪个段将会变得非常简单而直接:

  1. 如果有“Segment override prefix”,那么就使用这个prefix所指定的段。
  2. 否则就使用默认的段。

看看:

   AC     LODS [BYTE DS:ESI]
3E AC     LODS [BYTE DS:ESI]

从上面的表中可以查出,3E是表示段DS,但是实际上在这里即使不直接指明3E,处理器也是会使用DS的,因为DS是指令LODS的默认段。

最后值得一提的是64,它表示的是段FS,也许读者会对FS不太熟悉,平时好像很少会用到。没关系,我们来简单介绍一下:FS一般是由SEH(结构化异常处理)所使用,但是由于SEH不属于OpCode格式的范畴,所以我们在这里不必深究,知道有这个概念就行了。

LOCK Prefix (F0)

对于这个Prefix,Intel的文档已经解释得很清楚了,不过它的具体意义对OpCode的格式学习并无任何帮助,有兴趣的读者可以在<<Intel Architecture Software Developer's Manual Volume 2: Instruction Set Reference>>的3-387页看到关于它的详细解释。在OpCode的格式学习中,我们只需要知道F0表示的是助记符LOCK就足够了。

The End Of Prefixes

结束了吗?

是的!关于Prefixes的格式学习到此就告一段落了。在接下来的章节中,我们将会学习最激动人心的一部分:如何对OpCode进行手工解码。



(注:如果出现链接打不开的情况,请去掉IE浏览器的“工具->Internet选项->高级->总是以UTF-8发送URL”前面的勾。谢谢!)