找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 5351|回复: 2

简述AMD64体系下的指令编码方案

[复制链接]

65

主题

115

回帖

1万

积分

用户组: 超级版主

OS与VM研究学者

UID
1043
精华
35
威望
789 点
宅币
8286 个
贡献
1094 次
宅之契约
0 份
在线时间
2065 小时
注册时间
2015-8-15
发表于 2018-7-8 01:32:23 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
本帖最后由 tangptr@126.com 于 2021-3-31 14:07 编辑

本文姑且可以作为编写64位shellcode的教程吧,虽然写shellcode不是本文的本意。

首先我们先看看在AMD64体系下的编码资源吧。
相比较IA-32体系,AMD64体系下通用寄存器被扩展至64位,并增加至16个,分别是:
rax rcx rbx rdx rsp rbp rsi rdi r8 r9 r10 r11 r12 r13 r14 r15。
原有的六个段寄存器(es cs ss ds fs gs)保持不变,但其作用被限制。
原有的32位标志位寄存器eflags被扩展为64位的rflags,但高32位被保留。
原有的八个多媒体扩展协处理器寄存器保持不变(mm0-mm7)。
原有的八个流式多媒体扩展协处理器寄存器被扩展为16个(xmm0-xmm15)。
原有的八个控制寄存器由八个(实际上只能用四个)扩展为16个(实际上只能用五个)(cr0-cr15)。
原有的八个调试寄存器由八个(实际上只能用六个)扩展为16个(但还是只能用六个)(dr0-dr15)。
寻址空间从原IA-32体系的36位扩展至48位。

在谈谈寄存器的编码。为了兼容,AMD在设计上可以说是大致保持不变。
下面贴出AMD64体系的寄存器编码表,注意,数字均以二进制数表示。
Capture.JPG

AMD64体系的指令编码基石——REX前缀
REX前缀是一种指令前缀,可以说是一种操作数大小重置前缀,其作用是将32位的操作数扩展为64位。在REX前缀的辅助下,指令可访问64位的寄存器与64位的地址,并可以与传统前缀共用,如重复执行前缀(rep),内存总线锁定前缀(lock)等。在与其他前缀冲突的情况下,以REX前缀为准,忽略传统前缀。该冲突一般是来自66H前缀。

首先我们需要了解一下REX前缀的结构,它是一个单字节的不定值前缀,取值范围是0x40-0x4F,共计16种。从高位到低位排序,得:
Capture.JPG
高4位固定为4,低4位可变,分别是:
REX.W:操作数宽度重置
REX.R:ModRM.Reg域重置
REX.X:SIB.Index域重置
REX.B:SIB.Base域,ModRM.R/M域以及操作码Reg域重置

我们需要详细了解这4位是干啥的:

1. REX.W前缀
REX.W位用于重置操作数宽度,其是否置位指明了操作数的大小:
当REX.W置位时,操作数大小为64位。
当REX.W复位时,操作数大小为缺省操作数大小。
当REX.W置位时,若指令存在66H前缀时,则前缀间发生冲突,操作数大小取决于66H和REX.W的位置。
当REX.W复位时,若指令存在66H前缀时,则前缀间不发生冲突,此时66H作用不被忽略,操作数大小为16位。

举例说明,在下面这条指令中:
mov eax,ebx
它的正常情况下的编码是89 D8,当我们增加REX.W前缀和66H前缀时,指令将发生转义:
48 89 D8        mov rax,rbx
66 48 89 D8        mov rax,rbx
48 66 89 D8        mov ax,bx
66 40 89 D8        mov ax,bx
66 89 D8        mov ax,bx

