内核功能导致重启_操作系统开发之——内核初探

7dac9272ad91be0915aedd520b22e7de.png

生活不易,长期没更新,属实抱歉。这是目前GuEeOS的源码结构,源码目前还没上传到Github。重构真的很不容易,写完UEFI内核基本调试部分笔者会上传至Github的!上次我们进入了个比较潦草的“内核”,我们现在来好好研究内核该怎么做。

ebd7ce79fecfbbaab811631094e09c8d.png

说到内核大家可能马上会想到Linux的Kernel,其实呢,只要是比较现代的操作系统,都有内核,比如WindowsNT以上的内核都是.NT内核;OSX的内核是Darwin-xnu,Darwin-xnu是开源的,但是OSX的图形界面不是开源的,这点大家要注意。还有很多很多操作系统,大家开源平台上一搜一大把。

Linux是比较独特的,他是宏内核,维护和开发难度很大。这里随便简单科普一下宏内核和微内核的区别:

宏内核,将所有核心功能写好后链接成一个大程序,这样的好处是内核功能调用内存开销比较小,只有函数调用时的栈消耗,效率极高。缺点就是难以维护和开发,可能改个功能就会牵一发动全身,导致其他地方也需要进行修改。修改一个函数的实现,有个热补丁技术,大家可以自己去了解一下,另一个缺点就是不稳定,容易死机。

微内核,将文件系统,内存管理,图形界面,网络功能等分别写成几个模块,只将部分功能放进内核,各模块彼此独立运行,通过消息机制通讯,因此导致了内存和性能开销较大。优点就是非常稳定,某个模块挂了,重启就好,内核因为工作量不大而不容易挂,这种内核开发起来也比较简单。

Windows和OSX都是混合微内核,尤其是OSX,X-Window是在内核实现的,因此图形界面快的飞起,这也是为什么苹果图形动画流畅、以及大部分美术人员喜欢使用mac工作的一个原因。这种内核结构也是我们设计的方向(文章写到一半,xbook2宣布内核改成宏内核了,哈哈),我们将部分模块依然放进内核,其他的就做成模块。

48ed5399b16c4debe572bbfc59cdb954.png

大概结构就是差不多这样,这是目前GuEeOS重构后的系统架构,只是暂定,以后会根据需要进行较小的改动。现在大体的设计目标有了,我们内核还需要一些前缀,笔者承诺要做图形界面的,那我们就先进入图形界面,后续都不再进入真正文本模式,我们用图形模式来模拟文本模式,然后实现一个printk

进入传统的VBE(VESA BIOS Extension,VESA是视频电子标准协会,还想深入了解自己搜吧!)图形界面很简单,保护模式前调用int10中断就可以,但是ax和bx的值怎么设置就说来话长了,现代操作系统都采用UEFI的GOP(Graphics Output Protocol)获取分辨率,稍微复杂些,以后再探讨。

fa0ab4eb228acd90a1464c0d5efe564b.png

(来源:https://www.uefi.org/)

我们统一进入800*600*24 Bits高分辨率模式,24bits就可以设置0~255的RGB三原色了,笔者的屏幕不够大,使用更高的分辨率不方便调试。看了表格,可知,设置VBE图形模式要将AX设置为0x4F02,BX呢?这就涉及到我们要选择什么分辨率了,笔者为大家找到了个比较完整的分辨率表:

e085e13127e3c8b8bc7d8fe680c740ae.png

(来源: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版本有啥笔者就不说了fb0398bdd389afb74ea18efae796d4a3.png,感兴趣的自己慢慢看,上面的标准和魔数在文档都有。set_VBE还有一段很重要的代码,这里就是在保存相应的显存信息,注释已经写清楚了。到这一步,我们不妨测试一下吧!

652121a7d6c455176fc0f49ce8994147.png

细心的人可以发现,分辨率比之前高多了!我们成功的进入了图形模式!下面我们还不能直接操作显存,为什么,因为我们开启了分页机制,显存还没映射上去呢!我们只需要加入这一段就行:

...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;}

f47632c9b6e8069958cc09167a26bb9a.png

忙活半天,终于能用了!现在我们可以绘制任何形状了。当然了,我们这节的目的是模拟出文本界面,其他图形读者自行测试吧,给几个Demo:

f86ad9f309396408f7ff82a846e19225.png

9a0d0dc5ad488957feb166c97e78f163.png

fabcca3d84603482690bfa558501a37c.png

下面才是正文,咳咳,我们要模拟文本模式,要做到能输出文字和自动换行就可以了,因为原生的文本模式默认就这俩功能。像其他功能清屏,定位,符号处理和键盘输入这些,即使是文本模式,也需要自己开发出来才有(其次就是...天色不早了)。

我们要图形上绘制文本,最简单的部分就是取点阵字体,不多,我们只要标准的ascii中的基本符号和数字英文就行。

3eaedf9498368026e4fc9824a7c6a899.png

(图片来源于网络)

空格~就行,我们把点阵取出来,空的地方用0表示,有内容的地方用1表示:

4f63464931daed97440cabc6806e99a2.png

我们储存结构就是这样。很多现成的软件都可以对字体进行取模,但是要弄清楚相应的储存结构。如果读者坚持要自己做的话可以用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;}

d5224610b65f2e518b42e6fa6b49eb49.png

很好!我们的'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);...

f6e7f2b960644f7c06ae9d17467b3b87.png

测试很成功!代码的话先这么实现吧,printk下节再说,笔者该睡觉了0f6aee67009e136daae491fa8f12327a.png

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值