Intel硬编码(一):Opcode Map、定长指令与指令前缀

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/Apollon_krj/article/details/77508073

Intel CPU(基于P6微架构)的机器指令(硬编码)格式如下图所示:
这里写图片描述

一条指令由:**指令前缀(Instruction Prefixes) + 操作码(Opcode) + ModR/M + SIB + 偏移(displacement) + 立即数(Immediate data)**几部分组成,一条指令至少需要有Opcode,其它几部分,在不同指令中可能存在可能不存在。今天我们主要来看Opcode、Instruction Prefixes、Immediate data三部分在不同指令中的使用。

1、Opcode Map:

由于汇编指令对应的硬编码数目很多,为其所有汇编指令设计出专门的硬编码是不切实际的,所以一种格式的硬编码,有可能因其具体的某一bit位不同而表示的汇编指令也有所微小差异,此微小差异一般是Opcode引起的,而由ModR/M和SIB的bit位来体现的(关于这点我们放到不定长指令中去讨论)。现在研究学习的定长指令(指的是一个Opcode对应的指令长度是一定的),其对应的汇编指令格式是固定的(比如0X40不能加立即数和偏移量就只能表示inc eax;而B0只能加一个字节的立即数指令长度为2字节,B0 XX即为mov al,XX不能能表示其他任何指令),但不管是定长还是不定长的硬编码,其都可以从下面的Opcode Map表(TableA-2和TableA-3)中找查出来(红色圈出来的是比较重要的以及我们要分析的定长指令):

这里写图片描述

一个1Byte的定长指令,其16进制为类似于“AB”的形式,而第一个A是Opcode Map表中的行号,第二个的B是其列号,行号和列号就能确定一个具体的指令。比如:第4行第5列索引出来的指令为(绿色圈出来):inc ebp,第5行第0列为**push eax **等等。

但是由于指令的Opcode部分不止有一个字节的,还有两个字节的,那么两个字节的仅仅用该表是无法表示的,设计人员在设计时,留了一个特殊的位置即0F,0F作为两字节指令的第一字节,而第二字节再另外一张表中。也就是说所有两字节的指令都是以0F开头的(注意:这里说的两字节都是仅仅指Opcode的长度)。而另外一张表(TableA-4与TableA-5)如下所示:
这里写图片描述

A-4,A-5与A-2、A-3的查找方式相同,由于2字节长的Opcode比较复杂,这里不做重点研究。我们重点来研究1Byte的Opcode,其具体细节我们分类来看。

2、修改通用寄存器的定长指令:

修改通用寄存器的常用定长指令,由于第4行前8列(Left)的inc reg指令和后8列(Right)的dec reg指令,还有第5行的前8列的push reg指令与后8列的pop reg指令。以及第9行前8列的xchg eax,reg指令、第11行前8列的mov reg8,imm8指令、后8列的mov reg32,imm32指令等等(我们发现没有mov reg16,imm16,至于为什么我们在指令前缀中便会提到)。

比如第五行:

前八列:
50 <–> push eax
51 <–> push ecx
52 <–> push edx
53 <–> push ebx
54 <–> push esp
55 <–> push ebp
56 <–> push esi
57 <–> push edi
后八列:
58 <–> pop eax
59 <–> pop ecx
5A <–> pop edx
5B <–> pop ebx
5C <–> pop esp
5D <–> pop ebp
5E <–> pop esi
5F <–> pop edi

第九行前八列:

90 xchg eax,eax <==> nop
91 <–> xchg eax,ecx
92 <–> xchg eax,edx
93 <–> xchg eax,ebx
94 <–> xchg eax,esp
95 <–> xchg eax,ebp
96 <–> xchg eax,esi
97 <–> xchg eax,edi

其它指令我们就不在一一列出来,只要稍微熟悉一下Opcode Map就可以烂熟于心了,我们可以发现,这些指令是有规律可偱的,都遵循寄存器编号的顺序来为汇编指令设计编码的,而寄存器变号我们也有必要提一下:
这里写图片描述

3、修改EIP并且与JCC对应的定长指令:

硬编码中的Opcode后面的立即数并非是跳转的地址,“跳转地址=当前指令地址+当前指令长度+imm”

(1)、第七行:近距离JCC跳转

条件跳转:Opcode后面跟一个立即数的偏移,因此指令共两个字节
立即数是有符号的:最高位为0(7F)向下跳,最高位为1(80)向上跳

70 <–> JO(O标志位为1跳转)
71 <–> JNO
72 <–> JB/JNAE/JC
73 <–> JNB/JAE/JNC
74 <–> JZ/JE
75 <–> JNZ/JNE
76 <–> JBE/JNA
77 <–> JNBE/JA
78 <–> JS
79 <–> JNS
7A <–> JP/JPE
7B <–> JNP/JPO
7C <–> JL/JNGE
7D <–> JNL/JGE
7E <–> JLE/JNG
7F <–> JNLE/JG

测试如下:
这里写图片描述

