x86-64的指令编码入门(翻译)

x86-64的指令编码入门(翻译)

https://www.systutorials.com/beginners-guide-x86-64-instruction-encoding/

x86和x86-64指令在Intel或者AMD的手册中都已经写好了。然而,它们并不是很容易读懂,特别是针对新手。在这章中,我会给出一系列有用的手册,这些手册会让你们理解和学习x86-64指令编码,一个简短的介绍和一个例子帮助你学习x86-64指令的格式和编码。更多的细节,你可能需要继续阅读后面列出来的参考文献。

AT&T语法和Intel语法

在你学习下列的部分之前,我们先理清以下有关汇编语言的语法,AT&T和Intel语法。在Intel文档,通常使用Intel语法。在Linux的GNU工具链,默认的语法通常是AT&T语法。

两者之间最大的区别是源和目的的位置是相反的。Intel语法使用“目的,源”,而AT&T语法是"源,目的"。注意有多余一个源操作数,例如enter指令,没有相反的顺序。对有SIB或者替换的操作数,格式是不同的。例如,英特的指令是[rdi+0xa],AT&T是0xa(%rdi)。有关两者的区别,请阅读 AT&T语法与Intel语法

x86-64指令编码的参考文档

下面是一系列参考文献和有用的文档,你能在后期使用他们去编码更多的指令。

找出一个x86-64指令编码的工具和贴士

为了可以快速地找出一个指令的编码,你能使用GNU Assembler as和 objdump tool,例如,为了找出addq 10(%rdi), %r8的编码,你能按照如下操作

首先,创建一个文件add.s,包含一行

addq 10(%rdi), %r8

接下来,把这个add.s文件用汇编程序转成object文件

as add.s -o add.o

最后,反汇编object文件

objdump -d add.o

那么它会打印出

add.o:     file format elf64-x86-64
​
Disassembly of section .text:
​
0000000000000000 <.text>:
   0:   4c 03 47 0a             add    0xa(%rdi),%r8
​

这里

4c 03 47 0a

就是一个4字节的addq的指令编码。

如果你想在Intel语法下检查这个指令,你可以用下面的指令反汇编

objdump -d --disassembler-options=intel-mnemonic add.o

你会得到

add.o:     file format elf64-x86-64
​
Disassembly of section .text:
​
0000000000000000 <.text>:
   0:   4c 03 47 0a             add    r8,QWORD PTR [rdi+0xa]

x86-64指令编码的简单介绍

x86-64指令的每一位都是按照一定的变量形式编码。每个指令的组成包括:

  • 一个操作码

  • 一个寄存器 and/or 由ModR/M位组成的地址说明符,并且有时是“范围-索引-基地址”位(如果需要的话)

  • 一个位移或者一个立即数字段(如果需要的话)

+-------+--------+---------+--------+--------------+-----------+
|  REX  | OpCode | Mod R/M |   SIB  | Displacement | Immediate |   
+-------+--------+---------+--------+--------------+-----------+
        |        |         |        |              |
 0,1byte 1-3bytes  0,1byte   0,1byte  0,1,2,4 bytes  0,1,2,4 bytes

///------------------------此处是自己补充---------------------------///

其中REX部分展开
   7    6   5    4    3     2    1    0
+----+----+----+----+----+----+----+----+
| 0  |  1 |  0 | 0  | W  | R  |  X |  B |
+----+----+----+----+----+----+----+----+

在64位机中
W位为1                                           //w:可能是wide,表示扩展为64位
R位为1,用来扩展ModRM.reg域,000~111--->0000~1111  //R:Register
X位为1,用来扩展SIB.index域                        //X:可能是extent
B位为1,用来扩展SIB.Base, ModRM.r/m以及Opcode.reg  //B:Base
其中Mod R/M部分内部的结构是
7    6 5        3 2    0
+-----+----------+-----+
| Mod |Reg/OpCode| R/M |
+-----+----------+-----+

[7-6]为Mod(2位)

mod模式寻址表

ModRM.mod寻址模式描述
00[base]提供[base]形式的memory寻址
01[base+disp8]提供[base+disp8]形式的memory寻址
10[base+disp32]提供[base+disp32]形式的memory寻址
11register提供register寻址

[5-3]为Reg/OpCode(3位)

3位组成8个寄存器ID值,从000-111,对应于 RAX、RCX、RDX、RBX、RSP、RBP、RSI 以及 RDI

 

[2-0]为寻址register或memory(3位)

 

 

 

///----------------此处是自己的补充------------------///

SIB展开的内部结构是
 7     6 5     3 2    0
+-------+-------+------+
| Scale | Index | Base |
+-------+-------+------+

一个手动编码x86-64的例子

我们看看

add r8, QWORD PTR [rdi+0xa]   //Intel语法

的例子,我们来看看是怎样编码成

