目录

1. 离开主引导扇区

1.1 主引导扇区的一般用途

1.2 改造主引导扇区

2. 给汇编程序分段

2.1 汇编程序分段概述

2.2 汇编程序分段指令

2.3 段的汇编地址

2.3.1 默认的段汇编地址

2.3.2 指定段的对齐方式

2.3.3 获取段的汇编地址

2.4 段内汇编地址

2.4.1 默认段内汇编地址

2.4.2 指定段内汇编地址

3. 加载器与用户程序头部段

3.1 加载器与用户程序头部的关系

3.2 用户程序头部段包含的信息

3.2.1 程序头部段

3.2.2 用户程序总长度

3.2.3 应用程序入口点

3.2.4 段重定位表

3.2.5 程序头部最终布局

3.3 加载器的工作流程

3.3.1 流程概述

3.3.2 确定用户程序在硬盘上的位置

3.3.3 确定用户程序在内存中的加载位置

4. 外围设备及其接口

4.1 外围设备概述

4.2 IO接口概述

4.2.1 基于总线通信

4.2.2 南桥芯片

4.3 IO端口与端口访问

4.3.1 IO端口的实现

4.3.2 端口访问指令

4.4 实例:通过硬盘控制器端口读取扇区数据

4.4.1 块设备

4.4.2 硬盘访问模式

4.4.3 硬盘端口号

4.4.4 读取扇区步骤

5. 过程调用

5.1 将读取硬盘的功能抽象为过程

5.1.1 过程功能

5.1.2 过程调用点说明

5.1.3 参数与返回值的传递

5.1.4 运行现场保护

5.2 过程调用与返回原理

5.2.1 转移类指令概述

5.2.2 过程调用与返回实现

5.3 8086过程调用方式

5.3.1 16位相对近调用

5.3.2 16位间接绝对近调用

5.3.3 16位直接绝对远调用

5.3.4 16位间接绝对远调用

6. 用户程序重定位

6.1 加载整个用户程序

6.2 程序段的重定位

6.2.1 用户程序布局

6.2.2 为何需要重定位

6.2.3 重定位过程实现

6.2.4 移位指令

6.2.5 逻辑段地址回填

6.2.6 重定位上机验证

6.3 将控制权交给用户程序

6.4 8086无条件跳转指令汇总

6.4.1 相对短转移和16位相对近转移

6.4.2 16位间接绝对近转移

6.4.3 16位直接绝对远转移

6.4.4 16位间接绝对远转移

6.4.5 通过retf切换到另一个代码段

7. 用户程序工作流程

7.1 初始化段寄存器

7.2 put_string过程分析

7.2.1 put_string过程主体流程

7.2.2 文本显示相关控制

7.2.3 put_char过程分析


1. 离开主引导扇区

1.1 主引导扇区的一般用途

① 在将操作系统安装到硬盘的过程中,除了将操作系统的指令和数据写入硬盘,通常还要更新主引导扇区的内容

② 此时主引导扇区将作为跳板,用于加载并启动操作系统

1.2 改造主引导扇区

将我们的主引导扇区程序改造为一个加载器,用于加载并执行用户程序,因此需要完成如下任务,

① 从硬盘读取用户程序并加载到内存

② 程序通常是分段的,载入内存后需要重新计算段地址,即做段的重定位

③ 将处理器的控制权交给用户程序

2. 给汇编程序分段

2.1 汇编程序分段概述

① 8086处理器的工作模式就是将内存分成逻辑上的段,指令的获取和数据的访问一律按[段地址 : 偏移地址]的方式进行

② 相应地,一个规范的汇编程序,应当包括代码段、数据段、附加段和栈段,这样一来,段的划分和段与段之间的界限在程序加载到内存之前就已经准备好

典型的汇编语言程序布局如下图所示,

说明:由于8086实模式下的段长上限为64KB,如果汇编程序中段的长度超过64KB,就需要拆分为多个段

2.2 汇编程序分段指令

NASM汇编器使用SECTION或者SEGMENT指令来定义段,格式如下,

SECTION 段名称
SEGMENT 段名称

说明1:段名称主要用来引用一个段,NASM对一个程序中段的数量没有限制,但是一个程序中的段名称不可重复

说明2:NASM汇编器不关心也不知道段的用途,段只是用来分隔程序中的不同内容

说明3:一旦定义段,其后面的内容就都属于该段,除非又出现了另一个段的定义

说明4:如果程序中没有段定义语句(例如之前章节的主引导扇区程序),则程序内容默认地自成一个段

2.3 段的汇编地址

2.3.1 默认的段汇编地址

假设程序中有如下段定义,

我们通过编译后的二进制文件来分析各段的汇编地址,通过两条mov指令的机器码,可以分析出mydata & string标号的汇编地址,也就是data1 & data2段的汇编地址

① data1段的汇编地址为0x0

② data2段的汇编地址为0x4

③ code段的汇编地址为0xc

结论:段的汇编地址,是段相对于程序起始处的偏移量,在32 & 64位处理器上,这个偏移量默认按4B对齐

为了实现对齐,汇编器用0在段间进行填充

说明:段的汇编地址就是段内第一个元素(数据或指令)的默认汇编地址

2.3.2 指定段的对齐方式

8086处理器要求段在内存中的起始物理地址至少为16B对齐,这样才好计算段地址。相应地,在程序的段中,也可以指定段的对齐方式

此时需要使用align子句,我们在上一节的程序中增加段的对齐设置,

同样分析编译后的二进制文件,

① data1段的汇编地址为0x0

