从头开始编写操作系统(11) 第10章:为内核做准备1

来源:互联网 发布:淘宝肥王书店 编辑:程序博客网 时间:2024/05/18 00:11

译自:http://www.brokenthorn.com/Resources/OSDev10.html

第10章:为内核做准备1
by Mike, 2008

本系列文章旨在向您展示并说明如何从头开发一个操作系统。

介绍

欢迎! :)

我们已经完成很多了,不是么?你应该已经注意到了操作系统开发的复杂性了。而且这还将会变得越来越复杂。

这是我们的第一个分成两部分的章节。第一部分会详细讲解新引入的代码。将包含基本的汇编语言32位图形编程,包括:基本的VGA编程概念,访问显存,打印字符串,清屏,以及更新硬件光标。这将会有一部分数学,但不多。

演示程序本身是完整的。它将在第二部分和一个完整的Stage2代码一起展示。包括新的FAT12驱动器,软盘驱动器。当然这不是一个真的驱动器,但是这将说明驱动器的功能,以及为什么它是如此有用。所有的代码都是我们在引导加载器中FAT12加载代码的修改版本。所以我们还会详细的讨论FAT12。

第二部分,会加载并执行在1MB除的一个基本内核(纯二进制的)映像。

我们开始内核编程的时候,我们涉及不同的可执行文件格式,我们得确保stage2能够正确地执行这些目标文件。因此在那时,我们会增加一些代码到当前的stage2引导加载器中,确保它能够正确地加载我们的新内核。这时后面的内容。:)

记住这些,本章包括以下内容:

·      基本VGA 编程概念

·      访问显示器

·      打印字符

·      打印字符串

·      CRT 微控制器理论,·     更新硬件光标·      

·      清屏

本章将会很多第七章的内容。实模式下的地址映射,默认的I/O端口地址等。在我们讨论显存地址空间和VGA端口访问时,有前面的知识将会很有用。

准备好了吗?

显示器

VGA 理论

视频图形阵列Video GraphicsArray (VGA) IBM1987年提出的计算机显示标准。称之为阵列是因为它原来是在工业标准结构(ISA)板上的一个芯片,(而在MDA, CGA,EGA上则是多个芯片)。正因为所有部件都在一个ISA板上,使它很容易连接到主板上。

VGA包括视频缓冲器,视频DAC, CRT控制器, 时序发生器, 图形控制器,属性控制器。注意我们要在开发视频驱动的时候才详细的讨论这些。 这主要是为了节省篇幅,并且在更复杂的考虑这些问题之前,简单的为VGA编程。

视频缓冲器

视频缓冲器是映射到显存的一段内存区域。我们可以改变映射的内存区域范围。在引导时,BIOS将它映射到0xA0000 显存映射到0xA0000 (参考:第7章中实模式下的地址映射一节) 这很重要!

视频DAC

视频数模转换器(DAC) 包含一个将数字信号转换为用于显示的模拟信号的调色板。这中模拟信号使用红绿蓝三种颜色的强度来表示,我们会在后面详细的介绍,不理解也没有关系。

CRT 控制器

这个控制器产生水平和垂直的同步时钟信号,在显存中生成光标和下划线。我们在后面再详细介绍,我们在更新光标时会使用这个控制器

时序发生器

时序发生器用于产生基本时序信号,用于控制缓存器的更新。允许系统在显示间隔时访问内存。本章不会再涉及它,当我们讨论视频驱动时会详细的讨论这些,别着急。

图形控制器

这是显存和属性控制器之间的接口以及显存和CPU的接口,在显示时,内存数据从视频缓冲区(显存)取出并发送到属性控制器。在图像模式中,数据从并行方式转换为串行方式,在文本模式中,数据就按照并行方式发送到属性控制器。

如果你并理解也别着急,我们会在视频驱动开发的时候再详细介绍这些。现在,你需要记住:图形控制器根据显示时钟从显存按照并行方式提取数据。简单来说就是在显存(默认映射到0xA0000) 写数据会(根据当前的显示模式)影响到显示的结果,这在我们显示字符时很重要。

要知道改变图形控制器的地址映射范围是可能的。初始化时,BIOS将它映射到0xA0000.

视频模式

"视频模式"是对显示的一种描述,它描述了内存应该如何被访问,数据如何被显示。