4c 03 47 0a

在ISA参考手册2A卷,我们找到ADD指令,找到关于 ADD r64, r/m64的编码

OpCode          Instruction       Op/En    64-bit    Compat/Leg Mode   Description
REX.W+03/r      ADD r64, r/m64      RM      Valid     N.E.             Add r/m64 to r64.

并且,从REX的描述中

在64位模式中,指令默认的大小是32位,使用REX前缀在REX.W中能使之扩展到64位

所以,我们得到

REX.W=1

‘R’, ‘X’ 和 'B'位与操作数的编码有关(查看参考手册2A卷Table2-4 REX prefix Fields [BITS:0100WRXB]")

REX.X位修改SIB索引字段

SIB在这个指令中没有使用,因此

REX.X = 0

我们看看操作数的编码,从add指令的操作数编码

Op/En     Operand 1         Operand 2     Operand 3    Operand 4
RM        ModRM:reg(r,w)   ModRM: r/m(r)   NA           NA

在RM编码有2个操作数。第一个是ModRM:reg(r,w), 第二个是ModRM:r/m(r). 图2-4 无SIB位的内存编址;不用REX.X" 表明了这种情况的编码

REX     |    OpCode    |   ModRM

0100 WRXB                 mod  reg        r/m
0100 1100     0x03        01  1.000(r8)   0.111(rdi,1和0之后打点表示R位置1扩展了寄存器的位数)

 

译者:注意观察,reg表示目的,r/m表示源,reg的扩展位决定了R位是0还是1,r/m的扩展位决定了B位是0还是1

//---------------补充:http://staffwww.fullcoll.edu/aclifton/cs241/lecture-instruction-format.html ---------///

  • W,如果设置的话,表示64位操作数

  • R被用来表示ModR/M字节中Reg字段中的扩展位。因为Reg字段只有3位,它通常能访问8个寄存器,一个扩展位能够使它访问r8-r15

  • X is used as an extra bit in the Index field of the SIB byte. As the Index field names a register, this has the same justification as the R field above.

  • X被用来设置SIB字节的Index字段的扩展位。因为Index字段声明了一个寄存器,这就跟上面的R字段表达了同样的意思

  • B被用来作为SIB字节的Base字段,并且有时作为ModR/M字节的R/M字段的扩展位。这两个字段都声明了一个寄存器,所以当访问r8-r15的时候,这个扩展位是需要的

//-------------------------------------------------------------------------------------------------------------------------------------------///

REX.R和REX.B位与ModRM位上的字节是互相配合的,在ModRM上有3个部分,分别是'mod', 'reg'和'r/m'

在Intel的开发者手册中,在卷2中,表2-2 32-bit Addressing Forms with the ModR/M Byte展示了Mod的意思,(尽管它是32位操作数,但是从2.2.1.1, 在64位模式下,这些格式并没有改变,需要定义在64位环境下的字段也提供了额外的REX前缀并且同样值也能够被使用)

尽管该表也应用64位模式,它并不表明额外的寄存器,例如r8。因此,我们仅仅为了addq指令使用而去找出Mod位的使用方法。因为0xa能够在一个字节上表示,我们能够使用disp8去保持整个编码的短小。从[EDI]+disp8行(事实上,所有的disp8都使用同样的'Mod'位)

Mod = 01 (int bits)

下表是64位通用寄存器的编码

_.Reg  Register
----------------
0.000   RAX
0.001   RCX
0.010   RDX
0.011   RBX
0.100   RSP
0.101   RBP
0.110   RSI
0.111   RDI
1.000   R8
1.001   R9
1.010   R10
1.011   R11
1.100   R12
1.101   R13
1.110   R14
1.111   R15

_.Reg位就是REX前缀的标志位,例如REX.B和REX.R,这要根据特定的指令和操作数组合

对addq指令而言,r8是1.000,rdi是0.111。因此,在标志位上,我们得到

reg = 000
r/m = 111
REX.B = 0 (from 'rdi')
REX.R = 1 (from 'r8')

现在,让我们把它们放在一起

通过把'WRXB'放在一起[BITS: 0100WRXB],我们得到REX的前缀是

0100 1100

然后把0x03放在REX.W+03/r中,那么操作码的16进制表示为

4c 03

然后把mod, reg 和 r/m放在一起,我们得到ModRM的字节表示为

01 000 111

16进制的表示

47

紧跟ModRM字节之后的是位移0xa(10的16进制表示形式),它是一个字节(disp8)

把所有这些放在一起,我们最终得到add r8,[rdi+0xa]的编码

4c 03 47 0a

在这个例子,为了展现这个过程,我已经展现了如何手工做一个指令编码,而这一般是汇编器做的。你可能通过参考文档的相关细节使用同样的方法编码其他的指令。

享受底层编程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值