Songqian Li's Blog

去历史上留点故事

3.3 让我们对显示器说点什么吧

3.3.1 CPU 如何与外设通信——IO 接口

IO 接口功能:

  1. 设置数据缓冲,解决 CPU 与外设的速度不匹配
  2. 设置信号电平转换电路
  3. 设置数据格式转换
  4. 设置时序控制电路来同步 CPU 和外部设备
  5. 提供地址译码

CPU 通过总线访问 IO 接口,但是同一时刻 CPU 只能和一个 IO 接口通信,当很多的 IO 接口同时想和 CPU 对话时,面对众多接口时 CPU 应该如何选择?这个工作不应该由 CPU 来做,CPU 太忙了,还是好刚使在刀刃上吧,既然分层能解决问题,咱们再加一层,这一层的责任是除了仲裁 IO 接口的竞争,还要连接各种内部总线。由于它的使命,它的名字就叫做输入输出控制中心(I/O control hub,ICH),也就是南桥芯片,如图 3-11 所示。
image.png
说到了南桥,多少还要提一句北桥芯片的作用。图 3-11 上标上北桥部分其实是散热片,在它先买你才是北桥。由于南桥和北桥一般是成对出现的, 至少在支持 Intel CPU 的主板中是这样的(话说, AMD 为了减少 CPU 同北桥交换数据的成本,已经把北桥的工作放到了 CPU 内部,所以支持 AMD 的板子上未必有北桥芯片)。南桥用于连接 pci、pci-express、AGP 等低速设备, 北桥用于连接高速设备,如内存。
CPU 通过内部总线连接到南桥芯片中的内部,这个内部总线是专用的,它只通向位于南桥中的 CPU 接口。
image.png
IO 接口在诞生之初,就被设计成要通过寄存器的方式同 CPU 通信,其内部有专用于数据交互的寄存器,只不过这里所说的这些寄存器位于 IO 接口中,为了区别于 CPU 内部的寄存器, IO 接口中的寄存器就称为端口(这可不是网络应用程序所开的那种端口,如网络服务器会启动 80 端口,这是两码事)。

3.3.2 显卡概述

mbr 运行在实模式下,所以在实模式下也可以用 BIOS 0x10 中断打印字符串,这是因为中断表只在实模式下存在, 保护模式下没有中断向量表。其次是不希望有更多的依赖,好不容易脱离了对操作系统的依赖,又引入了一个新的依赖,这不科学。
显卡是 pci 设备,所以是安装在主板上 pci 插槽上的, pci 总线是共享井行架构,并行数据就要保证数据发送后必须同时到达目的地,因为这关系到数据的顺序,不能发过去后成一团乱麻 。虽然貌似并行传输是高效的,但对于要保证同 时接收 位数据,这是有困难的,随着并行数据的位宽越来越大,这种困难也越来越明显。于是串行传输 很好地解决了这一问题,一次只发一位,这样顺序问题解决了,数据到目的地看再组合到 起就成了。于 是就有了 PCI Express 总线,这就是串行设备,简称 pcie。
某些 IO 接口也叫适配器,适配器是驱动某外部设备的功能模块。显卡也称为显示适配器,不过归根结底它就是 IO 接口,专门用来连接 CPU 和显示器。我们想操作显示器,没有直接的办法,只能通过它的 IO 接口一一显卡。

3.2.3 显存、显卡、显示器

显存是从 0xB8000~0xBFFFF,范围是 32KB,一屏可以显示 2000 个字符,显示器上的每个字符占 2 字节大小,故每屏字符实际占用 4000 字节。这样,我们的 32KB 的显存可以容纳 32KB/4000B 约等于 8 屏的数据。
image.png
屏幕上每个字符的低宇节是字符的 ASCII 码,高字节是字符 属性元信息。在高字节中,低 4 位是字符前景色,高 4 位是字符的背景色。颜色用 RGB 红绿蓝三种基色调和,第 4 位用来控制亮度,若置 1 则呈高亮,若为 0 则为一般正常亮度值。第 7 位用来控制字符是否闪烁(不是背景闪烁)。这两字节如图 3-13 所示。
大家知道,用 红色、 绿色、 蓝色这 种颜色以任意比例混合,可以搭配出其他颜色,其他颜色被认为都可以由这三种颜色组合 而成。不过由于在文本模式下的颜色极其有限, RGB 的各部分比例要么是 1(全部),要么是 0(没有),所以其组合出的颜色屈指可数。
image.png
从上面可以看出只要亮度位 ,颜色就是变亮变浅。大家可以结合 位来测试上面的颜色。