第一条指令中,编码使用了REX.W前缀,使得指令转义为访问64位寄存器。
第二条指令中,编码同时使用了REX.W和66H前缀,此时前缀间发生冲突,由于66H在前,故66H被忽略,指令操作数被重置为64位大小。在常见的反汇编器中,这个66H一般会被视为一条单字节的无效指令。
第三条指令中,编码同时使用了REX.W和66H前缀,此时前缀间发生冲突,由于REX.W在前,故REX.W被忽略,指令操作数被重置为16位大小。
第四条指令中,编码使用了空REX前缀和66H前缀,此时前缀间不发生冲突,故66H发挥作用,指令操作数被重置为16位大小。
第五条指令中,编码使用了66H前缀,使得指令转义为访问16位寄存器。

2. REX.R前缀
REX.R前缀用于扩展ModRM.Reg域,使得原本3位寄存器编码被扩展为4位,用于访问新增寄存器:
当REX.R置位时,ModRM.Reg取值范围是1000-1111
当REX.R复位时,ModRM.Reg取值范围是0000-0111
举例说明,当我们对指令mov rax,r10进行编码时,我们需要考虑如下几点:
我们需要扩展操作数宽度,因此,我们需要REX.W置位。
我们需要使用新增的寄存器r10,因此,我们需要扩展ModRM.Reg域,故我们需要REX.R置位。
从而得出REX=01001100,即4C。
由于r10在寄存器编码表中对应010,因此ModRM.Reg=010。
由于rax在寄存器编码表中对应000,因此ModRM.R/M=000。
由于我们对两个寄存器进行操作,因此ModRM.Mod=11。
从而得出ModRM=11010000,即D0。
根据AMD64手册第三卷第三章,通用寄存器间mov指令的操作码是89。
综上所述,指令的编码是4C 89 D0。

3. REX.X前缀
REX.X前缀用于扩展SIB.Index域,使得新增通用寄存器可用于含SIB的指令中并作为因数寄存器使用。
当REX.X置位时,SIB.Index取值范围是1000-1111。
当REX.X复位时,SIB.Index取值范围是0000-0111。
举例说明,当我们对指令mov rax,[rbx+r10*8]进行编码时,我们需要考虑如下几点:
我们需要扩展操作数宽度,因此,我们需要REX.W置位。
我们需要使用新增的寄存器r10,因此,我们需要扩展SIB.Index域,故我们需要REX.X置位。
从而得出REX=01001010,即4A。
由于rbx在寄存器编码表中对应011,因此SIB.Base=011
由于r10在寄存器编码表中对应010,因此SIB.Index=010
由于SIB因数为8,因此SIB.Scale=11
从而得出SIB=11010011,即D3。
由于rax在寄存器编码表中对应000,因此ModRM.Reg=000
由于我们是在将内存赋值到寄存器,因此ModRM.Mod=00
由于SIB字节的存在,因此ModRM.R/M=100
从而得出ModRM=00000100,即04。
根据AMD64手册第三卷第三章,内存赋值到寄存的mov指令的操作码是8B。
综上所述,指令的编码是4A 8B 04 D3。

4. REX.B前缀
REX.B的前缀用于扩展SIB.Base域,ModRM.R/M域以及操作码中Reg域、
当REX.B置位时,上述三域的取值范围是1000-1111。
当REX.B复位时,上述三域的取值范围是0000-0111。

4.1 REX.B扩展SIB.Base域
当我们对指令mov rax,[r8+rcx*8]进行编码时,我们需要考虑如下几点:
我们需要扩展操作数宽度,因此,我们需要REX.W置位。
我们需要使用新增的寄存器r8,因此,我们需要扩展SIB.Base域,故我们需要REX.B置位。
从而得出REX=01001001,即49。
由于r8在寄存器编码表中对应000,因此SIB.Base=000
由于rcx在寄存器编码表中对应001,因此SIB.Index=001
由于SIB因数为8,因此SIB.Scale=11
从而得出SIB=11001000,即C8。
由于rax在寄存器编码表中对应000,因此ModRM.Reg=000
由于我们是在将内存赋值到寄存器,因此ModRM.Mod=00
由于SIB字节的存在,因此ModRM.R/M=100
从而得出ModRM=00000100,即04。
根据AMD64手册第三卷第三章,内存赋值到寄存的mov指令的操作码是8B。
综上所述,指令的编码是49 8B 04 C8。

