消失的这一星期,我写了一个加减乘除法操作系统

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

前言

这星期研究了一下操作系统启动,并且写了个可以计算加减乘除的操作系统(这貌似挺起来有点搞笑),当然先展示效果图,但是当我录制完mp4后,格式只有3MB,但当我转换为gif时,比较大,没办法在文章中预览了,所以只能看一张图片了,但是已上传到服务器,地址如下:https://houxinlin.com/res/1.mp4

下面这张图是将系统写入U盘并设置U盘启动后的效果图,当然也能在模拟器上展示,但是没有比跑在实体机上有成就感,型号为Thinkpad E430

消失的这一星期,我写了一个加减乘除法操作系统
image.png

功能只有计算加减乘除和清屏,比如输入`1+3`时,将在下一行打印结果,并输入CLS时,将屏幕清空,比较单一,整个过程也比较简单,一开始开始我也觉得难,但当我不断学习后,才发现,实现这样一个小功能,确实没什么特别复杂的,只是设计到的东西有点多,知识有点广。

如何运行

这个系统可以从这里下载。

http://houxinlin.com:8080/os-image.bin

下载后刻录到U盘中即可,我没有在Window上通过相关软件刻录,而是在Linux上通过dd命令写入到U盘中的,不能保证Window中刻录软件会成功,但可以用扇区编辑工具直接把这个文件复制到U盘扇区,比如借助HxD工具。

刻录到U盘后一定要设置Bios为legacy启动模式,我们的系统不支持uefi,但现在大部分人电脑都是uefi,记得一定要设置,其他得原因就自求多福吧。

源码可以从这里下载,要编译源码,需要先安装交叉编译器i686-elf。

https://github.com/houxinlin/cool-os.git

启动原理

到这里,该说说原理了,但是让你失望的是,我没有时间把他说的很详细,因为还有别的工作,等日后有时间,我会详细的说每个流程,原因是每一个知识点都需要很多文字,当我们要写一个操作系统,少不了汇编,需要先熟悉NASM或者GAS汇编,但这里也不可能介绍汇编基础,这里讲一个笑话,一位教了一辈子汇编与微机原理的老师,讲课时候很自大,经常说一些潜在意思,比如"你们以前老师不知道教清楚没,教的有我清楚没",可是当被问起NASM和MASN的区别时,吐出一句我不知道NASM。

回到正题吧,如果想从一开始了解计算机启动过程,那我也不知道从那开始了解,在我现有的知识中,他会从CPU上的一个RESET引脚开始说起,当加电后,他会进行一个初始化,会强制将cs:ip寄存器初始化为0xF000:0xFFF0 ,另外由于开机的时候处于实模式,在实模式下需要将段寄存器左移4位在加上偏移地址,于是最终物理地址将是0xFFFF0,而这个地址便是BIOS的入口地址,BIOS执行他自己的逻辑后会跳转到0x7c00执行,而这个地址开始的代码,是我们自己写的,注意,cs:ip寄存器会强制初始化为0xF000:0xFFF0这句话,这是很多书所说,但是他们忘记加这是对于现代CPU了,而在8086这款CPU上,会强制初始化为0xFFFF:0x0000的,但是这个结果不论怎么算,最终物理地址都是0xFFFF0。

如果你想开机后在屏幕上输出一句话,那是非常简单的,但想输出中文、或者其他文字,那就麻烦了,所以我们只拿英文做示范,当学会汇编后,不到5行汇编即可完成,但你不能凭空写,这需要先参考BIOS中断号各个作用和其参数,比如下面代码,当调用10h中断时候,BIOS会根据ah和al中的参数,做响应,这里的中断也同Linux中80h中断类似,但如果你对这方面有经验的话。

mov  al , 'a' 
mov ah , 0eh
int 10h

这个过程就是个参考手册的过程,这个文档我见过最详细的一篇就是维基百科,但是这玩意你懂的,进不去,所以我找到另一篇比较详细的文章。

http://www.ablmcc.edu.hk/~scy/CIT/8086_bios_and_dos_interrupts.htm#