VGA支持两种模式:APA图像模式,和字符模式.

APA 图形模式

所有点都可寻址的(APA)显示模式,用在显示器、点矩阵、以及包含像素数组的设备里,其中的每一个单元都可以直接的改变。在这中显示模式中,每个单元都称为像素,像素可以被直接操作,几乎所有的图形模式都使用这种方法。通过修改像素缓冲区,我们就会影响到屏幕上对应的像素。

像素

一个像素是能被显示的小单元。在显示时,它代表一个最小的颜色单位。也就是说,一个点。点的大小依赖于当前的分辨率和显示模式。

字符模式

字符模式,在屏幕上的内容是字符,而不是像ATA模式中的像素。

视频控制器使用两个缓冲区来实现字符模式:一个字符映射缓冲区:将每一个要显示的字符和他要显示的象素对应起来。和一个每个单元要显示什么字符串缓冲区。改变字符映射缓冲区会直接影响到字符本身。这允许我们使用新的字符集。通过改变屏幕缓冲区,屏幕缓冲区代表每一个单元显示的字符,会改变显示在屏幕上的字符。有些字符模式,允许改变属性,比如字符的颜色、闪烁、下划线、反转、亮度等。

MDA, CGA,EGA

要知道VGA是基于MDA, CGA, EGA的,VGA支持也支持这些适配器的模式,理解这些模式有助于理解VGA.

MDA 理论

到我出生的时候,在1981年IBM开发了一种用于显卡的标准,他们是单色显示适配器(MDA)和单色显示和打印机适配器(MDPA)。

MDA不包含任何一种图形模式。他只使用字符模式,(mode 7)使用8025行的分辨率来显示字符。

在旧的PC机上,这是通用的标准。

CGA - 理论

1981IBM也开发了彩色图形适配器 (CGA) PC机的第一个彩色显示标准。

CGA只支持16中颜色的调色板,这是因为它限制每像素使用4比特表示。

CGA支持两种字符模式和两种图形模式包括:

·      40x25 字符 (16)模式

·      18x25 字符(16 ) 模式

·      320x200 像素 (4 ) 图形模式s

·      640x200 像素 (单色) 图形模式

通过一些技巧可以创建新的"没有官方文档"的显示模式, 后文有更详细的解说。

EGA - 理论

IBM1984开发,加强的图形适配器(EGA)引进了16色以及640x350的分辨率。

向80x86系列一样,VGA适配器向后兼容,因此为了保证向后兼容性,BIOS启动时,是模式7 (即原来的MDA模式), 支持80列25行字符显示,这对我们来说很重要, 这是我们要处理的模式!

VGA 内存地址

VGA控制器使用的显存映射到内存0xA00000xBFFFF. 参考第7章中实模式内存映射一节。

一般的,显存按照下面的方式映射:

o  0xA0000 -0xBFFFF 用于图形模式的显存

o  0xB0000 -0xB7777 单色字符模式

o  0xB8000 -0xBFFFF 彩色字符模式及CGA兼容图形模式

通过再内存映射中使用不同的地址,可以在一台PC机上同时使用ECG, CGA,VGA

可以使用CRT控制器改变视频适配器的内存映射一般这是使用视频驱动来完成的。 后文会详细介绍。

也可以改变由哪个控制器使用这些内存。这样我们就可以开发出新的或是没有文档的模式。一个例子是"ModeX".

记得修改文本缓冲区和显示缓冲区会直接影响屏幕上的显示结果。这是因为显示控制器会根据刷新率来更新显示数据,显示控制器通过VGA端口向显示器中的CRT控制器发送命令,产生垂直和水平的扫描信号使CRT更新显示器的显示结果,又因为字符缓冲区映射到上面的地址:

所以写这段内存区域会改变屏幕上的显示。

比如,我们在模式7。模式7是彩色字符模式,所以内存开始于0xB8000。显示控制器使用字符缓冲区决定显示什么,在0xB8000 写数据会改变屏幕上显示的字符。

        %define VIDMEM  0xB8000        ; 显存

 

        mov     edi, VIDMEM            ;显存指针

        mov     [edi], 'A'             ;打印'A'

        mov     [edi+1], 0x7           ; 字符属性

上面的代码会在屏幕的左上角上显示"A",在黑背景上的白字符(属性),酷 :)

