> 文章列表 > 自己开发X86操作系统--LIUNUXOS

自己开发X86操作系统--LIUNUXOS

自己开发X86操作系统--LIUNUXOS

为了纪念Linus Torvalds创始开发的linux,我将自己开发的os命名为LIUNUXOS。

LIUNUXOS其原码分为两个部分,汇编工程和c/c++工程,地址分别为:
LIUNUXOS汇编工程原码地址
LIUNUXOS c/c++工程原码地址

在这些工程中,源程序的文件名和其功能名称一一对应,从其名字就可以猜出其具体功能。
自己开发X86操作系统--LIUNUXOS

1. 汇编工程。

汇编部分都是用16/32位x86汇编编写,其开发工具是masm和link。整个工程又分3个模块:

mbr.com:是一个com程序,功能是加载执行loader.com程序。x86架构规定,Bios开机自检后,加载硬盘第一个扇区到内存地址0:7c00h,然后cpu从该地址开始执行。mbr中包含64字节的DPT,在DPT前面是4字节的扇区号,存放着LIUNUX_OS_DATA结构体的扇区号,这样mbr就可以通过bios调用int13h来执行扇区读写功能。

loader.com:是一个com程序,功能是加载执行kernel.exe。因为mbr只有一个扇区,除去64字节的DPT和末尾的0x55aa最多只能有446字节的指令,无法实现kernel.exe模块的加载工作(可执行文件格式一般比较复杂,内存加载运行时一般要做各种设置和重定向的工作),所以专门设计了loader程序来加载kernel.exe程序。

笔者用的微软的masm和link程序编译汇编原码。masm和link生成的exe是16位程序,其头部带有exe特有的数据格式,而mbr扇区被加载到0:7c00后会直接跳转到该地址执行指令,所以写到磁盘第一个扇区的mbr程序需要是一个纯代码形式的文件,16位的com程序是一个纯代码文件,满足我们的要求。

由于微软的masm和link开发年代久远,只能运行于32位系统,已经不能被64位操作系统支持,故上述mbr和loader这2个程序编译时,需要在windows xp(或者windows vista/7的32位平台)下,在cmd切换到汇编工程目录,执行如下命令编译链接:
masm mbr;
link mbr;
exe2bin mbr.exe mbr.com

masm loader;
link loader;
exe2bin loader.exe loader.com
注意不要忘记前两条命令中的";",这是特殊用途符号。

这样编译出来的两个程序就是com格式的。com格式不懂的请自行百度。

kernel.exe:是一个16位和32位汇编混合的16位/32位程序,功能主要有:实模式下设置cmos时钟,8254计时器,ps2鼠标,8042/8048键盘,rs232串口,8259中断控制器以及中断门,陷阱门,任务门,IDT,GDT,LDT等,调用bios的int10h vesa接口设置图形显示模式,然后进入32位保护模式,并用32位汇编实现了windows PE文件的加载功能(大体包括虚拟段映射,导入导出表,重定位三个部分),在保护模式中加载kernel.dll后,跳转到kernel.dll中的__kernelEntry函数开始执行,__kernelEntry函数做完初始化动作并设置好进程切换的基础设置后,执行sti指令打开中断,系统进入多进程/多线程模式,cpu等待被调度。该模块也包含文本和图形化日志输出模块,在发生异常时会显示异常信息。

kernel.exe程序是整个系统的核心,因为现代计算机一般都是基于中断和异常驱动的,而该程序正是中断和异常的触发、中转中心,例如:基于8254计时器和cmos时钟的计时器中断被当作时间片切换的驱动来源,时间片的切换频率正是计时器的一个计时周期;键盘鼠标的中断被分发到kernel32.dll中的__kKeyboardProc函数和__kMouseProc函数中处理;所有的异常被调度到__kException函数中运行;等等。

整个汇编工程中没有很复杂的模块,最大的文件也就500百多行,所以,虽然汇编晦涩难懂,但是花点时间,或者对有经验的同学,还是很容易上手的。另外,开发目录中包含debug.exe和debug32.exe,这两个工具可以帮助调试和查找汇编开发的错误。