3.3.4 改进 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
;主引导程序
;
;LOADER_BASE_ADDR equ 0xA000
;LOADER_START_SECTOR equ
;-----------------------------------------------------
SECTION MBR vstart=0x7c00 ;告诉编译器,起始地址是0x7c00
mov ax,cs ;因为BIOS执行完毕后cs:ip为0x0:0x7c00,所以用cs初始化各寄存器(此时cs=0)
mov ds,ax ;ds、es、ss、fs不能给立即数初始化,需要用ax寄存器初始化
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00 ;初始化堆栈指针,因为目前0x7c00以下的内存暂时可用
mov ax,0xb800 ;选择显卡的文本模式
mov gs,ax ;使用GS段寄存器作为显存段基址

;清屏利用0x06号功能,上卷全部行,则可清屏
;-----------------------------------------------------
;INT 0x10
;-----------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:

mov ax,0x600 ;上卷行数:全部 功能号:06
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
;输出背景色为蓝色,前景色为红色,并且跳动的字符串“1 MBR”
;--------------------------------------------------------------------------
mov byte [gs:0x00], '1'
mov byte [gs:0x01], 0x92

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

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

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

mov byte [gs:0x08], 'R'
mov byte [gs:0x09], 0x12
;--------------------------------------------------------------------------

jmp $ ;使用程序悬停在此

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

第 13 行和第 14 行是往 gs 寄存器中存入段基址。
显存段基址放在哪个寄存器中都是没关系的,对于访问的是数据来说,如果不用也做段基址寄存器,就要在寻址中“显式地”指明要用哪个段寄存器的值作为段基址。这个“显式的”的段寄存器叫作段跨越前缀,有的书中叫段超越前缀,个人觉得意义不明确。何为超越?由于有“跨段访问”的说法,所以咱们这里统一为段跨越前缀。“段跨越”相对好理解,如 CPU 的访存策略是“段地址+段内偏移地址”。堆栈段的寄存器是 SS ,代码段寄存器 CS ,这两者不存在默不默认之说,因为它们都不能改变。不过对于数据段来说却有些不同,默认的寄存器是 DS ,但其是可以改变的。一般访问数据时只要给出偏移地址就可以了,这是因为已经存在了默认的段寄存器 DS ,所以访存中给出的偏移地址便是相对于 DS 的偏移量,也就是说访问的地址属于以 DS 为起始的段(是指一般意义上的分段机制,不考虑实模式或保护模式)。但若不想用这个段了,或者访问的地址不属于这个段,想“跨越”这个默认段,而用新的段基址,“跨过”DS 的限制,这就是“跨越”的理解。而“前缀”的意思是在编译后的机器码中,指定的这个新的段寄存器会出现在 IA32 指令格式中的“前缀”宇段,可以参见表 3-1 “IA32 指令格式”。 基于以上两点,为代替默认段基址寄存器而改用的新的段基址寄存器,称为段跨越前缀。
在第 37 50 行执行的 mov 操作都是往显存中写字符。拿 37 行和 38 行举例,第 37 行的mov byte [gs: 0x00],'1',是往 gs 为数据段基址,以 gs 为偏移地址的内存中写入字符 ASCII 码。按之前我们讲过的,写入时,要写入 ASCII 0x31,这是最直接的做法。但编译器诞生的意义就是为了给大家带来方便,尽管我们可以把 37 行的代码改为mov byte [gs: 0x00], 0x31 ,但这样毕竟还要自己查 ASCII 码表 。编译器对于出现在代码中的字符,它会自动将其改为相应的 ASCII 码,免去了人工查表的过程。即使把表整个背下来了,本质上也是在脑子中经过了一次查表。所以,对于字符的输出,直接写出相应字符就行了,稍微有点人性化的编译器都会自动完成字符到编码的转换。
“mov byte [gs:0x00],‘l’”表示的意思是:把字符 ASCII 码写入以 gs: 0x00 为起始,大小为 1 字节的内存中。 word dword 分别表示 2 字节和 4 字节,意义同理。如果源操作数或目的操作数己经明确了数据宽度,在指令中就不必“显示地”指明操作数所占的空间大小了。例如 mov ax, 0x10,目的操作数是 16 位的,所以不用“显示地”在 ax 前或 0x10 前加个关键字 word。