打印字符

我们如何在屏幕上任意的x/y位置打印字符?

内存的一个特性是它是线性的。如果我们到了显示器的一行末尾,那么下一次就会到下一行上。正因为地址是线性的,我们就能够将x/y位置转换为线性地址,而且有一个公式来完成这个转换:x + y * 屏幕宽度.

 

这是一个例子。比如,我们要在(5,5)显示字符A。我们知道显存开始于0xb8000,使用公式,我们可以得到绝对地址:

               address = x + (y * screen width)

               address= 5 + (5 * 80)

               address= 5 + 400

               address= 405

 

               也就是说从显存开始的位置到5,5405个字节。

               再加上显存的基地址

 

               0xB8000 +405 = 0xB8195

这样,将字符A写到地址 0xB8195,就影响到屏幕上(5,5)的显示结果,酷?

知道了这些之后,就能像BIOS一样我们提供一种保存屏幕当前位置的方法,程序的剩余部分会用到下面的定义:

_CurX db 0                                    ; 当前x/y位置

_CurY db 0

 

%define        VIDMEM  0xB8000                ; 显存

%define        COLS    80                     ; 屏幕的宽和高

%define        LINES   25

%define        CHAR_ATTRIB 14                 ;字符属性 (黑底白字)

我们在模式7。这种模式80列,25行,显存开始于0xB8000. 但是,字符属性是什么?

字符模式7对于每个字符使用两个字节处理,记住!第1字节表示实际的字符,第2字节字符属性!因此当你要在模式7下载屏幕上显示一个字符,你需要写2个字节而不是1个。

属性字节提供了一种对颜色的支持,以及其他的特定属性,比如闪烁,其值可以是:

·      0 - Black

·      1 - Blue

·      2 - Green

·      3 - Cyan

·      4 - Red

·      5 - Magenta

·      6 - Brown

·      7 - Light Gray

·      8 - Dark Gray

·      9 - Light Blue

·      10 - Light Green

·      11 - Light Cyan

·      12 - Light Red

·      13 - Light Magenta

·      14 - Light Brown

·      15 - White

字符属性是定义特定属性的一个字节它同时定义前景属性和背景属性这一字节格式如下

·      Bits 0 - 2: 前景色

o  Bit 0:

o  Bit 1: 绿

o  Bit 2:

·      Bit 3: 前景强度

·      Bits 4 - 6: 背景色

o  Bit 4:

o  Bit 5: 绿

o  Bit 6:

·      Bit 7: 闪烁或背景强度

好,都准备好了,我们来显示一个字符!

设置

显示字符有些复杂,我们要记录我们当前所在的位置(x/y位置和内存位置),我们还要注意特定的字符,比如换行,我们还要监视一行的末尾,还有我们得更新光标的位置。

Putch32是一个保护模式下的程序,用于在stage2显示一个字符。别急,我们会在内核里用C语言重写的。这里使用汇编语言,我们可以比较汇编语言和C语言的关系。后文有更详细的解说。

这是启动代码:

Bits 32

 

%define        VIDMEM  0xB8000                ; 显存

%define        COLS    80                     ; 屏幕的宽和高

%define        LINES   25

%define        CHAR_ATTRIB 14                 ;字符属性 (黑底白字)

 

_CurX db 0                                    ; 当前x/y位置

_CurY db 0

 

;**************************************************;

;       Putch32()

;              -在屏幕上显示字符

;       BL=> 要显示的字符

;**************************************************;

 

Putch32:

 

        pusha                          ; 保存寄存器

        mov     edi, VIDMEM            ;取得显存指针

现在我们有了一些基本的定义。 _CurX_CurY保存了当前要写字符的x/y位置,通过增加_CurX, 我们就能移到当前行的下一个字符,EDI保存了显存的基地址,将字符写到[EDI],我们就可以在屏幕上显示字符了。

在我们显示字符之前,我们首先要确定在什么位置显示它。我们只需写到当前的x/y位置(_CurX和_CurY),再简单不过了。

如你所知,显存是线性的,我们需要将x/y位置转换为线性内存。我们有公式x + y * 屏幕宽度. 这个很容易计算。但要记住,现在每个字符有2字节, _CurX, _CurY,COLS, LINES, 是基于字符的而不是字节。i.e., COLS=80 字符,因为每个字符2字节,我们需要计算80*2. 很简单,不是吗?

