1 x86的编码格式
x86采取可变长度的方式进行编码,其主要原因应是8086的机器性能不高,导致编码越短越好。而8086的编码模式相较于x64较简单,且x64兼容x86编码,所以,以下阐述将由8086一直讲到x64
1.1 8086编码
8086采取以下编码格式
其中opcode是必须的,其他的是可选的
opcode
opcode的格式前6位表示操作,后2位分为d or s 以及w 位
Mod R/M
Mod R/M表示寻址方式,下表表示寻址的类型
ModRM.mod | 寻址模式 | 描述 |
---|
00 | [base] | 内存寻址,提供[base]形式的memory寻址 | 01 | [base+disp8] | 内存寻址,提供[base+disp8]形式的memory寻址 | 10 | [base+disp16] | 内存寻址,提供[base+disp32]形式的memory寻址 | 11 | register | 两个寄存器之间赋值 |
首先以mod=11为例进行阐述 首先制定reg域的编码规则,此规则适用于任何mod方式
REG | W=0 | W=1 |
---|
000 | AL | AX | 001 | CL | CX | 010 | DL | DX | 011 | BL | BX | 100 | AH | SP | 101 | CH | BP | 110 | DH | SI | 111 | BH | DI |
假设指令为mov %sp %bp 则指令序列为89 e5 ,根据上述的格式可知
89 为opcode,e5 为ModRM ,对89 进行分析
1000,1001==>[1000,00]为opcode
==>[01]为DW,其中W表示16位的值
对e5 进行分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oxZJsKYA-1655714796572)(./images/regreg.png)].
下面以mod==00与mod=01为例进行解释
-
mod==00表示直接按照内存基址的方式进行访问
R/M | 译码 |
---|
000 | [BX+SI] | 001 | [BX+DI] | 010 | [BP+SI] | 011 | [BP+DI] | 100 | [SI] | 101 | [DI] | 110 | | 111 | [BX] |
SI为源变址寄存器
DI为目的编制寄存器
这两个寄存器在8086中有特殊用途,此处不涉及,不赘述
由于ModR/M中的可执行操作可分为
- Register -> Memory
- Memory -> Register
因此,不管是何种方式,只要是有内存相关的数据,一定是放在r/m 域中,而单纯寄存器相关的放在Register 中
同理也可指mod=01及mod=10,其译码值只需要查表可知
1.2 x86指令编码
x86系列编码一直秉持着向下兼容的概念,同时又要支持新的寄存器及编码,其处理方式为在原有的8086的编码规则之上在指令头部加入prefix域,当译码器读到该域中的对应值时即可知当前为x86编码,x86编码的prefix值如下
编码值 | 含义 |
---|
66H | 改变指令期望的数据的默认大小,例如由16位变32位 | 67H | 改变指令期望的地址的默认大小,例如由16位变32位 | 2EH | CS寄存器 | 3EH | DS寄存器 | 26H | ES寄存器 | 64H | FS寄存器 | 65H | GS寄存器 | 26H | SS寄存器 | F3H | REP,REPE,重复指令,其数量由ECX决定,当ECX=0时停止 | F2H | 中止循环,当ZF标志位赋值 | F0H | 保证指令将有专门的共享内存使用,直到指令完全结束 这保证了在x86多处理同时处理的情况下,不会在单条指令操做受到干扰 |
其他的部分与8086编码逻辑一致,改变的只有数据的长度,可通过查表进行编码
1.3 x64编码
x64比x86不仅通用寄存器多了8个,更重要的是数据及地址长度可以扩展到64位,因此,其编码也较x86更复制,然后,秉承着向下兼容的原则,其编码方式在原有的prefix的基础上增加了REX prefix语法,将在x86下的prefix更名为legacy prefix,这也意味着当指令被译码的时候,如果碰到x86的头,依然以x86的方式进行译码,而碰到REX prefix的头时,则以x64的方式进行译码。
在这里着重指出的是legacy prefix和REX prefix共用同块空间。
x64最大可以允许16字节的指令,但目前为止,最大的也就15个字节
代表x64特殊的前缀为
0100, WRXB
也就是以4打头,后面跟WRXB位的指令,其中
W:表示wide,此处如果是1,则表示操作的对象是64位
R:表示Register,用于扩展原有的8个通用寄存器到16个寄存器,根据8086编码规则可知,在Mod R/M 域中,reg 域的取值范围位[0,7],如果加上R的[0,1],则整个的取值范围就可以扩展至[0000B, 1111B]共16个元素
X: 表示Extention,用于扩展SIB的Index域(后面会对SIB进行分析)
B:用于扩展Mod R/M 中的r/m 域,使得通用寄存器范围可以从8个扩展到16个,且访存也从8个扩展到16个
其搭配如下图所示
例1
现在以mov %rsp %rbp 为例进行阐述
首先这是寄存器之间的传值,因此可知mod==11,另外,寄存器使用的是64位的,所以,W=1,通过反汇编上述的汇编语句可知
48 89 e5 mov %rsp,%rbp
拆解上述的语句
查寄存器编码表
_.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
可知R.100 所代表的寄存器是RSP , B.101 所代表的寄存器是RBP
符合前面8086的编码规则,只不过加了一个前缀
例2
现在做一个访存的例子mov %rsp 10(%rbp)
通过gcc和objdump后获得以下的输出
48 89 65 0a mov %rsp,0xa(%rbp)
可知R.100 所代表的寄存器是RSP , B.101 所代表的寄存器是RBP ,由于没有对地址进行[base + index*scale]进行计算,因此SIB为0,偏移为10。
例3
访存mov %rsp (%rbp,%rax) ,由objdump出来的指令如下
48 89 64 05 00 mov %rsp,0x0(%rbp,%rax,1)
此处可以看到,目的值为M[%rbp + %rax *1 + 0],分析指令值
首先知道reg=R.100表示的是RSP
目的值需要通过查表得出计算方法,再根据指令值计算得出
现在给出对应的mod表,查表得出对应的操作
reg=B.100为[SIB+disp8]
很明显,disp8=0,SIB的值为05H
根据SIB的定义
其中SS表示的是scale,其取值范围为1.2.4.8
Encoded value (binary) | scale factor |
---|
00 | 1 | 01 | 2 | 10 | 4 | 11 | 8 |
Index表示的是索引寄存器的编号
base表示的是基地址寄存器的编号
以本例而言,其SIB的值为05H,也就是说
SS为0,index为0号寄存器(rax),base为5号寄存器(rbp),所以,计算得出的有效地址为
effective_address = 1 * rax + rbp + 00
符合汇编语言的要求
总结一下
-
寄存器间传值,Mod为11,且SIB,Disp及Imm都不需要 -
当进行访存时,Mod值的取值范围为{00,01,10},SIB为可选项
补全Mod R/M编码表
其他常用指令
由于是常用指令,往往是单独针对寄存器或者内存进行操作,因此不需要上述的模型,而是直接给出操作码的方式,经常会出现只需要1个字节即可完成所需的操作。
-
push 以push %rsp 为例,根据objdump打印出来的结果 54 push %rsp
查询AMD 64手册 push reg64 对应的操作码50+rq ,而rsp的寄存器编号为4 -
pop 5c pop %rsp
查询AMD 64手册 pop reg64 对应的操作码58+rq -
call 以下列C程序 int bar()
{
return 100;
}
int foo(){
int i = bar();
return i;
}
int main(){
int i = foo();
return i;
}
首先用gcc -S 来查看生成的汇编代码 .file "main.c"
.text
.globl bar
.type bar, @function
bar:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $100, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size bar, .-bar
.globl foo
.type foo, @function
foo:
.LFB1:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $0, %eax
call bar
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size foo, .-foo
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $0, %eax
call foo
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
可以看到call bar 然后再用objdump反汇编出指令看看有什么区别 main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <bar>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: b8 64 00 00 00 mov $0x64,%eax
d: 5d pop %rbp
e: c3 retq
000000000000000f <foo>:
f: f3 0f 1e fa endbr64
13: 55 push %rbp
14: 48 89 e5 mov %rsp,%rbp
17: 48 83 ec 10 sub $0x10,%rsp
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 00 00 00 00 callq 25 <foo+0x16>
25: 89 45 fc mov %eax,-0x4(%rbp)
28: 8b 45 fc mov -0x4(%rbp),%eax
2b: c9 leaveq
2c: c3 retq
000000000000002d <main>:
2d: f3 0f 1e fa endbr64
31: 55 push %rbp
32: 48 89 e5 mov %rsp,%rbp
35: 48 83 ec 10 sub $0x10,%rsp
39: b8 00 00 00 00 mov $0x0,%eax
3e: e8 00 00 00 00 callq 43 <main+0x16>
43: 89 45 fc mov %eax,-0x4(%rbp)
46: 8b 45 fc mov -0x4(%rbp),%eax
49: c9 leaveq
4a: c3 retq
其中call bar 的指令为 e8 00 00 00 00 callq 25 <foo+0x16>
查询AMD 64手册可知 CALL re/16off E8 iw
call指令的操作码为E8,E8直接读PC值,这样就跳到bar函数 后面所跟的值是当调用结束之后下一条指令相对当前指令的偏移量(这里没有Mod和SIB,因为不需要通过寄存器计算访存的位置),而这个偏移量计算公式为 displacement = destination address - address of next instruction
其中displacement采用的是大端的方式,应转换为小端表达,下面的例子更加明显 #include <stdio.h>
int main(void) {
printf("Hello, world!\n");
printf("Hello, world!\n");
return 0;
}
objdump出来的结果 /helloworld.c:3
804842c: 83 ec 0c sub $0xc,%esp
804842f: 68 f0 84 04 08 push $0x80484f0
8048434: e8 b7 fe ff ff call 80482f0
8048439: 83 c4 10 add $0x10,%esp
/helloworld.c:4
804843c: 83 ec 0c sub $0xc,%esp
804843f: 68 f0 84 04 08 push $0x80484f0
8048444: e8 a7 fe ff ff call 80482f0
8048449: 83 c4 10 add $0x10,%esp
其中 8048434: e8 b7 fe ff ff call 80482f0
The opcode for this instruction is E8 , followed by the relative offset that is computed by the following equation: destination address - address of next instruction . In this case, the relative offset of the first call is 80482f0 - 8048439 = FFFFFEB7 , and the relative offset of the second call is 80482f0 - 8048449 = FFFFFEA7 . -
ret 查询AMD 64测试可知 C3
实际执行如图
-
add,sub,mul 三种计算指令则涉及了寄存器及访存操作,其编码规则可参考mov,opcode查询手册,下面以add指令为主进行讲解 0: 48 01 c3 add %rax,%rbx
3: 48 03 04 25 0a 00 00 add 0xa,%rax
a: 00
b: 48 01 03 add %rax,(%rbx)
e: 48 01 04 18 add %rax,(%rax,%rbx,1)
12: 48 01 44 58 0a add %rax,0xa(%rax,%rbx,2)
R.000是rax
B.011是rbx
-
add %rax,(%rbx) 48 01 03 add %rax,(%rbx)
1.4 Intel MMX技术
MMX技术是Intel公司为增强 CPU 在音像、图形和通信应用方面而采取的技术,MMX技术是继Intel386?处理器(将体系结构扩展至32位)之后对Intel体系结构的最重要的加强。这些技术的指令能够加速处理有关图形、影像、声音等的应用,MMX 加强了在多媒体处理功能的不足,它可以利用其内建的多媒体指令来模拟3D绘图的处理、 MPEG的压缩/解压缩,立体声的音效等,只要是软件支持MMX CPU,即可以取代这些硬件的接口而达到多媒体的功效。
MMX技术是在CPU中加入了特地为视频信号(Video Signal),音频信号(Audio Signal)以及图像处理 (Graphical Manipulation)而设计的一套基本的、通用的整数指令、单指令、多数据(SIMD)技术,可简便地应用于各种多媒体及通信应用程序。
1.5 SSE Instructions
SSE instructions are an extension of the SIMD execution model introduced with the MMX technology. SSE instructions are divided into four subgroups:
大部分涉及到128位内存变量操作的,内存变量首地址必须要对齐16字节,也就是内存地址低4位为0,否则会引起CPU异常,导致指令执行失败,此错误编译器不检查.
|