我的OS | 为切换到C语言做准备
注意啊,没看下面的文章的要先看看下面的,再看这一篇啊。
我们刚才成功从启动层跳转到了内核程序,现在就应该从内核程序执行C语言了。但是,由于现在的CPU处于实模式中,而我们计划使用保护模式,所以要先切换过来。
在实模式中,我们只可以使用1MB以下的内存,所以我们需要先打开这个。
具体讲解如下。
首先我们要屏蔽所有的中断。万一哪个用户在切换CPU模式的时候碰了一下鼠标,就不好办了。所以,我们需要禁用所有中断。
完成以上命令只需要几行程序。
; 禁用所有PIC中断
; 由于OUT指令的第二个操作数(数据)必须是寄存器
mov al, 0xff
; 将0xff发送给PIC(0xff代表全部的一,禁用所有中断)
; 0x21是主PIC的端口
out 0x21, al
; 如果连续执行out指令,有的CPU无法执行
nop
; 然后禁用来自从PIC的中断
out 0xa1, al
; 禁止CPU级别的中断
cli
哎,大家可能只有在看笔者的程序时才会看到注释比程序还要多的情况呢。
接下来,我们要让CPU可以使用1MB以上的内存。这是因为CPU为了兼容以前的操作系统,在激活之前只可以使用1MB的内存。
; 下面的内容是函数,应确保CPU不会擅自执行
wait_KBD_out:
; 读取PIC中积攒的数据
in al, 0x64
and al, 0x02
; 清空垃圾数据
in al, 0x60
; 如果AND指令的结果不是0就重新跳转到wait_KBD_out
; Jump if not zero(不是零则跳转)
jnz wait_KBD_out
; 返回
ret
; 让CPU能访问1MB以上的内存
; 读取
call wait_KBD_out
; 数据
mov al, 0xd1
; 发送信号
out 0x64, al
; 读取
call wait_KBD_out
; 启用的信号
mov al, 0xdf
; 发送信号
out 0x60, al
; 继续读取
call wait_KBD_out
然后,CPU就可以访问1MB以上的内存了。
接下来,我们要让CPU切换到保护模式。
切换到保护模式只需要让CPU中CR0寄存器的bit0为1就可以了。当然,在此之前,我们要先设定临时的GDT。
; 以下是数据段,也要确保CPU不会擅自执行
; 判断地址是否可以被16整除
alignb 16
tmp_gdt:
; 空
resb 8
; 可读写的段,32bits
dw 0xffff, 0x0000, 0x9200, 0x00cf
; 可执行的段,32bits(C程序用)
dw 0xffff, 0x0000, 0x9a28, 0x0047
gdtr0:
; 很简单的程序,看不懂就不要看了
dw 8 * 3 - 1
; tmp_gdt的内容
dd tmp_gdt
; 判断地址是否可以被十六整除
alignb 16
; 设置临时的GDT
lgdt [gdtr0]
; 读取Control Register 0的内容到EAX寄存器中
mov eax, cr0
; 设置EAX寄存器的值
; 设置bits31为0(为了禁止分页,我们想要使用分段)
and eax, 0x7fffffff
; 设置bit0为1(切换到保护模式)
or eax, 0x00000001
; 将EAX寄存器中的值重新写入到CR0中
mov cr0, eax
; 切换CPU模式后需要执行跳转指令才能启用
jmp init_protect_mode
init_protect_mode:
; 在GDT中设定的可读写的段
mov ax, 1 * 8
; 初始化所有的段寄存器(在启用保护模式后都会改变)
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
到这里,CPU应该就已经是保护模式的状态了。接下来,我们要执行C语言程序。
C语言的程序其实最终是和kernel.asm连为一体的(生成的机器语言会结合为一体),所以,只需要在kernel.asm的最后留下一个标号C_kernel然后跳转到该标号就可以了。
; 必须由汇编处理的到此为止,接下来加载C语言的程序
; C语言程序最终会被加载到后面的标号C_kernel中
jmp C_kernel
C_kernel:
好的,kernel.asm的源代码就是这样的
org 0x7e00
初始化寄存器
mov指令将右边的寄存器或数的值复制到右边的寄存器中
将CS(Code Segment)的值赋给AX寄存器
mov ax, cs
将AX寄存器(从CS寄存器取来)的值赋给DS(Data Segment)寄存器
mov ds, ax
将AX寄存器的值赋给ES(Extra Segment)寄存器
mov es, ax
将AX寄存器的值赋给SS(Stack Segment)寄存器
mov ss, ax
将这段代码的起始位置赋值给SP寄存器(Stack Point)
mov sp, 0x7e00
调整画面模式
int 0x10 AH寄存器 = 0x00 切换显卡的模式
AL = 显卡的模式
0x03 16色的字符模式,80 x 25
0x12 VGA图形模式,640 x 480 x 4位彩色模式,独特的4面存储模式
0x13 VGA图形模式,320 x 200 x 8位彩色模式,调色板模式
0x6a 扩展VGA图形模式,800 x 600 x 4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)
mov ax, 0x0013
int 0x10
禁用所有PIC中断
由于OUT指令的第二个操作数(数据)必须是寄存器
mov al, 0xff
将0xff发送给PIC(0xff代表全部的一,禁用所有中断)
0x21是主PIC的端口
out 0x21, al
如果连续执行out指令,有的CPU无法执行
nop
然后禁用来自从PIC的中断
out 0xa1, al
禁止CPU级别的中断
cli
让CPU能访问1MB以上的内存
读取
call wait_KBD_out
数据
mov al, 0xd1
发送信号
out 0x64, al
读取
call wait_KBD_out
启用的信号
mov al, 0xdf
发送信号
out 0x60, al
继续读取
call wait_KBD_out
设置临时的GDT
lgdt [gdtr0]
读取Control Register 0的内容到EAX寄存器中
mov eax, cr0
设置EAX寄存器的值
设置bits31为0(为了禁止分页,我们想要使用分段)
and eax, 0x7fffffff
设置bit0为1(切换到保护模式)
or eax, 0x00000001
将EAX寄存器中的值重新写入到CR0中
mov cr0, eax
切换CPU模式后需要执行跳转指令才能启用
jmp init_protect_mode
init_protect_mode:
在GDT中设定的可读写的段
mov ax, 1 * 8
初始化所有的段寄存器(在启用保护模式后都会改变)
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
必须由汇编处理的到此为止,接下来加载C语言的程序
C语言程序最终会被加载到后面的标号C_kernel中
jmp C_kernel
下面的内容是函数,应确保CPU不会擅自执行
wait_KBD_out:
读取PIC中积攒的数据
in al, 0x64
and al, 0x02
清空垃圾数据
in al, 0x60
如果AND指令的结果不是0就重新跳转到wait_KBD_out
Jump if not zero(不是零则跳转)
jnz wait_KBD_out
返回
ret
以下是数据段,也要确保CPU不会擅自执行
判断地址是否可以被16整除
alignb 16
tmp_gdt:
空
resb 8
可读写的段,32bits
dw 0xffff, 0x0000, 0x9200, 0x00cf
可执行的段,32bits(C程序用)
dw 0xffff, 0x0000, 0x9a28, 0x0047
gdtr0:
很简单的程序,看不懂就不要看了
dw 8 * 3 - 1
tmp_gdt的内容
dd tmp_gdt
判断地址是否可以被十六整除
alignb 16
C_kernel:
我们执行一下试试:
虚拟机看起来在不断重启,开始还以为是bug,后来一想,本来C_kernel就是一个空的标号,而且,翻一翻日志信息还可以看到类似于以下的内容:
这行信息说明了CPU正在保护模式中,所以kernel.asm运行的很正常!
我们修改一下源代码,让C_kernel不是一个空的标号。
org 0x7e00
初始化寄存器
mov指令将右边的寄存器或数的值复制到右边的寄存器中
将CS(Code Segment)的值赋给AX寄存器
mov ax, cs
将AX寄存器(从CS寄存器取来)的值赋给DS(Data Segment)寄存器
mov ds, ax
将AX寄存器的值赋给ES(Extra Segment)寄存器
mov es, ax
将AX寄存器的值赋给SS(Stack Segment)寄存器
mov ss, ax
将这段代码的起始位置赋值给SP寄存器(Stack Point)
mov sp, 0x7e00
调整画面模式
int 0x10 AH寄存器 = 0x00 切换显卡的模式
AL = 显卡的模式
0x03 16色的字符模式,80 x 25
0x12 VGA图形模式,640 x 480 x 4位彩色模式,独特的4面存储模式
0x13 VGA图形模式,320 x 200 x 8位彩色模式,调色板模式
0x6a 扩展VGA图形模式,800 x 600 x 4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)
mov ax, 0x0013
int 0x10
禁用所有PIC中断
由于OUT指令的第二个操作数(数据)必须是寄存器
mov al, 0xff
将0xff发送给PIC(0xff代表全部的一,禁用所有中断)
0x21是主PIC的端口
out 0x21, al
如果连续执行out指令,有的CPU无法执行
nop
然后禁用来自从PIC的中断
out 0xa1, al
禁止CPU级别的中断
cli
让CPU能访问1MB以上的内存
读取
call wait_KBD_out
数据
mov al, 0xd1
发送信号
out 0x64, al
读取
call wait_KBD_out
启用的信号
mov al, 0xdf
发送信号
out 0x60, al
继续读取
call wait_KBD_out
设置临时的GDT
lgdt [gdtr0]
读取Control Register 0的内容到EAX寄存器中
mov eax, cr0
设置EAX寄存器的值
设置bits31为0(为了禁止分页,我们想要使用分段)
and eax, 0x7fffffff
设置bit0为1(切换到保护模式)
or eax, 0x00000001
将EAX寄存器中的值重新写入到CR0中
mov cr0, eax
切换CPU模式后需要执行跳转指令才能启用
jmp init_protect_mode
init_protect_mode:
在GDT中设定的可读写的段
mov ax, 1 * 8
初始化所有的段寄存器(在启用保护模式后都会改变)
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
必须由汇编处理的到此为止,接下来加载C语言的程序
C语言程序最终会被加载到后面的标号C_kernel中
jmp C_kernel
下面的内容是函数,应确保CPU不会擅自执行
wait_KBD_out:
读取PIC中积攒的数据
in al, 0x64
and al, 0x02
清空垃圾数据
in al, 0x60
如果AND指令的结果不是0就重新跳转到wait_KBD_out
Jump if not zero(不是零则跳转)
jnz wait_KBD_out
返回
ret
以下是数据段,也要确保CPU不会擅自执行
判断地址是否可以被16整除
alignb 16
tmp_gdt:
空
resb 8
可读写的段,32bits
dw 0xffff, 0x0000, 0x9200, 0x00cf
可执行的段,32bits(C程序用)
dw 0xffff, 0x0000, 0x9a28, 0x0047
gdtr0:
很简单的程序,看不懂就不要看了
dw 8 * 3 - 1
tmp_gdt的内容
dd tmp_gdt
判断地址是否可以被十六整除
alignb 16
C_kernel:
jmp $
然后执行一下。
欸,为什么还是在重启呢?
笔者看了看日志信息,发现有这样一句话:
00014146775e[CPU0 ] interrupt(): gate descriptor is not valid sys seg (vector=0x0d)
00014146775e[CPU0 ] interrupt(): gate descriptor is not valid sys seg (vector=0x08)
意思就是说,这个段是一个无效的,所以CPU在重启。他还说了
EIP=0000004c (0000004c)
就是说,CPU将要执行0x7e00:0x004c处的指令。我们翻一翻lst文件就可以了。
就是说CPU正在执行mov ss, ax这条指令,然后还有一句
00014146775e[CPU0 ] exception(): 3rd (13) exception with no resolution, shutdown status is 00h, resetting
CPU是听了shutdown status这个东西,然后就重启了。
根据CPU说的东西,我们能知道,是我们的GDT有问题。
笔者还是想要跳过这个bug,于是将这段初始化寄存器的代码注释掉了。没想到,注释掉后能够正常运行?笔者打开debug模式,获取了段寄存器的值。
???这个bochs不会是太先进了吧?
于是笔者在VMWare上实验了一下。
果然,将这段代码注释掉就可以正常运行,不注释掉就会报一般保护性异常。。。
好吧,虽然今天成功切换到了保护模式,但是还有很多需要问BIOS的东西没有问,我们现在来问一问。
首先是画面的模式。这个我们可以在C语言中定义为一个常量,但是笔者还是想要在汇编中获取然后保存到0x0ff0处。
其次,是键盘上所有灯的状态。Capslock与Numlock都要从这里获取。
最后就是内存的大小了。这个不用我说了吧。
; 键盘上的LED灯
LEDS equ 0x0ff0
; 画面模式
VMODE equ 0x0ff1
; 分辨率X
SCRNX equ 0x0ff3
; 分辨率Y
SCRNY equ 0x0ff5
; VRAM的起始位置
VRAM equ 0x0ff7
; 调整画面模式
; int 0x10 AH寄存器 = 0x00 切换显卡的模式
; AL = 显卡的模式
; 0x03 16色的字符模式,80 x 25
; 0x12 VGA图形模式,640 x 480 x 4位彩色模式,独特的4面存储模式
; 0x13 VGA图形模式,320 x 200 x 8位彩色模式,调色板模式
; 0x6a 扩展VGA图形模式,800 x 600 x 4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)
mov ax, 0x0013
int 0x10
; int 0x16 AH = 0x02 获取键盘指示灯状态
; 中断号码
mov ah, 0x02
; 调用中断
int 0x16
; 将AL寄存器中获取的值存放到内存地址LEDS中
mov [LEDS], al
; 画面模式
mov byte [VMODE], 8
; 分辨率X
mov word [SCRNX], 320
; 分辨率Y
mov word [SCRNY], 200
; VRAM的起始位置
mov dword [VRAM], 0x000a0000
运行。
啊,果然还是不行啊。具体为什么,我们明天再研究。肯定是段寄存器的问题,不过也挺好,一步一个脚印的走着才会有进步嘛。加油!我的Cunix!
你学会了吧?