80符号位为1为负数:
跳转地址 = 当前指令地址 + 当前指令长度 + imm = 004193BE + 2 + 80 = 419340 < 004193BE ,即向上(之前的低地址)跳转了。

7F符号位为0为正数:
跳转地址 = 当前指令地址 + 当前指令长度 + imm = 004193C0 + 2 + 7F = 419441 > 004193C0,即向下(之后的高地址)跳转了。

(2)、0F80~0F8F远距离JCC跳转:
后面跟一个四字节的立即数,指令长共6字节。立即数正负以7FFFFFFF、80000000为界。0F800F8F和707F对应列的指令一样只不过立即数字节数不同。这里也不再列出(注意0F80~0F8F是双字节Opcode应该在A-4与A-5中查找:第八行)。
4、其它修改EIP的定长指令:

同JCC指令的硬编码一样,其硬编码中Opcode后面的立即数也不是要跳转的地址,计算方式同JCC相同:“跳转地址=当前指令地址+当前指令长度+imm”

(1)、与ECX相关的跳转指令(循环指令):

E0 <–> loopne/loopnz Ib(dec ecx) (ZF=0 && ECX != 0)
E1 <–> loope/loopz Ib(dec ecx) (ZF=1 && ECX != 0)
E2 <–> loop Ib(dec ecx) (满足ECX != 0就跳转)
E3 <–> jecxz/jrcxz Ib (满足ECX=0跳转)
注意: Ib即为byte类型立即数(Immediate data),Iw则是Immediate data word,Id即为Immediate data dword,Ap即六字节长度的直接地址

测试如下所示:
这里写图片描述

(2)、直接call与间接call指令:
所谓直接call即编译时确定地址,间接call即地址存在内存中,并且在内存中的地址也是运行时才确定。

E8 <–> call Id
E9 <–> jmp Id
EA <–> jmp Ap,jmp CS:Id (**前四个字节为跳转地址,后两个字节为段选择子.**即高两字节赋给CS,低四字节赋给EIP)
EB <–> jmp Ib
FF <–> call dword ptr [edx]

E8call为直接call,call后面的地址即为要跳转的地址,FFcall为间接call,后面跟的内存那只能够存放着即将要跳转的地址。比如用对象指针访问一个普通成员函数和一个虚函数,其call的硬编码都不同:
这里写图片描述

(3)、ret与reft:

C3 <–>ret (pop eip)
C2 <–>ret Iw (pop eip后,栈顶esp = esp + Iw)
CB <–>retf (出栈8字节,低四字节赋给EIP,高四字节的低两字赋给CS)
CA <–>retf Iw (在CB的基础上再做一步esp = esp + Iw)

5、指令前缀:

我们在上面提过,修改寄存器的指令中不存在16位寄存器修改的硬编码。而这些与16位寄存器相关的编码是通过加上**指令前缀(Instruction Prefixes)**的方式来实现的,有指令前缀则原本的32位寄存器操作指令,就会变为16位寄存器操作指令来用,不仅是定长指令如此,不定长指令也是如此。但指令前缀不仅能进行16位32位寄存器操作硬编码转换,我们一一来看几种常用的指令前缀:

1、段前缀:
首先在32位汇编中,8个段寄存器:ES、CS、SS、DS、FS、GS、LDTR、TR(顺序固定),不再用段寄存器寻址而只做权限控制。段寄存器其实是个结构体,共96位,用汇编指令只能访问其中16位。

2E - CS
36 - SS
3E - DS
26 - ES
64 - FS
65 - GS

如下所示:
这里写图片描述
其中8925是Opcode,而不同的指令前缀代表了不同的段寄存器。
注意:如果没有特殊说明即没有人为指定段前缀,且:

①[]里不存在ebp/esp/edi则默认为DS:[]
②[]里存在ebp/esp则默认为SS:[]
③[]里存在edi默认是ES:[],esi默认是DS:[]

2、操作指令前缀:修改默认长度
这个即所谓指令前缀解决无16位寄存器操作指令的问题:0X66前缀修饰Opcode,则修正32位长度为16位:
如下所示(无论定长指令50还是不定长指令89均相同):
这里写图片描述

3、操作指令前缀:修改默认寻址方式
0X67作为前缀修改操作数宽度(将硬编码默认对应的操作数宽度改为16位)
如下所示(操作指令前缀将寻址方式按16位汇编的寻址方式进行寻址):
这里写图片描述
同一Opcode因为有无指令前缀而长度不同,因此加上指令前缀前后相当于Opcode的指令长度也是不定的,但是这些“不定长”可预见的。有前缀则指令长度加一。而真正的不定长指令却不是如此的。

**注意:**指令前缀也是属于第一张Opcode Map(包括A-2/A-3)之中的,一个前缀占用一个“位置”,就像双字节的Opcode其首字节也是在第一张Opcode Map的0F位置的。否则是无法区分一条指令到底是前缀还是单字节Opcode还是双字节Opcode等等。

参考资料: Intel Architecture Software Developer’s Manual Volume 2: Instruction Set Reference(Intel白皮书卷2:指令集参考

阅读更多
换一批

没有更多推荐了,返回首页