操作系统真象还原<第一部分>

(。・∀・)ノ゙嗨起来!!!

介绍及其问题

如何访问到 IO 接口呢?
答案就是 IO 接口上面有一些寄存器,访问 IO 接口本质上就是访问这些寄存器,这些寄存器就是人们常说的端口。这些端口是人家 IO 接口给咱们提供的接口。人家接口电路也有自己的思维(系统),看到寄存器中写了什么就做出相应的反应。

用户态与内核态是对 CPU 来讲的,是指 CPU 运行在用户态(特权 3 级)还是内核态(特权 0 级),很多人误以为是对用户进程来讲的。

用户进程陷入内核态是指:由于内部或外部中断发生,当前进程被暂时终止执行,其上下文被内核的中断程序保存起来后,开始执行一段内核的代码。是内核的代码,不是用户程序在内核的代码,用户代码怎么可能在内核中存在,所以“用户态与内核态”是对 CPU 来说的

当应用程序陷入内核后,它自己已经下 CPU 了,以后发生的事,应用程序完全不知道,它的上下文环境已经被保存到自己的 0 特权级栈中了,那时在 CPU 上运行的程序已经是内核程序了。所以要清楚,内核代码并不是成了应用程序的内核化身,操作系统是独立的部分,用户进程永远不会因为进入内核态而变身为操作系统了。