② data2段的汇编地址为0x10

③ code段的汇编地址为0x20

可见程序中各段的汇编地址均实现了16B对齐

2.3.3 获取段的汇编地址

为了便于获取段的汇编地址,NASM汇编器提供了如下表达式,

section.段名称.start

我们使用如下示例进行验证,

2.4 段内汇编地址

2.4.1 默认段内汇编地址

根据上节的验证,尽管定义了段,但是引用某个标号时,该标号的汇编地址依然是从整个程序起始处计算的,而不是从段的起始处计算的

2.4.2 指定段内汇编地址

通过vstart子句,可以设置段内元素(指令和数据)的汇编地址计算基准,我们使用如下示例进行验证

① 段的偏移地址没有改变,依然相对于程序起始处计算

② 段内元素汇编地址根据VSTART子句的设置进行计算,之所以称VSTART子句是设置基准,是因为该值不一定为0

比如示例中data1段的mydata标号,汇编地址就是从VSTART子句指定的0x100开始计算

说明1:程序中tail段没有使用VSTART子句指定段内汇编计算基准,因此program_end标号的汇编地址要从整个程序起始处计算

因为他是整个程序中的最后一行,所以他的汇编地址从数值上,就是整个程序的大小(以字节计算)

说明2:没有标号就没有对汇编地址的引用,因为标号就是汇编地址的符号化表示

3. 加载器与用户程序头部段

3.1 加载器与用户程序头部的关系

① 主引导扇区一定会被执行,但是主引导扇区太小,功能有限,所以一般用作跳板,加载和引导后续程序执行

② 将用户程序也存储到硬盘中,将主引导扇区改造成一个加载器,他的功能就是加载并执行用户程序

③ 一般来说,加载器和用户程序是在不同时间、不同地点、由不同的人开发的,彼此互为黑盒

④ 为了协调加载器与用户程序的工作,加载器必须了解一些必要的信息,足以知道如何加载用户程序

⑤ 加载器与用户程序之间确定的协议,必须包含在应用程序的固定位置,加载器则固定从该位置读取信息

协议信息一般位于用户程序头部

3.2 用户程序头部段包含的信息

3.2.1 程序头部段

① 用户程序头部信息一般以一个段的形式出现在程序中

② 因为是头部,所以必须是第一个被定义的段,且总是位于程序的起始处

下图为课程配套示例程序的头部段,

下面以该程序头部段为例,说明程序头部至少要包含的信息

3.2.2 用户程序总长度

① 用户程序总长度使用program_end标号的汇编地址表示,根据上文,由于最后定义的trail段没有使用VSTART子句,所以该段段内汇编从程序起始处计算

② 加载器根据用户程序总长度,计算需要读取的扇区数

③ 保存用户程序总长度使用了dd伪指令,即一个32位的数据,确保可以容纳

3.2.3 应用程序入口点

① 所谓应用程序入口点(Entry Point),就是用户程序要执行的第1条指令的位置,加载器需要知道该位置,以便在最后跳转到该位置开始执行用户程序

② 应用程序入口点包括段地址和偏移地址,其中偏移地址使用dw声明(2B)在实模式下已经足够,因为8086的段内偏移地址就是16位。段汇编地址则使用dd声明,也就是4B

说明1:使用32位保存程序入口点段汇编地址

① 需要特别注意,此处保存的是段汇编地址,也就是基于程序起始处计算的段的偏移地址,因此在一个稍大型的汇编程序中,超过64KB是很正常的

② 后续将看到,当加载器对段进行重定位之后,此处将被替换为该段所在物理内存的逻辑段地址,这个逻辑段地址只有16位,这是8086体系结构确定的

说明2:此处默认是将用户程序一次性全部加载到内存中,所以在实模式下,用户程序的总长不能超过当前可用的内存数量

3.2.4 段重定位表

① 段重定位表中记录的也是段的汇编地址

② 段的重定位是加载器的工作,因此加载器需要知道用户程序包含多少个段,以及每个段在用户程序内的位置

③ 用户程序可以包含多个段,当程序被加载到内存后,加载器需要确定每个段被加载的物理地址,并据此计算出逻辑段地址(这就是段重定位的过程)

④ 段重定位表的项数是通过计算动态得到的

3.2.5 程序头部最终布局

程序头部段的最终布局如下图所示,其中每格代表1B

3.3 加载器的工作流程

3.3.1 流程概述

① 读取用户程序的起始扇区,获取用户程序头部信息

② 根据用户程序头部信息,将整个用户程序读入内存

③ 计算段的物理地址和逻辑段地址,即段的重定位

④ 跳转到用户程序执行,即将处理器的控制权交给用户程序

3.3.2 确定用户程序在硬盘上的位置

在示例程序中,规定用户程序在硬盘上的起始逻辑扇区号为100,我们使用equ伪指令定义该常数

说明1:使用equ伪指令声明的数值不占用任何汇编地址,也不在运行时占用任何内存单元,他仅仅代表一个数值,和C语言中的宏定义类似

说明2:示例程序中主引导扇区使用VSTART子句,将段内元素汇编地址的计算基准设置为0x7C00

这是因为BIOS将主引导扇区加载到[0x0000 : 0x7C00]处运行,通过上述设置即实现了编译时汇编地址和运行时偏移地址的统一

3.3.3 确定用户程序在内存中的加载位置

3.3.3.1 可用的物理内存空间

① 根据8086的memory map,0x0000 ~ 0x9FFFF共640KB的空间用于访问DRAM