4.2 REX.B扩展ModRM.R/M域
当我们对指令mov r10,rax进行编码时,我们需要考虑如下几点:
我们需要扩展操作数宽度,因此,我们需要REX.W置位。
我们需要使用新增的寄存器r10,因此,我们需要扩展ModRM.R/M域,故我们需要REX.B置位。
从而得出REX=01001001,即49。
由于r10在寄存器编码表中对应010,因此ModRM.R/M=010
由于rax在寄存器编码表中对应000,因此ModRM.Reg=000
由于我们在对两个寄存器进行操作,因此ModRM.Mod=11
从而得出ModRM=11000010,即C2。
根据AMD64手册第三卷第三章,通用寄存器间的mov指令操作码是89。
综上所述,指令的编码时49 89 C2.

4.3 REX.B扩展操作码Reg域
当我们对指令mov r10,0x1234567890ABCDEF进行编码时,我们需要考虑如下几点:
我们需要扩展操作数宽度,因此,我们需要REX.W置位。
我们需要使用新增的寄存器r10,因此,我们需要扩展操作码Reg域,故我们需要REX.B置位。
从而得出REX=01001001,即49。
由于r10在寄存器编码表中对应010,因此操作码Reg=010。
源操作数时64位立即数,故displacement=0x1234567890ABCDEF。
根据AMD64手册第三卷第三章,立即数赋值到通用寄存器的mov指令操作码是B8+rq,因此操作码是B8+2=BA。
综上所述,指令的编码是49 BA EF CD AB 90 78 56 34 12。

5. REX前缀总结
在AMD64体系中,长模式的缺省操作数大小仍然为32位,这是Intel在设计IA-32架构时的锅。为了兼容原有IA-32体系,AMD将新增的长模式下的缺省操作数大小也定为32位。
原有的IA-32架构使用3位二进制数标识寄存器,因此AMD设计了REX前缀用于扩展寄存器标识位到4位,这就是REX.RXB前缀的作用。当REX前缀不存在时,则仍然使用3位二进制数标识寄存器。
为了平衡16位,32位以及64位操作数大小的使用,AMD设计为:无REX.W前缀时,根据是否有66H前缀选择使用16位或32位大小的操作数;当有REX.W前缀时,使用64位操作数大小。
值得注意的是,空REX前缀(40H)并非毫无作用,对于八位通用寄存器而言,空REX前缀会将这些编码范围100-111的寄存器解译为spl, bpl, sil, dil,而不是ah, ch, dh, bh寄存器。这也解释了为什么无法对类似mov r8b,ah这样的指令进行编码了。详情参见上文的寄存器编码表。

6. REX前缀的特例
这些特例的特点在于,它们不需要使用REX前缀,其缺省操作数就是64位大小。
第一类指令是操作数为rip寄存器的指令,如jmp指令。
第二类指令是操作数为rsp寄存器的指令,如pop指令。
这些指令无需使用REX.W前缀,但仍然可以通过66H前缀将操作数重置为16位大小。

谈完了REX前缀,我们再谈谈另一大AMD64体系中指令编码方案的重点:寻址模式。
在AMD64体系中,虽然长模式的缺省操作数大小是32位,但缺省地址数大小是64位。不过我们仍然可以通过67H前缀将地址数大小重置为32位。
我们举多个例子来说明这个问题,顺便重温一下REX前缀的使用。
下面的例子中,不再详细解说编码细节,如ModRM,SIB字节怎么编排,操作码如何选择等等。

