生活不易,长期没更新,属实抱歉。这是目前GuEeOS的源码结构,源码目前还没上传到Github。重构真的很不容易,写完UEFI和内核基本调试部分笔者会上传至Github的!上次我们进入了个比较潦草的“内核”,我们现在来好好研究内核该怎么做。
说到内核大家可能马上会想到Linux的Kernel,其实呢,只要是比较现代的操作系统,都有内核,比如WindowsNT以上的内核都是.NT内核;OSX的内核是Darwin-xnu,Darwin-xnu是开源的,但是OSX的图形界面不是开源的,这点大家要注意。还有很多很多操作系统,大家开源平台上一搜一大把。
Linux是比较独特的,他是宏内核,维护和开发难度很大。这里随便简单科普一下宏内核和微内核的区别:
宏内核,将所有核心功能写好后链接成一个大程序,这样的好处是内核功能调用内存开销比较小,只有函数调用时的栈消耗,效率极高。缺点就是难以维护和开发,可能改个功能就会牵一发动全身,导致其他地方也需要进行修改。修改一个函数的实现,有个热补丁技术,大家可以自己去了解一下,另一个缺点就是不稳定,容易死机。
微内核,将文件系统,内存管理,图形界面,网络功能等分别写成几个模块,只将部分功能放进内核,各模块彼此独立运行,通过消息机制通讯,因此导致了内存和性能开销较大。优点就是非常稳定,某个模块挂了,重启就好,内核因为工作量不大而不容易挂,这种内核开发起来也比较简单。
Windows和OSX都是混合微内核,尤其是OSX,X-Window是在内核实现的,因此图形界面快的飞起,这也是为什么苹果图形动画流畅、以及大部分美术人员喜欢使用mac工作的一个原因。这种内核结构也是我们设计的方向(文章写到一半,xbook2宣布内核改成宏内核了,哈哈),我们将部分模块依然放进内核,其他的就做成模块。
大概结构就是差不多这样,这是目前GuEeOS重构后的系统架构,只是暂定,以后会根据需要进行较小的改动。现在大体的设计目标有了,我们内核还需要一些前缀,笔者承诺要做图形界面的,那我们就先进入图形界面,后续都不再进入真正文本模式,我们用图形模式来模拟文本模式,然后实现一个printk。
进入传统的VBE(VESA BIOS Extension,VESA是视频电子标准协会,还想深入了解自己搜吧!)图形界面很简单,保护模式前调用int10中断就可以,但是ax和bx的值怎么设置就说来话长了,现代操作系统都采用UEFI的GOP(Graphics Output Protocol)获取分辨率,稍微复杂些,以后再探讨。
(来源:https://www.uefi.org/)
我们统一进入800*600*24 Bits高分辨率模式,24bits就可以设置0~255的RGB三原色了,笔者的屏幕不够大,使用更高的分辨率不方便调试。看了表格,可知,设置VBE图形模式要将AX设置为0x4F02,BX呢?这就涉及到我们要选择什么分辨率了,笔者为大家找到了个比较完整的分辨率表:
(来源:https://en.wikipedia.org/wiki/VESA_BIOS_Extensions)
可以很快找到我们要设置的分辨率号为0x115,黑色部分都是有标准定义的,不过既然在虚拟机里面,大家更不用担心没有这个分辨率,除非是上古机器和上古软件。当然,该走的流程还是要走,我们确实要踏踏实实“验证”该分辨率模式是否可用。我们使用VESA 2.0这个版本,这个版本加入了个Linear Frame Buffer技术,这项技术可以让我们直接向显存写入数据后,显示设备就会自动读取显示相应像素,我们就不用再像之前一样调用BIOS中断了。具体怎么操作,我们会loader.asm里面加一点代码:
;这东西貌似不识别汇编,大家可以复制到其他编辑器查看。LOADER_ADDR equ 0x70000VBEMODE equ 0x115 ; 800*600*24Bits模式号; 以下数字仅表示偏移量VCOLOR equ 0 ; 颜色位数SCREENX equ 2 ; 分辨率宽SCREENY equ 4 ; 分辨率高VRAM equ 6 ; 显存起始位置[bits 16]... inc di loop .printfcheck_VBE_exists: mov ax,0x9000 ;缓冲区从0x90000开始 mov es,ax mov di,0 mov ax,0x4f00 ;检查是否支持VBE int 0x10 cmp ax,0x004f ;Ax==0x004f 是否成立,失败则跳转到VBE_fail jne VBE_failcheck_VBE_version: mov ax,[es:di+4] ;检查VBE版本,必须是2.0 cmp ax,0x200 jb VBE_failget_VBE_information: mov cx,VBEMODE ;模式号 mov ax,0x4f01 ;检查模式号 int 0x10 cmp ax,0x004f ;Ax==0x004f 是否成立,失败则跳转到VBE_fail jne VBE_failset_VBE: mov bx,VBEMODE+0x4000 mov ax,0x4f02 int 0x10 ;这里要是注释掉,依然是文本模式 mov byte [VCOLOR],8 ;记录图形模式信息 mov ax,[es:di+0x12] ;分辨率宽,保存到 0x70002 mov [SCREENX],ax mov ax,[es:di+0x14] ;分辨率高,保存到 0x70004 mov [SCREENY],ax mov eax,[es:di+0x28] ;显存起始地址,保存到 0x70006 mov [VRAM],eax jmp load_GDTRVBE_fail: ;机器挂起,CPU进入休眠状态 jmp $ hltload_GDTR: ;保存GDT地址到GDTR lgdt[gdt]...
这里可能信息量有点大哈,我们根据注释来描述,其他还不懂就是汇编功底不行了哈。首先,就是上面一些“宏常量”,他们只是一些偏移量,通过赋值来保存图形界面的信息。check_VBE_exists中,先将段基地址设置到正确的缓冲区0x9000,接着我们通过0x4f00来检查是否支持VBE模式,接着偏移地址,check_VBE_version检查VBE版本是否为2.0,接着就是set_VBE开始设置显示模式,这里的VBEMODE+0x4000注意了,加上0x4000是为了启用Linear Frame Buffer。大家可能会对这些标准感到头疼,没事,笔者为大家找到了官方资料:
/* * VBE 2.0 * http://www.phatcode.net/res/221/files/vbe20.pdf *《VESA BIOS Extension (VBE) Core Functions Standard Version 2.0》*//* * VBE 3.0 * http://www.petesqbsite.com/sections/tutorials/tuts/vbe3.pdf *《vbecore3》*/
3.0版本有啥笔者就不说了,感兴趣的自己慢慢看,上面的标准和魔数在文档都有。set_VBE还有一段很重要的代码,这里就是在保存相应的显存信息,注释已经写清楚了。到这一步,我们不妨测试一下吧!
细心的人可以发现,分辨率比之前高多了!我们成功的进入了图形模式!下面我们还不能直接操作显存,为什么,因为我们开启了分页机制,显存还没映射上去呢!我们只需要加入这一段就行:
...Page_Dir_Address equ 0x1000Page_Table_Address equ 0x2000 ;目录项起始地址Video_Table_Address equ 0x3000 ;视频模式目录项起始地址... add edi,4 loop .Create_Page ;依葫芦画瓢,这里不过多解释了 mov eax,[0x70000+VRAM] shr eax,22 shl eax,2 mov edx,Video_Table_Address|0x07 mov [Page_Dir_Address+eax],edx mov edi,Video_Table_Address mov esi,[0x70000+VRAM] or esi,0x07 mov cx,1024.mapping_VRAM: mov dword [edi],esi add edi,4 add esi,0x1000 loop .mapping_VRAMEntry_to_paging: mov eax,Page_Dir_Address...
现在显存映射好了,我们计算一下显存起始地址:0x80070000,哈哈开头代码都暗示大家啦。怎么测试呢,笔者在此举个例子:比如显存地址是0x0,如果我们要写入RGB的红色分量,就将地址0x2的值设置为0~255的值,蓝色分量即是将地址0x1的值设置为0~255的值,绿色分量即是将地址0x0的值设置为0~255的值,下一个像素以此类推,都是显示方式要注意,这段显存是连续的,对于屏幕来说就是一行一行的,很像传统的显像管显示的电视一样。
接下来我们用C语言测试,我们写两个头文件吧!
/* FileName: types.h */#ifndef _TYPES_H_#define _TYPES_H_#ifndef NULL#define NULL ((void*)0)#endif#ifndef __cplusplus#define bool_t _Bool#define true 1#define false 0#else#define _Bool bool#define bool_t bool#define false false#define true true#endiftypedef unsigned int uint32_t;typedef unsigned short uint16_t;typedef unsigned char uint8_t;typedef signed int int32_t;typedef signed short int16_t;typedef signed char int8_t;typedef unsigned int size_t;#endif
上面是一些以后我们会常用的数据类型。
/* File: vbe.h */#ifndef _VBE_H_#define _VBE_H_#include #define VBE_ADDR 0x80070000#define Pixel_Byte 3typedef struct VBE_S { uint16_t ColorNumber; uint32_t Width; uint32_t Height; uint8_t *VRAM;} VBE_S;extern uint32_t ScreenWidth, ScreenHeight;void init_VBE(void);void putPixel(int32_t x, int32_t y, uint8_t r, uint8_t g, uint8_t b);#endif
上面的VBE_S的数据结构是根据显存信息结构定的,标准里面已经说明了,下面我们先修改一下Makefile,下一节会细讲Makefile的用法和源码结构的整理,大家先不用这么讲究。
...C_FILE: $(CC) $(C_KERNEL_FLAGS) start.c -o start.o $(CC) $(C_KERNEL_FLAGS) vbe.c -o vbe.okernel.bin: ASM_FILE C_FILE $(LD) $(LD_FLAGS) -o $(KERNEL_FILE) _Start.o start.o vbe.o...
现在,我们要实现两个函数,这里显存地址需要多次转换。可能有点绕。
/* File: vbe.c */#include static VBE_S *VBE;static uint32_t ScreenLength;uint32_t ScreenWidth, ScreenHeight;void init_VBE(void) { VBE->ColorNumber = *((uint16_t*)VBE_ADDR); VBE->Width = (uint32_t)(*((uint16_t*)(VBE_ADDR + 2))); VBE->Height = (uint32_t)(*((uint16_t*)(VBE_ADDR + 4))); VBE->VRAM = (uint8_t*)(*((uint32_t*)(VBE_ADDR + 6))); ScreenWidth = VBE->Width; ScreenHeight = VBE->Height; // 这是屏幕一行的rgb的数量总和数量,即屏幕一行占用的内存地址范围 ScreenLength = ScreenWidth * Pixel_Byte;}void putPixel(int32_t x, int32_t y, uint8_t r, uint8_t g, uint8_t b) { // 超出范围直接略过 if (x >= VBE->Width || y >= VBE->Height) return; // 由于是显存是线性的,我们转换到二维需要这么做 int pos = y * ScreenLength + x * Pixel_Byte; VBE->VRAM[pos + 0] = b; VBE->VRAM[pos + 1] = g; VBE->VRAM[pos + 2] = r;}
现在我们测试一下,就画一条斜线吧:
/* File: start.c */#include int os_main(void) { init_VBE(); int i = 0; for (; i < 500; ++i) { putPixel(i, i, 255, 255, 255); } /* 进入死循环 */ while(1); return 0;}
忙活半天,终于能用了!现在我们可以绘制任何形状了。当然了,我们这节的目的是模拟出文本界面,其他图形读者自行测试吧,给几个Demo:
下面才是正文,咳咳,我们要模拟文本模式,要做到能输出文字和自动换行就可以了,因为原生的文本模式默认就这俩功能。像其他功能清屏,定位,符号处理和键盘输入这些,即使是文本模式,也需要自己开发出来才有(其次就是...天色不早了)。
我们要图形上绘制文本,最简单的部分就是取点阵字体,不多,我们只要标准的ascii中的基本符号和数字英文就行。
(图片来源于网络)
即空格到~就行,我们把点阵取出来,空的地方用0表示,有内容的地方用1表示:
我们储存结构就是这样。很多现成的软件都可以对字体进行取模,但是要弄清楚相应的储存结构。如果读者坚持要自己做的话可以用OpenCV搞定,笔者就是用OpenCV做的:先用目的字体把所有按顺序字符输出来(一个背景和一个前景,像前两张图那样),截图,两个嵌套for循环和像素判断就能搞定。取模完毕后,将数据放进fonts.h文件里就行。
好了,现在我们要把字符打印功能做好,我们是自上而下输出一个字符,记得在vbe.h补充相应的函数声明:
/* File: vbe.c */...uint32_t ScreenWidth, ScreenHeight;#include "fonts.h"void init_VBE(void) {...void putChar(uint8_t n, int32_t x, int32_t y, uint8_t r, uint8_t g, uint8_t b) { int set_x, set_y, rows, cols, subs_ascii, subs_bit; // 这里读者根据自己取模的大小决定 rows = x + 8, cols = y + 16, subs_ascii = 0; if (n < 32 || n > 126) n = 95; else n -= 32; for (set_y = y; set_y < cols; set_y++, subs_ascii++) { for (set_x = x, subs_bit = -1; set_x < rows; set_x++, subs_bit++) { if ((AsciiFonts[n][subs_ascii] >> (7 - subs_bit) & 1) == 1) { putPixel(set_x, set_y, r, g, b); } } } return;}
我们做个小测试:
/* File: start.c */#include int os_main(void) { init_VBE(); int i = 0, j = 0; for (; i < ScreenWidth; ++i) { for (j = 0; j < ScreenHeight; ++j) { putPixel(i, j, 255, 255, 255); } } putChar('A', 0, 0, 0, 0, 0); /* 进入死循环 */ while(1); return 0;}
很好!我们的'A'打印出来了,现在我们实现字符串打印,同理,我们多次调用putChar,然后改变x或者y值就可以了:
/* File: vbe.c */...void putString(const char* string, int32_t x, int32_t y, uint8_t r, uint8_t g, uint8_t b) { while (*string) { if (x >= ScreenWidth) { y += 16; x = 0; } if (*string == '\n') { y += 16; x = 0; *string++; continue; } putChar(*string, x, y, r, g, b); x += 8; *string++; }}
/* File: start.c */... // 这里测试字符串 putString("!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", 0, 0, 0, 0, 0); // 这里测试换行 putString("Hello\nWorld!", 0, 20, 255, 0, 0);...
测试很成功!代码的话先这么实现吧,printk下节再说,笔者该睡觉了。