② 内存中0x00000 ~ 0x0FFFF的范围,被用于加载主引导扇区,并且设置了主引导扇区程序所使用的栈

③ 因此,可以使用0x10000 ~ 0x9FFFF共576KB的空间加载用户程序

说明:事实上,如果将低端的内存空间合理安排,可以释放出更多空间,只是在本课程中没有必要

3.3.3.2 计算加载用户程序的逻辑段地址

说明1:加载用户程序的物理地址确定为0x10000,该地址位于可用物理内存范围内,且为16B对齐,便于计算逻辑段地址

使用双字单元phy_base来存储该物理地址,因为只有32位的空间才能容纳20位物理地址

说明2:计算加载用户程序的逻辑段地址,就是将物理地址除以16,此处使用无符号除法

除法运算后,商在AX中,该值即是加载用户程序的逻辑段地址,我们将DS & ES指向该段,之后便开始读取用户程序头部

说明3:因为phy_base位于代码段,在取出phy_base存储的物理地址时,使用了段超越前缀

说明4:为何不用equ伪指令声明加载用户程序的物理地址 ?

在声明应用程序在硬盘上的位置时,使用了equ伪指令,为何此处不使用equ伪指令,而是使用dd伪指令预留并初始化内存呢 ?

这是因为此处加载用户程序的物理地址超过了16位,如果使用equ伪指令声明,在实模式下无法用16位的寄存器处理

4. 外围设备及其接口

4.1 外围设备概述

① 外围设备(Peripheral Equipment)种类繁多,一般可分为输入设备和输出设备(IO设备)

② 不同种类外围设备的工作原理、通信标准、接线方式等都不相同,但是却需要统一纳入处理器的管理

③ 因此需要一种转换器,这就是IO接口

a. 在处理器端,按处理器的信号规程工作,将处理器的信号转换成外围设备能接受的信号

b. 在设备端,按设备的信号规程工作,将设备的信号转换成处理器能接受的信号

④ IO接口的实现,依据复杂程度不同,可以是一个电路板,也可能是一块芯片

4.2 IO接口概述

4.2.1 基于总线通信

由于外围设备众多,因此不可能将所有IO接口直接和处理器连接,而是通过总线(Bus)实现连接

4.2.2 南桥芯片

① 由于总线是公用的,因此设备不能同时与处理器通信,这就需要一块芯片来连接不同的总线,并协调各个IO接口对处理器的访问

② 这种芯片称作输入输出控制设备集中器(IO Controller Hub,ICH)芯片,在PC上,就是所谓的南桥芯片

③ 处理器通过局部总线连接到ICH内部的处理器接口电路,之后在ICH内部,又通过总线与各个IO接口相连

说明:设备总线

因为同类型的设备也很多,也涉及线路复用和仲裁的问题,因此也有自己的总线体系,称为通信总线或设备总线(e.g. USB总线,SATA总线)

4.3 IO端口与端口访问

4.3.1 IO端口的实现

① 处理器通过端口(Port)与外围设备通信,本质上端口就是一些寄存器

端口寄存器与处理器内部寄存器的不同之处在于端口寄存器位于IO接口电路中

② 根据不同的目的,一个IO接口可以拥有多个端口,以SATA接口为例,有如下4种端口,

a. 命令端口:处理器通过命令端口向外围设备发送命令

b. 状态端口:处理器通过状态端口判断外围设备工作状态

c. 参数端口:处理器通过参数端口向外围设备发送操作命令所需的参数

d. 数据端口:处理器通过数据端口读写外围设备数据

③ 根据不同的体系结构,对端口的寻址有2种方式,

a. 端口统一编址,即将端口号映射到内存地址空间

b. 端口独立编址

说明1:Intel处理器早期使用端口独立编址,现在既有内存映射也有独立编址

说明2:独立编址的端口都是统一编号的,在Intel系统中,允许65536个端口

说明3:处理器访问内存时使用的地址和访问端口时使用的端口号,都是通过地址总线传递的,并通过M/IO#引脚进行区分,以使得二者互不干扰

访问过程的控制通过控制总线实现;数据的读写通过数据总线实现

说明4:处理器可以直接读写以下3个地方的数据,

① 处理器内部的寄存器

② 内存单元

③ 端口

4.3.2 端口访问指令

对于独立编址的端口,不能使用类似mov的指令访问,而是使用专门的in & out指令,格式如下,

in al/ax, dx/imm8
out dx/imm8, al/ax

说明1:端口宽度

端口有自己的数据宽度,在早期的系统中,端口可以是8位的,也可以是16位的,现在有些端口是32位的

具体宽度,由IO接口制造者决定;较长的宽度有助于加快数据传输速率

说明2:端口号的指定

① 使用DX寄存器存储要访问的端口号

② 如果要访问的端口号小于256,可以使用8位立即数指定端口号

说明3:与端口交互数据寄存器的指定

① 访问8位端口时使用AL寄存器

② 访问16位端口时使用AX寄存器

说明4:in & out指令不允许使用其他通用寄存器,也不允许使用内存单元作为操作数

说明5:in & out指令不影响任何标志位

4.4 实例:通过硬盘控制器端口读取扇区数据

4.4.1 块设备

硬盘读写的基本单位是扇区,因此处理器与硬盘之间的数据交换是成块的,所以硬盘是典型的块设备

4.4.2 硬盘访问模式

① CHS模式

向硬盘控制器分别发送磁头号、柱面号和扇区号,用于定位要访问的扇区

② LBA模式

将所有扇区统一编址,向硬盘控制器发送逻辑扇区号,用于定位要访问的扇区

