Featured image of post 《操作系统真象还原》第三章——完善MBR

《操作系统真象还原》第三章——完善MBR

本文解释了显卡和显存的作用,以及如何通过内存映射技术提高访问速度。介绍了显卡的工作原理,包括显存的职责,ASCII码在屏幕显示中的作用,以及如何通过BIOS中断函数操作显存来显示字符。还涉及了操作系统加载和内核加载的过程,展示了如何通过MBR和loader.S文件实现基本的屏幕内容显示

本章节所有代码托管在miniOS_32

章节任务

操作显存显示内容

上一节,我们介绍了计算机在开机后BIOS是如何从磁盘中加载mbr程序并运行的,为了测试mbr程序,我们使用BIOS中断写了一段在屏幕上显示内容的代码,以此来测试BIOS加载mbr程序的过程,如果BIOS能够成功加载mbr程序并运行,则必然会在屏幕上显示我们在mbr程序中写的内容

本节我们将介绍显卡与显存,而我们的第一个任务,就是将通过显存来控制屏幕输出内容,替换掉BIOS中断的方法

前置知识

显卡与显存

为了显示文字,通常需要两种硬件,一是显示器,二是显卡。

  • 显卡的职责是为显示器提供内容,并控制显示器的显示模式和状态
    • 显卡未必一定是独立的插卡。为了节省使用者的成本,有的显卡会直接做在主板上,这样的显卡也有个名字,叫集成显卡。
    • 显卡控制显示器的最小单位是像素。
  • 显示器的职责是将显卡的内容以视觉可见的方式呈现在屏幕上。

显存就是存放要在显示器上显示的内容的器件,因为它位于显卡上,故称显示存储器,简称显存,要显示的内容都预先写入显存。

所以,显存可以认为就是一块特殊的内存,用于存储显示在屏幕上的内容,**为了给出要显示的字符,处理器需要访问显存,把字符的ASCII码写进去。**但是,显存是位于显卡上的,访问显存需要和显卡这个外围设备打交道。同时,多一道手续自然是不好的,这当中最重要的考量是速度和效率。

为此,计算机系统的设计者们,决定把显存映射到处理器可以直接访问的地址空间里,也就是内存空间里。

如图所示,8086可以访问1MB内存。其中,0x00000~9FFFF属于常规内存,由内存条提供;0xF0000~0xFFFFF由主板上的一个芯片提供,即ROM-BIOS。而0xB8000~0xBFFFF这段物理地址空间,是留给显卡的,由显卡来提供,用来显示文本。

image-20241209234527776

屏幕是如何显示内容的

**显卡的工作是周期性地从显存中提取这些比特,并把它们按顺序显示在屏幕上。如果是比特“0”,则像素保持原来的状态不变,因为屏幕本来就是黑的;如果是比特“1”,则点亮对应的像素。**其工作原理如下所示

image-20241210154304475

屏幕上的每个字符对应着显存中连续2字节,前一个是字符的ASCII代码,后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)。

如图所示,字符“H”的ASCII代码是0x48,其显示属性是0x07;字符“e”的ASCII代码是0x65,其显示属性是0x07。

image-20241210154611380

字符的显示属性(1字节)分为两部分,

  • 低4位定义的是前景色
  • 高4位定义的是背景色
  • 色彩主要由R、G、B这3位决定
  • K是闪烁位,为0时不闪烁,为1时闪烁
  • I是亮度位,为0时正常亮度,为1时呈高亮

如下,给出了背景色和前景色的所有可能值

image-20241210154821420

mbr功能实现

编写mbr.S代码

 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
;主引导程序MBR,由BIOS通过jmp 0:0x7c00跳转
 
;------------------------------------------
;vstart=0x7c00表示本程序在编译时,起始地址编译为0x7c00
SECTION MBR vstart=0x7c00
;由于BIOS是通过jmp 0:0x7c00跳转到MBR,故此时cs为0
;对于 ds、es、fs、gs 这类 sreg,CPU 中不能直接给它们赋值,没有从立即数到段寄存器的电路实现,只有通过其他寄存器来中转,这里我们用的是通用寄存器ax来中转。
    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,0x0600
    mov bx,0x0700
    mov cx,0        ;左上角(0,0)
    mov dx,0x184f   ;右下角(80,25)
                    ;0x18=24,0x4f=79
    int 0x10        ;调用BIOS中断函数
;------------------------------------------
 
;将要显示的字符串写入到显存中
    mov byte [gs:0x00],'1'  ;在第一个字节的位置写入要显示的字符“1”
                            ;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
    mov byte [gs:0x01],0xA4
 
    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,用于指定后续数据将被重复多少次