kernel.exe虽然是整个系统的核心,但却是用汇编编写的,其主要的代码开销还是在GDT、IDT、tss等初始化、kernel.dll加载上等。开发的目标模块小功能简单时,汇编跟高级语言差别还不明显,但是如果开发比较复杂和逻辑功能比较强大的程序,汇编的可维护性、可扩展性、开发速度、难度、调试等直线上升。为了可扩展性、提高开发效率、可维护性,操作系统必须使用高级语言开发,这也是必须在汇编中完成pe文件加载执行的原因。后面的几个工程kernel.dll、main.dll、liunuxSetup.exe、liunux_seutp,都是使用visual stuidio c/c++或者linux gcc高级语言开发工具完成的。

liunuxos开机启动界面:
自己开发X86操作系统--LIUNUXOS
kernel.exe根据硬件配置和支持,要求选择几种分辨率的图像显示模式,按数字2选择1600x1200x32位模式启动:
自己开发X86操作系统--LIUNUXOS

按F1启动命令行窗口程序(窗口一般只有边框、客户区和一个关闭按钮),并从键盘输入输入"hello liunuxos!",按ESC退出cmd:
自己开发X86操作系统--LIUNUXOS

2. c/c++工程

c/c++工程包含4个模块:liunuxSetup.exe(windows系统下的安装程序)、linux_setup(linux系统下的安装程序)、kernel.dll、main.dll。其中,windows安装程序和linux安装程序用于把程序安装到windows或者linux系统中。kernel.dll和main.dll是liunuxos的核心部分。

liunuxSetup.exe
liunuxos系统用的virtualbox虚拟机,其他虚拟机如vmware证明不支持vesa图形模式,故无法正常显示vesa支持的图形模式。

liunuxos安装时需要如下几个程序:mbr.com,loader.com,kernel.exe, kernel.dll,main.dll,liunuxSetup.exe(windows系统),linux_setup(linux系统),font.db,HZK16(汉字16X16字体,可选)。其中font.db是英文字体,根绝bios资料,bios地址空间0xffa6e开始的地址存放着所有ASCII编码的8x8点阵字体,font.db正是从该地址中提取出来的。HZK16是我自己从网上看到的汉字16x16点阵字体,显示方式和ASCII的基本一致,代码中有多处使用的方法。

下图是liunux安装时的程序汇总:
自己开发X86操作系统--LIUNUXOS
此时点击安装程序liunuxSetup.exe即可完成LIUNUXOS的安装。
自己开发X86操作系统--LIUNUXOS
自己开发X86操作系统--LIUNUXOS

安装程序liunuxSetup.exe和os借助于结构体LIUNUX_OS_DATA实现新系统的启动以及安装文件的存储和查找:

typedef struct  
{int flag;			//0		标志 LJG0short loaderSecCnt;	//4		loader占用扇区数int loaderSecOff;	//6		loader扇区号偏移short kernelSecCnt;	//10	kernel.exe占用扇区数int kernelSecOff;	//12	kernel.exe扇区号偏移int mbrSecOff;		//16	mbr扇区号偏移int mbr2SecOff;		//20	mbr2扇区号偏移short fontSecCnt;	//24	字体占用扇区数int fontSecOff;		//26	字体扇区号偏移short kerdllSecCnt;	//30	kernel.dll占用扇区数int kerdllSecOff;	//32	kernel.dll扇区号偏移short maindllSecCnt;	//36	main.dll占用扇区数int maindllSecOff;		//38	main.dll扇区号偏移char reserved[22];		//42	保留
}LIUNUX_OS_DATA,*LPLIUNUX_OS_DATA;

liunuxSetup.exe双击运行后,在磁盘上查找超过512kb的连续的扇区(扇区默认是512B大小),一般是利用ntfs或者fat32文件系统空闲的磁盘间隙,以这片空闲扇区的第一个扇区号为起始扇区号,分别计算LIUNUX_OS_DATA结构体中各个字段的值,接下来,先将LIUNUX_OS_DATA结构体写入空闲扇区的第一个扇区,然后将其各个字段对应的文件依次写入对应的扇区号地址。另外,写入的时候还需要重新整合mbr.com程序,其具体做法如下:将mbr.com扩展到512字节,64字节DPT 从设备"\\\\.\\PHYSICALDRIVE0"(windows系统)或者"/dev/sda"(linux系统)读取,在DPT前面4字节写入LIUNUX_OS_DATA所在的扇区号,mbr扇区末尾写入X55AA,并将此扇区写入当前系统的MBR中,下次启动时,mbr代码将会从LIUNUX_OS_DATA中找到loader.com,loader.com又会从LIUNUX_OS_DATA中找到kernel.exe,kernel.dll,main.dll,font.db等,依次完成系统的启动。