说明:LBA28与LBA48模式

① LBA28是最早的逻辑扇区编址方式,使用28位表示逻辑扇区号,每个扇区512B,因此可以管理128GB的硬盘(512 * (0xFFFFFFF + 1 ))

② 随着硬盘技术的发展,LBA28模式所能管理的硬盘已经落后,因此业界推出了LBA48模式,即使用48位表示逻辑扇区号

本章仍使用LBA28模式

4.4.3 硬盘端口号

① 每个PATA和SATA接口分配了8个端口

② ICH芯片内部通常集成两个PATA/SATA接口,分别是主硬盘接口和从硬盘接口,其中主硬盘接口分配的端口号为0x1F0 ~ 0x1F7;从硬盘接口分配的端口号为0x170 ~ 0x177

③ 以主硬盘接口为例,说明各端口的功能

4.4.4 读取扇区步骤

4.4.4.1 设置要读取的扇区数量

说明1:设置要读取扇区数的0x1f2端口为8位端口,因此只能读写255个扇区。需要注意的是,如果写入该端口的值为0,则表示要读写256个扇区

说明2:后续每读取一个扇区,该端口的数值减一,因此如果在读写过程中发生错误,该端口包含着尚未读取的扇区数

4.4.4.2 设置起始LBA扇区号

假设DI : SI存储起始逻辑扇区号

说明:设置起始LBA扇区号的端口均为8位端口,因此LBA28模式需要4个端口设置,其中最后一个端口多余4位,在实现中用于指定硬盘及其访问模式,如下图所示,

之所以要指定主硬盘 / 从硬盘,是因为每个PATA/SATA接口允许挂接2块硬盘,分别是主盘(Master)和从盘(Slave)

4.4.4.3 发送读硬盘命令

4.4.4.4 等待硬盘操作完成

说明:0x1f7端口既是命令端口,又是状态端口,在通过该端口发送读写命令之后,就可以轮询该端口,确定硬盘操作是否完成,该端口状态位如下图所示,

4.4.4.5 连续取出数据

假设DS : BX存储目标缓冲区地址

说明1:0x1f0为硬盘的数据端口,是一个16位端口,因此每次读取2B,共读取256次,即可读取一个扇区

说明2:最后还有一个0x1f1端口,该端口为错误寄存器,包含硬盘驱动器最后一次指令命令后的状态(错误原因)

5. 过程调用

5.1 将读取硬盘的功能抽象为过程

由于读取硬盘是一个通用功能,且在本章程序中会被反复调用,因此可以将其抽象为一个过程,从而实现汇编语言中的模块化设计

5.1.1 过程功能

① 将硬盘中一个指定扇区的内容读取到指定内存地址处

② 指定扇区的逻辑扇区号由DI : SI提供

③ 指定内存地址由DS : BX提供

说明:实现为过程的好处是可以反复调用,如下图中黄色部分

5.1.2 过程调用点说明

① 读取用户程序的第1个扇区

② 读取用户程序的剩余扇区

可见在调用read_hard_disk_0过程前已经设置好DS段寄存器

5.1.3 参数与返回值的传递

① 过程一般都要根据提供的参数处理一定的事务,处理后,将结果提供给调用者

② 用寄存器来存储参数和结果是最常用最简单的方法,如本节示例

③ 由于寄存器数量有限,一种通用的方法是用栈来传递参数

④ 如果需要批量传递数据到过程中(e.g. 向过程传递要处理的字符串,可能很长,且每次调用不定长),可以将批量数据存放在内存中,然后将他所在内存的首地址存放在寄存器中传递给过程

对于具有批量数据的返回结果,也可用同样的方法

5.1.4 运行现场保护

① 被调用的过程中使用的寄存器,可能调用者也在使用。如果不加处理,过程中对寄存器的修改,在过程返回后,就会影响调用者的执行

② 解决这个问题的方法就是在过程起始处将要用到的寄存器压栈保存,在子程序返回前出栈恢复

因此过程的标准框架如下,

过程标号:过程中使用的寄存器入栈过程内容过程中使用的寄存器出栈返回(ret / retf)

说明:注意寄存器进出栈的顺序

5.2 过程调用与返回原理

5.2.1 转移类指令概述

① 可以修改IP,或者同时修改CS和IP的指令,统称为转移指令

② 8086的转移指令分为如下几类,

a. 无条件转移指令(jmp)

b. 条件转移指令(e.g. jnz)

c. 循环指令(loop)

d. 过程调用(call)

e. 中断

这些转移指令转移的前提条件不同,但是转移的基本原理是相同的

说明1:段内转移与段间转移

① 只修改IP时,称为段内转移。根据对IP的修改范围不同,段内转移又分为短转移和近转移

② 同时修改CS和IP时,称为段间转移

说明2:call指令与jmp指令非常类似,只是jmp指令不将CS、IP压栈

5.2.2 过程调用与返回实现

过程的调用与返回,依靠栈实现,以本节read_hard_disk_0过程为例,

5.2.2.1 过程调用

使用call指令调用过程时(此时为相对短调用),

① 将当前IP寄存器中的值压栈

② 修改IP,跳转到过程执行

说明1:在执行call指令时,IP指向call指令的下一条指令,这正是过程的返回地址,因此call指令压栈的是过程的返回地址

说明2:call指令不影响任何标志位

5.2.2.2 过程返回

使用ret指令过程返回时,

① 出栈一个字到IP

说明1:由于之前call指令压栈的是过程返回地址,ret指令将其出栈并装入IP,即可实现过程返回