1. 当目标操作数为32位操作数,64位地址数时
在指令mov dword ptr [rax],1中,操作数为32位,地址数为64位,因此无需提供REX前缀和67H前缀,最终编码为C7 00 01 00 00 00。
在一定情况下,增加REX前缀也是正确的:
在增加空REX前缀后,指令编码为40 C7 00 01 00 00 00,它的REX.W=0不产生任何冲突。
在增加REX.RX前缀后,指令编码为46 C7 00 01 00 00 00,由于指令不使用ModRM.Reg进行寻址,也没使用SIB.Index寄存器,因此作用被忽略。
然而,在增加REX.W或REX.B前缀后,指令就会转义:
在增加REX.B前缀后,意味着ModRM.Reg域将被扩展,指令中的rax将被重置为r8。
在增加REX.W前缀后,意味着操作数大小被重置为64位,此时指令变成了mov qword ptr [rax],1。值得注意的是,虽然这条指令是64位操作数大小,但根据AMD64手册,这里的立即数是32位,而非64位。

2. 当目标操作数为64位操作数,64位地址数时
前文的例子说过即便操作数被重置为64位,立即数仍然是32位。我们就再举个例子,在指令mov qword ptr [r8],1中:
上文例子的rax寄存器变成了r8寄存器,操作数大小还被重置为了64位。
因此我们需要提供REX.WB前缀,最终编码为49 C7 00 01 00 00 00。

3. 当目标操作数为64位操作数,且含有SIB时
在指令mov qword ptr [r8+r10*8+0xC],rax中,
操作数为64位,SIB.Base和SIB.Index也为64位寄存器,因此需要REX.WXB前缀,最终编码为:4B 89 44 D0 0C。

4. 当目标操作数为64位操作数,且使用32位寻址模式时
在指令mov qword ptr [eax],1中,
操作数为64位,但使用了32位的寻址模式,因此需要同时使用REX.W和67H前缀,最终编码为:67 48 C7 00 01 00 00 00。

5. 当目标操作数为32位操作数,且使用32位寻址模式时
在指令mov dword ptr [eax],1中,
操作数为32位,且使用了32位的寻址模式,此外由于我们是在长模式中编码,因此需要使用67H前缀,最终编码时67 C7 00 01 00 00 00。

最后再说一点:
64位立即数不能直接赋值到64位立即数表示的指针中,因为这将导致指令长度将会超过16个字节,Intel手册没有明说一条指令至长允许多长,给出的图示表以及相关说明(详见Intel手册的第二卷第2.1章)也有一定的误导作用,可能会使人误认为最长允许17个字节。实际上,AMD规定了一条指令至长是不可以超过15个字节的(详见AMD64手册第三卷第1.1.2章),因此mov reg/mem64,imm64就被砍成了mov reg/mem64,imm32。为了保证指令长度一定小于15字节,当指令长度超过15个字节时(这种情况是存在的,只要你反复给指令增加前缀即可:比如无限制增加传统操作数大小重置前缀,也就是66 66 66 66 66 66 66 66 66...),处理器将会抛出#GP异常以终止指令执行。

参考资料:
AMD64 Architecture Programmer's Manual, Volume 3: General-Purpose and System Instructions。(2017年3月版)
回复

使用道具 举报

55

主题

275

回帖

9352

积分

用户组: 管理员

UID
77
精华
16
威望
237 点
宅币
8217 个
贡献
251 次
宅之契约
0 份
在线时间
254 小时
注册时间
2014-2-22
发表于 2018-7-8 03:31:32 | 显示全部楼层
楼主乃真·汇编高手。
回复 赞! 靠!

使用道具 举报

0

主题

14

回帖

57

积分

用户组: 小·技术宅

UID
7247
精华
0
威望
2 点
宅币
39 个
贡献
0 次
宅之契约
0 份
在线时间
2 小时
注册时间
2021-6-21
发表于 2021-6-21 07:26:38 | 显示全部楼层
  谢谢分享
回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-4-16 23:27 , Processed in 0.065532 second(s), 33 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表