汇编语言之实模式
基础知识
在进入主题前,先总结下关于二进制的一些知识点,我个人认为这非常的令人困惑:
- 1 bit, 指1位,0或者1
- 1 byte, 指1个字节,有8位
- 1 word, 指1个字,有2个字节,16位
- 1 dword, 指1个双字,有4个字节,32位
- 1 qword, 指1个四字,有8个字节,64位
8086 处理器
8086是 Intel 公司的第一款16位处理器,诞生于1978年。(对没错,下面的知识源于上个世纪70年代)
通用寄存器
8086处理器内部有8个16位通用寄存器,如下:
- AX
- BX
- CX
- DX
- SI
- DI
- BP
- SP
其中,AX
, BX
, CX
, DX
又可以各自拆成2个8位的寄存器来使用:
|
|
段
内存对于软件来说可以认为是一个由0和1构成的线性的表,而处理器简单来说会从内存中读取内容并执行指令。这就会造成一个问题,也就是程序与数据的二义性,或者说在没有上下文的情况下给你一段内存中的内容,你不知道这到底是数据还是指令。
我们可以将数据和指令分开各自存放在一块连续的区域,这样处理器只要知道指令区块的起始地址,便可以一直执行下去。而如果需要拿到某个具体的数据或者跳转到一个特定的指令,只需要给出其“段起始地址:段偏移量”即可。
8086内部有4个段寄存器,分别为:
- CS (Code Segment) 用来指向代码段的起始地址
- DS (Data Segment) 用来指向数据段的起始地址
- ES (Extra Segment) 附加段,用来指向额外的数据段,在大程序中可能会有用
- SS (Stack Segment) 用来指向栈段的起始地址
栈段
栈是一个具有后进先出(LIFO)的数据结构,只能从一端进行入栈(push)和出栈(pop)操作,具体细节不赘述。首先有一点要明确的是,栈段和其他的段本质上没有区别,栈只是人为创造的概念,而下面的 push
和 pop
指令也只是处理器为了配合这一概念所作出的支持,你仍然可以对栈段中的任意位置进行访问,这两个指令可以看作是处理器操作栈的 shortcuts。
定义栈需要初始化栈寄存器 SS
和栈指针 SP
。
当我们使用 push
指令时,本质上是把操作数的内容存到了栈顶,相当于暂存了他的内容。以下指令是等价的:
|
|
|
|
当我们使用 pop
指令时,本质上是把当前栈顶中的内容赋给了操作数。以下指令的意图是等价的:
|
|
|
|
寻址
需要注意的是,8086处理器可访问最大1MB的内存,也就是 2^20 字节,但是它的段寄存器和指令指针寄存器(IP
)都是16位的,也就是最大只能访问 2^16 字节。为了实现这一目标,处理器在形成物理地址时,会先将16位段寄存器的内容左移4位,然后再和16位的偏移地址相加,便形成了20位的物理地址。
寄存器寻址
操作执行时,操作的数在寄存器中
|
|
立即寻址
操作执行时,操作的数是一个立即数
|
|
内存寻址
操作执行时,操作的数是一个偏移地址
|
|
基址寻址
在指令的地址部分使用基址寄存器 BX 或者 BP 来提供偏移地址
|
|
基址寄存器也可以使用 BP。但与上面的例子不同的是,在形成20位的物理地址时,段寄存器使用的 SS 而不是默认的 DS。这意味着它常用于访问栈中的内容。
基址寻址还允许在基址寄存器上使用一个偏移量。
|
|
处理器会将段寄存器(bx 时是 DS, bp 时是 SS)左移4位加上基址寄存器中的值并再加上或减去偏移量。
变址寻址
变址寻址与基址寻址类似,唯一的不同在于它使用的是变址寄存器 SI 或 DI, 而不是 基址寄存器 BX 或 BP。
基址变址寻址
基址变址寻址的操作数可以使用一个基址寄存器 (BX 或 BP), 外加一个变址寄存器 (SI 或 DI), 基本形式为:
|
|
可以将它类比为 C 语言中指针(基址寄存器)和偏移量(变址寄存器)的运算。
用户程序的加载与运行
现在我们知道,一个在内存中的程序,是分为很多个区块的(段),而处理器的一系列段寄存器保存了各个段的基本信息(段起始地址)。但是,程序不可能一直存在于内存中,为了执行它,我们一般要首先将它从硬盘上加载到内存中。而在刚加载完成后,我们的段寄存器是没有这个程序的相关信息的,也就是说我们分不清这一大串0和1到底那一块是代码那一块是数据,也不知道从哪里开始执行。为此,我们可以约定一个程序的前N字节为程序头,而其中就包含了我们所需要的基本信息。一个示范的用户头如下:
|
|
注意这并不一定就真实世界用户程序的对应格式,只是为了讲解!
外部设备的访问
处理器通过总线与各种 IO 设备交流。总线 (Bus) 可以认为是一排电线,所有的外围设备如键盘,显示器等包括处理器都连在总线上,而输入输出控制设备集中器( I/O Control Hub)则负责管理总线,决定哪个外围设备与处理器进行通信。
外围设备和处理器之间的通信是通过相应的 I/O 接口进行的。具体地说,处理器是通过端口(port)来和外围设备打交道的。本质上,端口就是一些寄存器,不过位于 I/O 接口电路中。
端口在不同的计算机系统中有不同的实现方式。在一些计算机系统中,端口号被映射到内存地址空间中。比如,0x00000 ~ 0xe0000 为真实内存地址,而 0xe0001 - 0xfffff 为 I/O 端口。而在另一些计算机系统中,端口是独立编址的,同时通过一个引脚来控制当前访问的是真实内存地址还是 I/O 端口。
下面通过独立编址,以个人计算机 PATA/SATA 接口为例,简要说明访问 I/O 端口的过程:
PATA/SATA 接口用于访问硬盘,有好几个端口,分别为命令端口,状态端口,参数端口和数据端口。而 ICH 芯片(一般意义上的南桥)通常集成了两个接口,分别为主硬盘和副硬盘。主硬盘分配的端口号是0x1f0~0x1f7,副硬盘分配的端口号是0x170~0x177。
读数据:
|
|
in 指令的目的操作数必须是寄存器 AL
或者为 AX
,当访问8位端口时,使用寄存器 AL
, 访问16位端口时使用 AX
。注意我们只能访问0~255号端口,下面指令时无效的!
|
|
写数据:
|
|
out 指令的要求与 in 类似,不在赘述。
中断
以通俗的话解释中断,就是在一些事件被触发后,处理器暂停当前的任务,并跳转到对应事件的处理程序中。当处理完毕时,再跳转回来之前的状态继续执行。
这些事件具体有哪些我们不做过多讨论,我们主要关注下处理器是如何处理这些中断的,因为某些情况下我们可能会想要复写这些中断处理程序。
实模式下的中断向量表 (Interrupt Vector Table)
处理器可以识别256个中断,理论上就有256段处理程序。我们在物理地址起始处,即 0x00000 处开始的1KB空间内设置一个中断向量表,表有256项,每一项占2个字,即4个字节,刚好1KB。表的索引即为对应的中断号,而表项中的内容即为对应中断处理程序的实际地址。这样,当发生对应中断时,处理器就可以通过中断向量表找到对应中断处理程序的实际地址,并跳转过去执行。
中断发生的过程
- 保护中断的现场。首先将标志寄存器压栈,并清除它的 IF 位和 TF 位。然后将代码寄存器 CS 和指令指针寄存器 IP 压栈。
- 执行中断处理程序。将终端号乘以4就得到了中断处理程序在表中的偏移地址。将中断程序的偏移地址和短地址分别传送给 IP 和 CS, 开始处理中断。
- 返回中断点继续执行。当执行到中断处理程序的最后一条指令 iret 时,处理器从栈中弹出 IP, CS 和 标志寄存器原本的内容,自此恢复执行。