说明2:尽管ret / retf指令通常与call指令配对使用,但是ret / retf指令的执行并不依赖call指令,这里就涉及对ret / retf指令本质的理解

Linux 0.11内核代码head.s中就使用该特性,跳转到main函数执行

说明3:ret指令本质上只是将当前栈顶的字出栈到IP,因此在过程中必须注意栈的使用与恢复,确保在过程返回时,栈顶元素为返回地址

说明4:ret / retf指令不影响任何标志位

5.3 8086过程调用方式

5.3.1 16位相对近调用

call [near] 标号或目标处汇编地址示例:
call read_hard_disk_0
call 100

① 近调用:段内调用

② 相对:基于IP计算相对偏移量,由于是近调用,使用2B编码,偏移量范围为-32768 ~ 32767B

说明1:此处使用标号和使用汇编地址本质一样,因为在汇编阶段,汇编器会将标号转换为汇编地址的数值

说明2:关键字near不是必须的,如果call指令中没有提供任何关键字,编译器将其视为近调用

5.3.2 16位间接绝对近调用

call [near] r / m示例:
call bx
call [0x3000]

① 近调用:段内调用

② 间接:目标地址通过寄存器或内存给出

③ 绝对:目标地址不是基于IP的偏移量,而是被调用过程的真实偏移地址

说明1:这里的关键字near同样可以省略

说明2:由于间接绝对近调用的机器码操作数是16位的绝对地址,因此他可以调用当前代码段任何位置处的过程

5.3.3 16位直接绝对远调用

call 16位段地址 : 16位偏移地址示例:
call 0x0000 : 0x7C00

① 远调用:可以调用到另一个代码段内的过程(注意,这里是"可以",而不是"必须")

② 直接:目标地址直接在指令中以立即数的形式给出

③ 绝对:目标地址为绝对地址

说明1:执行16位直接绝对远调用时,先将CS压栈,再将IP压栈,之后用指令中的段地址和偏移地址设置CP & IP实现跳转

说明2:与远调用配对使用的过程返回指令为retf指令,他从栈中先后出栈2个字到IP和CS寄存器,实现过程返回

5.3.4 16位间接绝对远调用

call far m示例:
call far [proc_1]proc_1 dw 0x7C00, 0x0000 ;偏移地址, 段地址

① 远调用:可以调用到另一个代码段内的过程

② 间接:目标地址通过内存给出

③ 绝对:目标地址为绝对地址

说明1:此处的关键字far不能省略,否则汇编器将理解为16为间接绝对近调用

说明2:在指令索引的内存处,有2个字,其中低字为偏移地址,高字为段地址

6. 用户程序重定位

6.1 加载整个用户程序

之前仅仅读取了用户程序的第1个扇区,下面将根据第1个扇区中的头部信息解析用户程序的总长度,并将整个用户程序加载到内存中

说明1:计算还需要读取的扇区个数

因为之前已经读取了1个扇区,此处需要将其减去

说明2:读取目标缓冲区地址的设置

在read_hard_disk_0过程中,使用DS : BX表示读取扇区时的目标缓冲区地址,我们分析一下这2个寄存器在读取第1个扇区过程中的状态

① 读取第1个扇区之前

[DS : BX] = [0x1000 : 0x0000]

② 读取第1个扇区之后

[DS : BX] = [0x1000 : 0x0200]

在后续的读取过程中,如果保持DS寄存器的值不变,仅依靠递增BX寄存器的值是不可行的。因为BX可寻址的范围只有64KB,如果用户程序超过64KB,则无法索引

因此在实现中,在每次读取1个扇区之后,将DS寄存器的值加0x20,也就是将段地址递增512B,以此来索引目标缓冲区

而每次读取时,BX寄存器的值均为0

说明3:在读取用户程序剩余扇区的前后,保存并恢复了DS寄存器的值,因此在加载整个用户程序之后,DS恢复为0x1000,仍指向物理地址0x10000的起始处

6.2 程序段的重定位

6.2.1 用户程序布局

假设用户程序中的程序段布局如下图所示,其中每个段的汇编地址,也就是段相对于程序起始处的偏移量以字母表示

将上述程序加载到内存后,布局如下图所示,

可见无论将用户程序加载到内存中的什么位置,各段之间的相对位置是不变的,只是偏移量相对的phy_base有所不同

6.2.2 为何需要重定位

① 用户程序重定位表中记录的是段的汇编地址,无法在运行时直接使用

② 加载器需要根据用户程序加载的实际物理地址计算出程序中各个段的逻辑段地址,并回填到重定位表中,才能在运行时实现正确的寻址

6.2.3 重定位过程实现

段的重定位需要将段的汇编地址转换为逻辑段地址,需要完成如下步骤,

① 计算段的物理地址

② 将段的物理地址左移4位(除以16)形成逻辑段地址

③ 将逻辑段地址回填至重定位表中

在示例程序中,上述步骤实现为calc_segment_base过程,

说明1:此处使用移位指令实现物理地址到逻辑段地址的转换,也可以使用除法实现。在加法完成后,DX : AX中存储的是段的20位物理地址

过程中,将AX左移4位,DX循环右移4位,之后将AX与DX按位或,即可在AX中得到逻辑段地址

如此计算,是因为无法对DX : AX进行整体右移,只能分别右移,再拼接

说明2:为了简化段的重定位,我们在各个层次均确保了16B对齐

① 汇编程序中,通过align子句,每个段的汇编地址16B对齐

② 加载用户程序的物理地址phy_base也是16B对齐

