我的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, 0x64and 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 16tmp_gdt:; 空resb 8; 可读写的段,32bitsdw 0xffff, 0x0000, 0x9200, 0x00cf; 可执行的段,32bits(C程序用)dw 0xffff, 0x0000, 0x9a28, 0x0047gdtr0:; 很简单的程序,看不懂就不要看了dw 8 * 3 - 1; tmp_gdt的内容dd tmp_gdt; 判断地址是否可以被十六整除alignb 16
; 设置临时的GDTlgdt [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_modeinit_protect_mode:; 在GDT中设定的可读写的段mov ax, 1 * 8; 初始化所有的段寄存器(在启用保护模式后都会改变)mov ds, axmov es, axmov fs, axmov gs, axmov ss, ax
到这里,CPU应该就已经是保护模式的状态了。接下来,我们要执行C语言程序。
C语言的程序其实最终是和kernel.asm连为一体的(生成的机器语言会结合为一体),所以,只需要在kernel.asm的最后留下一个标号C_kernel然后跳转到该标号就可以了。
; 必须由汇编处理的到此为止,接下来加载C语言的程序; C语言程序最终会被加载到后面的标号C_kernel中jmp C_kernelC_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 250x12 VGA图形模式,640 x 480 x 4位彩色模式,独特的4面存储模式0x13 VGA图形模式,320 x 200 x 8位彩色模式,调色板模式0x6a 扩展VGA图形模式,800 x 600 x 4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)mov ax, 0x0013int 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设置临时的GDTlgdt [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_modeinit_protect_mode:在GDT中设定的可读写的段mov ax, 1 * 8初始化所有的段寄存器(在启用保护模式后都会改变)mov ds, axmov es, axmov fs, axmov gs, axmov ss, ax必须由汇编处理的到此为止,接下来加载C语言的程序C语言程序最终会被加载到后面的标号C_kernel中jmp C_kernel下面的内容是函数,应确保CPU不会擅自执行wait_KBD_out:读取PIC中积攒的数据in al, 0x64and al, 0x02清空垃圾数据in al, 0x60如果AND指令的结果不是0就重新跳转到wait_KBD_outJump if not zero(不是零则跳转)jnz wait_KBD_out返回ret以下是数据段,也要确保CPU不会擅自执行判断地址是否可以被16整除alignb 16tmp_gdt:空resb 8可读写的段,32bitsdw 0xffff, 0x0000, 0x9200, 0x00cf可执行的段,32bits(C程序用)dw 0xffff, 0x0000, 0x9a28, 0x0047gdtr0:很简单的程序,看不懂就不要看了dw 8 * 3 - 1tmp_gdt的内容dd tmp_gdt判断地址是否可以被十六整除alignb 16C_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 250x12 VGA图形模式,640 x 480 x 4位彩色模式,独特的4面存储模式0x13 VGA图形模式,320 x 200 x 8位彩色模式,调色板模式0x6a 扩展VGA图形模式,800 x 600 x 4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)mov ax, 0x0013int 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设置临时的GDTlgdt [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_modeinit_protect_mode:在GDT中设定的可读写的段mov ax, 1 * 8初始化所有的段寄存器(在启用保护模式后都会改变)mov ds, axmov es, axmov fs, axmov gs, axmov ss, ax必须由汇编处理的到此为止,接下来加载C语言的程序C语言程序最终会被加载到后面的标号C_kernel中jmp C_kernel下面的内容是函数,应确保CPU不会擅自执行wait_KBD_out:读取PIC中积攒的数据in al, 0x64and al, 0x02清空垃圾数据in al, 0x60如果AND指令的结果不是0就重新跳转到wait_KBD_outJump if not zero(不是零则跳转)jnz wait_KBD_out返回ret以下是数据段,也要确保CPU不会擅自执行判断地址是否可以被16整除alignb 16tmp_gdt:空resb 8可读写的段,32bitsdw 0xffff, 0x0000, 0x9200, 0x00cf可执行的段,32bits(C程序用)dw 0xffff, 0x0000, 0x9a28, 0x0047gdtr0:很简单的程序,看不懂就不要看了dw 8 * 3 - 1tmp_gdt的内容dd tmp_gdt判断地址是否可以被十六整除alignb 16C_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; 分辨率XSCRNX equ 0x0ff3; 分辨率YSCRNY 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, 0x0013int 0x10; int 0x16 AH = 0x02 获取键盘指示灯状态; 中断号码mov ah, 0x02; 调用中断int 0x16; 将AL寄存器中获取的值存放到内存地址LEDS中mov [LEDS], al; 画面模式mov byte [VMODE], 8; 分辨率Xmov word [SCRNX], 320; 分辨率Ymov word [SCRNY], 200; VRAM的起始位置mov dword [VRAM], 0x000a0000
运行。
啊,果然还是不行啊。具体为什么,我们明天再研究。肯定是段寄存器的问题,不过也挺好,一步一个脚印的走着才会有进步嘛。加油!我的Cunix!
你学会了吧?