3.4 bochs 调试方法

bochs 的硬件调试体现在:

  1. 调试时可以查看页表、 gdt idt 等数据结构;
  2. 可以查看栈中数据;
  3. 可以反汇编任意内存;
  4. 实模式、保护模式互相变换时提醒;
  5. 中断发生时提醒。

3.4.1 一般用法

  1. xp /nuf <addr> 查看内存
  2. u/1 <addr>将内存数据反汇编成指令

“DEbugger control” 类:

  1. q|quit|exit退出调试状态,关闭虚拟机
  2. show mode 每次 CPU 变换模式时就提示,模式是指保护模式、实模式
  3. show int每次有中断时就提示,同时显示三种中断类型,这三种中断类型包括"softint"、“extint"和"iret”。也可以单独显示某类中断,例如show softint
  4. show call每次有函数调用发生时就会提示
  5. traceontraceoff每执行一条指令,bochs 都会将反汇编的代码打印到控制台
  6. u|disasm [/num] [start] [end] 将物理地址 start end 之间的代码反汇编,如果不指定地址,则反汇编 EIP 指向的内存,num 指定反汇编的指令数

"Execution control"类:

  1. c|cont|continue向下持续执行。
  2. s|step [count]执行 count 条指令。若不指定, count 默认为 1。此指令若遇到函数调用,则会进入函数中去执行;
  3. p|n|next执行 1 条指令,若待执行的指令是函数调用,不管函数内有多少指令,把整个函数当作整体来执行。

"Breakpoint management"类:

  1. 以地址打断点
    1. vb|vbreak [seg:off]以虚拟地址添加断点,程序执行到此虚拟地址时停下来,注意虚拟地址是“段: 段内偏移”的形式。最常用的是 vb;
    2. lb|lbreak [addr]以线性地址打断点;
    3. pb|pbreak|b|break [addr]以物理地址打断点;
  2. 以指令数打断点
    1. sb [delta]意味着再执行 delta 条指令程序中断;
    2. sba [time] CPU 从运行开始,执行第 time 条指令时中断,从 0 开始的指令数
  3. 以读写 IO 打断点
    1. watch:显示所有读写断点
    2. watch r|read [phy_addr]设置读断点
    3. watch w|write [phy_addr]设置写断点
    4. unwatch [phy_addr]清除在此地址上的读写断点
    5. blist显示所有断点信息;
    6. bpd|bpe [n]禁用/启用断点,n 是断点号;
    7. d|del|delete [n]删除断点,n 是断点号,用 blist 查出来

"CPU and memory contents"类:

  1. x /nuf [line_addr]显示线性地址的内容;
  2. xp /nuf [phy_addr]显示物理地址的内容;
  3. info查看信息的指令族
    1. info CPU
    2. info fpu
    3. info idt
    4. info gdt [num]
    5. info ldt
    6. info tss
    7. info ivt [num]
    8. info flags|eflags
    9. info sreg|dreg|creg显示所有段寄存器|调试寄存器|控制寄存器的值

3.5 硬盘介绍

针对硬盘的 IO 接口是硬盘控制器。硬盘控制器同硬盘的关系,融通显卡和显示器一样,他们都是专门驱动外部设备的模块电路,CPU 只同它们说话,由它们将 CPU 的话转译给外部设备。这是他们的共同点,但不同的是显卡和显示器是分开的,硬盘控制器和硬盘是连接在一起的。但在很久很久以前,硬盘控制器和硬盘也是分开的,后来业界的几个老大合作开发初一中新的接口,这样才将硬盘和硬盘控制器整合在一起,为了凸显“整合”之意,硬盘控制器和硬盘终于在一起了,这种接口便称为集成设备电路(Integrated Drive Electronics, IDE)。随着 IDE 接口标准的影响力越来越广泛,全球变转化协议将此接口使用的技术规范归纳称为全球硬盘标准,这样就产生了 ATA(Advanced Technology Attachment)。由于前几年刚出道大营盘串行接口(Serial ATA,SATA),由于其是串行,所以之前的 ATA 接口只好称为并行 ATA,即(Parallel ATA,PATA)。
以前一般的主机只支持 4 个并口硬盘,但自从出现串口硬盘后, 情况就变了,支持多少块硬盘,取决于主板的能力。有的主板同时兼容这两种接口。
image.png
常用的端口寄存器示意图:
image.png

