Intel 8086仿真模拟器。
tests
文件夹中,也可以在网上查找汇编代码,稍加修改即可运行。
mainUI.exe
即可运行,也可以执行以下代码运行:
$ python mainUI.py
其他操作示例:
tests
文件夹中的汇编文件,再次Load后运行
命令行交互CLI比GUI多的功能:
运行方法:在程序根目录命令行中执行:
$ python main.py ./tests/Requirement/bubble_sort.asm -n
第一个参数为需要执行的汇编程序(在
tests
文件夹)
后面有两个可选参数:
--nodebug
:可简写为
-n
,关闭debug,持续运行直到断点或结束。示例:
$ python main.py ./tests/Interrupt/show_date_time.asm -n
--interrupt
:可简写为
-i
,显示中断信息。示例:
$ python main.py ./tests/Interrupt/int3_test.asm -n -i
0000:0000
到
0000:03FF
,共1024个单元。
1000:0000
开始放置,空间大小固定为
100h
。
运行结果如下:
该示例多次调用DOS中断服务,获取系统日期和时间并打印,如需查看中断信息,可在命令行添加
-i
选项。
用户可以自行编写中断例程,安装方法有两种:
isr.py
文件
这里提供一个用户中断例程示例
isr7c.asm
,实现将字符串转换为大写,
ds:si
指向字符串首地址:
assume cs:code
code segment
upper:
push cx
push si
change:
mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
pop si
pop cx
code ends
end upper
测试代码如下:
assume cs:code,ds:data
data segment
msg db 'abcdefghijklmnopqrstuvwxyz$'
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0 ;调用upper中断例程,转换为大写
int 7ch
mov dx,offset msg ;lea dx,msg
mov ah,9
int 21h ; 调用BIOS中断例程,打印msg
mov ax,4c00h
int 21h
code ends
end start
运行测试方式:
$ python main.py ./tests/Interrupt/int7c_test.asm -n -i
运行结果:
可以看到本测试用例进行了3次中断调用:
中断服务程序isr7c.asm
将测试程序中的小写字母转换为大写
中断服务程序dos_isr_21h
的子程序09h
打印该字符串
中断服务程序dos_isr_21h
的子程序4ch
带返回值结束
8086中软中断不能嵌套,硬中断可以嵌套。
本模拟程序中凡调用中断例程均允许嵌套
x86构架的开端Intel 8086所有的内部寄存器、内部及外部数据总线都是16位宽,运算器、寄存器均为16位,是完全的16位微处理器,采用小端模式。20位外部地址总线,物理定址空间为1MB。
使用最常见的Intel x86指令集。
汇编语言大小写不敏感,为了统一风格,我们采用大写。
有效地址EA
段地址SA
变长指令:单条指令的长度不固定,常用的指令设计成较短指令,不常用的指令设计成较长指令。
指令编码非常紧凑:经常使用隐式操作数,使操作数不直接出现在二进制指令中。这样节省指令长度。
丰富的操作数寻址方式
8086CPU指令系统,它采用1~6个指令字节的变字长,包括操作码(第一字节)、寻址方式(第二字节)和操作数(第三到第六字节)三部分组成,指令格式如下:
指令定义了处理器要执行的操作。操作码通常位于第一字节,某些指令的操作码会扩展到第二字节(即ModR/M字节)的REG域,故有时候REG域也被称为REG/Opcode域,用来指出该域的两种用途。
绝大多数的指令的第一字节的高6个比特位(即BYTE1[7:2])是操作码,BYTE1[1]是D标志位,指明操作的方向,BYTE1[0]是W标志位,指示操作数的宽度。
| Mod编码(二进制)| 释义 |
| -------- | -------- | -------- |
| 00 | 存储器模式,无位移量字节;如果R/M=110,则有一个16位的位移量 |
| 01 | 存储器模式,8位位移量字节(1个字节) |
| 10 | 存储器模式,16位位移量字节(2个字节)存储器模式,16位位移量字节(2个字节) |
| 11 | 寄存器模式(无位移量) |
REG域(BYTE2[5:3],即寄存器域)用来指示一个寄存器,可以是源操作数,也可以是目的操作数,由第一字节的D标志位指示。具体编码格式如下:
R/M域(BYTE2[2:0],即寄存器/存储器域),用来指示另一个操作数,可以在存储器中,也可以在寄存器中。R/M域编码含义依赖于MOD域的设定。如果MOD=11(寄存器到寄存器模式),则R/M域标识第二个寄存器操作数。如果MOD是存储器模式(即00,01,10),则R/M指示如何如何计算存储器操作数的有效地址。
根据8086指令格式的形式,我们通过一个编码矩阵的形式来集中体现操作码(第一字节码)的对应编码。后续的第二~六字节因为涉及到不同的寻址方式、寄存器/存储器的选取以及立即数的值,这些都会导致每一条指令编码的不同,所以在设计指令格式时我们并未对后面字节进行详细编码,而是用抽象形式来体现后续字节。
ADD
Eb Gb
ADD
Ev Gv
ADD
Gb Eb
ADD
Gv Ev
ADD
AL Ib
ADD
AX Iv
PUSH
ES
POP
ES
ADC
Eb Gb
ADC
Ev Gv
ADC
Gb Eb
ADC
Gv Ev
ADC
AL Ib
ADC
AX Iv
PUSH
SS
POP
SS
AND
Eb Gb
AND
Ev Gv
AND
Gb Eb
AND
Gv Ev
AND
AL Ib
AND
AX Iv
XOR
Eb Gb
XOR
Ev Gv
XOR
Gb Eb
XOR
Gv Ev
XOR
AL Ib
XOR
AX Iv
INC
AX
INC
CX
INC
DX
INC
BX
INC
SP
INC
BP
INC
SI
INC
DI
PUSH
AX
PUSH
CX
PUSH
DX
PUSH
BX
PUSH
SP
PUSH
BP
PUSH
SI
PUSH
DI
(1)表格的列代表Opcode Byte的前4位,即Hi;行代表Opcode Byte的后4位,即Lo。
(2)在单元中每一个指令名称的下方都标有该指令所对应的寄存器和相应字长,部分指令还标有寻址方式。具体的寄存器的分类介绍请详见文档寄存器部分
GRP3a
TEST
Eb Ib
GRP3b
TEST
Ev Iv
Direct address. The instruction has no ModR/M byte; the address of the operand is encoded in the instruction. Applicable, e.g., to far JMP (opcode EA).
A ModR/M byte follows the opcode and specifies the operand. The operand is either a general-purpose register or a memory address. If it is a memory address, the address is computed from a segment register and any of the following values: a base register, an index register, a displacement.
The reg field of the ModR/M byte selects a general register.
Immediate data. The operand value is encoded in subsequent bytes of the instruction.
The instruction contains a relative offset to be added to the address of the subsequent instruction. Applicable, e.g., to short JMP (opcode EB), or LOOP.
The ModR/M byte may refer only to memory. Applicable, e.g., to LES and LDS.
The instruction has no ModR/M byte; the offset of the operand is encoded as a WORD in the instruction. Applicable, e.g., to certain MOVs (opcodes A0 through A3).
The reg field of the ModR/M byte selects a segment register.
JMP与CALL译码过程:
jmp word ptr [adr] -> jmp [adr], opbyte=2 -> ip = word(adr)
jmp dword ptr [adr] -> jmp [adr], opbyte=4 -> ip = word(adr), cs = word(adr + 2)
jmp cs:ip -> cs = cs ip = ip
jmp ip/reg -> ip = word(ip/reg)
call word ptr [adr] -> call [adr], opbyte=2 -> push ip, jmp [adr]
call dword ptr [adr] -> call [adr], opbyte=4 -> push cs, push ip, jmp [adr]
call cs:ip -> push cs, push ip, jmp cs:ip
call ip/reg -> push ip, jmp ip/reg
CMPSB
cmpsb
将DS:[SI]处的字节8位表示的值减去ES:[DI]处的字节8位表示的值,结果影响标志寄存器PSW,若方向标志位DF=0,则DI与SI均加1,若方向标志位DF=1,则DI与SI均减1
CMPSW
cmpsw
将DS:[SI]处的字16位表示的值减去ES:[DI]处的字16位表示的值,结果影响标志寄存器PSW,若方向标志位DF=0,则DI与SI均加2,若方向标志位DF=1,则DI与SI均减2
LODSB
lodsb
将DS:[SI]处的字节8位拷贝至AL,若方向标志位DF=0,则SI加1,若方向标志位DF=1,则SI减1
LODSW
lodsw
将DS:[SI]处的字16位拷贝至AX,若方向标志位DF=0,则SI加2,若方向标志位DF=1,则SI减2
STOSB
stosb
将ES:[DI]处的字节8位拷贝至AL,若方向标志位DF=0,则DI加1,若方向标志位DF=1,则DI减1
STOSW
stosw
将ES:[DI]处的字16位拷贝至AX,若方向标志位DF=0,则DI加2,若方向标志位DF=1,则DI减2
SCASB
scasb
将AL表示的值减去ES:[DI]处的字节8位表示的值,结果影响标志寄存器PSW,若方向标志位DF=0,则DI加1,若方向标志位DF=1,则DI减1
SCASW
scasw
将AX表示的值减去ES:[DI]处的字16位表示的值,结果影响标志寄存器PSW,若方向标志位DF=0,则DI加1,若方向标志位DF=1,则DI减1
REP MOVS/LODS/STOS
rep movsw
若CX≠0,则重复一下操作:1、movsw;2、CX减1
REPE CMPS/SCAS
repe cmpsw
若CX≠0,则重复一下操作:1、movsw;2、CX减1;3、若ZF不为1则退出循环
REPZ CMPS/SCAS
repz cmpsw
若CX≠0,则重复一下操作:1、movsw;2、CX减1;3、若ZF不为1则退出循环
REPNE
REPNE CMPS/SCAS
repne cmpsw
若CX≠0,则重复一下操作:1、movsw;2、CX减1;3、若ZF不为0则退出循环
REPNZ
REPNZ CMPS/SCAS
repnz cmpsw
若CX≠0,则重复一下操作:1、movsw;2、CX减1;3、若ZF不为0则退出循环
每个汇编器都有一套不同的伪指令,我们采用了MASM非简化的伪指令。
存储模型采用MASM 5.0支持的Small(小型)存储模型,所有代码在一个 64KB的段内,数据一般存储在其他64KB的段内(包括数据段、堆栈段和附加段)。
标号、内存变量名、子程序名和宏名等都是标识符。
汇编时,我们将标号、变量、段名分开处理。
数字标号(即变量):标识了变量的地址,为在代码中引用该变量提供了方便
代码标号:冒号(:)结尾,通常用作跳转和循环指令的目标地址
标号和变量的属性
段属性:所在段的段地址
偏移属性:段内偏移地址
类型属性:
标号:负数,近调用为-1,远调用为-2。
short短标号:标号在本段,距离在-128~+127之间
near近标号:标号在本段,距离在-32768~+32767之间
far远标号:当引用标号的指令和标号不在同一段
变量:正数,其值为每个数据项的字节数。
DB定义的专变量的类型值为1
DW定义的变量的类型值为2
DD定义的变量的类型值为4
与这3个属性相关的数值回送算符分别属是 SEG , OFFSET, TYPE
MOV AX, SEG X ; 将变量X所在的段地址送入AX
MOV BX, OFFSET Y ; 将变量Y的偏移地址送入BX
MOV CX, TYPE Z ; 将变量Z的类型值送入CX
具有语法检查机制,不符合标准的语法将报错(Compile Error)。
define ten bytes 其后每个数据10字,压缩 BCD数据分配存储单元,可分配 10 个字节,但最多 只能输入18 个数字,数据后面不需要加 "H"
DT 123456
duplicate 一般用来保留数据区, "DB 64 DUP(?)" 可为堆栈段保留 64 个字节
DB 3 DUP (0)
类型 PTR 变量 [ ± 常数表达式 ],类型为BYTE、WORD 、DWORD 、FWORD 、 QWORD 或 TBYTE
MOV AX,WORD PTR [BX]
LABEL
使变量具有不同的类型属性
VAL LABEL WORD
偶对齐伪指令。下面的内存变量从下一个偶地址单元开始分配
ALIGN
ALIGN Imm 对齐伪指令。Imm为2的幂。
ALIGN 2
ORG EXP 调整偏移量伪指令
ORG 100H
代表程序结束
ASSUME
将段与段寄存器对应起来
ASSUM CS:CODESG
SEGMENT ENDS
定义一个段
CODE SEGMENT
...
CODE ENDS
SHORT
指明标号在本段,距离在-128~+127之间
JMP SHORT S
指明标号在本段,距离在-32768~+32767之间
JMP NEAR PTR S
指明引用标号的指令和标号不在同一段
JMP FAR PTR S
返回变量或标号的段地址
MOV AL, SEG VAR
OFFSET
返回变量或标号的偏移地址
MOV AL, OFFSET VAR
返回变量或标号的类型
ADD AX, TYPE S
测试代码遵循 《汇编语言》王爽
一书中的格式
下面使用一段简单的汇编语言源程序来说明。
assume cs:codesg
codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H
int 21H
codesg ends
XXX segment
XXX ends
segment 和 ends 是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。segment 和 ends 的功能是定义一个段,segment 说明一个段开始,ends 说明一个段结束。一个段必须有一个名称来标识,使用格式为:
段名 segment
段名 ends
(2) end
end 是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令 end,就结束对源程序的编译。
(3) assume
这条伪指令的含义为“假设“。它假设某一段寄存器和程序中的某一个用 segment...ends
定义的段相关联。通过 assume 说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。
如上述程序中定义了一个名为 codesg 的段,在这个段中存放代码,所以这个段是一个代码段。在程序的开头,用 assume cs:codesg
将用作代码段的段 codesg 和 CPU 中的段寄存器 cs 联系起来
(4) db、dw、dd
当我们希望像 C 语言数组使用连续内存存储较多数据时,可以使用 db、dw、dd 指令。
使用方法为
db 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
其中 DB 定义字节型数据,DW 定义字型数据,DD 定义双字型数据。
(5) dup
dup 和 db、dw、dd 等数据定义伪指令配合使用,用来进行数据的重复。如:
db 3 dup (0,1,2)
定义了 9 个字节,他们是 0、1、2、0、1、2、0、1、2,相当于 db 0,1,2,0,1,2,0,1,2。
用汇编语言写的源程序,包括伪指令和汇编指令。源程序中的汇编指令组成了最终由计算机执行的程序,而伪指令是由编译器来处理的,它们并不实现我们编程的最终目的。这里所说的程序就是指源程序中最终由计算机执行、处理的指令或数据。
汇编源程序中,除了汇编指令和伪指令外,还有一些标号,如"codesg"。一个标号指代了一个地址。比如 codesg 在 segment 的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理成为一个段的段地址。
源程序是由一些段构成的。我们可以在这些段中存放代码、数据、或将某个段当作栈空间。
一个程序结束后,应该将 CPU 的控制权交还给使它得以运行的程序,我们称这个过程为: 程序返回。要实现程序返回,应该在程序的末尾添加返回的程序段。如上述源程序中
mov ax,4c00H
int 21H
它们实现了安全退出程序的功能。
一般来说,程序在编译时被编译器发现的错误是语法错误。
在源程序编译后,在运行时发生的错误是逻辑错误。
本次实验的测试用例采用8086汇编语言编写,伪指令包含MASM5.0核心指令,采用Small存储模型。我们首先设计出斐波那契数列汇编程序和冒泡排序汇编程序两个基本测试用例,并在此基础上设计了数据传送类、测试指令类、算术类、字符串类、综合类、查找类、基本要求测试用例类等汇编程序。
数据传送类(Data_trandfer)
值得注意的是,8086不支持 L1 或者 L2 cache memory。但为了更真实地模拟cpu运行,我们将cpu中的一级缓存、二级缓存等抽象为一个cpu类下的指令缓存器:cache memory。
我们假设只有一条cache line(大小为64KB),让cache memory存入已经载入内存中的程序段。
8086系统的时钟频率为4.77MHz~10MHz,每个时钟周期约为200ns。我们通过sleep函数降低时钟频率,每个周期均sleep一定时间以观察cpu运行细节。
8086处理器的流水线超级简单,只有取指和执行两级。BIU(Bus Interface Unit)单元负责取指,EU(Execution Unit)单元负责指令译码。故而划分取指周期T1和执行周期T2:
取值周期(控制指令流)
执行周期(控制数据流):译码
It generates the 20 bit physical address for memory access.
It fetches instructions from the memory.
It transfers data to and from the memory and I/O.
Maintains the 6 byte prefetch instruction queue(supports pipelining).
Pre-fetches up to 6 instructions(8086最长指令为6字节) in advance。
我们这里按照假设每次pre-fetch 6条指令。
当EU执行转移类指令时,指令队列立即清空,BIU又重新开始从内存中取转移目标处的指令代码送往指令队列。
BIU fills in the queue until the entire queue is full.(6 byte FIFO)
BIU restarts filling in the queue when at least two locations of queue are vacant.
Pipelining:Fetching the next instruction (by BIU from CS) while executing the current instruction 。
Gets flushed whenever a branch instruction occurs.
Rules of Segmentation Segmentation process follows some rules as follows:
The starting address of a segment should be such that it can be evenly divided by 16.
Minimum size of a segment can be 16 bytes and the maximum can be 64 kB.
我们假设4个段长度均为最大长度64kB(10000H),4个段默认分布如上图。存储器对应关系如下:
It is a 16 bit register. It holds offset of the next instructions in the Code Segment.
IP is incremented after every instruction byte is fetched.
IP gets a new value whenever a branch instruction occurs.
CS is multiplied by 10H to give the 20 bit physical address of the Code Segment.
Address of the next instruction is calculated as CS x 10H + IP.
The BIU has a Physical Address Generation Circuit.
It generates the 20 bit physical address using Segment and Offset addresses using the formula:
Physical Address = Segment Address x 10H + Offset Address
增加了一个指令寄存器IR用于存放当前指令。
Performs 8 and 16 bit arithmetic and logic operations
The instruction decoder decodes instruction in IR and sends the information to the control circuit for execution.
对指令进行分类,调用对应模块执行。
AX、BX、CD、DX
SP、BP、SI、D
The EU fetches an opcode from the queue into the instruction register.
6 Status flags:
carry flag(CF)
parity flag(PF)
auxiliary carry flag(AF)
zero flag(Z)
sign flag(S)
overflow flag (O)
3 Control flags:
trap flag(TF)
interrupt flag(IF)
direction flag(DF)
流水线优化
《Intel Software Developer’s Manual》
《深入理解计算机系统》
《汇编语言》
《编译原理》
《x86汇编语言 从实模式到保护模式完整版》
https://codegolf.stackexchange.com/questions/4732/emulate-an-intel-8086-cpu
http://www.c-jump.com/CIS77/CPU/x86/index.html
https://fms.komkon.org/EMUL8/HOWTO.html
https://www.swansontec.com/sintel.html
http://ref.x86asm.net/coder32.html
https://wiki.osdev.org/X86-64_Instruction_Encoding