x86指令编码
最开始玩儿逆向的时候,就很想搞明白x86的机器码,想知道反汇编引擎的工作原理。后来又觉得这个东西没什么好玩儿的,就是一步一步匹配出来的。而且大部分的资料写的都很奇怪,看半天也不知道他的重点在哪里,难以看下去。清明放假意味着更没什么正事儿要做,又一次看了x86指令的组成,在这里总结一下,希望自己写的不要那么模棱两可。
总览一下
前缀: 即指令前缀,可有可无,作用是调整操作数大小(16位和32位转换)、调整地址大小(16位和32位转换)、重复前缀、锁前缀、调整段选择子等。
REX: 前缀用于x86_64,用来扩充寄存器大小、寄存器数目、寻址大小、操作数大小等。
操作码: 是指令中最核心的部分,表明操作的类型,mov、call、jmp、cmp、push、pop等都有自己的操作码,而且操作码与后面几项密切相关。
MOD_REG_R/M: 将一个字节分为2:3:3,MOD表示模式,REG表示寄存器,R/M表示寄存器或内存,具体用法后面再说。
SIB: 是MOD_REG_R/M的扩充,也分为2:3:3,分别是Scale:Index:Base,用于表示[Base + Index*2^scala]这种特殊寻址,Base和Index都是寄存器ID。
地址偏移和立即数: 很好理解,在前面的基础上地址偏移做修饰,立即数用于某些情况。
可以看出,除了操作码,其他信息都不是必须的。
指令前缀
指令前缀的作用已经提过,有这么几种。
66h用于改变操作数位数,比如32位模式下使用16位数据或者16位模式下使用32位数据。有的资料也称66h为寄存器超越前缀: ,专注于32位模式下使用16位寄存器和16位模式下使用32位寄存器。但是据我观察寄存器超越前缀: 的叫法并不太正确。看这个例子:
66:C705 00004800 3412 mov word ptr [480000], 1234
32位下使用16位的操作数,机器码出现66h前缀。
67h用于改变地址位数,如32位模式下使用16位地址。66h和67h同时出现时,67h在后面。我觉得这个前缀很难写出来,因为这个时候已经不会在使用16位的地址了。
段选择子前缀用于改变段选择子。因为Windows用的是平坦模式,段选择子指向的段描述符的基址都是0,所以这个往往会被忽略。看一个例子:
64:8B35 18000000 mov esi, dword ptr fs:[18]
64h就是FS的的段选择子前缀,其它段寄存器的前缀也在下面给出:
CS: 2E
DS: 3E
ES: 26
FS: 64
GS: 65
SS: 36
锁前缀用于写内存的指令,将总线锁住,防止被干扰。看一个例子:
F0:0FC118 lock xadd dword ptr [eax], ebx
重复前缀用于rep/repz/repnz/repe/repne等指令。rep和repz的前缀都是F3,根据后面的操作码来区分;repnz的前缀是F2。rep系列指令能加快连续内存数据的处理。还是看例子:
F3:A7 repe cmps dword ptr es:[edi], dword ptr [esi]
操作码
opcode一般只有8位,很少有2位,浮点数一类可能会有3位的。
具体的操作码可以查看intel或者AMD的opcode表。有一些非常常见的操作码在有一定经验之后自然就记住了,其他的还是查表比较好。
MOD_REG_R/M
这也是可选项,根据操作码判断是否存在这个字节。这个字节可以决定是否有SIB、地址偏移和立即数。
MOD_REG_R/M分为三个部分,MOD两位,REG三位,R/M三位。
MOD只有四种情况:00表示不使用地址偏移;01表示8位地址偏移;10表示16位或32位地址偏移,由默认大小和指令前缀确定;11表示只使用寄存器,此时R/M是寄存器而不是地址。
REG三位可以表示8个寄存器,用寄存器ID来表示。000~111。
R/M表示寄存器时跟REG表示方式一样,表示地址时如下所示:
000 [eax]
001 [ecx]
010 [edx]
011 [ebx]
100 SIB
101 Disp
110 [esi]
111 [edi]
R/M为100时表明后面有SIB,用于扩展寻址方式。这里按规律来说应该是[esp],但是却留给了SIB。
R/M为101时表示用到地址偏移。这里同样黑掉了[ebp]寻址。
[ESP]和[EBP]寻址方式只能留到SIB中确定。
SIB、地址偏移和立即数
当R/M为100时用到SIB。SIB分为三部分,Scale两位,Index三位,Base三位。寻址方式为[Base+Index*2^scale]。base和index都是寄存器ID,scale从0~3,0也可以表示不使用index,具体看操作码。
当R/M位101时用到地址偏移,在上面的基础上加上一个偏移量。
立即数根据操作码确定。
看几个例子:
66:C70448 5634 mov word ptr [eax+ecx*2], 3456
66指令前缀,C7操作码,04MOD_REG_R/M,R/M=100,有SIB。SIB=48=01001000,RID[000]+RID[001]*2^01 = eax+ecx*2,后面的是立即数
C705 00004B00 78563412 mov dword ptr [4B0000], 12345678
C7是操作码,05是MOD_REG_R/M,R/M=101,表示用到地址偏移,不用SIB
A1 00004800 mov eax, dword ptr [480000]
A1是操作码,针对eax寄存器指令往往会比较短
8925 00004800 mov dword ptr [480000], esp
89是操作码,MOD_REG_R/M=25=00100101,reg=100表示ESP寄存器,R/M=101表示用到地址偏移,操作码可以指定数据传输方向
8B15 56341200 mov edx, dword ptr [123456]
8B是操作码,MOD_REG_R/M=15=00010101,跟上面指令类似,不过是操作码发生了变化,数据传输方向也变为地址到寄存器
C780 00010000 78563412 mov dword ptr [eax+100], 12345678
C7是操作码,MOD_REG_R/M=80=10000000,MOD=10表示用到8位地址偏移。后面依次是地址偏移和立即数
希望看完这几个例子大家能基本上明白指令编码。
REX前缀
最后说一下REX前缀,专门用于64位。32位模式这个前缀无效。
段描述符指定段的默认位数,16位还是32位,但是只用一个字节表示,没有考虑到64位,所以AMD扩展了一个指令前缀。
64位要解决的关键问题有三个:操作数大小、地址大小、寄存器数目。在64位模式,32位操作数仍然是默认大小,但是对64位数的操作已经很多;虚拟地址扩展至64位;X64下寄存器数目增多,需要扩充相应位。
关键的0~3位解释如下:
REX.W表示指令操作数长度,为1时是64位操作数,可覆盖66h。
REX.R表示寄存器,如果是1,MOD_REG_R/M的REG域扩充为4位,以适应64位更多的寄存器。
REX.X表示Index,如果是1,SIB的index域也扩充为4位。
REX.B表示Base,如果是1,R/M域和Base域扩充为4位。
最后
凡事都是说起来好听,也很容易埋怨别人写的东西很烂不能看。有时候可能自己写的更烂,只有别人知道。我还是努力把自己知道的说清楚的。