因此,每个段在内存中的物理起始地址是16B对齐,通过简单的右移即可得到逻辑段地址

6.2.4 移位指令

6.2.4.1 逻辑右移指令shr(SHift logical Right)

shr r/m, imm8
shr r/m, cl

逻辑右移时,

① 左边高位补0

② 移出的比特位送到CF标志位中

说明1:源操作数为立即数1的逻辑右移指令时特殊设计的优化指令(比其他imm8格式指令少1B)

说明2:shr的配对指令是逻辑左移指令shl(SHift logical Left)

6.2.4.2 循环右移指令ror(ROtate Right)

ror r/m, imm8
ror r/m, cl

循环右移时,

① 移出的比特位即送到CF标志位,也送到左边的空位

说明:ror的配对指令是循环左移指令rol(ROtate Left)

6.2.5 逻辑段地址回填

需要注意的是,段重定位表中,存储段的汇编地址使用了4B,而回填的逻辑段地址只使用低2B

6.2.6 重定位上机验证

① 作为对照,用户程序头部段信息如下

② 重定位之前,物理地址0x10000处内容如下

可见用户程序中的5个段汇编地址为0x00000020 / 0x000000F0 / 0x00000100 / 0x00000280 / 0x000002C0

那么对应的物理地址应为0x00010020 / 0x000100F0 / 0x00010100 / 0x00010280 / 0x000102C0

③ 重定位之后,物理地址0x10000处内容如下

可见重定位之后的5个段逻辑段地址为0x1002 / 0x100F / 0x1010 / 0x1028 / 0x102C,均符合预期

说明:如果在VirtualBox虚拟机中验证本章程序,需要注意虚拟机硬盘的设置。因为程序中使用了0x1F0 ~ 0x1F7端口,需要将硬盘控制器设置为IDE控制器

6.3 将控制权交给用户程序

如下一条jmp指令,即实现了跳转到用户程序入口点执行,

此时DS = 0x1000,而[0x1000 : 0x04]处存储的正是用户程序入口点的偏移地址(低字)和段地址(高字)

6.4 8086无条件跳转指令汇总

6.4.1 相对短转移和16位相对近转移

jmp short 标号或目标处汇编地址
jmp near 标号或目标处汇编地址
jmp 标号或目标处汇编地址示例:
jmp label

① 短转移 & 近转移:段内转移

② 相对:基于IP计算相对偏移量,短转移偏移量使用1B编码,近转移偏移量使用2B编码

说明1:此处使用标号或汇编地址是等价的

说明2:如果省略short与near,将由汇编器根据相对偏移量决定使用短转移还是近转移

6.4.2 16位间接绝对近转移

jmp r/m示例:
jmp bx
jmp [0x3000]

① 近转移:段内转移

② 间接:跳转地址由寄存器或内存给出

③ 绝对:寄存器或内存中给出绝对地址,直接替换IP

④ 16位:绝对地址为16位

6.4.3 16位直接绝对远转移

jmp 16位段地址 : 16位偏移地址示例:
jmp 0x0000 : 0x7C00

① 远转移:段间转移

② 直接:直接给出段地址和偏移地址

③ 绝对:绝对地址,直接替换CS & IP

④ 16位:段地址 & 偏移地址为16位

6.4.4 16位间接绝对远转移

jmp far m示例:
jmp far [0x2002]

① 远转移:段间转移

② 直接:段地址和偏移地址由内存出的2个字给出,其中低字位偏移地址,高字为段地址

③ 绝对:绝对地址,直接替换CS & IP

④ 16位:段地址 & 偏移地址为16位

说明:此处关键字far不能省略

6.4.5 通过retf切换到另一个代码段

① 段间跳转的本质,使用目标地址的段地址 & 偏移地址设置CS & IP寄存器

② retf指令的本质,是从栈中先后出栈2个字到IP和CS寄存器,因此可以使用retf指令实现段间跳转

③ 使用retf指令实现段间跳转实例

说明:此处也可以使用常规的远调用指令(call far)或远转移指令(jmp far)来实现段间跳转,以下实例基于原书配套程序修改

① 首先在数据段中定义双字单元,用于存储远调用目的地址的段地址和偏移地址

② 将目的地址的段地址和偏移地址存储到entry标号处,之后使用远调用指令(使用远转移指令也可以)

7. 用户程序工作流程

7.1 初始化段寄存器

① 当加载器使用jmp far [0x04]跳转到用户程序执行时,就使用入口点的段地址和偏移地址设置了CS & IP寄存器,即实现了代码段的设置

但是DS、SS、ES仍使用加载器的设置值

② 此处设置了用户程序的数据段(DS)和栈段(SS),而ES仍使用加载器的设置,指向加载用户程序的物理地址0x10000处

说明1:用户程序栈段的设置

此处使用resb(reserve byte)指令预留了256个字节,作为栈使用。需要注意3点,

① 由于此处只是预留内存但是没有初始化,因此编译时会有如下警告

② resb伪指令以字节为单位预留内存,resw伪指令以字为单位预留内存,resd伪指令以双字为单位预留内存,所以使用如下形式也可以预留256B

resw 128
resd 64

③ 也可以使用times伪指令实现预留栈段的目标

times 256 db 0 

说明2:栈段的vstart从0开始,因此stack_end标号的汇编地址可以作为栈的初始值使用,用于给SP赋值

说明3:段寄存器初始化顺序

此处先初始化SS,后初始化DS。因此进入用户程序入口点时,DS还指向用户程序头部段,我们还需要从中获取重定位后的逻辑段地址