3.5.4 常用的硬盘操作方法

大概步骤:
最主要的顺序就是 command 寄存器一定得是最后写,因为一旦 command 寄存器被写入后,硬盘就开始干活了。

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

硬盘工作完成后,常用的获取数据方式,也就是数据传送方式包括:

  1. 无条件传送方式
    1. 随时准备好了数据,CPU 随取随拿都没问题,如寄存器、内存等设备
  2. 查询传送方式
    1. 也称为程序 I/O,PIO,是指传输之前,由程序先去检测设备的状态,数据源在一定的条件下才能传送数据,例如硬盘等设备
  3. 中断传送方式
    1. 也称为中断驱动 I/O。上面的“查询传送方式”是 CPU 轮询,效率低。如果数据源设备将数据准备好后再通知 CPU 来取,效率就高了。该方法是当数据源设备准备好数据之后,通过发中断来通知 CPU 来拿数据,这样避免了 CPU 花在查询上的时间。
  4. 直接存储器存取方式(DMA)
    1. 在上面的“中断传送方式”中,通过中断来通知 CPU,CPU 就要通过压栈来保护现场,还要执行传输指令,最后还要恢复现场。该方法是不让 CPU 参与运输,完全由数据源设备和内容直接传输。CPU 直接到内存中拿数据就好了。不过 DMA 是由硬件实现的,需要 DMA 控制器才行。
  5. I/O 处理机传送方式
    1. DMS 方式中数据输入之后或输出之前还是以后一部分工作要由 CPU 来完成的,例如数据交换、组合、校验等。既然已经引用一个硬件(DMA)了,那一不做二不休再引入一个硬件吧。于是 I/O 处理机诞生了。它专门用来处理 IO,并且他其实是一种处理器,只不过用的是另一套擅长 IO 的指令系统,随时可以处理数据。

3.6 让 MBR 使用硬盘(实践)

3.6.1 改造 MBR

我们的如 MBR 受限于 512 字节大小的,在那么小的空间中,设法为内核准备好环境,更没法将内核成功加载到内存井运行 所以我们要在另一个程序中完成初始化环境及加载内核的任务,这个程序我们称之 loader ,即加载器。 loader 会在下一节中实现。问题来了,loader 在哪里?如何跳过去执行?这就是新款 MBR 的使命,简而言之就是负责从硬盘上把 loader 加载到内存,并将接力棒交给它。
由于 MBR 是占据了硬盘的第 0 扇区(以逻辑 LBA 方式,扇区从 0 开始编号,若是以物理 CHS 方式, 扇区则从 开始编号),第 扇区是空闲的 可以用,但离得太近总感觉不如隔开 点心里踏实,所以把 loader 放到第 扇区。如 IBR 从第 肩区中把它读出来。读出来放到哪里呢?原则上是找个空闲地方就行 了,在表 1-1“实模式下的内存布局”中查看下,只要在“用途”列中注明“可用区域”的地方都可以用 0x500~0x7BFF 和 0x7E00~9FBFF 这两段内存区域都可以。

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
;主引导程序
;-----------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00 ;告诉编译器,起始地址是0x7c00
mov ax,cs ;因为BIOS执行完毕后cs:ip为0x0:0x7c00,所以用cs初始化各寄存器(此时cs=0)
mov ds,ax ;ds、es、ss、fs不能给立即数初始化,需要用ax寄存器初始化
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00 ;初始化堆栈指针,因为目前0x7c00以下的内存暂时可用
mov ax,0xb800 ;选择显卡的文本模式
mov gs,ax ;使用GS段寄存器作为显存段基址

;清屏
;利用0x06号功能,上卷全部行,则可清屏
;-----------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;-----------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值
mov ax,0x600 ;上卷行数:全部 功能号:06
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
;输出背景色为蓝色,前景色为红色,并且跳动的字符串“1 MBR”
;--------------------------------------------------------------------------
mov byte [gs:0x00], '1'
mov byte [gs:0x01], 0x92

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

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

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

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

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

jmp LOADER_BASE_ADDR ;跳转到Loader

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

mov eax ,esi ;恢复eax

;2---将LBA地址存入0x1f3~0x1f6

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

