SmlOS二-系统启动前期处理

  • 内容
  • 评论
  • 相关

正式进入内核

前面已经说明了我们的bootloader如何加载磁盘文件,并跳转到内核代码。

也就是会最终跳转到OsHead.nas所生成的代码位置。

我们会用尽量少的汇编语言,但是有些必须要汇编实现的内容,我们都会放到这里,为正式进入c实现的系统内核做准备。


初始化显示相关设置

VBE简要介绍

有一个组织叫 VESA(视频电子标准协会)

他们制定了显卡相关显示相关标准VBE (VESA BIOS EXTENSION),定了一些显卡相关的标准,比如标准应用程序接口,图形接口,显示模式,刷新率等

我们首先要检测机器是否支持VBE,防止在不兼容的机器上运行失败。

当然,一般来讲现代的机器都是支持的。

检测操作:

;确认VBE是否存在
 MOV AX,0x9000
 MOV ES,AX
 MOV DI,0 ; ES:DI 存放VBE信息
 MOV AX,0x4f00
 INT 0x10
 CMP AX,0x004f
 JNE scrn320

; 检测VBE版本
 MOV AX,[ES:DI+4]
 CMP AX,0x0200
 JB scrn320 ; if (AX < 0x0200) goto scrn320

我们设定AX,DI,AX 寄存器内容,然后执行 INT 0x10后,如果支持VBE,那么机器会在AX寄存器中写入0x004f

检测如果不支持VBE,使用200*320的屏幕分辨率。

如果支持,就继续检测VBE版本。

因为即使支持VBE,也不一定支持指定对应屏幕模式,所以如果不支持,继续低分辨率模式。

; 取得画面模式信息
    MOV     CX,VBEMODE
    MOV     AX,0x4f01
    INT     0x10
    CMP     AX,0x004f
    JNE     scrn320

这里取得画面模式信息,这里VBEMODE是个宏定义。目前设置为0x105(即1024*768)的分辨率。

这里是常见的VMEMODE值:

VBEMODE 分辨率
0x103 800*600
0x105 1024*768
0x107 1280*1024

调用成功后 AX=0x004f

而取得的画面模式也会被写入内存ES:DI开始
的256字节,有以下6个重要信息。

地址 说明 其他
WORD ES:DI+0x00 模式属性
WORD ES:DI+0x12 x分辨率
WORD ES:DI+0x14 y分辨率
BYTE ES:DI+0x19 颜色数 我们是8,代表8位色
BYTE ES:DI+0x1b 颜色的指定方法 我们是4,代表使用调色板
DWORD ES:DI+0x28 VARM 显存地址

下面来确认其中信息,在这里需要检测颜色数,调色板模式和属性模式。

; 画面模式信息确认
 CMP BYTE [ES:DI+0x19],8
 JNE scrn320
 CMP BYTE [ES:DI+0x1b],4
 JNE scrn320
 MOV AX,[ES:DI+0x00]
 AND AX,0x0080
 JZ scrn320 ; 模式属性的bit7是0  所以放弃

如果全部都支持,进行画面模式切换。

;画面模式切换
 MOV BX,VBEMODE+0x4000
 MOV AX,0x4f02
 INT 0x10
 MOV BYTE [VMODE],8 ; 记录下画面模式
 MOV AX,[ES:DI+0x12]
 MOV [SCRNX],AX
 MOV AX,[ES:DI+0x14]
 MOV [SCRNY],AX
 MOV EAX,[ES:DI+0x28]
 MOV [VRAM],EAX
 JMP keystatus

在这里调用0x10进行切换。

将分辨率信息,画面模式,VRAM(显存地址)地址等信息记录到我们自己设定的内存位置,留做以后备用。

keystatus:  

    MOV AH,0X02
    INT 0X16
    MOV [LEDS],AL       ;保存键盘led灯状态

我们还额外保存led灯信息,也是备用。


进入保护模式

这可能是这里最重要的知识点了,这里做个简要介绍。

保护模式(Protected Mode)

是从80386开始支持,区别于实模式(Real Mode)

前面说了,实模式最大内存只支持1M,保护模式则支持4G

而且相比于实模式增加了内存保护和虚拟内存分页系统,增加了段间保护机制

为了进入保护模式,首先先关闭所有中断,防止后续操作的时候产生中断导致出错。

关中断核心代码如下:

MOV AL,0XFF
OUT 0X21,AL     ;屏蔽主pic 中断
NOP             ;连续out,有可能出错,nop一下
OUT 0Xa1,AL     ;屏蔽从pic中断
CLI             ;禁止CPU级别中断

pic是什么

这里简要介绍下pic相关。

PIC(可编程中断控制器)

PIC分主PIC和从PIC,主PIC与CPU直接相连。

image

这是从网上找的PIC的图片。

可以看到从PIC连接到主PIC。

主PIC通过数据总线与CPU相连。

对于CPU来说,中断控制器输入外部设备,所以我们使用out指令与之通讯。

当然,PIC是很古老的设备,现在一般都是APIC,不过这些设备都支持向前兼容,所以我们还是可以把APIC当成PIC用。

A20总线