;$表示该行指令地址,$$表示该段(section)的开始地址,因此($-$$)表示程序总共占用的字节大小
;由于mbr程序的最后两个字节的内容(标志位0x55,0xaa)是固定的,而一个扇区总共是512字节,
;因此要把前510(512-2)个字节的内容给填满。
;故times 510-($-$$) db 0这段代码就表示用0将本扇区剩余空间进行填充
    times 510-($-$$) db 0
    db 0x55,0xaa

编译代码

1
nasm mbr.S -o mbr.bin

将程序写入磁盘,注意一定要用绝对路径

1
dd if=/home/minios/osCode/miniOS_32/ch3/task1/mbr.bin of=/home/minios/bochs/hd60M.img bs=512 count=1 conv=notrunc

运行,运行时也一定要在bochs安装目录下

1
./bin/bochs -f boot.disk

image-20241210162056918

从硬盘中加载程序到内存

目前为止,我们已经实现的事情是

  • 完成了一个简单的mbr程序,它存放在磁盘的0盘0道1扇区

  • BIOS启动后会自动从磁盘的0盘0道1扇区的位置寻找代码文件,如果该代码文件的最后两个字节的内容为0x55和0xaa,就说明该文件是mbr程序,于是BIOS就将该文件加载到内存的0x7c00位置开始运行

接下来我们要做的事情是

  • 由于mbr程序只有512字节大小,不能完成内核的所有初始化任务,因此需要有另外一个程序,专门负责加载内核程序
  • 因此mbr真正的任务其实是将将来的loader程序从磁盘加载到内存的某一位置

是的,mbr的任务和BIOS的任务是一样的:

BIOS将mbr加载到内存的0x7c00之后运行mbr,mbr运行后将loader程序从磁盘再加载到内存的某一位置,唯一的区别在于,mbr从磁盘加载文件到内存的任务由我们自定义实现

因此,接下来我们就要对磁盘有一个简单的认识

磁盘的基本介绍

磁盘的读写操作

让硬盘工作,需要通过读写硬盘控制器的端口,端口就是位于 I0 控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器,这里需要解释一些概念:

  • 硬盘控制器属于IO接口,硬盘控制器同硬盘的关系,如同显卡和显示器一样,它们都是专门驱动外部设备的模块电路,CPU 只同它们说话,由它们将 CPU的话转译给外部设备。这是它们的共同点,但不同的是显卡和显示器是分开的,硬盘控制器和硬盘是连接在一起的。
  • IO接口通过寄存器的方式同CPU通信,其内部有专用于数据交互的寄存器,只不过这里所说的这些寄存器位于IO接口中,为了区别于CPU内部的寄存器,I0 接口中的寄存器就称为端口(这可不是网络应用程序所开的那种端口,如网络服务器会启动80端口,这是两码事)
IO端口的访问

Intel 汇编语言的形式是:操作码目的操作数,源操作数。Intel 采用这种格式的原因可能是觉得这样表达“目的操作数”=“源操作数”更形象,如同a-6这种形式。

in 指令用于从端口中读取数据,其一般形式是:

  • in al,dx;
  • in ax,dx;

其中al和ax用来存储从端口获取的数据,dx是指端口号

这是固定用法,只要用in指令,源操作数(端口号)必须是dx,而目的操作数是用 al,还是 ax,取决于dx端口指代的寄存器是8位宽度,还

是16位宽度。

out指令用于往端口中写数据,其一般形式是:

  • out dx,al;
  • out dx,ax;
  • out立即数,al;
  • out立即数,ax

另外

  • in指令中,端口号只能用dx寄存器
  • out指令中,可以选用dx寄存器或立即数充当端口号
硬盘控制器的主要端口介绍

image-20241210165025235

这里也需要解释一些概念

  • 端口是按照通道给出的,并非是针对某块硬盘,通道其实就是主板跟硬盘通信的IDE接口插槽,一个主板一般对应有两个这样的插槽,也就是说,一个主板对应两个通道(主通道与副通道),一个通道对应两块硬盘(主盘和副盘)
  • 每个通道都分别有主盘和从盘
  • 要想操作某个通道上的某块硬盘,需要单独指定,这一点可通过指定device寄存器的第4位实现,0表示使用主盘,1表示使用从盘

另外,端口用途在读硬盘和写硬盘时还是有点区别的

比如拿 Primary通道上的 0x1F1端口来说,读操作时若读取失败,里面存储的是失败状态信息,所以称为error 寄存器,并且 0x1F2端口中存储未读的扇区数写操作时就变成了feauture 寄存器,此寄存器用于写命令的参数。

