ICS2017 Programming Assignment
  • Introduction
  • PA0 - 世界诞生的前夜: 开发环境配置
    • Installing a GNU/Linux VM
    • First Exploration with GNU/Linux
    • Installing Tools
    • Configuring vim
    • More Exploration
    • Transferring Files between host and container
    • Acquiring Source Code for PAs
  • PA1 - 开天辟地的篇章: 最简单的计算机
    • 在开始愉快的PA之旅之前
    • 开天辟地的篇章
    • RTFSC
    • 基础设施
    • 表达式求值
    • 监视点
    • i386手册
  • PA2 - 简单复杂的机器: 冯诺依曼计算机系统
    • 不停计算的机器
    • RTFSC(2)
    • 程序, 运行时环境与AM
    • 基础设施(2)
    • 输入输出
  • PA3 - 穿越时空的旅程: 异常控制流
    • 更方便的运行时环境
    • 等级森严的制度
    • 穿越时空的旅程
    • 文件系统
    • 一切皆文件
  • PA4 - 虚实交错的魔法: 分时多任务
    • 虚实交错的魔法
    • 超越容量的界限
    • 分时多任务
    • 来自外部的声音
    • 编写不朽的传奇
  • PA5 - 从一到无穷大: 程序与性能
    • 浮点数的支持
    • 通往高速的次元
    • 天下武功唯快不破
  • 杂项
    • 为什么要学习计算机系统基础
    • 实验提交要求
    • Linux入门教程
    • man入门教程
    • git入门教程
    • i386手册指令集阅读指南
    • i386手册勘误
    • 指令执行例子
Powered by GitBook
On this page
  • 指令格式
  • 指令集细节

Was this helpful?

  1. 杂项

i386手册指令集阅读指南

i386手册有一章专门列出了所有指令的细节, 附录中的opcode map也很有用. 我们需要实现的n86架构是x86的子集, 在这里, 我们先对x86指令系统作一些简单的梳理. 当你对x86指令系统有任何疑惑时, 请查阅i386手册, 关于指令系统的一切细节都在里面.

指令格式

x86指令的一般格式如下:

+-----------+-----------+-----------+--------+------+------+------+------------+-----------+
|instruction| address-  |  operand- |segment |opcode|ModR/M| SIB  |displacement| immediate |
|  prefix   |size prefix|size prefix|override|      |      |      |            |           |
|-----------+-----------+-----------+--------+------+------+------+------------+-----------|
|   0 OR 1  |  0 OR 1   |   0 OR 1  | 0 OR 1 |1 OR 2|0 OR 1|0 OR 1| 0,1,2 OR 4 |0,1,2 OR 4 |
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
|                                     number of bytes                                      |
+------------------------------------------------------------------------------------------+

除了opcode(操作码)必定出现之外, 其余组成部分可能不出现, 而对于某些组成部分, 其长度并不是固定的. 但给定一条具体指令的二进制形式, 其组成部分的划分是有办法确定的, 不会产生歧义(即把一串比特串看成指令的时候, 不会出现两种不同的解释). 例如对于以下指令:

100017:    66 c7 84 99 00 e0 ff ff 01 00      movw   $0x1,-0x2000(%ecx,%ebx,4)

其组成部分的划分如下:

+-----------+-----------+-----------+--------+------+------+------+------------+-----------+
|instruction| address-  |  operand- |segment |opcode|ModR/M| SIB  |displacement| immediate |
|  prefix   |size prefix|size prefix|override|      |      |      |            |           |
|-----------+-----------+-----------+--------+------+------+------+------------+-----------|
|                            66                 c7     84     99    00 e0 ff ff    01 00   |
+------------------------------------------------------------------------------------------+

凭什么0x84要被解释成ModR/M字节呢? 这是由opcode决定的, opcode决定了这是什么指令的什么形式, 同时也决定了opcode之后的比特串如何解释. 如果你要问是谁来决定opcode, 那你就得去问Intel了.

在n86中, address-size prefix和segment override prefix都不会用到, 因此NEMU也不需要实现这两者的功能.

另外我们在这里先给出ModR/M字节和SIB字节的格式, 它们是用来确定指令的操作数的, 详细的功能会在将来进行描述:

ModR/M byte
7    6    5    4    3    2    1    0
+--------+-------------+-------------+
|  mod   | reg/opcode  |     r/m     |
+--------+-------------+-------------+


SIB (scale index base) byte
7    6    5    4    3    2    1    0
+--------+-------------+-------------+
|   ss   |    index    |    base     |
+--------+-------------+-------------+

事实上, 一个字节最多只能区分256种不同的指令形式. 当指令形式的数目大于256时, 我们需要使用另外的方法来识别它们. x86中有主要有两种方法来解决这个问题:

  • 一种方法是使用转义码(escape code).

    x86中有一个2字节转义码0x0f, 当指令opcode的第一个字节是0x0f时,

    表示需要再读入一个字节才能决定具体的指令形式(部分条件跳转指令就属于这种情况).

    后来随着各种SSE指令集的加入, 使用2字节转义码也不足以表示所有的指令形式了,

    x86在2字节转义码的基础上又引入了3字节转义码,

    当指令opcode的前两个字节是0x0f和0x38时,表示需要再读入一个字节才能决定具体的指令形式.

  • 另一种方法是使用ModR/M字节中的扩展opcode域来对opcode的长度进行扩充.

    有些时候, 读入一个字节也还不能完全确定具体的指令形式,

    这时候需要读入紧跟在opcode后面的ModR/M字节,

    把其中的reg/opcode域当做opcode的一部分来解释, 才能决定具体的指令形式.

    x86把这些指令划分成不同的指令组(instruction group),

    在同一个指令组中的指令需要通过ModR/M字节中的扩展opcode域来区分.

指令集细节

要实现一条指令, 首先你需要知道这条指令的格式和功能, 格式决定如何解释, 功能决定如何执行. 而这些信息都在instruction set page中, 因此你务必知道如何阅读它们. 我们以mov指令的opcode表为例来说明如何阅读:

     Opcode       Instruction       Clocks        Description

< 1> 88 /r        MOV r/m8,r8       2/2           Move byte register to r/m byte
< 2> 89 /r        MOV r/m16,r16     2/2           Move word register to r/m word
< 3> 89 /r        MOV r/m32,r32     2/2           Move dword register to r/m dword
< 4> 8A /r        MOV r8,r/m8       2/4           Move r/m byte to byte register
< 5> 8B /r        MOV r16,r/m16     2/4           Move r/m word to word register
< 6> 8B /r        MOV r32,r/m32     2/4           Move r/m dword to dword register
< 7> 8C /r        MOV r/m16,Sreg    2/2           Move segment register to r/m word
< 8> 8D /r        MOV Sreg,r/m16    2/5,pm=18/19  Move r/m word to segment register
< 9> A0           MOV AL,moffs8     4             Move byte at (seg:offset) to AL
<10> A1           MOV AX,moffs16    4             Move word at (seg:offset) to AX
<11> A1           MOV EAX,moffs32   4             Move dword at (seg:offset) to EAX
<12> A2           MOV moffs8,AL     2             Move AL to (seg:offset)
<13> A3           MOV moffs16,AX    2             Move AX to (seg:offset)
<14> A3           MOV moffs32,EAX   2             Move EAX to (seg:offset)
<15> B0 + rb ib   MOV r8,imm8       2             Move immediate byte to register
<16> B8 + rw iw   MOV r16,imm16     2             Move immediate word to register
<17> B8 + rd id   MOV r32,imm32     2             Move immediate dword to register
<18> C6 /0 ib (*) MOV r/m8,imm8     2/2           Move immediate byte to r/m byte
<19> C7 /0 iw (*) MOV r/m16,imm16   2/2           Move immediate word to r/m word
<20> C7 /0 id (*) MOV r/m32,imm32   2/2           Move immediate dword to r/m dword

---------------------------------------------------------------------------
NOTES:
moffs8, moffs16, and moffs32 all consist of a simple offset relative
to the segment base. The 8, 16, and 32 refer to the size of the data. The
address-size attribute of the instruction determines the size of the
offset, either 16 or 32 bits.
---------------------------------------------------------------------------

注:
标记了(*)的指令形式的Opcode相对于i386手册有改动, 具体情况见下文的描述.