前面说了实模式只能访问1M左右的内存,是因为只有20根地址总线,可以访问的地址是2^20=1M

而我们现在的及训觉一般都有32位地址总线,能支持4G内存空间

我们打开A20总线,这时候可以访问1MB以上地址。

核心操作如下:

CALL waitkbdout ;清空i8042的输入缓冲区
MOV AL,0XD1
OUT 0X64,AL
CALL waitkbdout
MOV AL,0xdf     ;开启A20总线  
OUT 0X60,AL
CALL waitkbdout

而waitkbdout是是等待A20Gate命令处理被完成的汇编函数

然后进入保护模式,不过进入保护模式之前需要设置好保护模式相关参数,才能进入。

[INSTRSET "i486p"]  ;使用486指令集
 LGDT [GDTR0]       ; 加载GDTR寄存器,临时设置
 MOV EAX,CR0
 AND EAX,0X7fffffff ;禁止分页模式
 OR EAX,0X00000001  ;开启保护模式
 MOV CR0,EAX

 ...

 ;---------------------------------------------     
GDT0:
    RESB    8                       ;空描述符
    DW  0xffff,0x0000,0x9200,0x00cf ;32位可读写数据段描述符   段限长 4G-1,段基址为0      
                                    ;也就是说该段可寻址0~4G-1    DPL = 0 内核数据段
    DW  0xffff,0x0000,0x9a28,0x0047 ;32位可读可执行代码段描述符 段限长512KB,段基址为0x280000
                                    ;DPL = 0    内核代码段

    DW      0

GDTR0:                                      
    DW  8*3-1                       ; GDT限长
    DD  GDT0                        ; GDT基址
    ALIGNB  16

这里先加载GDTR寄存器,但这里只是临时设置,后面会再次更改,这里只是为了进保护模式的需要。


简要介绍GDT和IDT。

GDT(全局段号记录表)

使用GDT可以支持段的分段机制,这里必须指定段的大小,起始地址,管理属性。

IDT(中断记录表)

中断表用于注册相应中断处理程序的执行地址。

CR0寄存器是CPU的控制寄存器,保护模式的开启就是通过设置CR0寄存器的值来实现的。

更改cr0,就正式进入了保护模式。

 JMP protectMode ;切换到保护模式,指令解释变化,必须立马跳转
进入保护模式之后,首先对段寄存器进行初始化设置。
;进入保护模式
protectMode:
 MOV AX,1*8 ; 重新设置段寄存器
 MOV DS,AX ; DS, ES, FS, GS, SS都指向GDT中的内核数据段描述符
 MOV ES,AX
 MOV FS,AX
 MOV GS,AX
 MOV SS,AX

在切换到保护模式之后,必须立马执行跳转,这里为什么要Jmp,是因为切换到保护模式之后,CPU的解释模式发生的变化,开始在执行前一条指令的时候解释下一条或多条指令。

所以模式切换后,必须马上跳转。


复制C语言实现的系统内核代码

然后进行复制操作,复制内核代码(即用之后用C语言实现的系统内核)

这里指定的内核代码段的内存基地址是0x00280000。

首先复制内核512K到基地址,当然512K远远大于内核文件的大小,所以512k已经够用了。

后面一段是将整个在boot.nas时读取的所有扇区数据全部复制到1M(0x00100000)之后的地方去。

    MOV     ESI,bootpack    ;源地址
    MOV     EDI,BOTPAK      ;目的地址
    MOV     ECX,512*1024/4  ;复制的大小,512K
    CALL    memcpy  
;----------------------------------------------
;将代码复制到0x00100000
    MOV     ESI,0x7c00      
    MOV     EDI,DSKCAC      
    MOV     ECX,512/4
    CALL    memcpy
;----------------------------------------------
;将从映像中读入的数据从0x8200处复制到0x00100200处
    MOV     ESI,DSKCAC0+512 
    MOV     EDI,DSKCAC+512  
    MOV     ECX,0
    MOV     CL,BYTE [CYLS]  
    IMUL    ECX,512*18*2/4
    SUB     ECX,512/4   
    CALL    memcpy
...

; 将DS:ESI复制到ES:EDI地址
memcpy:
    MOV     EAX,[ESI]       
    ADD     ESI,4
    MOV     [EDI],EAX
    ADD     EDI,4
    SUB     ECX,1
    JNZ     memcpy          
    RET 

一切复制完毕之后,正式启动c实现的内核。

;内核的启动
    ...
 JZ skip ; 没有要传送的数据时
    ...
skip:
  MOV ESP,[EBX+12] ; 栈初始值 ESP = 0x310000
  JMP DWORD 2*8:0x0000001b

在用C语言生成的内核文件中,文件头部并非代码段的头部,需要文件头部信息,计算出内核文件代码段的大小,复制到指定的内存区块,然后再跳转到指定的内核基址。

并在最后指定了内核栈,即指定ESP的值,这里是0x310000。

最后在跳转到第二个段,也就是我们在临时gdt中指定的段,并且偏移0x0000001b开始执行。

也就是内存地址0x280000 + 1b的位置。

至此,跳入C语言的内核代码区。