image-20241210170743306

image-20241210170825788

image-20241210170845183

image-20241210170937594

image-20241210171015783

image-20241210171113460

image-20241210171141288

常用的硬盘操作方法

image-20241210171257764

image-20241210171449336

让MBR使用硬盘

编写mbr程序

上述我们对硬盘做了几个简单的介绍,接下来我们就要改造目前的mbr程序,让其能够读取硬盘,并把硬盘上的loader程序加载到内存

关于loader程序也有以下几点需要说明:

  • loader中需要定义一些内核需要的数据结构,因此加载到内存就不能被覆盖
  • 随着内核的功能越来越多,其占用的内存也就越来越多,故其内存地址也会想越来越高的位置发展,所以loader程序尽量放在低处

所以,我们对loader程序的规划如下:

  • loader程序的磁盘位置在第0块磁盘的第2扇区
  • loader程序在内存中的加载地址为0x900

对此,我们可以将上述配置写入一个单独的文件中,并使用类似宏命令定义上述配置

boot.inc

1
2
3
;-----loader and kernel-----
LOADER_BASE_ADDR equ 0x900	;loader在内存中位置
LOADER_START_SECTOR equ 0x2	;loader在磁盘中的逻辑扇区地址,即LBA地址

定义好loader的基本配置后,我们就可以着手改造mbr程序了

mbr.S

  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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
;主引导程序MBR,由BIOS通过jmp 0:0x7c00跳转
 
;------------------------------------------
%include "boot.inc"
;vstart=0x7c00表示本程序在编译时,起始地址编译为0x7c00
SECTION MBR vstart=0x7c00
;由于BIOS是通过jmp 0:0x7c00跳转到MBR,故此时cs为0
;对于 ds、es、fs、gs 这类 sreg,CPU 中不能直接给它们赋值,没有从立即数到段寄存器的电路实现,只有通过其他寄存器来中转,这里我们用的是通用寄存器ax来中转。
    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,0x0600
    mov bx,0x0700
    mov cx,0        ;左上角(0,0)
    mov dx,0x184f   ;右下角(80,25)
                    ;0x18=24,0x4f=79
    int 0x10        ;调用BIOS中断函数
;------------------------------------------
 
;------------------------------------------
;将要显示的字符串写入到显存中
    mov byte [gs:0x00],'1'      ;在第一个字节的位置写入要显示的字符“1”
                                ;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
    mov byte [gs:0x01],0xA4
 
    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
 
    mov eax,LOADER_START_SECTOR ;起始扇区地址
    mov bx,LOADER_BASE_ADDR     ;要写入的内存地址
    mov cx,1                    ;待读入的扇区数目
    call rd_disk_m16            ;调用磁盘读取程序
 
    jmp LOADER_BASE_ADDR;
 
;------------------------------------------
;函数功能:读取硬盘的n个扇区
;参数:
    ;eax:LBA扇区号
    ;bx:要写入的内存地址
    ;cx:待读入的扇区数目
 
rd_disk_m16:
    ;out命令,通过端口向外围设备发送数据
    ;其中目的操作数可以是8为立即数或者寄存器DX,源操作数必须是寄存器AL或者AX
    ;因此需要将之前ax中保存的值进行备份
    mov esi,eax
    mov di,cx
 
;正式读写硬盘
;第一步:设置要读取的扇区数目
    mov dx,0x1f2
    mov ax,cx
    out dx,ax
    
    mov eax,esi		    ;恢复eax的值
 
;第二步:将LBA的地址存入0x1f3~0x1f6
    ;LBA的0~7位写入端口0x1f3
    mov dx,0x1f3
    out dx,al
 
    ;LBA的8~15位写入端口0x1f4
    mov cl,8
    shr eax,cl		    ;将ax右移8位
    mov dx,0x1f4
    out dx,al
 
    ;LBA的16~23位写入端口0x1f5
    shr eax,cl
    mov dx,0x1f5
    out dx,al
 
    ;LBA的24~27位写入端口0x1f6
    shr eax,cl		    ;右移8位
    and al,0x0f		    ;与0000 1111相与,取后四位
    or al,0xe0		    ;设置7~4位为1110,表示lba模式
    mov dx,0x1f6
    out dx,al
 
;第三步,向0x1f7端口写入读命令,0x20
    mov dx,0x1f7
    mov al,0x20
    out dx,al
 