这使问题变得复杂,但也不难:

        ;-------------------------------;

        ;   取得当前位置              ;

        ;-------------------------------;

 

        xor     eax, eax               ;  清eax

 

               ;--------------------------------

               ;currentPos = x + y * COLS! x,y 在_CurX _CurY里

               ;因为每个字符两字节COLS=一行的字符数

               ;我们将这个数乘2,就得到了屏幕宽度

               ;再乘 _CurY就到了当前行

               ;--------------------------------

 

               mov     ecx, COLS*2            ;模式7中 每字符两字节,一行有COLS*2字节

                mov     al,byte [_CurY]       ; y位置

               mul     ecx                    ;y*COLS

               push    eax                    ;保存eax--乘积

这是公式的一部分 y * 屏幕宽度 (以字节为单位),或者 _CurY * (COLS*字节/字符). 我们把它保存在栈里,然后继续后面的公式。

               ;--------------------------------

               ;现在y * 屏幕宽度在eax里,现在,只需要加上_CurX.

               ; 但是,_CurX 是当前的字符数,而不是字节数。

               ; 因为每字符有两字节,我们得先将_CurX乘2

               ; 再加到 屏幕宽度*y 上

               ;--------------------------------

 

               mov     al, byte [_CurX]       ; _CurX*2

               mov     cl, 2

               mul     cl

               pop     ecx                    ;弹出y*COLS的结果

               add     eax, ecx

我们用 _CurX 2得到当前的字节位置,然后我们将弹出的 y * COLS结果与之相加,完成了我们的公式x+y*COLS

现在EAX保存了我们要打印的字符位置偏移量(以字节为单位)我们把它加到EDI上,EDI保存有显存的基地址:

               ;-------------------------------

               ;现在eax不保存有偏移地址,加上基地址(保存在edi中)

               ;-------------------------------

 

               xor     ecx, ecx

               add     edi, eax               ;加到基地址上

现在EDI保存了实际要写的字节位置。BL保存了要写的字符,如果这个字符是换行符,我们希望传转到下一行,否则打印这个字符:

        ;-------------------------------;

        ;  监视换行符                   ;

        ;-------------------------------;

 

        cmp     bl, 0x0A               ;是换行符吗?

        je      .Row                   ;是,转下一行

 

        ;-------------------------------;

        ;   打印字符                    ;

        ;-------------------------------;

 

        mov     dl, bl                 ;取得字符

        mov     dh, CHAR_ATTRIB        ; 字符属性

        mov     word [edi], dx         ; 写显存以显示

 

        ;-------------------------------;

        ;  更新当前位置                 ;

        ;-------------------------------;

 

        inc     byte [_CurX]           ; 到下一字符

        cmp     [_CurX], COLS          ; 到一行末尾了吗?

        je      .Row                   ;是,到下一行

        jmp     .done                  ;不是,完成

好的,下面,到下一行的代码:

        ;-------------------------------;

        ;   到下一行                    ;

        ;-------------------------------;

 

.Row:

        mov     byte [_CurX], 0        ; 返回到第0列

        inc     byte [_CurY]           ; 到下一行

 

        ;-------------------------------;

        ;   恢复寄存器,返回;

        ;-------------------------------;

 

.done:

        popa                           ; 恢复寄存器,返回

        ret

显示字符串

好的,我们可以显示一个字符,当我们看到这个字符的时候感觉很不错吗,我不这么想:)

为了显示一些特定的信息,我们需要显示整个字符串所以我们需要一个程序来监视当前位置(并更新它),并且显示字符串里的一个字符,我们只需要一个循环:

Puts32:

 

        ;-------------------------------;

        ;   保存寄存器             ;

        ;-------------------------------;

 

        pusha                          ; 保存寄存器

        push    ebx                    ;复制字符串地址

        pop     edi

这是我们的 Puts32() 函数,它需要的参数: EBX保存要显示的0终结的字符串地址。因为Putch32()函数要使用BL来保存要显示的字符,我们要保存一个EBX的副本,正是我们这里做的一样。

循环:

.loop:

 

        ;-------------------------------;

        ;   取字符;

        ;-------------------------------;

 

        mov     bl, byte [edi]         ; 取下一个字符

        cmp     bl, 0                  ;是0吗 (0终结)?

        je      .done                  ;是,退出

我们使用EDI来确定字符串中要显示的当前字符。注意对0终结的测试。如果找到,退出,现在是显示字符:

        ;-------------------------------;

        ;  显示字符;

        ;-------------------------------;

 

        call    Putch32                ;否,显示

现在我们要做的是转到下一字符,再循环:

        ;-------------------------------;

        ;  下一个字符;

        ;-------------------------------;

 

.Next:

        inc     edi                    ;转到下一个字符

        jmp     .loop

 

.done:

        ;-------------------------------;

        ;   更新光标;

        ;-------------------------------;

 

        ;最好是在显示完整个字符串之后再更新光标,因为直接的VGA很慢

 

        mov     bh, byte [_CurY]       ; 取得当前位置

        mov     bl, byte [_CurX]

        call    MovCur                 ;更新光标

 

        popa                           ; 恢复寄存器,返回

        ret

好!我们有了在32位保护模式下显示字符串的方法了。不难吧?等等MovCur 是什么?下面看。

更新硬件光标

现在我们可以显示字符和字符串了,你可能会注意到一些问题:光标没有移动!光标只是一个下划线,在BIOS里用于表示当前的输出位置。

光标别硬件CRT微控制器控制,我们需要知道一些vga编程的知识来移动光标。

CRT 微控制器

CRT用户警告

尽管我鼓励学习和试验新东西,但你也要知道在操作系统开发的环境下,你可以直接控制硬件,直接控制任何事情。

CRT显示器失效可能很暴力,可能会爆炸,高速的玻璃碎片飞溅。可能会设置为硬件不可控的刷新频率,增大设备或芯片的失效几率,产生不可预计的或是破坏性的结果。

因此如果你要使用代码测试,我推荐你使用已经验证过的代码在虚拟机上测试,再在真实的硬件上使用。

我不同完全涵盖常规视频编程的内容,我们在后面的视频驱动开发时再完整叙述。

下面是CRT控制器!

端口映射

CRT控制器使用一个 数据寄存器 映射 端口 0x3D5。还记得第7章里的端口表吗?CRT控制器 使用一个特殊的寄存器—— 索引寄存器, 来决定在数据寄存器中的数据表示什么。

因此,为了给CRT控制器法数据,我们需要写两个值,一个写到索引寄存器 (决定我们要写的数据的类型), 另一个写到数据寄存器。不难吧:)

索引寄存器映射到端口0x3D5或0x3B5.
数据寄存器映射到端口0x3D4或0x3B4.

不只有着两个寄存器(如Misc.输出寄存器), 但我们现在只关心这两个。

索引寄存器映射

默认的,索引寄存器如下表:

CRT 微控制器——索引寄存器

偏移

CRT控制器 寄存器

 

 

 

0x0

水平扫描总时间

0x1

水平显示结束

0x2

水平消隐开始

0x3

水平消隐结束

0x4

水平回扫开始

0x5

水平回扫结束

0x6

垂直扫描总时间

0x7

溢出

0x8

行扫描预置

0x9

最大扫描行

0xA

光标起始

0xB

光标结束

0xC

显存起始地址(高)

0xD

显存起始地址(低)

0xE

光标位置(高位)

0xF

光标位置(低位)

0x10

垂直回扫开始

0x11

垂直回扫结束

0x12

垂直显示结束

0x13

偏移/逻辑屏宽度

0x14

下划线位置

0x15

垂直消隐开始

0x16

垂直消隐结束

0x17

模式控制   

0x18

行比较

 

通过再索引寄存器写偏移量,它指明数据寄存器指向什么位置(也就是什么意思)

当前我们要考虑的是,0xE和0xF:

·      0x0E: 光标位置高字节

·      0x0F: 光标位置低字节

这个偏移表示当前硬件光标的偏移位置,(还记得格式 x + y * 屏幕宽度 !), 分成高低两字节。

移动硬件光标

首先要知道,光标的索引是 0x0E0x0F,我们要先把它写到端口 0x3D4:

        mov     al, 0x0f

        mov     dx, 0x03D4

        out     dx, al