;LBA 15~8位写入端口0x1f4
mov cl ,8
shr eax ,cl ;逻辑右移8位,将eax的最低8位移掉,让最低8位al的值变成接下来8位
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 ;一个扇区512字节,一次读取2字节,需要读取256次
mul dx ;结果放在ax里
mov cx ,ax ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字
; 共需di*512/2次,所以di*256
mov dx ,0x1f0
.go_on_read:
in ax, dx
mov [bx], ax ;bx是要读取到的内存地址
add bx, 0x02
loop .go_on_read ;循环cx次
ret

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

程序最开始的%include "boot.inc”,这个%include nasm编译器中的预处理指令,意思是让编译器在编译之前把 boot.inc 文件包含进来。任何编译器都应该有 include 之类的能够包含其他文件的预处理指令, 不要认为底层的汇编语言就应该简陋到一穷二白,哈哈,这和语言是没关系的,是编译器为了开发人员方便管理代码,应该加的。 boot.inc 的内容很简单,目前就两句话,文件内容如下:

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

boot.inc 是配置文件,关于加载器的配置信息就卸载里面,今后还会在此添加更多的配置信息。大家看到的这两句也是预处理命令,是 nasm 提供的宏,和 C 语言中的宏是一回事。只不过 nasm 中的语法是:宏名 equ 值,而 C 语言中的宏是由#define 指令来实现的。所以LOADER_BASE_ADDRLOADER_START_SECTOR是两个宏名。
LOADER_BASE_ADDR定义了 loader 在内存中的位置,MBR 要把 laoder 从硬盘读入后放到抵触。
LOADER_START_SECTOR定义了 loader 在硬盘上的逻辑扇区地址,即 LBA 地址。
第 4~48 行和上一版本没区别,第 50~52 行为函数rd_disk_m_16传递参数。在此说明一下汇编语言中定义的函数(或者称为例程,proc),由于汇编语言能够直接操作寄存器,所以其传递参数可以用寄存器,也可以用栈。由于 C 语言中不能直接操作寄存器,所以这里体验一会用寄存器来传递参数的函数是怎样实现的。用寄存器传参数,没有固定的形式,原则上用哪个寄存器都行,只要更具实际应用,别把还有用的寄存器值给覆盖就行,如果真需要用到某个正在使用中的寄存器,只要提前把该寄存器备份好就行了,如备份到其他寄存器或压入栈中。此函数需要三个参数,我么你选择用 eax、bx、bx 寄存器来传递参数。
在寄存器 eax 中的是带读入的扇区起始地址,赋值后 eax 为定义的宏LOADER_START_SECTOR,即 0x2.
寄存器 cx 是读入的扇区数,cx 其值为 1。到底读入几个扇区,事由实际文件大小来决定的。由于将来会写一个简单的 loader,其大小肯定不会超过 512 字节,所以次数度融入的扇区数置入为 1 即可。
数据从硬盘读进来后放在内存中哪里呢?这就要用寄存器 bx 来指定。在这里, bx 寄存器值为 LOADER BASE ADDR ,即 0x900 。函数名 rd disk m_l6 的意思是“在 16 位模式下读硬盘”。
第 64 行的“ mov esi, eax ”是把 eax 中的值先备份到 esi 。因为 out 指令中会被用到,这会影响到 eax 的低 位。
第 65 行是备份读取的扇区数到 di 寄存器, di 寄存器是 16 位的,和 ex 大小一致。 ex 的值会在读取数据时用到,所以在此提前备份。
第 67~70 行,按照咱们操作硬盘的约定,先选定一个通道,再往 sector count 寄存器中写扇区数。往端口中写入数据用 out 指令,注意 out 指令中 dx 寄存器是用来存储端口号的。先查看 bochs 配置文件关于硬盘的配置部分
image.png
虚拟硬盘属于 ata0,是 Primary 通道,所以其 sector count 寄存器是由 0x1f2 端口来访问的。顺便再看第二行的 ata0-master,path=“hd60M.img”,这说明 hd60M.img 是主盘。
第 74~95 行是将 LBA 地址写入三个 LBA 寄存器和 device 寄存器的低 4 位。端口 0x1f3 是寄存器 LBA low,端口 0x1f4 是寄存器 LBA mid,端口 0x1f5 是寄存器 LBA high。shr 指令是逻辑右移指令,这里主要通过此指令置换出地址的相应部分,写入相应的 LBA 寄存器。第 93 行用了“or”指令和 0xe0 做或运算,拼出 device 寄存器的值。高 4 位为 e,即高 4 位的二进制表示为 1110,其第 5 位和第 7 位固定为 1,第 6 位为 1 表示启用 LBA。
第 97~100 行便是写入命令,因为这里是读操作,所以读扇区的命令是 0x20.通过 out 指令写入 command 端口 0x1f7 后,硬盘就开始工作了。
第 102~109 行检测 status 寄存器的 BSY 位。由于 status 寄存器依然是 0x1f7 端口,所以不需要再为 dx 重新赋值。 105 行的 nop 表示空操作,即什么也不做,只是为了增加延迟,相当于 sleep 一小下,目 的是减少打扰硬盘的工作。对同一端口在读写两种操作时有不同的用途,在读硬盘时,此端口中的值是硬盘的工作状态。第 106 行是将 Status 寄存器的值读入到 al 寄存器,通过第 107 行的 and “与”操作,保留第 4 位和第 7 位,第 4 位若为 1,表示数据已经准备好,可以传输了。若第 7 位为 1,表示硬盘现在正忙着。只要判断第 4 位是否为 1 就好了,用第 108 行的 cmp 指令和 0x08 做减法运算,判断第 4 位是否为 1。 cmp 指令并不改变操作数的值,只是根据结果去设置标志位,从而咱们根据标志位反着去判断结果。 cmp 指令会影响的标志位有 ZF、CF、PF 等,这里咱们借助 ZF 位来判断 cmp 的结果。于是用第 109 行的 jnz .not_ready 来判断结果是否不等于 0,即若等于 0,则 status 寄存器的第 4 位为 1,这表示可以读数据了。若不等于 0,说明 status 寄存器的第 4 位为 0,表示硬盘正忙(此时 status 寄存器第 7 位肯定为 1)。not_ready 是个标号,于是跳回去继续判断硬盘状态,直到硬盘把数据准备好才跳出这个循环。
第 111~122 行是从硬盘取数据的过程。由于 data 寄存器是 16 位,即每次 in 操作只读入 2 字节,根据读入的数据总量(扇区数*512 字节)来求得执行 in 指令的次数。这里的乘法用 mul 指令,在实模式下,mul 指令可以做 8 位乘法和 16 位乘法,格式是:mul 操作数。操作数可以是寄存器或内存。乘法运算至少要有两个数参与才行,这里的操作数只是一个乘数,被乘数隐含在 al 或 ax 寄存器中。如果操作数是 8 位,被乘数就是 al 寄存器的值,乘积就是 16 位,位于 ax 寄存器,积的低 16 位在 ax 寄存器。
虽然我们进行的是 16 位的乘法,其结果是 32 位,但由于我们知道这两个乘数 ax 的值和 dx 的值都不大,ax 的实际的值其实是 1,乘出来的这个结果,其高位是 0,所以在第 115 行的“mov cx, ax”我们只将这个结果的低 16 位移入 cx 作为循环读取的次数。此处用 8 位乘法不合适,因为 256 超过了 8 位寄存器表示的范围。在第 118~122 行通过循环来讲数据写入 bx 寄存器指向的内存,没读入 2 个字节,bx 所指的地址便+2.值得注意的是由于在实模式下偏移地址为 16 位,所以呀用 bx 只会访问到 0~FFFFh 的偏移。待写入的地址超过 bx 的范围是,从硬盘上取出的数据会把 0x0000~0xffff 的覆盖,所以此处加载的程序不能超过 bx 的范围时,从硬盘上读出的数据会把 0x0000~0xffff 的覆盖,所以 此处加载的程序不能超过 64KB,即 2 的 16 次方等于 65536。由于本 mbr 是用来加载 loader 的,所以 loader.bin 要小于 64KB 才行。当然,我们最终的 loader 不超过 2KB,将来的内核他也不会超过 70KB。
第 123 行返回指令 ret,它用来从函数中返回。执行完第 123 行后,程序便回到了第 55 行,这里是 MBR 交出接力棒的一刻,采用 jmp 是唯一合适的选择。Jmp 的操作数是 LOADER_BASE_ADDR,即 0x900,这是要跳到内核加载器了,完成第二棒的交接。(第一棒是 BIOS 交给 MBR)。
进行编译:

1
2
nasm -I include/ -o mbr.bin mbr.S
dd if=./mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc

3.6.2 实现内核加载器

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
%include "boot.inc"
section loader svtart=LOADER_BASE_ADDR

; 输出背景色绿色,前景色红色,并且跳动的字符串"2 Loader"
mov byte [gs:0x00], '2'
mov byte [gs:0x01], 0xA4

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 $ ; 通过死循环使程序悬停在此

编译 loader

1
nasm -I include/ -o boot/loader.bin loader.S

将 loader 写入硬盘

1
dd if=boot/loader.bin of=hd60M.img bs=512 count=1 seek=2 conv=notrunc

运行 bochs,如果程序正确的话,MBR 会跳转到 loader.bin 去运行,屏幕会显示“2 loader”。
image.png

相关文章
评论
分享
  • 《操作系统真象还原》:第八章 内存管理系统

    8.1 makefile 简介 这部分可参考阮一峰的讲解:https://www.ruanyifeng.com/blog/2015/02/make.html 8.1.1 makefile 是什么 makefile 是 Linu...

    《操作系统真象还原》:第八章 内存管理系统
  • 《操作系统真象还原》:第七章 中断

    7.1 中断是什么,为什么要有中断 运用中断能够显著提升并发,从而大幅提升效率。 7.2 操作系统是中断驱动的 略 7.3 中断分类 把中断按事件来源分类,来自 CPU 外部的中断就称为外部中断,来自 CPU 内部的中断称为内部...

    《操作系统真象还原》:第七章 中断
  • 《操作系统真象还原》:第六章 完善内核

    6.1 函数调用约定简介 咱们实验使用cdecl。这里提一下stdcall,cdecl与stdcall的区别在于由谁来回收栈空间。 stdcall是被调用者清理参数所占的栈空间。 举例来说: 12int subtract(int ...

    《操作系统真象还原》:第六章 完善内核
  • 《操作系统真象还原》:第五章 保护模式进阶——加载内核

    5.3 加载内核 5.3.1 用 C 语言写内核 第一个 C 语言代码: 1234int main(void) { while(1); return 0;} 这个内核文件什么都没做,通过while(1)这个死循...

    《操作系统真象还原》:第五章 保护模式进阶——加载内核
  • 《操作系统真象还原》:第五章 保护模式进阶——内存分页机制

    从这一刻起,我们才算开始了真正的操作系统学习之旅 5.1 获取物理内存容量 5.1.1 Linux 获取内存的方法 在 Linux 2.6 内核总是用detect_memory函数来获取内存容量的。其函数本质上是通过调用 BI...

    《操作系统真象还原》:第五章 保护模式进阶——内存分页机制
  • 《操作系统真象还原》:第四章 保护模式入门

    4.1 保护模式概述 在本章大家会见到全局描述符表、中断描述符表、各种门结构,这是 CPU 提供给应用的,咱们用好就行。 保护模式强调的是“保护”,它是在 Intel 80286 CPU 中首次出现,这是继 8086 之后,Inte...

    《操作系统真象还原》:第四章 保护模式入门
  • 《操作系统真象还原》:第三章 完善MBR——CPU的实模式

    针对汇编 几个知识点: 第 1 行和第 4 行的 mov 操作,机器码第 1 个宇节都是B8,而另外第 2、3 行同样是 mov 指令,机器码却有天壤之别,似乎找不到共性。原因是机器码是由很多部分组成的,比如指令前缀、主操作码字...

    《操作系统真象还原》:第三章 完善MBR——CPU的实模式
  • 《操作系统真象还原》:第二章 编写 MBR

    先了解 CPU 的两种工作模式:实模式和保护模式 实模式(英语:Real mode)是 Intel 80286 和之后的 x86 兼容 CPU 的操作模式。实模式的特性是一个 20 比特的区段存储器地址空间(意思为只有 1MB 的存...

    《操作系统真象还原》:第二章 编写 MBR
  • 《操作系统真象还原》:第一章 环境配置

    第 0 章:一些你可能正感到迷惑的问题 摘记 0.28 MBR、EBR、DBR 和 OBR 各是什么 MBR 位于整个硬盘最开始的块, EBR 位于每个子扩展分区,各子扩展分区中只有一个逻辑分区。 MBR 和 EBR 位于分...

    《操作系统真象还原》:第一章 环境配置
  • 6月阅读总结

    “零拷贝”技术 Sogou C++ Workflow:搜狗公司的 C++服务器引擎,支持 500k QPS Reducing CPU scheduler latency in Linux:CPU 调度算法 BMQ 和 CFS 的对比...

    6月阅读总结