;第四步,检测硬盘状态
    .not_ready:
        nop             ;空操作,什么也不做,相当于sleep,只是为了增加延迟
        in al,dx        ;由于读取硬盘状态信息的端口仍旧是0x1f7,因此不需要再为dx指定端口号
        
        and al,0x88     ;取出状态位的第3位和第7位
                        ;其中第3位若为1表示硬盘控制器已经准备好数据传输,第7位为1表示硬盘忙
        
        cmp al,0x08     ;cmp为减法指令,如果两个操作数相等,标志寄存器ZF会被置为1
                        ;此处是为了判断第7位是否为1,如果为1,说明硬盘忙,则减法的结果应该为0,相应ZF寄存器的值会被置为1
        
        jnz .not_ready  ;若未准备好,则继续等
                        ;JNZ是条件跳转指令,它表示"Jump if Not Zero",
                        ;也就是如果零标志位(ZF)不为0,则进行跳转。否则不进行跳转

;第五步,从0x1f0端口读取数据
    mov ax,di           ;di是之前备份的要读取的扇区数
    mov dx,256

    mul dx              ;一个扇区512字节,每次读取两个字节,需要读取di*256次
                        ;mul指令表示乘法操作,当只有一个操作数时,被乘数隐含在al或者ax中

    mov cx,ax           ;cx在此时表示要循环读取的次数
 
    mov dx,0x1f0
    .go_on_read:
        in ax,dx        ;从0x1f0端口读取2个字节的数据到寄存器ax
        mov [bx],ax     ;读取的数据写入bx寄存器所指向的内存
        add bx,2        ;目的内存地址偏移2个字节继续循环读取剩余的数据
        loop .go_on_read
        ret
 
    times 510-($-$$) db 0
    db 0x55,0xaa

编译mbr程序

1
nasm ./mbr.S -o mbr.bin

将MBR程序写入磁盘的MBR扇区内

1
 dd if=/home/minios/osCode/miniOS_32/ch3/task2/mbr.bin of=/home/minios/bochs/hd60M.img bs=512 count=1 conv=notrunc

此时如果我们运行mbr程序,会发现程序依旧显示的是原来mbr的内容,这是因为我们并未实现loader程序,也就是说此时磁盘的0块2号扇区的位置是空的

image-20241210162056918

实现内核加载器

上个部分,我们完成了mbr程序的书写,并使用汇编实现了一个从磁盘加载文件到内存的函数

本部分内容我们将使用这个函数将loader加载到内存

为了验证是否成功的将loader程序加载到指定内存并运行,我们也简单将loader程序实现了一个屏幕打印功能来做测试,具体与内核相关的功能我们在后续章节详述

编写loader程序

loader.S

 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
%include "boot.inc"
 
SECTION vstart=LOADER_BASE_ADDR
;------------------------------------------
;将要显示的字符串写入到显存中
    mov byte [gs:0x00],'2';在第一个字节的位置写入要显示的字符“1”
    ;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
    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 ./loader.S -o loader.bin

将loader程序写入磁盘内

按照之前的约定,我们对loader程序在磁盘中的位置安排在2号扇区内

1
dd if=/home/minios/osCode/miniOS_32/ch3/task2/loader.bin of=/home/minios/bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc
  • bs=512bs 代表块大小(Block Size)。指定数据块的大小为 512 字节。即,每次读取和写入操作将处理 512 字节的数据。通常,磁盘的扇区大小为 512 字节。
  • count=1count 指定了要复制的块数。在这里值为 1,表示只复制 1 个块(512 字节)。因此,loader.bin 文件的前 512 字节将被写入到目标磁盘映像的特定位置。
  • seek=2seek 指定了目标文件中的偏移量(以块为单位)。此处的 seek=2 表示将目标文件的写入位置移动到距离文件开头 2 个块的位置。由于每个块是 512 字节,所以这意味着文件将被写入到磁盘映像的 1024 字节处(2 * 512 = 1024 字节)。该选项常用于在磁盘映像文件中跳过某些区域(例如引导扇区),并将文件内容写入指定的偏移位置。
  • conv=notruncconv 是转换选项,notrunc 表示在写入时不截断输出文件。默认情况下,如果目标文件已经存在,dd 会将其截断为 0 长度文件,然后开始写入。如果使用 notrunc,则文件将继续写入而不截断它。这样可以确保在目标磁盘映像中不会丢失原有数据,且 loader.bin 会追加写入到指定位置。

在boch安装位置处运行bochs模拟器

1
 ./bin/bochs -f boot.disk

image-20241210180841466

如上,屏幕输出了loader程序的内容,因此mbr成功将loader程序从我们规定的磁盘位置加载到了我们规定的内存位置

网站已运行
发表了20篇文章 · 总计 138,209字