mbr执行时会根据LIUNUX_OS_DATA结构体,找到loader.com,将loader.com读入内存中并直接跳转执行;loader程序稍微复杂一些,主要功能是汇编代码实现的16位exe文件加载程序,将kernel.exe加载到内存中重定位后执行之。

linux_setup
linux安装程序linux_setup大约也是相似功能。但是其读写的磁盘设备是/dev/sda。

kernel32.dll
kernel32.dll是系统的主要组成部分,是代码量最多的模块。其主要包括以下模块:

  1. 进程线程创建和调度。时钟计时器每过一个计时周期就会触发一次中断,这就是liunuxos的线程切换频率,大概为1ms。

    当前支持两种进程/线程切换方式,第一种只需要一个tss任务状态段。进入中断后,中断程序保存硬件寄存器等执行现场,在切换为新的内核堆栈中依次push进新线程的硬件寄存器和其他现场寄存器,然后执行iret指令后,新的寄存器和现场被弹出到新线程中并执行。

    第二种线程切换需要两个tss,一个是所有进程共用的,一个在任务门中。时钟中断发生时,进程的硬件寄存器被自动保存在共享的tss中,在中断程序中,将进程共享的tss内容拷贝到内存中的该进程信息表中(进程信息表基地址在系统启动时被设置,通过pid可以找到进程的所有信息和寄存器、现场数据),然后把要被调度执行的进程的tss数据复制到共享tss中,中断程序执行iret指令返回后,新的线程已经被切换。在进程调度时,使用的比较简单的轮转调度,就是按照pid进程号,依次轮转调度执行。
    应用进程的栈初始化大小是1MB,系统进程的栈初始化大小是64kb。

    进程是资源的载体,其每个线程都是单独调度的,只是在进程切换时修改cr3,而在进程内部的线程之间切换时并不修改cr3,代码中利用pid这一标识判断线程是否在同一个进程中。

    该模块支持任意pe文件被加载执行。

    liunuxos不支持apic模式,虽然是多进程多线程,但是只支持一个cpu。

  2. 文件系统读写。主要是ntfs、fat32、iso9660文件系统的读取操作。文件系统的资料网上很多,请自行百度,我只是重复的搬砖而已。硬盘驱动器在虚拟机创建时只能支持ATA和SATA模式。

  3. 内存管理。主要是内存分配和虚拟内存的实现。关于虚拟内存,网上资料很多,但是没有找到切实可行的方法。我的做法分为两步:
    首先,在kernel.exe进入保护模式时,开启分页,具体的分页策略是:物理地址和线性地址一一对应的关系。第二步,在进程创建时,复 制页目录表,但是只包括系统空间的部分页表,其他页表设置为0,同时调用全局的内存分配程序(采用slab算法,分配的是物理地址而不是线性地址),分配的内存大小页面对齐,然后修改进程的页表项,并将其映射为从4gb依次往低地址的递减的虚拟线性地址。
    此种分页下,系统进程的线性地址都是物理地址,并且可以被每个用户进程访问,但是应用程序的代码段、数据段、堆栈段地址都是线性递减的。

    liunuxos虽然支持虚拟内存,但是不支持内存交换等功能。

    liunuxos具体的内存地址分布可以查看def.h文件。

  4. 键盘鼠标功能。支持键盘输入,鼠标左键右键点击等。此部分内容自行百度。

  5. soundblaster声卡播放wav格式音乐。虚拟机创建时必须指定声卡为soundblaster型。

  6. x86断点,单步调试和调试寄存器的使用。

  7. 基本的图形操作接口。窗口控件、矩形、圆、点、线的绘制和填充等简单图像化接口。

  8. 浮点中断的简单处理。

  9. elf文件和pe文件的加载执行。关于这部分,网上资料也很多,可自行百度。

main.dll

  1. 试图实现一个类似于windows cmd的shell环境,支持几十条命令的执行,详细内容可看代码。

  2. 本模块试图实现类似于windows
    explorer.exe的桌面程序,支持右键菜单和简单的窗口管理。该模块中还有一个比较复杂的功能:图形化文件管理器。通过鼠标双击可以层次化的浏览和打开磁盘中的文件夹,以及文本文件和bmp文件等简单文件。

自己开发X86操作系统--LIUNUXOS

在os开发过程中,我感受最深的就是,自己对硬件设备的了解非常少,虽然可供驱动的设备很多,但是硬件设备知识的细节过多,操作过程过于繁琐,而且对电气特性不够了解,这是os开发中的最大障碍。