如果先初始化DS,则后续无法再使用DS访问程序头部段

7.2 put_string过程分析

7.2.1 put_string过程主体流程

① put_string用于输出以'\0'字符结尾的字符串,每次输出一个字符

② 输入参数中,DS : BX为字符串起始地址

说明1:此处使用or cl, cl指令判断是否到达'\0'字符,or cl, cl虽然不会改变cl寄存器中的值,但是会更新标志寄存器

此处也可以使用cmp cl, 0指令判断,更加直观

说明2:在put_string过程中调用了put_char过程,即进行了过程的嵌套调用。因为每次调用过程中,处理器都将返回地址压栈,因此只要栈是安全的,嵌套的过程就能逐层返回

过程嵌套的层数在理论上是没有限制的,现实中唯一的限制就是栈的大小。在实模式下栈空间最大为64KB,而没执行一次过程调用压栈返回地址需要2个或4个字节,而且每个过程内部还可能需要使用栈空间

7.2.2 文本显示相关控制

7.2.2.1 光标位置

① 光标(Cursor)是在屏幕上有规律地闪烁的一条小横线,通常用于指示下一个要显示的字符位置

② 在VGA文本模式下,光标位置存储在显卡内部的2个光标寄存器中,每个寄存器8位,合起来组成一个16位的数值

③ 光标寄存器可读可写

说明1:光标的合法位置

在VGA文本模式下,共有25行,每行80个字符,因此光标的合法位置为0 ~ 1999(25 * 80 - 1),当光标位置 > 1999时,则超出屏幕,需要进行滚屏操作

注意,显卡从来不自动移动光标位置,完全由程序员维护

说明2:如何访问当前光标

由于显卡操作非常复杂,因此有众多的内部寄存器,为了不过多占用处理器的IO空间,很多内部寄存器只能通过索引寄存器间接访问

光标寄存器索引值为0x0e和0x0f,在访问当前光标时,

① 向索引端口0x3D4写入一个值,指定要访问的内部寄存器索引

② 通过数据端口0x3D5进行数据读写

7.2.2.2 回车与换行

① 回车(Carriage Return,CR)

行为:将光标位置回到当前行行首

ASCII码:0x0d

光标位置计算:先将当前光标位置除以80,再将商(即当前行号)乘以80

② 换行(Line Feed,LF)

行为:将光标位置向下移动一行,但是列不变

ASCII码:0x0a

光标位置计算:将当前光标位置加80

说明:如果既回车又换行,光标位置将到达下一行的行首,该过程称作回车换行(CR/LF)

7.2.2.3 滚屏操作

当光标位置超过1999时,即超出屏幕范围,此时需要进行滚屏操作,步骤如下,

① 将显存中第1 ~ 24行的数据移动到0 ~ 23行(行号从0开始)

② 擦除显存中最后一行内容

7.2.3 put_char过程分析

put_char过程流程如下图所示,

下面说明关键功能代码实现,

7.2.3.1 获取当前光标位置

① 从两个光标寄存器中分别读取当前光标位置的高低8位

② 在AX寄存器中完成拼接后,保存到BX寄存器中

7.2.3.2 处理回车字符

说明:无符号乘法指令mul

mul r/m
;操作数为乘数
;被操作数为隐含的;imul为有符号乘法指令,用法与mul相同

① 如果在指令中指定的是8位寄存器或者8位操作数的内存地址,则被乘数为8位,在寄存器AL中

相乘后,乘积为16位,在寄存器AX中

mul bh
mul byte [0x2002]

② 如果在指令中指定的是16位寄存器或者16位操作数的内存地址,则被乘数为16位,在寄存器AX中

相乘后,乘积为32位,低16位在寄存器AX中,高16位在寄存器DX中

mul bx
mul word [0x2002]

③ 如果在指令中指定的是32位寄存器或者32位操作数的内存地址,则被乘数为32位,在寄存器EAX中

相乘后,乘积为64位,低32位在寄存器EAX中,高32位在寄存器EDX中

mul ebx
mul dword [0x2002]

注意:8086不支持,从80386开始支持

④ 如果在指令中指定的是64位寄存器或者64位操作数的内存地址,则被乘数为64位,在寄存器RAX中

相乘后,乘积为128位,低64位在寄存器RAX中,高64位在寄存器RDX中

mul rbx
mul qword [0x2002]

注意:8086和32位处理器不支持,只有64位处理器支持

7.2.3.3 处理换行字符

7.2.3.4 显示可打印字符

说明1:由于在put_char过程起始处已经将ES寄存器压栈保存,因此可以使用ES指向显存起始地址

说明2:由于显存中一个字符对应2B(ASCII码 + 显示属性),此处通过移位实现乘2与除2,进而实现光标位置与对象显存位置的转换

7.2.3.5 实现滚屏操作

说明:关于清除最后1行后光标位置的计算

① 如果是换行导致的滚屏操作,滚屏后的光标位置应该退回1行,但是列不变

② 如果是输出字符导致的滚屏操作,滚屏后的光标位置应该在最后一行的行首

这两种情况都需要将原先BX寄存器中保存的光标位置减80

7.2.3.6 重置光标位置

重置光标后,即实现的了光标跟随,指向要下一个要显示字符的位置

