80386 模拟器源码分析 (2)
解释 Opcode Map 表格中的字段含义,并通过 0x00 ADD
指令来验证。
Byte vs Word
参考 2.2 Data Types 和 IntelDataType
- Byte 为 8 位
- Word 为 16 位
- DoubleWord 为 32 位
- Quadword 为 64 位
Codes for Operant Type
指定位数,并忽略 Oprand Size
- b 为 8 位
- w 为 16 位
- d 为 32 位
通过 oprand szie 指定位数
- c 为 8/16 位
- v 为 16/32 位
- p 为 32/48 位
还有两个 a(仅 BOUND 指令)、s 用得较少,先忽略。
指令长度 (operand size attribute)
x86 specifies register sizes using prefix bytes. For example, the same “0xb8” instruction that loads a 32-bit constant into eax can be used with a “0x66” prefix to load a 16-bit constant, or a “0x48” REX prefix to load a 64-bit constant.
Here we’re loading the same constant 0x12 into all the different sizes of eax:
0: 48 b8 12 00 00 00 00 00 00 00 mov rax,0x12 a: b8 12 00 00 00 mov eax,0x12 f: 66 b8 12 00 mov ax,0x12 13: b0 12 mov al,0x12 15: c3 ret
0x66
0x66
为 16 位指令前缀,比如指定 gcc 为 -m16
,可以看到如下指令
如果无法指定
-m16
,参考 交叉编译 使用 docker 来进行编译。
$ gcc -m16 hello.c
$ objdump -S a.out
080483ab <main>:
80483ab: 66 55 push %bp
80483ad: 66 89 e5 mov %sp,%bp
80483b0: 66 83 ec 10 sub $0x10,%sp
80483b4: 67 66 c7 45 fc 01 00 movw $0x1,-0x4(%di)
80483bb: 00 00 add %al,(%eax)
80483bd: 67 66 c7 45 f8 02 00 movw $0x2,-0x8(%di)
80483c4: 00 00 add %al,(%eax)
...
0xc7
0xc7
为 32 位指令前缀,比如指定 gcc 为 -m32
,可以看到如下指令
$ gcc -m32 hello.c
$ objdump -S a.out
080483ab <main>:
80483ab: 55 push %ebp
80483ac: 89 e5 mov %esp,%ebp
80483ae: 83 ec 10 sub $0x10,%esp
80483b1: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp)
80483b8: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%ebp)
...
0x48
如果你是 ARM 机器,参考 交叉编译 使用 docker pull randomdude/gcc-cross-x86_64-elf
镜像即可
0x48
为 64 位指令前缀。
$ docker pull randomdude/gcc-cross-x86_64-elf
$ docker run -it --name gcc-64 -v "$PWD:/home/project" randomdude/gcc-cross-x86_64-elf
# 进入 docker 容器
$ cd /home/project
$ gcc -m64 hello.c
$ objdump -S a.out
0000000100003f84 <_main>:
0000000000000660 <main>:
660: 55 push %rbp
661: 48 89 e5 mov %rsp,%rbp # 64 位机器指令
664: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) # 64 位机器,也会使用 0xc7 的 32 位指令
66b: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp)
...
其他指令前缀
- Prefix group 1
- 0xF0: LOCK prefix
- 0xF2: REPNE/REPNZ prefix
- 0xF3: REP or REPE/REPZ prefix
- Prefix group 2
- 0x2E: CS segment override
- 0x36: SS segment override
- 0x3E: DS segment override
- 0x26: ES segment override
- 0x64: FS segment override
- 0x65: GS segment override
- 0x2E: Branch not taken
- 0x3E: Branch taken
- Prefix group 3
- 0x66: Operand-size override prefix
- Prefix group 4
- 0x67: Address-size override prefix
modR/M
modR/M 字节共 8 位,许多指令码后会跟此字节,用来表示操作的地址说明、其他补充说明等。
其实一开始,我始终没有搞明白为什么有这个字节,按照之前我对 ARM 的理解,应该是 16 或 32 位统一一起看,然后编码在这 16 位或 32 位指令中的。
Intel 中的编码,就是 8 位一组一组看,看上去更合理写,但每个都是变长的指令(这个就挺令人困扰的),需要根据前一个字节确认是否需要读取下一个字节。
然后 ARM 的指令更紧凑一些,一条指令就是 32 位(从指令长度上看,更优雅合理),然后需要的信息都通过指令 opcode 不同来做不同的编码和位置调整。
毕竟 x86_64 是 CISC,更为复杂,更需要考虑兼容等。而 ARM 的 RISC 会更精简些。
比如,还是上面指令长度的样例,在 ARM 下的二进制表达如下:
$ gcc hello.c
$ objdump -S a.out | awk -F"\n" -v RS="\n\n" '$1 ~ /main/'
0000000100003fa8 <_main>:
100003fa8: 20 00 80 d2 mov x0, #1 // 每条指令都是 32 位
100003fac: 41 00 80 d2 mov x1, #2
100003fb0: 00 00 01 8b add x0, x0, x1
100003fb4: c0 03 5f d6 ret
...
只能说,各有各的优势,此处不展开详细评论了。
8 位会拆成 3 段,分别是 mod(2 bits)
+ REG/Opcode (3 bits)
+ R/M (3 bits)
, 参考 Encoding memory and register operands
mod |
reg/opcode |
r/m |
用于说明地址访问模式(寄存器还是内存,以及该怎么读) 2 bits, selects memory or register access mode: 0: memory at register r/m 1: memory at register r/m+byte offset 2: memory at register r/m + 32-bit offset 3: register r/m itself (not memory) |
目标寄存器,或对 opcode 做补充说明 3 bits, usually a destination register number. For some instructions, this is actually extra opcode bits. |
来源寄存器或其他地址说明(如果是 100,还需要 SIB 字段) 3 bits, usually a source register number. Treated as a pointer for mod!=3, treated as an ordinary register for mod==3. If r/m==4, indicates the real memory source is a SIB byte. |
其中,对于 r8/r16/r32
或 R/M 下的 EAX/AX/AL
的选择,也就是具体选 8 位寄存器还是 16 位、32 位,取决于前面提到的指令长度。
不同寄存器对应的编码,也回答了这个问题:为什么8086CPU不支持将数据直接送入段寄存器的操作? ,简单点说 modR/M 的编码中,没有段寄存器。
Table 17-2. 16-Bit Addressing Forms with the ModR/M Byte r8(/r) AL CL DL BL AH CH DH BH r16(/r) AX CX DX BX SP BP SI DI r32(/r) EAX ECX EDX EBX ESP EBP ESI EDI /digit (Opcode) 0 1 2 3 4 5 6 7 REG = 000 001 010 011 100 101 110 111 Effective +---Address--+ +Mod R/M+ +--------ModR/M Values in Hexadecimal--------+ [BX + SI] 000 00 08 10 18 20 28 30 38 [BX + DI] 001 01 09 11 19 21 29 31 39 [BP + SI] 010 02 0A 12 1A 22 2A 32 3A [BP + DI] 011 03 0B 13 1B 23 2B 33 3B [SI] 00 100 04 0C 14 1C 24 2C 34 3C [DI] 101 05 0D 15 1D 25 2D 35 3D disp16 110 06 0E 16 1E 26 2E 36 3E [BX] 111 07 0F 17 1F 27 2F 37 3F [BX+SI]+disp8 000 40 48 50 58 60 68 70 78 [BX+DI]+disp8 001 41 49 51 59 61 69 71 79 [BP+SI]+disp8 010 42 4A 52 5A 62 6A 72 7A [BP+DI]+disp8 011 43 4B 53 5B 63 6B 73 7B [SI]+disp8 01 100 44 4C 54 5C 64 6C 74 7C [DI]+disp8 101 45 4D 55 5D 65 6D 75 7D [BP]+disp8 110 46 4E 56 5E 66 6E 76 7E [BX]+disp8 111 47 4F 57 5F 67 6F 77 7F [BX+SI]+disp16 000 80 88 90 98 A0 A8 B0 B8 [BX+DI]+disp16 001 81 89 91 99 A1 A9 B1 B9 [BX+SI]+disp16 010 82 8A 92 9A A2 AA B2 BA [BX+DI]+disp16 011 83 8B 93 9B A3 AB B3 BB [SI]+disp16 10 100 84 8C 94 9C A4 AC B4 BC [DI]+disp16 101 85 8D 95 9D A5 AD B5 BD [BP]+disp16 110 86 8E 96 9E A6 AE B6 BE [BX]+disp16 111 87 8F 97 9F A7 AF B7 BF EAX/AX/AL 000 C0 C8 D0 D8 E0 E8 F0 F8 ECX/CX/CL 001 C1 C9 D1 D9 E1 E9 F1 F9 EDX/DX/DL 010 C2 CA D2 DA E2 EA F2 FA EBX/BX/BL 011 C3 CB D3 DB E3 EB F3 FB ESP/SP/AH 11 100 C4 CC D4 DC E4 EC F4 FC EBP/BP/CH 101 C5 CD D5 DD E5 ED F5 FD ESI/SI/DH 110 C6 CE D6 DE E6 EE F6 FE EDI/DI/BH 111 C7 CF D7 DF E7 EF F7 FF
解释其中一个 opcode
首先注意一点,x86 的指令和 arm 的指令刚好相关,比如设置寄存器一个值:
看第一个 ADD 指令
0 1 2 3 4 5 +-----------------------------------------------------------+ | ADD | 0|---------+---------+---------+---------+---------+---------+ | Eb,Gb | Ev,Gv | Gb,Eb | Gv,Ev | AL,Ib | eAX,Iv | +---------+---------+---------+---------+---------+---------+
对于 0x00
来说,分别有两个操作符,分别是 Eb
与 Gb
,其实也就是 modR/M
的 1 个字节。其中
b
表示,这是个 8 字节寄存器,也就是r8
,也就是AL
(或其他 CL、DL 等)v
表示,可能是word
(16 位) 或double word
(32 位),也就是AX
或EAX
(或其他 CX、ECX 等)
E
是modR/M
中的R/M
G
是modR/M
中的REG/OPCODE
根据 ADD 指令的解释,是将 Gb
加到 Eb
中,并将最终结果存储在 Eb
中。
Opcode Instruction Clocks Description
00 /r ADD r/m8,r8 2/7 Add byte register to r/m byte
举个例子子,如果是 0x00 C8
,那所代表的含义就是 AL = CL ADD AL
,也就是 CL
寄存器的值加上 AL
寄存器的值,并将最终接口存储在 AL
中。
这里和 coder32 也可以结合起来一起看。
0x00 ADD 指令代码测试
在 docker i386/gcc
中运行:
# 新建一个 add.s 的汇编文件,代码如下
$ cat add.s
.globl main
main:
mov $0x1, %al
mov $0x2, %cl
add %cl, %al
ret
# 编译
$ gcc -m16 add.s
# 运行
$ ./a.out
# 结果
$ echo $?
3
# 查看二进制代码
$ objdump -S a.out
...
080483ab <main>:
80483ab: b0 01 mov $0x1,%al
80483ad: b1 02 mov $0x2,%cl
80483af: 00 c8 add %cl,%al
80483b1: c3 ret
...
0x01 ADD 指令测试
同理,我们可以测试一下 0x01
(Ev,Gv),v
下的 word
情况(也就是 16 位)
$ cat add.s
.globl main
main:
mov $0x1, %ax
mov $0x2, %cx
add %cx, %ax
$ gcc -m16 add.s
$ objdump -S a.out | awk -F"\n" -v RS="\n\n" '$1 ~ /main/'
...
080483ab <main>:
80483ab: 66 b8 01 00 mov $0x1,%ax
80483af: 66 b9 02 00 mov $0x2,%cx
80483b3: 66 01 c8 add %cx,%ax // 通过 66 来声明是 16 位操作,也就是 word
80483b6: c3 ret
...
32 位测试代码,v
下的 double world
情况(也就是 32 位)
$ cat add.s
.globl main
main:
mov $0x1, %eax
mov $0x2, %ecx
add %ecx, %eax
ret
$ gcc add.s
$ objdump -S a.out | awk -F"\n" -v RS="\n\n" '$1 ~ /main/'
...
080483ab <main>:
80483ab: b8 01 00 00 00 mov $0x1,%eax
80483b0: b9 02 00 00 00 mov $0x2,%ecx
// 不做声明,默认为 32 位
80483b5: 01 c8 add %ecx,%eax
80483b7: c3 ret
...
0x66 复写的默认值,参考 Operand-size and address-size override prefix
CS.d | REX.W | Prefix (0x66 if operand, 0x67 if address) | Operand size | Address size | |
---|---|---|---|---|---|
Real mode / Virtual 8086 mode |
N/A | N/A | No | 16-bit | 16-bit |
N/A | N/A | Yes | 32-bit | 32-bit | |
Protected mode / Long compatibility mode |
0 | N/A | No | 16-bit | 16-bit |
0 | N/A | Yes | 32-bit | 32-bit | |
1 | N/A | No | 32-bit | 32-bit | |
1 | N/A | Yes | 16-bit | 16-bit | |
Long 64-bit mode | Ignored | 0 | No | 32-bit | 64-bit |
Ignored | 0 | Yes | 16-bit | 32-bit | |
Ignored | 1 | No | 64-bit1 | 64-bit | |
Ignored | 1 | Yes | 64-bit | 32-bit |