解释 Opcode Map 表格中的字段含义,并通过 0x00 ADD 指令来验证。

Byte vs Word

参考 2.2 Data TypesIntelDataType

  • 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)

参考 Machine Code in x86

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)
 ...

其他指令前缀

参考 Legacy_Prefixes

  • 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/r32R/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 的指令刚好相关,比如设置寄存器一个值:

  • 在 ARM 下是 mov x0, #1,参考 ARM
  • 在 x86 下是 mov $0x1, %rax,参考 x86

看第一个 ADD 指令

       0         1         2         3         4         5
 +-----------------------------------------------------------+
 |                              ADD                          |
0|---------+---------+---------+---------+---------+---------+
 |  Eb,Gb  |  Ev,Gv  |  Gb,Eb  |  Gv,Ev  |  AL,Ib  | eAX,Iv  |
 +---------+---------+---------+---------+---------+---------+

对于 0x00 来说,分别有两个操作符,分别是 EbGb,其实也就是 modR/M 的 1 个字节。其中

  • b 表示,这是个 8 字节寄存器,也就是 r8,也就是 AL (或其他 CL、DL 等)
    • v 表示,可能是 word (16 位) 或 double word(32 位),也就是 AXEAX(或其他 CX、ECX 等)
  • EmodR/M 中的 R/M
  • GmodR/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

References