在说BIOS,可能就知道他是个基本输入输出系统,但是,他在编写操作系统的时候,起到至关重要的作用,是他跳转到我们操作系统第一行代码的,并且提供了中断,当我们想在屏幕上打印一个字符时,只需要调用他内置的中断即可,还有读取硬盘时,也可以借助他的中断,前提是设置好参数,这些参数都在不同的寄存器里。

而我们常说的选择一个引导设备,就是指选择一个操作系统所在位置,当我们把系统写入到U盘后,那么这个U盘称为引导设备,BIOS会将这个设备的第一个扇区复制到内存地址0x7C00的物理内存中,注意一个扇区是512字节,也就是会把U盘中前512个字节复制到0x7C00处,之后BIOS会跳转的这里执行代码,所以我们操作系统的第一行代码就从这里开始执行,这块代码通常都是由汇编完成,但是有个前提是这512字节最后两个字节是0x55、0xaa,用来表明这个扇区是一个引导扇区,如果最后两个字节不是0x55、0xaa,那么BIOS也不会执行。这个被称为魔数,这个扇区还有个名字,叫MBR,另外这个过程是可调试的,如果你使用bochs,可以通过b 0x7c00打下断点,从这里开始就可以调试你的代码了

为啥叫引导扇区呢?因为操作系统内核不再这里,但是内核也不能直接启动,需要借助512字节的代码做一下助推。

另外这里又牵扯到很多知识,比如为什么是0x7C00?,最后两位为什么是0x55、0xaa?乌龟的屁股吧。

但是注意此时CPU运行在实模式,CPU一开始就是这个模式,还有一个模式是保护模式,这两个模式自行百度吧,如果想了解,需要长篇大论,还有一个问题是512字节怎么够一个系统?,所以我们在这512字节里做的事是,手动加载内核到内存,并跳转到这里执行,并且切换到保护模式,如果在详细说,就是设置GDT、IDT等信息。

切换到保护模式是由一个叫CR0寄存器控制的,这个寄存器最低位被叫做机器状态,他的第0位叫保护允许位,机器在上电后他的值是0,可以手动切换,值是1时进入保护模式。

但是进入到保护模式后,BIOS中断就没办法用了,你可能想知道用不了BIOS中断,还怎么输出文字?

没错,是用不了,但是取而代之的是直接向显卡特定地址直接写入数据,其原理是将某个外设的内存映射到一定范围的地址空间中, CPU通过地址总线访问该内存区域时会落到这个外设的内存中,这种映射让CPU访问外设的内存就如同访问主板上的物理内存一样,比如显卡上有块内存叫显存,它被映射到主机物理内存上的0xB8000-0xBFFFF, CPU访问这片内存就是访问显存,住这片内存上写字节就是向屏幕上打印内容。

这里在插一句,如果只是在这512字节里面做测试,玩一玩BIOS中断,一般两条命令就可以搞定,第一条用来编译,第二条用来启动模拟器,或者直接写入到U盘,但是真的想写一个有一丝丝用的系统,建议还是先学make,他能帮助做一些编译相关的工作,省去一些命令行上的操作,如果你从网上下载一些操作系统的demo,几乎少不了make命令。

GDT

我们在说说GDT,中文名字叫全局描述表,当进入保护模式的时候,必须设置这个表,通过指令lgdt加载,并会由寄存器GDTR保存入口,用来提供程序执行时需要的内存的各种信息,GDTR是48位的寄存器,最低两个字节0-15表示GDTR的限长,最高4个字节16-47表示基址,表示GDT表在内存中的开始位置。全局描述符表相当于是描述符的数组,数组中的每个元素都是8节的描述符。

这个表也比较复杂,一句两句说不清,在以后会单独的文章介绍他。

IDT

IDT中文为中断描述符表,和GDT类似,记录了0~255的中断号和调用函数之间的关系,当CPU接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后跳转到这里执行中断处理程序。

他也比较复杂,在以后会单独的文章介绍他。

键盘中断

当进行键盘输入编写时,需要使用键盘中断功能,如果是在BIOS中读取字符,这比较简单,短短几句就可以实现,如下。