1M 是 2 的 20 次方,1MB 内存需要 20 位的地址 才能访问到,
如何做到用 16 位寄存器访问 20 位地址空间呢?
在 8086 的寻址方式中,有基址寻址,这是用基址寄存器 bx 或 bp 来提供偏移地址的,如“mov [bx],0x5;”指令便是将立即数 0x5 存入 ds:bx 指向的内存。( 直接寻址

image-20201013164456919

这是因为 CPU 设计者在地址处理单元中动了手脚,该地址部件接到“段基址+段内偏移地址”的地址后,自动将段基址乘以 16,即左移了 4 位,然后再和 16 位的段内偏移地址相加,这下地址变成了 20 位了吧

Section Headers:列出了程序中所有的 section,这些 section 是 gcc 编译器帮忙划分的。
Program Headers:列出了程序中的段,即 segment,这是程序中 section 合并后的结果。
Section to Segment mapping:列出了一个 segment 中包含了哪些 section。

CPU 内部的段寄存器(Segment reg)如下。
(1)CS—代码段寄存器(Code Segment Register),其值为代码段的段基值。
(2)DS—数据段寄存器(Data Segment Register),其值为数据段的段基值。
(3)ES—附加段寄存器(Extra Segment Register),其值为附加数据段的段基值,称为“附加”是因为此段寄存器用途不像其他 sreg 那样固定,可以额外做他用。
(4)FS—附加段寄存器(Extra Segment Register),其值为附加数据段的段基值,同上,用途不固定,使用上灵活机动。
(5)GS—附加段寄存器(Extra Segment Register),其值为附加数据段的段基值。
(6)SS—堆栈段寄存器(Stack Segment Register),其值为堆栈段的段值。

OSI 七层模型
它规定数据的第一层,也就是最外层物理层,这一层包含的是电路相关的数据。发送方和接收方都彼此认同最外层的就是电路传输用的数据。每一层中的前几个固定的字节必须是描述当前层的属性,根据此属性就能找到需要的数据。各层中的数据部分都是更上一层的数据,如第一层(物理层)中的数据部分是第二层(数据链路层)的属性+数据,第三层(网络)的数据部分是第四层(传输层)TCP 或 UDP 的属性+数据。各层都是如此,直到第七层(应用层)的数据部分才是真正应用软件所需要的数据。由此可见,对方一大串数据发过来后,经过层层剥离处理,到了最终的接收方(应用软件),只是一小点啦。

image-20201013165647506

C 程序大体上分为 预处理、编译、汇编和链接 4 个阶段。
预处理阶段是预处理器将高级语言中的宏展开,去掉代码注释,为调试器添加行号等。
编译阶段是将预处理后的高级语言进行词法分析、语法分析、语义分析、优化,最后生成汇编代码。
汇编阶段是将汇编代码编译成目标文件,也就是转换成了目标机器平台上的机器指令。
链接阶段是将目标文件连接成可执行文件。

MBR 与 EBR 介绍

MBREBR分区工具创建维护的,不属于操作系统管理的范围,因此操作系统不可以往里面写东 西,注意这里所说的是“不可以”,其实操作系统是有能力读写任何地址的,只是如果这样做的话会破坏 “系统控制权接力赛”所使用的数据,下次开机后就无法启动了。OBR 是各分区(主分区或逻辑分区)最 开始的扇区,因此属于操作系统管理。

DBR、OBR、MBR、EBR 都包含引导程序,因此它们都称为引导扇区,只要该扇区中存在可执行的 程序,该扇区就是可引导扇区。若该扇区位于整个硬盘最开始的扇区,并且以 0x550xaa 结束,BIOS 就认为该扇区中存在 MBR,该扇区就是 MBR 引导扇区。若该扇区位于各分区最开始的扇区,并且以 0x550xaa 结束,MBR 就认为该扇区中有操作系统引导程序 OBR,该扇区就是 OBR 引导扇区

DBR、OBR、MBR、EBR 结构中都有引导代码和结束标记 0x550xaa,它们最大的区别是分区表只在 MBREBR 中存在,DBROBR 中绝对没有分区表。

image-20201013170354801

在 CPU 眼里,为什么我们插在主板上的物理内存不是它眼里“全部的内存”。 地址总线宽度决定了可以访问的内存空间大小,如 16 位机的地址总线为 20 位,其地址范围是 1MB,32 位地址总线宽度是 32 位,其地址范围是 4GB。但以上的地址范围是指地址总线可以触及到的边界,是指计算机在寻址上可以到达的疆域。

物理内存多大都没用,主要是看地线总线的宽度。还要看地址总线的设计,是不是全部用于访问 DRAM。所以说,地址总线是决定我们访问哪里、访问什么,以及访问范围的关键。

image-20201014144139482

CPU工作原理:控制单元要取下一条待运行的指令,该指令的地址在程序计数器 PC 中,在 x86CPU 上,程序计数器就是 cs:ip。于是读取 ip寄存器后,将此地址送上地址总线,CPU 根据此地址便得到了指令,并将其存入到指令寄存器 IR 中。这时候轮到指令译码器上场了,它根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作数类型,若是在内存中,就将相应操作数 从内存中取回放入自己的存储单元,若操作数是在寄存器中就直接用了,免了取操作数这一过程。操作码有了, 操作数也齐了,操作控制器给运算单元下令,开工,于是运算单元便真正开始执行指令了。ip 寄存器的值被加上当前指令的大小,于是 ip又指向了下一条指令的地址。接着控制单元又要取下一条指令了,流程回到了本段开头,CPU 便开始了日复一日的循环,由于 CPU 特别不容易坏,所以唯一它停下来的条件就是停电。

CPU 中的一级缓存 L1二级缓存 L2,它们都是 SRAM,即静态随机访问存储器,它是最快的存储器。SRAM 是用寄存器来存储数据的,这就是 SRAM 的原因。寄存器是使用触发器实现的,这也是一种存储电路,工作速度极快,是纳秒级别的。

CPU 中的寄存器大致上分为两大类。

  1. 第一类是其内部使用的,对程序员不可见。“是否可见”不是说寄存器是否能看得见,是指程序员是否能使用。CPU 内部有其自己的运行机制,是按照某个预定框架进行的,为了CPU 能够运行下去,必然会有一些寄存器来做数据的支撑,给 CPU 内部的数据提供存储空间。这一部分对外是不可见的,我们无法使用它们,比如全局描述符表寄存器 GDTR、中断描述符表寄存器 IDTR、局部描述符表寄存器 LDTR、 任务寄存器 TR、控制寄存器 CR0~3、指令指针寄存器 IP、标志寄存器 flags、调试寄存器 DR0~7。
  2. 第二类是对程序员可见的寄存器。我们进行汇编语言程序设计时,能够直接操作的就是这些寄存器,如段寄存器、通用寄存器。

通用寄存器

image-20201014150329706

image-20201014150218707

CPU8086寻址方式,从大方向来看可以分为三大类:
(1)寄存器寻址;
(2)立即数寻址;
(3)内存寻址。
在第三种内存寻址中又分为:
(1)直接寻址;
(2)基址寻址;
(3)变址寻址;
(4)基址变址寻址。

标志位寄存器

image-20201014163727780

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
以下标志位仅在 8088 以上 CPU 中有效。

第 0 位的是 CF 位,即 Carry Flag,意为进位。运算中,数值的最高位有可能是进位,也有可能是借位,所以 carry 表示 这两种状态。不管最高位是进位,还是借位,CF 位都会置 1,否则为 0。它可用于检测无符号数加减法是否有溢出,因为 CF 为 1 时,也就是最高位有进位或借位,肯定是溢出。
再说点没用的,第 1、3、5、15 位没有专门的标志位,空着占位用。
第 2 位为 PF 位,即 Parity Flag,意为奇偶位。用于标记结果低 8 位中 1 的个数,如果为偶数,PF 位
为 1,否则为 0。注意啦,是最低的那 8 位,不管操作数是 16 位,还是 32 位。奇偶校验经常用于数据传
输开始时和结束后的对比,判断传输过程中是否出现错误。
第 4 位为 AF 位,即 Auxiliary carry Flag,意为辅助进位标志,用来记录运算结果低 4 位的进、借位
情况,即若低半字节有进、借位,AF 为 1,否则为 0。
第 6 位为 ZF 位,即 Zero Flag,意为零标志位。若计算结果为 0,此标志为 1,否则为 0。
第 7 位为 SF 位,即 Sign Flag,意为符号标志位。若运算结果为负,则 SF 位为 1,否则为 0
第 8 位为 TF 位,即 Trap Flag,意为陷阱标志位。此位若为 1,用于让 CPU 进入单步运行方式,若为0,则为连续工作的方式。平时我们用的 debug 程序,在单步调试时,原理上就是让 TF 位为 1。可见,软件上的很多功能,必须有硬件的原生支持才能得以实现。
第 9 位为 IF 位,即 Interrupt Flag,意为中断标志位。若 IF 位为 1,表示中断开启,CPU 可以响应外部可屏蔽中断。若为 0,表示中断关闭,CPU 不再响应来自 CPU 外部的可屏蔽中断,但 CPU 内部的异常还是要响应的,因为它关不住。
第 10 位为 DF 位,即 Direction Flag,意为方向标志位。此标志位用于字符串操作指令中,当 DF 为 1 时,指令中的操作数地址会自动减少一个单位,当 DF 为 0 时,指令中的操作数地址会自动增加一个单位,意即给地址的变化提供个方向。其中提到的这个单位的大小,取决于用什么指令。
第 11 位为 OF 位,即 Overflow Flag,意为溢出标志位。用来标识计算的结果是否超过了数据类型可
表示的范围,若超出了范围,就像水从锅里溢出去了一样。若 OF 为 1,表示有溢出,为 0 则未发生溢出。
专门用于检测有符号数运算结果是否有溢出现象。

以下标志位仅在 80286 以上 CPU 中有效。相对于 8088,它支持特权级和多任务。
第 12~13 位为 IOPL,即 Input Output Privilege Level,这用在有特权级概念的 CPU 中。有 4 个任务特权级,即特权级 0、特权级 1、特权级 2 和特权级 3。故 IOPL 要占用 2 位来表示这 4 种特权级。如果您对此感到迷茫,不用担心,这些将来咱们在保护模式下也得实践。
第 14 位为 NT,即 Nest Task,意为任务嵌套标志位。8088 支持多任务,一个任务就是一个进程。当一个任务中又嵌套调用了另一个任务(进程)时,此 NT 位为 1,否则为 0。

以下标志位仅用于 80386 以上的 CPU。
第 16 位为 RF 位,即 Resume Flag,意即恢复标志位。该标志位用于程序调试,指示是否接受调试故
障,它需要与调试寄存器一起使用。当 RF 为 1 时忽略调试故障,为 0 时接受。
第 17 位为 VM 位,即 Virtual 8086 Model,意为虚拟 8086 模式。这是实模式向保护模式过渡时的产物,现在已经没有了。CPU 有了保护模式后,功能更加强大了,但为了兼容实模式下的用户程序,允许将此位置为 1,这样便可以在保护模式下运行实模式下的程序了。实模式下的程序不支持多任务,而且程序中的地址就是真实的物理地址。所以在保护模式下每运行一个实模式下的程序,就要为其虚拟一个实模式环境,故称为虚拟模式。

以下标志位仅用于 80486 以上的 CPU。
第 18 位为 AC 位,即 Alignment Check,意为对齐检查。什么是对齐呢?是指程序中的数据或指令其内存地址是否是偶数,是否是 16、32 的整数倍,没有余数,这样硬件每次对地址以自增地方式(每次自加 2、16、32 等)访问内存时,自增后的地址正好对齐数据所在的起始地址上,这就是对齐的原理。对齐并不是软件逻辑中的要求,而是硬件上的偏好,如果待访问的内存地址是 16 或 32 的整数倍,硬件上好处理,所以运行较快。若 AC 位为 1 时,则进行地址对齐检查,为 0 时不检查。

以下标志位只对 80586(奔腾)以上 CPU 有效。
第 19 位为 VIF 位,即 Virtual Interrupt Flag,意为虚拟中断标志位,虚拟模式下的中断标志。
第 20 位为 VIP 位,即 Virtual Interrupt Pending,意为虚拟中断挂起标志位。在多任务情况下,为操作
系统提供的虚拟中断挂起信息,需要与 VIF 位配合。
第 21 位为 ID 位,即 Identification,意思为识别标志位。系统经常要判断 CPU 型号,若 ID 为 1,表示当前 CPU 支持 CPU id 指令,这样便能获取 CPU 的型号、厂商等信息。若 ID 为 0,则表示当前 CPU 不支持 CPU id 指令。
其余剩下的 22~31 位都没有实际用途,纯粹是占位用,为了将来扩展。

flags 寄存器一般与跳转有关联

条件转移指令中所说的条件就是指标志寄存器中的标志位。

jxx 中的 xx,就是各种条件的分类,每种 条件有不同的转移指令。

image-20201014164609509

这些转移指令是由意义明确的字符拼成的 。。。

1
2
3
4
5
6
7
8
9
10
a 表示 above
b 表示 below
c 表示 carry
e 表示 equal
g 表示 great
j 表示 jmp
l 表示 less
n 表示 not
o 表示 overflow
p 表示 parity

方便我们去记忆

摘录的一段话

1
2
3
4
如果您有一般的软件开发经验,就会了解,很少有程序能一下就编译通过。当然,如果您的编程经验 无比丰富,代码无比规范,无比了解编译器,确实不需要虚拟机来调试了,编写完成后直接就能运行。以 上我用了三个“无比”,打造了似乎没有人能达到这种水平的假象,其实是有的。不知道大家听说过 Jon Skeet 没有,他是谷歌软件工程师,《C# In Depth》就是他的作品。看看别人对他是怎样评价的,看完之后您就 知道我说的并不夸张了。
“他并不需要调试器,只要他盯着代码看几眼,Bug 自己就跑出来了”。
“他根本不需要什么编程规范,他的代码就是规范”。
“如果他的代码没有通过编译,编译器厂商就会道歉”。

学无止境。。。。。。

部署环境

软件 是 VMware Workstation Pro

虚拟机镜像 是 ubuntu 16.04 x64

虚拟环境(加载在虚拟机中):bochs-2.6.2

Bochs 下载安装

官网 https://sourceforge.net/projects/bochs/files/bochs/

下载 bochs-2.6.2

在虚拟机里面 解压压缩包

1
2
--tar zxvf bochs-2.6.2.tar.gz
--cd bochs-2.6.2

然后开始 configure、make、make install 三步曲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
./configure \
--prefix=/your_path/bochs-2.6.2 \
--enable-debugger\
--enable-disasm \
--enable-iodebug \
--enable-x86-debugger \
--with-x \
--with-x11

--prefix=/your_path/bochs 是用来指定 bochs 的安装目录,根据个人实际情况将 your_path 替换为自己待安装的路径。
--enable-debugger 打开 bochs 自己的调试器。
--enable-disasm 使 bochs 支持反汇编。
--enable-iodebug 启用 io 接口调试器。
--enable-x86-debugger 支持 x86 调试器。
--with-x 使用 x windows。
--with-x11 使用 x11 图形用户接口。

configure 之后,会生成 Makefile,可以开始编译了。

如果报错 ( 其余报错 google 一般都有

1
2
undefined reference to 'pthread_create'
undefined reference to 'pthread_join'

我们需要在 Makefile文件LIBS =这句最后面添加上-lpthread

1
LIBS =  -lm -lgtk-x11-2.0 -lgdk-x11-2.0 -lpangocairo-1.0 -latk-1.0 -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lpangoft2-1.0 -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lfontconfig -lfreetype -lpthread

然后重新编译

1
2
--sudo make
--sudo make install

配置 bochs

设置配置文件 bochsrc.disk 放在 bochs 安装目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Configuration file for Bochs
# 运行过程中能够使用的内存,本例为 32MB。
# 关键字为:megs
megs: 32

# 设置对应真实机器的 BIOS 和 VGA BIOS。
romimage: file=/实际路径/bochs-2.6.2/share/bochs/BIOS-bochs-latest
vgaromimage: file=/实际路径/bochs-2.6.2/share/bochs/VGABIOS-lgpl-latest

# Bochs 所使用的磁盘,软盘的关键字为 floppy。
# 若只有一个软盘,则使用 floppya 即可,若有多个,则为 floppya,floppyb…
#floppya: 1_44=a.img, status=inserted

# 选择启动盘符。
#boot: floppy #默认从软盘启动,将其注释
boot: disk #改为从硬盘启动。我们的任何代码都将直接写在硬盘上,所以不会再有读写软盘的操作。

# 设置日志文件的输出。
log: bochs.out

# 开启或关闭某些功能。
# 下面是关闭鼠标,并打开键盘。
mouse: enabled=0
keyboard:keymap=/实际路径/bochs-2.6.2/share/bochs/keymaps/x11-pc-us.map

# 硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14

# 下面的是增加的 bochs 对 gdb 的支持,这样 gdb 便可以远程连接到此机器的 1234 端口调试了
# gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
--sudo bin/bochs -f bochsrc.disk

出现 ...
You can also start bochs with the -q option to skip these menus.

1. Restore factory default configuration
2. Read options from...
3. Edit options
4. Save options to...
5. Restore the Bochs state from...
6. Begin simulation
7. Quit now

Please choose one: [6]

回车后 会出现2个窗口

在主窗口 输入c 进行进程,

提示没有的这个“bootable device”就是启动盘,现在就创建启动盘吧。

1
2
3
4
5
6
7
8
-fd 创建软盘。
-hd 创建硬盘。
-mode 创建硬盘的类型,有 flat、sparse、growing 三种。
-size 指创建多大的硬盘,以 MB 为单位。
-q 以静默模式创建,创建过程中不会和用户交互。

bin/bximage -hd -mode="flat" -size=60 -q hd60M.img
这个命令串中最后一个 hd60M.img 是咱们创建的虚拟硬盘的名称。

然后继续运行

之前报错原因是 boot failed: could not read the boot disk,这是无法读取启动盘。

而现在这里的报错是 boot failed: not a bootable disk,这 不是一个启动盘。( 下部分解决

其实在这里配置 就完了。。。

编写MBR主引导记录

从按下主机上的 power 键后,第一个运行的软件是 BIOS。

BIOS 全称叫 Base Input & Output System,即基本输入输出系统。

实模式下1MB 内存的布局 :image-20201013211922504

BIOS 的主要工作是

  1. 检测、初始化硬件,怎么初始化的?硬件自己提 供了一些初始化的功能调用,BIOS 直接调用就好了。BIOS 还做了一件伟大的事情,
  2. 建立了中断向量表,这样 就可以通过“int 中断号”来实现相关的硬件调用,当然 BIOS 建立的这些功能就是对硬件的 IO 操作,也就是输入输出,但由于就 64KB 大小的空间,不可能把所有硬件的 IO 操作实现得面面俱到,而且也没必要实现那么多, 毕竟是在实模式之下,对硬件支持得再丰富也白搭,精彩的世界是在进入保护模式以后才开始,所以挑一些重要的、保证计算机能运行的那些硬件的基本 IO 操作,就行了。
  3. 校验启动盘中位于 0 盘 0 道 1 扇区的内容,在计算机中是习惯以 0 作为起始索引的,因为人们已经习惯了偏移量的概念, 无论是机器眼里和程序员眼里,用“相对”的概念,即偏移量来表示位置显得很直观,所以很多指令中的 操作数都是用偏移量表示的。0 盘 0 道 1 扇区本质上就相当于 0 盘 0 道 0 扇区。MBR 所在的位置是磁盘上最开始的那个扇区。此扇区末尾的两个字节分别是魔数 0x55 和 0xaa,BIOS 便认为此扇区中确实存在可执 行的程序(此程序便是久闻大名的主引导记录 MBR),便加载到物理地址 0x7c00,随 后跳转到此地址,继续执行。

BIOS 跳转到 0x7c00 是用 jmp 0:0x7c00 实现的,这是 jmp 指令的直接绝对远转移 用法,段寄存器 cs 会被替换,这里的段基址是 0,即 cs 由之前的 0xf000 变成了 0。 如果此扇区的最后 2 个不是 0x55 和 0xaa,即使里面有可执行代码也无济于事了,BIOS 不认,它也 许还认为此扇区是没格干净呢。

MBR 的大小必须是 512 字节,这是为了保证 0x55 和 0xaa 这两个 魔数恰好出现在该扇区的最后两个字节处,即第 510 字节处和第 511 字节处,这是按起始偏移为 0 算起的。 由于我们的 bochs 模拟的是 x86 平台,所以是小端字节序,故其最后两个字节内容是 0xaa55,写到一起后 似乎有点不认识了,不要怕,拆开就是 0x55 和 0xaa。

nasm的安装与使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
--sudo apt install nasm

usage: nasm [-@ response file] [-o outfile] [-f format] [-l listfile]
[options...] [--] filename
or nasm -v (or --v) for version info

-t assemble in SciTech TASM compatible mode
-g generate debug information in selected format
-E (or -e) preprocess only (writes output to stdout by default)
-a don't preprocess (assemble only)
-M generate Makefile dependencies on stdout
-MG d:o, missing files assumed generated
-MF <file> set Makefile dependency file
-MD <file> assemble and generate dependencies
-MT <file> dependency target name
-MQ <file> dependency target name (quoted)
-MP emit phony target

-Z<file> redirect error messages to file
-s redirect error messages to stdout

-F format select a debugging format

-o outfile write output to an outfile

-f format select an output format

-l listfile write listing to a listfile

-I<path> adds a pathname to the include file path
-O<digit> optimize branch offsets
-O0: No optimization
-O1: Minimal optimization
-Ox: Multipass optimization (default)

-P<file> pre-includes a file
-D<macro>[=<value>] pre-defines a macro
-U<macro> undefines a macro
-X<format> specifies error reporting format (gnu or vc)
-w+foo enables warning foo (equiv. -Wfoo)
-w-foo disable warning foo (equiv. -Wno-foo)

-h show invocation summary and exit

--prefix,--postfix
this options prepend or append the given argument to all
extern and global variables

开始代码啦

功能:在屏幕上打印字符串“1 MBR”,背景色为黑色,前景色为绿色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
;主引导程序
;------------------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
; 清屏利用 0x06 号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为 0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0x600
mov bx, 0x700
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0x184f ; 右下角: (80,25),
; VGA 文本模式中,一行只能容纳 80 个字符,共 25 行。
; 下标从 0 开始,所以 0x18=24,0x4f=79
int 0x10 ; int 0x10

;;;;;;;;; 下面这三行代码获取光标位置 ;;;;;;;;;
;.get_cursor 获取当前光标位置,在光标位置处打印字符。
mov ah, 3 ; 输入: 3 号子功能是获取光标位置,需要存入 ah 寄存器
mov bh, 0 ; bh 寄存器存储的是待获取光标的页号

int 0x10 ; 输出: ch=光标开始行,cl=光标结束行
; dh=光标所在行号,dl=光标所在列号

;;;;;;;;; 获取光标位置结束 ;;;;;;;;;;;;;;;;

;;;;;;;;; 打印字符串 ;;;;;;;;;;;
;还是用 10h 中断,不过这次调用 13 号子功能打印字符串
mov ax, message
mov bp, ax ; es:bp 为串首地址,es 此时同 cs 一致,
; 开头时已经为 sreg 初始化

; 光标位置要用到 dx 寄存器中内容,cx 中的光标位置可忽略
mov cx, 5 ; cx 为串长度,不包括结束符 0 的字符个数
mov ax, 0x1301 ;子功能号 13 显示字符及属性,要存入 ah 寄存器,
; al 设置写字符方式 ah=01: 显示字符串,光标跟随移动
;(1)al=0,显示字符串,并且光标返回起始位置。
;(2)al=1,显示字符串,并且光标跟随到新位置。
;(3)al=2,显示字符串及其属性,并且光标返回起始位置。
;(4)al=3,显示字符串及其属性,光标跟随到新位置。
mov bx, 0x2 ; bh 存储要显示的页号,此处是第 0 页,
; bl 中是字符属性,属性黑底绿字(bl = 02h)
int 0x10 ; 执行 BIOS 0x10 号中断
;;;;;;;;; 打字字符串结束 ;;;;;;;;;;;;;;;

jmp $ ; 使程序悬停在此

message db "1 MBR"
times 510-($-$$) db 0
db 0x55,0xaa

$$ 是指本 section 的起始地址,上面说过了 $ 是本行所在的地址,故 $-$$ 是本行到 本 section 的偏移量。由于 MBR 的最后两个字节是固定的内容,分别是 0x550xaa,要预留出这 2 个字节,故本 扇区内前 512-2=510 字节要填满,用 510 字节减去上面通过 $-$$ 得到的偏移量,其结果便是本扇区内的剩余量,也就是要填充的字节数。由此可见 “times 510 -($-$$) db 0” 是在用 0 将本扇区剩余空间填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
编译
--nasm -o mbr.bin mbr.S

Linux 命令:dd
dd 是用于磁盘操作的命令,可以深入磁盘的任何一个扇区,它也可以删除 Linux 操作系统自己的文件。
-- dd --help
用法:dd [操作数] ...
 或:dd 选项
 
Copy a file, converting and formatting according to the operands.
bs=BYTES read and write up to BYTES bytes at a time
cbs=BYTES convert BYTES bytes at a time
conv=CONVS convert the file as per the comma separated symbol list
count=N copy only N input blocks
ibs=BYTES read up to BYTES bytes at a time (default: 512)
if=FILE read from FILE instead of stdin
iflag=FLAGS read as per the comma separated symbol list
obs=BYTES write BYTES bytes at a time (default: 512)
of=FILE write to FILE instead of stdout
oflag=FLAGS write as per the comma separated symbol list
seek=N skip N obs-sized blocks at start of output
skip=N skip N ibs-sized blocks at start of input
status=LEVEL The LEVEL of information to print to stderr;
'none' suppresses everything but error messages,
'noxfer' suppresses the final transfer statistics,
'progress' shows periodic transfer statistics

N and BYTES may be followed by the following multiplicative suffixes:
c =1, w =2, b =512, kB =1000, K =1024, MB =1000*1000, M =1024*1024, xM =M
GB =1000*1000*1000, G =1024*1024*1024, and so on for T, P, E, Z, Y.

Each CONV symbol may be:
ascii from EBCDIC to ASCII
ebcdic from ASCII to EBCDIC
ibm from ASCII to alternate EBCDIC
block pad newline-terminated records with spaces to cbs-size
unblock replace trailing spaces in cbs-size records with newline
lcase change upper case to lower case
ucase change lower case to upper case
sparse try to seek rather than write the output for NUL input blocks
swab swap every pair of input bytes
sync pad every input block with NULs to ibs-size; when used
with block or unblock, pad with spaces rather than NULs
excl fail if the output file already exists
nocreat do not create the output file
notrunc 不截断输出文件
noerror 读取数据发生错误后仍然继续
fdatasync 结束前将输出文件数据写入磁盘
fsync 类似上面,但是元数据也一同写入

FLAG 符号可以是:

append 追加模式(仅对输出有意义;隐含了conv=notrunc)
direct 使用直接I/O 存取模式
directory 除非是目录,否则 directory 失败
dsync 使用同步I/O 存取模式
sync 与上者类似,但同时也对元数据生效
fullblock 为输入积累完整块(仅iflag)
nonblock 使用无阻塞I/O 存取模式
noatime 不更新存取时间
nocache Request to drop cache. See also oflag=sync
noctty 不根据文件指派控制终端
nofollow 不跟随链接文件
count_bytes treat 'count=N' as a byte count (iflag only)
skip_bytes treat 'skip=N' as a byte count (iflag only)
seek_bytes treat 'seek=N' as a byte count (oflag only)

Sending a USR1 signal to a running 'dd' process makes it
print I/O statistics to standard error and then resume copying.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
常用的几个 dd指令参数
if=FILE
read from FILE instead of stdin
此项是指定要读取的文件。
of=FILE
write to FILE instead of stdout
此项是指定把数据输出到哪个文件。
bs=BYTES
read and write BYTES bytes at a time (also see ibs=,obs=)
此项指定块的大小,dd 是以块为单位来进行 IO 操作的,得告诉人家块是多大字节。此项是统计配置
了输入块大小 ibs 和输出块大小 obs。这两个可以单独配置。
count=BLOCKS
copy only BLOCKS input blocks
此项是指定拷贝的块数。
seek=BLOCKS
skip BLOCKS obs-sized blocks at start of output
此项是指定当我们把块输出到文件时想要跳过多少个块。
conv=CONVS
convert the file as per the comma separated symbol list
此项是指定如何转换文件。
append append mode (makes sense only for output; conv=notrunc suggested

介绍完了 指令dd 该我们安排了

1
2
-- sudo dd if=/your_path/mbr.bin of=/your_path/bochs/hd60M.img bs=512 count=1 conv=notrunc
-- sudo dd if=/home/fyz/sc/bochs-2.6.2/mbr.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=1 conv=notrunc

输入文件是刚刚编译出来的 mbr.bin,输出是我们虚拟出来的硬盘 hd60M.img,块大小指定为 512 字节, 只操作 1 块,即总共 1*512=512 字节。由于想写入第 0 块,所以没用 seek 指定跳过的块数。 执行上面的命令后,会有如下输出。

1
2
3
记录了1+0 的读入
记录了1+0 的写出
512 bytes copied, 0.0248785 s, 20.6 kB/s

mbr.bin 已经写进 hd60M.img 的第 0 块了。

开始模拟了

1
-- sudo bin/bochs -f bochsrc.disk

image-20201014004258509

回车后 输入c继续 得到结果。。。

image-20201014004414356

完善 MBR

先看下显卡各种模式的内存分布 image-20201014171240987

从起始地址 0xB8000 到 0xBFFFF,这片 32KB 大小的内存区域是用于文本显示。

我们往 0xB8000 处输出的字符直接会落到显存中,显存中有了数据,自然显卡就将其搬到显示器屏幕上了,这后续的事情咱们是不需要处理的,咱们只要保证写进显存的数据是正确的就可以。

image-20201014171446377

用 R 红色、G 绿色、B 蓝色这三种颜色以任意比例混 合,可以搭配出其他颜色,其他颜色被认为都可以由这三种颜色组合 而成。不过由于在文本模式下的颜色极其有限,RGB 的各部分比例要么是 1(全部),要么是 0(没有),所以其组合出的颜色屈指可数,为了让大家测试字符颜色更加方便, 给大家提供这三种颜色的组合。image-20201014171517655

改进 MBR,直接操作显卡

通过 BIOS 的输出改为通过显存。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
;主引导程序
;
;LOADER_BASE_ADDR equ 0xA000
;LOADER_START_SECTOR equ 0x2
;------------------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
;清屏
;利用 0x06 号功能,上卷全部行,则可清屏
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为 0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; VGA 文本模式中,一行只能容纳 80 个字符,共 25 行
; 下标从 0 开始,所以 0x18=24,0x4f=79
int 10h ; int 10h

; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4 ; A 表示绿色背景闪烁,4 表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4

jmp $ ; 通过死循环使程序悬停在此

times 510-($-$$) db 0
db 0x55,0xaa

编译 写入虚拟硬盘

1
2
3
4
5
6
7
编译
--nasm -o mbr.bin mbr.S
下面将生成的 mbr.bin 写入我们的虚拟硬盘,还是用 dd 命令。
--sudo dd if=/your_path/mbr.bin of=/your_path/bochs/hd60M.img bs=512 count=1 conv=notrunc
--sudo dd if=/home/fyz/sc/bochs-2.6.2/mbr.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=1 conv=notrunc
模拟
--sudo bin/bochs -f bochsrc.disk

结果

image-20201014193903262

让 MBR 使用硬盘

什么是硬盘?

首先说一下硬盘的概念:英文名:Hard Disk Drive,简称HDD,硬盘是电脑主要的存储媒介之一,由一个或者多个铝制或者玻璃制的碟片组成。 这个就是一个硬盘:大部分的硬盘是由磁头臂组支架,转轴,读写头,磁头臂,磁道,扇区,柱面,盘面组成的。

image-20201014202541118

磁头靠近主轴接触的表面,即线速度最小的地方,是一个特殊的区域,它不存放任何数据,称为启停区或着陆区(Landing Zone),启停区外就是数据区。在最外圈,离主轴最远的地放是“0”磁道,硬盘数据的存放就是从最外圈开始的。

盘面、磁道、柱面和扇区

硬盘的读写是和扇区有着紧密关系的。在说扇区和读写原理之前先说一下和扇区相关的”盘面”、“磁道”、和“柱面”。

1.盘面 :硬盘的盘片一般用铝合金材料做基片,高速硬盘也可能用玻璃做基片。

2.磁道 :磁盘在格式化时被划分成许多同心圆,这些同心圆轨迹叫做磁道(Track)。磁道从外向内从0开始顺序编号。硬盘的每一个盘面有300~1 024个磁道,新式大容量硬盘每面的磁道数更多。

3.柱面 :所有盘面上的同一磁道构成一个圆柱,通常称做柱面(Cylinder),每个圆柱上的磁头由上而下从“0”开始编号。数据的读/写按柱面进行,即磁头读/写数据时首先在同一柱面内从“0”磁头开始进行操作,依次向下在同一柱面的不同盘面即磁头上进行操作,只在同一柱面所有的磁头全部读/写完毕后磁头才转移到下一柱面,因为选取磁头只需通过电子切换即可,而选取柱面则必须通过机械切换。

4.扇区 :操作系统以扇区(Sector)形式将信息存储在硬盘上,每个扇区包括512个字节的数据和一些其他信息。一个扇区有两个主要部分:存储数据地点的标识符存储数据的数据段。扇区的第一个主要部分是标识符。包括组成扇区三维地址的三个数字:扇区所在的磁头(或盘面)、磁道(或柱面号)以及扇区在磁道上的位置即扇区号。扇区的第二个主要部分是存储数据的数据段

硬盘的读写原理

系统将文件存储到磁盘上时,按柱面、磁头、扇区的方式进行,即最先是第1磁道的第一磁头下(也就是第1盘面的第一磁道)的所有扇区,然后,是同一柱面的下一磁头,……,一个柱面存储满后就推进到下一个柱面,直到把文件内容全部写入磁盘。

系统也以相同的顺序读出数据。读出数据时通过告诉磁盘控制器要读出扇区所在的柱面号、磁头号和扇区号(物理地址的三个组成部分)进行。

扇区到来时,磁盘控制器读出每个扇区的头标,把这些头标中的地址信息与 期待检出的磁头和柱面号做比较(即寻道),然后,寻找要求的扇区号。待磁盘控制器找到该扇区头标时,根据其任务是写扇区还是读扇区,来决定是转换写电路, 还是读出数据和尾部记录。

硬盘控制器端口

硬盘控制器属于 IO 接口, 让硬盘工作,我们需要通过读写硬盘控制器的端口,端口的概念在此重复一下,端口就是位于 IO 控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器。

使用硬盘时的端口范围image-20201014203719299

端口可以被分为两组,Command Block registersControl Block registers。Command Block registers 用于向硬盘驱动器写入命令字或者从硬盘控制器获得硬盘状态,Control Block registers 用于控制硬盘工作 状态。在 Control Block registers 组中的寄存器已经精减了,而且咱们基本上用不到。

端口是按照通道给出的,一个通道上的主、从两块硬盘都用这些端口号。要想操作某通道上的某块硬盘,需要单独指定。 有个叫 device 的寄存器,顾名思义,指的就是驱动器设备,也就是和硬盘相关。不过此寄存器是 8 位的,一个通道上就两块硬盘,指定哪一个硬盘只用 1 位就够了,寄存器可是很宝贝的资源,不能浪费, 所以此寄存器是个杂项,很多设置都需集中在此寄存器中了,其中的第 4 位,便是指定通道上的主或从硬盘,0 为主盘,1 为从盘

data 寄存器 : 负责管理数据的,它相当于数据的门,数据能进,也能出,所以其 作用是读取或写入数据。数据的读写还是越快越好,所以此寄存器较其他寄存器宽一些,16 位(已经很不错了,表中其他寄存器都是 8 位的)。在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口, 数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。

Error 寄存器 (Feature 寄存器) : 读硬盘时,端口 0x171 或 0x1F1 的寄存器 ,只在读取硬盘失败时有用,里面才会记录失败的信息,尚未读取的扇区数在 Sector count 寄存器中。在写硬盘时,此寄存器有了别的用途,所以有了新的名字,叫 Feature 寄存器。有些命令需要指定额外参数,这些参数就写在 Feature 寄存器中。 强调一下,error 和 feature 这两个名字指的是同一个寄存器,只是因为不同环境下有不同的用途,为了区别这两种用途,所以在相应环境下有不同的名字。这两个寄存器都是 8 位宽度

Sector count 寄存器 : 用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减 1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。这是 8 位寄存器,最大值为 255,若 指定为 0,则表示要操作 256 个扇区。

CHS : 硬盘中的扇区在物理上是用“柱面-磁头-扇区”来定位的(Cylinder Head Sector),但每 次我们要事先算出扇区是在哪个盘面,哪个柱面上,这太麻烦了,这对于磁头来说很直观,它就是根据这些信息来定位扇区的。这就引出了LBA。。。

LBA 的定义 **: 一种逻辑上为扇区址的方法,全称为逻辑块地址(Logical Block Address)。LBA 分为两种,一种是 LBA28,用 28 位比特来描述一个扇区的地址。最大寻址范围是 2 的 28 次方等 于 268435456 个扇区,每个扇区是 512 字节,最大支持 128GB。 **另外一种是 LBA48,用 48 位比特来描述一个扇区的地址,最大可寻址范围是 2 的 48 次方, 等于 281474976710656 个扇区,乘以 512 字节后,最大支持 131072TB,即 128PB。

LBA 寄存器LBA low、LBA mid、LBA high(8位)。LBA low 寄存器用来存储 28 位地址的第 0~7 位,LBA mid 寄存器用来存储第 8~15 位, LBA high 寄存器存储第 16~23 位。

device 寄存器 :宽度是 8 位,它的低 4 位用来存储 LBA 地址 的第 24~27 位。结合上面的三个 LBA 寄存器。第 4 位用来指定通道上的主盘或从盘,0 代表主盘,1 代表从盘。第 6 位用来设置是否启用 LBA 方式,1 代表启用 LBA 模式,0 代表启用 CHS 模式。另外的两位: 第 5 位和第 7 位是固定为 1 的,称为 MBS 位。

image-20201014210843981

Status寄存器 : 在读硬盘时,端口 0x1F7 或 0x177 的寄存器(8位)。用来给出硬盘的状态信息第 0 位是 ERR 位,如果此位为 1,表示命令出错了,具体原因可见 error 寄存器。第 3 位是 data request 位,如果此位为 1,表示硬盘已经把数据准备好了,主机现在可以把数据读出来。第 6 位是 DRDY, 表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行一些命令。第 7 位是 BSY 位,表示硬盘是否繁忙,如果为 1 表示硬盘正忙着,此寄存器中的其他位都无效。另外的 4 位暂不关注。

image-20201014210935449

command寄存器 : 在写硬盘时,端口 0x1F7 或 0x177 的寄存器。它和 status 寄存器是同一个。此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了

我们系统主要执行3个命令( 感兴趣的 可以去看 ATA 手册

1
2
3
(1)identify:0xEC,即硬盘识别。
(2)read sector:0x20,即读扇区。
(3)write sector:0x30,即写扇区。

总结下寄存器 error、featurestatus、command,大家可以这样来助记:这两组都是同一寄存器(也就是同一端口)多个用途,对同一端口写操作时,硬盘控制器认为这是个命令,对同一端口读操作时,硬盘控制器认为是想获得状态。

常用的硬盘操作方法

最主要的顺序就是 command 寄 存器一定得是最后写,因为一旦 command 寄存器被写入后,硬盘就开始干活啦,它才不管其他寄存器中 的值对不对,一律拿来就用,有问题的话报错就好啦。其他寄存器顺序不是很重要。

(1)先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。
(2)往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。
(3)往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4位,选择操作的硬盘(master 硬盘或 slave 硬盘)。
(4)往该通道上的 command 寄存器写入操作命令。
(5)读取该通道上的 status 寄存器,判断硬盘工作是否完成。
(6)如果以上步骤是读硬盘,进入下一个步骤。否则,完工。
(7)将硬盘数据读出。

硬盘工作完成后,它已经准备好了数据,咱们该怎么获取呢?一般常用的数据传送方式如下。

(1)无条件传送方式。
(2)查询传送方式。
(3)中断传送方式。
(4)直接存储器存取方式(DMA)。
(5)I/O 处理机传送方式。

第 1 种 “无条件传送方式”,应用此方式的数据源设备一定是随时准备好了数据,CPU 随时取随时拿都没问题,如寄存器、内存就是类似这样的设备,CPU 取数据时不用提前打招呼。

第 2 种 “查询传送方式”,也称为程序 I/O、PIO(Programming Input/Output Model),是指传输之前, 由程序先去检测设备的状态。数据源设备在一定的条件下才能传送数据,这类设备通常是低速设备,比 CPU 慢很多。CPU 需要数据时,先检查该设备的状态,如果状态为“准备好了可以发送”,CPU 再去获取数据。硬盘有 status 寄存器,里面保存了工作状态,所以对硬盘可以用此方式来获取数据。

第 3 种 “中断传送方式”,也称为中断驱动 I/O。上面提到的“查询传送方式”有这样的缺陷,由于 CPU 需要不断查询设备状态,所以意味着只有最后一刻的查询才是有意义的,之前的查询都是发生在数据尚未准 备好的时间段里,所以说效率不高,仅对于不要求速度的系统可以采用。可以改进的地方是如果数据源设备将数据准备好后再通知 CPU 来取,这样效率就高了。通知 CPU 可以采用中断的方式,当数据源设备准备好 数据后,它通过发中断来通知 CPU 来拿数据,这样避免了 CPU 花在查询上的时间,效率较高。

第 4 种 “直接存储器存取方式(DMA)”。在中断传送方式中,虽然极大地提高了 CPU 的利用率,但通过中断方式来通知 CPU,CPU 就要通过压栈来保护现场,还要执行传输指令,最后还要恢复现场。没有浪费 CPU 资源,不让 CPU 参与传输,完全由数据源设备和内存直接传输。CPU 直接到内存中拿数据就好了。这就是此方式中“直接”的意思。不过 DMA 是由硬件实现的,不是软件概念,所以需要 DMA 控制器才行。

第 5 种 “I/O 处理机传送方式”。不知大家发现了没有,在说上面每一种的时候都把它们各自说得特别好,似乎完美不可替代了。DMA 已经借助其他硬件了,CPU 已经很轻松了,难道还有更爽的方式?是啊,DMA 方式中 CPU 还嫌爽的不够,毕竟数据输入之后或输出之前还是有一部分工作要由 CPU 来完成的,如数据交换、组合、校验等。 如果 DMA 控制器再强大一点,把这些工作帮 CPU 做了就好。也是哦,既然为了解放 CPU,都已经引用一个硬件(DMA)了,干脆一不做二不休,再引入一个硬件吧。于是,I/O 处理机诞生啦,听名字就知道它专门用于处理 IO,并且它其实是一种处理器,只不过用的是另一套擅长 IO 的指令系统,随时可以处 理数据。有了 I/O 处理机的帮忙,CPU 甚至可以不知道有传输这回事,这下 CPU 才真正爽到家啦。同样, 这也是需要单独的硬件来支持。

综上所述,硬盘不符合第 1 种方法,因为它需要在某种条件下才能传输。第 4 种和第 5 种需要单独 4的硬件支持,先不说我们的 bochs 能否模拟这两种硬件,单独学习这两类硬件的操作方法就很头疼,所以我们用了第 2、3 这两种软件传输方式。

改造MBR

由于MBR 只有 512 字节,这小小的空间,着实干不了什么大事 。。。所以做个稍微大一点的改进,经过这个改进后,我们的 MBR 可以读取硬盘。

这时候 loader ,即加载器出现了,它在另一个程序中完成初始化环境及加载内核的任务。

MBR 负责从硬盘上把 loader 加载到内存

细节要求 : 首先 loader 中要定义一些数据结构(如 GDT 全局描述符表,不懂没关系,以后会说),这些数据结构将来的内核还是要用的,所以 loader 加载到内存后不能被覆盖。 其次,随着咱们不断添加功能,内核必然越来越大,其所在的内存地址也会向越来越高的地方发展,所以,尽量把 loader 放在低处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
 ;主引导程序
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax

;清屏
;利用 0x06 号功能,上卷全部行,则可清屏
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为 0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为 VGA 文本模式中,一行只能容纳 80 个字符,共 25 行
; 下标从 0 开始,所以 0x18=24,0x4f=79
int 10h ; int 10h

; 输出字符串:MBR
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
;A 表示绿色背景闪烁,4 表示前景色为红色

mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4

mov eax,LOADER_START_SECTOR ; 起始扇区 lba 地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,1 ; 待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)

jmp LOADER_BASE_ADDR

;-------------------------------------------------------------------------------
;功能:读取硬盘 n 个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA 扇区号
; bx=将数据写入的内存地址
; cx=读入的扇区数
mov esi,eax ;备份 eax
mov di,cx ;备份 cx
;读写硬盘:
;第 1 步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数

mov eax,esi ;恢复 ax

;第 2 步:将 LBA 地址存入 0x1f3 ~ 0x1f6

;LBA 地址 7~0 位写入端口 0x1f3
mov dx,0x1f3
out dx,al

;LBA 地址 15~8 位写入端口 0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al

;LBA 地址 23~16 位写入端口 0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al

shr eax,cl
and al,0x0f ;lba 第 24~27 位
or al,0xe0 ; 设置 7~4 位为 1110,表示 lba 模式
mov dx,0x1f6
out dx,al

;第 3 步:向 0x1f7 端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al

;第 4 步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第 4 位为 1 表示硬盘控制器已准备好数据传输
;第 7 位为 1 表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等

;第 5 步:从 0x1f0 端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax
; di 为要读取的扇区数,一个扇区有 512 字节,每次读入一个字
; 共需 di*512/2 次,所以 di*256
mov dx, 0x1f0

.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret

times 510-($-$$) db 0
db 0x55,0xaa
1
2
3
4
5
6
7
8
9
10
11
汇编语言中,CPU对外设的操作通过专门的端口读写指令来完成;
读端口用IN指令,写端口用OUT指令。
例子如下:
IN AL,21H;表示从21H端口读取一字节数据到AL
IN AX,21H;表示从端口地址21H读取1字节数据到AL,从端口地址22H读取1字节到AH
MOV DX,379H
IN AL,DX ;从端口379H读取1字节到AL
OUT 21H,AL;将AL的值写入21H端口
OUT 21H,AX;将AX的值写入端口地址21H开始的连续两个字节。(port[21H]=AL,port[22h]=AH)
MOV DX,378H
OUT DX,AX ;将AH和AL分别写入端口379H和378H

boot.inc 是我们的配置文件,我们目前关于加载器的配置信息就写在里面。

1
2
3
;------------- loader 和 kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

编译 写入虚拟硬盘

1
2
3
4
5
添加个库目录 + 编译
--nasm -I include/ -o mbr.bin mbr.S
下面将生成的 mbr.bin 写入我们的虚拟硬盘,还是用 dd 命令。
--sudo dd if=/your_path/mbr.bin of=/your_path/bochs/hd60M.img bs=512 count=1 conv=notrunc
--sudo dd if=/home/fyz/sc/bochs-2.6.2/mbr.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=1 conv=notrunc

我们先不要运行 因为我们 loader 还没写,若此时执行此 MBR,CPU 会直接跳到 0x900 的地方。。。

现在我们开始写 loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
;loader.S
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR

mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4 ; A 表示绿色背景闪烁,4 表示前景色为红色

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4

mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4

mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4

mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4

jmp $ ; 通过死循环使程序悬停在此
1
2
3
4
5
6
7
8
9
10
11
12
13
添加个库目录 + 编译
--nasm -I include/ -o loader.bin loader.S
将生成的 loader.bin 写入硬盘第 2 个扇区。第 0 个扇区是 MBR,第 1 个扇区是空的未使用。( 我就喜欢 你咋的
--sudo dd if=./loader.bin of=/your_path/bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc
--sudo dd if=./loader.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=1 seek=2 conv=notrunc

得到
记录了0+1 的读入
记录了0+1 的写出
98 bytes copied, 0.0976747 s, 1.0 kB/s

模拟
--sudo bin/bochs -f bochsrc.disk

已经成功运行

image-20201015111210074

Reward
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • © 2015-2021 John Doe
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信