X86汇编语言从实模式到保护模式07:硬盘和显卡的访问控制相关推荐

  1. x86汇编语言从实模式百度云_Intel x86 CPU 32位保护模式杂谈之任务切换 上

    目录: 什么是任务 任务由什么组成 任务门描述符是什么东东?有了TSS描述符为什么要有任务门描述符? 参考文献 什么是任务 任务(task)是处理器可以分配.执行.挂起的工作单位,笔者认为和我们操作系 ...

  2. 硬盘和显卡的访问与控制(一)——《x86汇编语言:从实模式到保护模式》读书笔记01

    本文是<x86汇编语言:从实模式到保护模式>(电子工业出版社)的读书实验笔记. 这篇文章我们先不分析代码,而是说一下在Bochs环境下如何看到实验结果. 需要的源码文件 第一个文件是加载程 ...

  3. 16位模式/32位模式下PUSH指令探究——《x86汇编语言:从实模式到保护模式》读书笔记16...

    一.Intel 32 位处理器的工作模式 如上图所示,Intel 32 位处理器有3种工作模式. (1)实模式:工作方式相当于一个8086 (2)保护模式:提供支持多任务环境的工作方式,建立保护机制 ...

  4. 《x86汇编语言:从实模式到保护模式》视频来了

    <x86汇编语言:从实模式到保护模式>视频来了 很多朋友留言,说我的专栏<x86汇编语言:从实模式到保护模式>写得很详细,还有的朋友希望我能写得更细,最好是覆盖全书的所有章节. ...

  5. 《x86汇编语言:从实模式到保护模式》读书笔记之后记

    本来打算把整本书的读书笔记写完,可是由于有其他的计划(就叫做"B计划"吧)且优先级更高,所以我的读书笔记搁浅了.为了全力以赴执行B计划,我的博客要荒芜一段时间(我希望不要永远荒芜下 ...

  6. 处理器在实施任务切换时的操作——《x86汇编语言:从实模式到保护模式》读书笔记39

    处理器在实施任务切换时的操作--<x86汇编语言:从实模式到保护模式>读书笔记39 处理器可以通过以下四种方法实施任务切换: 1. call指令或者jmp指令的操作数是GDT内的某个TSS ...

  7. 任务切换——《x86汇编语言:从实模式到保护模式》读书笔记38

    任务切换--<x86汇编语言:从实模式到保护模式>读书笔记38 本文及后面的几篇博文是原书第15章的学习笔记. 本章依然使用第13章的主引导程序. 1. 协同式多任务与抢占式多任务 有两种 ...

  8. 任务切换的方法——《x86汇编语言:从实模式到保护模式》读书笔记37

    任务切换的方法--<x86汇编语言:从实模式到保护模式>读书笔记37 1. 中断门和陷阱门 在实模式下,内存最低端的1M是中断向量表,保存着256个中断处理过程的段地址和偏移.当中断发生时 ...

  9. 任务和特权级保护(五)——《x86汇编语言:从实模式到保护模式》读书笔记36

    任务和特权级保护(五)--<x86汇编语言:从实模式到保护模式>读书笔记36 修改后的代码,有需要的朋友可以去下载(c14_new文件夹).下载地址是: GitHub: https://g ...

  10. 任务和特权级保护(四)——《x86汇编语言:从实模式到保护模式》读书笔记35

    任务和特权级保护(四)--<x86汇编语言:从实模式到保护模式>读书笔记35 7. 正式进入用户程序的局部空间 67 mov ebx,message_1 68 call far [fs:P ...

最新文章

  1. 就在几天前,听说用了 YYYY-MM-dd 的程序员,都在加班改 Bug !
  2. sql PERCENTILE_CONT 计算一组数的线性差值
  3. 成功解决cx_Freeze打包的时候出现importError:can not import name idnadata
  4. java arrays方法_Java工具类Arrays中不得不知的常用方法
  5. Maven项目缺少Maven Dependencies解决方法总结
  6. 嵩天-Python语言程序设计程序题--第一周:Python基本语法元素
  7. linux c之#include <unistd.h> 总结
  8. kafka connect_Kafka Connect在MapR上
  9. jqc3ff继电器引脚图_单片机控制继电器驱动电路图原理分析
  10. dnf加物理攻击的卡片有哪些_DNF:节日宝珠之外百分比神器附魔,拍卖行100w,实用不氪金...
  11. 实现页面弹框背景虚化效果
  12. Mysql导出数据 (windows Linux)
  13. Oracle logmnr使用
  14. Git使用的奇技淫巧,看这篇就够了!
  15. html文件如何设置右键菜单,windows系统使用小技巧,创建属于自己的右键新建菜单-右键菜单设置...
  16. 炫龙T3-pro 9代cpu无csm兼容选项笔记本GPT硬盘纯uefi安装windows7系统方法
  17. 记一次360众测仿真实战靶场考核WP
  18. rocket基础知识
  19. 二叉树算法大总结:借助遍历的题型+需要借助递归返回多个信息的题型[本质:遍历]
  20. mybatis多数据源配置

热门文章

  1. linux 运行python效率高还是windows高_为什么使用Mac开发比Windows效率高?
  2. java保存文件的时候提示系统找不到路径_java.io.FileNotFoundException (系统找不到指定的路径。)...
  3. php i++和++i的区别,初学者搞懂i++和++i
  4. MATLAB信号处理之离散时间系统的时域分析
  5. 关于jquery的$(document).on()事件多次执行的问题
  6. oracle控制文件还原,Oracle的控制文件的恢复与重建
  7. 2013河北省职称计算机应用能力考试操作题答案,(2013河北省职称计算机应用能力考试操作题步骤详解PPT部分.doc...
  8. Oracle收集用户的权限
  9. java 左右两边数据类型不一样_java基础语法
  10. 大学计算机知识考试题,大学计算机基础重点知识考试试题