BITS 16
loop:
 mov ah,0x00
 int 16h
 jnz print_ok
 jmp loop
print_ok:
 mov  ah , 0eh
 int 10h
 jmp  loop
times 510-($-$$) db 0
dw 0xAA55

但我们在保护模式不能这样简单粗暴,而是要利用到IDT表,但在此前,你还要了解键盘扫描码,这和ascii码不同,但也类似,键盘内部有个叫作键盘编码器的芯片,它的作用是当键盘上发生按键操作时候,就向键盘控制器发送哪个键被按下,以及哪个按键弹起,但这个键盘控制器不再键盘中,而是在主板上,他负责接收来自键盘编码器的按键信息,将解码后保存,然后向中断代理发中断,这个中断处理程序将会读入以前保存过的按键信息。

而双方要规定一个协议,这个协议是对键盘上每个键的标识,所以,键盘扫描码就诞生了,但是一次按键有两个状态,按下和抬起,所以,一个键也就有两个编码,在按下和抬起时,都会发送扫描码,在中断处理程序中,取到的也是这个扫描码,需要我们自行转换为ascii码,这个时候你可以把a转换为b进行打印,完全由你控制,但这样显然不符合正常人需求。

而我们如何在中断程序中取出一个扫描码呢?

就必须使用in指令了,但是每次只能处理一个字节,所以,如果有组合键按下时,需要先定义一个全局变量,记录如Ctrl已经被按下,接着当A被按下时候,判断Ctrl是按下状态,然后执行相关操作,比如全选。

但是这个键盘扫描码有三套,目前我们用的都是AT格式的,这个自行了解吧。

当转换为ascii后,把这个值记录下来,并直到遇到回车时,把这个字符指针传递给其他一个单独的函数,这个单独的函数用来解析输入的字符,在这里,我们通过execute_command函数,来解析加减乘除。

还需要了解8259A中断控制芯片,我找了很多文章介绍他,但还是对他模模糊糊,直到在看《操作系统真相还原》相关章节的时候,才明白,他介绍的非常详细。

加法计算

到这里就是c语言基础了,在内核中,需要你完成字符打印函数、清除屏幕等基本显示功能,由于无法调用C库,诸如atoi、itoa需要我们自己完成,但另外愉快的是,他们都比较简单,下面这段代码没有操作系统知识也能看懂。

void print_int(int value)
{
    char result[255];
    itoa(value, result);
    print_string(result);
    print_nl();
}
void exec(int index, char *input, char ec_symbol)
{
    char temp[22];
    itoa(index, temp);
    int size = string_length(input);
    char ns_1[index];
    char ns_2[size - index];
    sub_string(input, 0, index, ns_1);
    sub_string(input, index + 1-1, ns_2);
    print_string(input);
    print_string("=");
    if (ec_symbol == '+')
    {
        int value = atoi(ns_1) + atoi(ns_2);
        print_int(value);
    }
    if (ec_symbol == '-')
    {
        int value = atoi(ns_1) - atoi(ns_2);
        print_int(value);
    }
    if (ec_symbol == '*')
    {
        int value = atoi(ns_1) * atoi(ns_2);
        print_int(value);
    }
    if (ec_symbol == '/')
    {
        int value = atoi(ns_1) / atoi(ns_2);
        print_int(value);
    }
}
//回车会执行到这里
void execute_command(char *input)
{
    if(compare_string(input,"CLS")==0){
        clear_screen();
        return;
    }
    int index = -1;
    index = string_index_of(input, '-');
    if (index != -1)
        exec(index, input, '-');
    index = string_index_of(input, '+');
    if (index != -1)
        exec(index, input, '+');
    index = string_index_of(input, '*');
    if (index != -1)
        exec(index, input, '*');
    index = string_index_of(input, '/');
    if (index != -1)
        exec(index, input, '/');
}

基本情况就是这了,在以后文章会单独说每个知识点。

原文始发于微信公众号(十四个字节):消失的这一星期,我写了一个加减乘除法操作系统