上表中的每一行给出了mov指令的不同形式, 每一列分别表示这种形式的opcode, 汇编语言格式, 执行所需周期, 以及功能描述. 由于NEMU关注的是功能的模拟, 因此Clocks一列不必关心. 另外需要注意的是, i386手册中的汇编语言格式都是Intel格式, 而objdump的默认格式是AT&T格式, 两者的源操作数和目的操作数位置不一样, 千万不要把它们混淆了! 否则你将会陷入难以理解的bug中.

首先我们来看mov指令的第一种形式:

     Opcode       Instruction       Clocks        Description
< 1> 88 /r        MOV r/m8,r8       2/2           Move byte register to r/m byte
  • 从功能描述可以看出, 它的作用是"将一个8位寄存器中的数据传送到8位的寄存器或者内存中",

    其中r/m表示"寄存器或内存".

  • Opcode一列中的编码都是用十六进制表示, 88表示这条指令的opcode的首字节是0x88,

    /r表示后面跟一个ModR/M字节, 并且ModR/M字节中的reg/opcode域解释成通用寄存器的编码,

    用来表示其中一个操作数.

  • 通用寄存器的编码如下:

二进制编码

000

001

010

011

100

101

110

111

8位寄存器

AL

CL

DL

BL

AH

CH

DH

BH

16位寄存器

AX

CX

DX

BX

SP

BP

SI

DI

32位寄存器

EAX

ECX

EDX

EBX

ESP

EBP

ESI

EDI

  • Instruction一列中, r/m8表示操作数是8位的寄存器或内存,

    r8表示操作数是8位寄存器, 按照Intel格式的汇编语法来解释,

    表示将8位寄存器(r8)中的数据传送到8位寄存器或内存(r/m8)中, 这和功能描述是一致的.

    至于r/m表示的究竟是寄存器还是内存, 这是由ModR/M字节的mod域决定的:

    当mod域取值为3的时候, r/m表示的是寄存器;

    否则r/m表示的是内存. 表示内存的时候又有多种寻址方式, 具体信息参考i386手册中的表格17-3.

看明白了上面的第一种形式之后, 接下来的两种形式也就不难看懂了:

< 2> 89 /r        MOV r/m16,r16     2/2           Move word register to r/m word
< 3> 89 /r        MOV r/m32,r32     2/2           Move dword register to r/m dword

但你会发现, 这两种形式的Opcode都是一样的, 难道不会出现歧义吗? 不用着急, 还记得指令一般格式中的operand-size prefix吗? x86正是通过它来区分上面这两种形式的. operand-size prefix的编码是0x66, 作用是指示当前指令需要改变操作数的宽度. 在i386中, 通常来说, 如果这个前缀没有出现, 操作数宽度默认是32位; 当这个前缀出现的时候, 操作数宽度就要改变成16位 (也有相反的情况, 这个前缀的出现使得操作数宽度从16位变成32位, 但这种情况在i386中极少出现). 换句话说, 如果把一个开头为89 ...的比特串解释成指令, 它就应该被解释成MOV r/m32,r32的形式; 如果比特串的开头是66 89..., 它就应该被解释成MOV r/m16,r16.

到现在为止, -三种形式你也明白了:

< 4> 8A /r        MOV r8,r/m8       2/4           Move r/m byte to byte register
< 5> 8B /r        MOV r16,r/m16     2/4           Move r/m word to word register
< 6> 8B /r        MOV r32,r/m32     2/4           Move r/m dword to dword register

和两种形式的mov指令涉及到段寄存器:

< 7> 8C /r        MOV r/m16,Sreg    2/2           Move segment register to r/m word
< 8> 8D /r        MOV Sreg,r/m16    2/5,pm=18/19  Move r/m word to segment register

n86去掉了段寄存器的实现, 我们可以忽略这两种形式的mov指令.

-这6种形式涉及到一种新的操作数记号moffs:

< 9> A0           MOV AL,moffs8     4             Move byte at (seg:offset) to AL
<10> A1           MOV AX,moffs16    4             Move word at (seg:offset) to AX
<11> A1           MOV EAX,moffs32   4             Move dword at (seg:offset) to EAX
<12> A2           MOV moffs8,AL     2             Move AL to (seg:offset)
<13> A3           MOV moffs16,AX    2             Move AX to (seg:offset)
<14> A3           MOV moffs32,EAX   2             Move EAX to (seg:offset)
---------------------------------------------------------------------------
NOTES:
moffs8, moffs16, and moffs32 all consist of a simple offset relative
to the segment base. The 8, 16, and 32 refer to the size of the data. The
address-size attribute of the instruction determines the size of the
offset, either 16 or 32 bits.
---------------------------------------------------------------------------

NOTES中给出了moffs的含义, 它用来表示段内偏移量, 但n86没有"段"的概念, 目前可以理解成"相对于物理地址0处的偏移量". 这6种形式是mov指令的特殊形式, 它们可以不通过ModR/M字节, 让displacement直接跟在opcode后面, 同时让displacement来指示一个内存地址.

-三种形式涉及到两种新的操作数记号:

<15> B0 + rb ib   MOV r8,imm8       2             Move immediate byte to register
<16> B8 + rw iw   MOV r16,imm16     2             Move immediate word to register
<17> B8 + rd id   MOV r32,imm32     2             Move immediate dword to register

其中:

  • +rb, +rw, +rd分别表示8位, 16位, 32位通用寄存器的编码.

    和ModR/M中的reg域不一样的是, 这三种记号表示直接将通用寄存器的编号按数值加到opcode中

    (也可以看成通用寄存器的编码嵌在opcode的低三位),

    因此识别指令的时候可以通过opcode的低三位确定一个寄存器操作数.

  • ib, iw, id分别表示8位, 16位, 32位立即数

最后3种形式涉及到一种新的操作码记号/digit, 其中digit为0~7中的一个数字:

<18> C6 /0 ib (*) MOV r/m8,imm8     2/2           Move immediate byte to r/m byte
<19> C7 /0 iw (*) MOV r/m16,imm16   2/2           Move immediate word to r/m word
<20> C7 /0 id (*) MOV r/m32,imm32   2/2           Move immediate dword to r/m dword

注:
标记了(*)的指令形式的Opcode相对于i386手册有改动, 具体情况见下文的描述.

上述形式中的/0表示一个ModR/M字节, 并且ModR/M字节中的reg/opcode域解释成扩展opcode, 其值取0. 对于含有/digit记号的指令形式, 需要通过指令本身的opcode和ModR/M中的扩展opcode共同决定指令的形式, 例如80 /0表示add指令的一种形式, 而80 /5则表示sub指令的一种形式, 只看opcode的首字节80不能区分它们.

注: 在i386手册中, 这3种形式的mov指令并没有/0的记号, 在这里加入/0纯粹是为了说明/digit记号的意思. 但同时这条指令在i386中也比较特殊, 它需要使用ModR/M字节来表示一个寄存器或内存的操作数, 但ModR/M字节中的reg/opcode域却没有用到 (一般情况下, ModR/M字节中的reg/opcode域要么表示一个寄存器操作数, 要么作为扩展opcode), i386手册也没有对此进行特别的说明, 直觉上的解释就是"无论ModR/M字节中的reg/opcode域是什么值, 都可以被CPU识别成这种形式的mov指令". x86是商业CPU, 我们无法从电路级实现来考证这一解释, 但对编译器生成代码来说, 这条指令中的reg/opcode域总得有个确定的值, 因此编译器一般会把这个值设成0. 在NEMU的框架代码中, 对这3种形式的mov指令的实现和i386手册中给出Opcode保持一致, 忽略ModR/M字节中的reg/opcode域, 没有判断其值是否为0. 如果你不能理解这段话在说什么, 你可以忽略它, 因为这并不会影响实验的进行.

到此为止, 你已经学会了如何阅读大部分的指令集细节了. 需要说明的是, 这里举的mov指令的例子并没有完全覆盖i386手册中指令集细节的所有记号, 若有疑问, 请参考i386手册.

除了opcode表之外, Operation, Description和Flags Affected这三个条目都要仔细阅读, 这样你才能完整地掌握一条指令的功能. Exceptions条目涉及到执行这条指令可能产生的异常, 由于n86不打算加入异常处理的机制, 你可以不用关心这一条目.

Previousgit入门教程Nexti386手册勘误

Last updated 6 years ago

Was this helpful?