上面的代码将索引地址0x0F (光标位置低字节)写到索引寄存器。现在我们写到数据寄存器 (端口 0x3d5)的值将表示光标位置的低字节:

        mov     al, bl                  ; al包含光标位置的低字节

        mov     dx, 0x03D5

        out     dx, al                 ;低字节

上面的代码设置了光标位置的低字节,设置高字节相似,我们要将索引设为 0x0E,就完成了。

完整的程序:

;**************************************************;

;       MoveCur()

;              -更新硬件光标

;       parm/bh = Y pos

;       parm/bl = x pos

;**************************************************;

 

Bits 32

 

MovCur:

 

        pusha                          ; 保存寄存器 (对这样的注释你不厌烦吗?)

 

        ;-------------------------------;

        ;   取得当前位置;

        ;-------------------------------;

 

        ;_CurX和_CurY是屏幕上的当前位置,而不是在内存里

        ;我们不同考虑字节对齐的问题

        ;同样可以使用这个公式: _CurX + _CurY * COLS

 

        xor     eax, eax

        mov     ecx, COLS

        mov     al, bh                 ;y pos

        mul     ecx                    ;y*COLS

        add     al, bl                 ;+x

        mov     ebx, eax

 

        ;--------------------------------------;

        ;   VGA寄存器 低字节;

        ;--------------------------------------;

 

        mov     al, 0x0f               ;光标位置低字节索引

        mov     dx, 0x03D4             ;写到CRT索引寄存器

        out     dx, al

 

        mov     al, bl                 ;当前位置在EBX中,BL包含低字节,BH高字节

        mov     dx, 0x03D5             ;写到数据寄存器

        out     dx, al                 ;低字节

 

        ;---------------------------------------;

        ;   VGA 寄存器 高字节;

        ;---------------------------------------;

 

        xor     eax, eax

 

        mov     al, 0x0e               ;光标位置高字节索引

        mov     dx, 0x03D4             ;写到CRT索引寄存器

        out     dx, al

 

        mov     al, bh                 ;当前位置在EBX中,BL包含低字节,BH高字节

        mov     dx, 0x03D5             ;写到数据寄存器

        out     dx, al                 ;高字节

 

        popa

        ret

下一步:清屏!

清屏

因为我们已经有了显示文本的方法,通过循环,并将当前位置置0,就达到了清屏的目的,很简单。

;**************************************************;

;       ClrScr32()

;              -清屏

;**************************************************;

 

Bits 32

 

ClrScr32:

 

        pusha

        cld

        mov     edi, VIDMEM

        mov     cx, 2000

        mov     ah, CHAR_ATTRIB

        mov     al, ' '

        rep     stosw

 

        mov     byte [_CurX], 0

        mov     byte [_CurY], 0

        popa

        ret

好了,我们有了显示文本的方法,同时它还会更新硬件光标,并且清屏,如果你愿意的话可以在stage2中增加简单的菜单来控制内核的加载,后文有更详细的解说。..

演示

 

我决定创建一个小的例子来演示本章中的内容,下一章将在本章的代码上扩展。

这个例子使用了本章中讨论的所有,它使用字符属性设置前景和背景色,使用ClrScr32()清屏,并设置背景色,酷?

http://www.brokenthorn.com/Resources/images/Tutorial10.jpg

你可以在这里下载它。

总结

这章有不少内容,更多的图像概念,我们讨论了基本的VGA功能,显示字符,字符串,清屏,更新硬件光标,通过改变字符属性,我们可以使用任意的颜色,你甚至可以设置颜色调用ClrScr32()来改变背景!酷,你觉得呢?它不再是令人厌烦的黑白了:)

下一章会完成Stage2, 并将加载并执行一个纯32位的内核映像到1MB,别急,当我们到了内核那部分,我们会改变内核构建的方式,并修改加载的方法,这样,我们就可以加载一个目标格式的内核——允许它有导入、导出符号,并混合C语言编程,我等不及了!

下一章,并不是一个一般意义上的学习新东西的章节。它的代码都是我们讲过的内容,为了更好的代码结构而做了修改,在基本文件系统(FAT12)驱动和软盘驱动之间增加了接口,它将完成我们关于Stage2的讨论。

我们会在后面再看看Stage2,使Stage2有更多的功能,来支持多引导,和B引导选项。在后面呢……;)

下次见