上一个v0.0.11的版本实现了execve系统调用,并且可以通过该系统调用来加载ELF可执行格式的文件(例如ram disk里的cpuid程式),所以当前的v1.0.0的版本就可以创建出各种ELF可执行文件出来,例如,shell(命令行程式),ls(可以显示出ram disk里包含的文件列表的程式)等等...

    v1.0.0的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
   
    Dropbox地址:点此进入Dropbox网盘  ,该版本位于zenglOX_v1.0.0的文件夹,该文件夹里的zenglOX_v1.0.0.zip的压缩包为源代码,zenglOX.iso为测试用的镜像(可以放在VirtualBox之类的虚拟机里进行测试),zenglOX.bin,initrd.img,grub4dos-0.4.4.zip,grubinst-1.1-bin-w32-2008-01-01.zip以及menu.lst这5个文件可以用于将U盘设置为启动盘,并且可以从U盘里启动zenglOX,这样就可以在真实的电脑里进行测试了,如何将U盘设置为启动盘的方法会在稍后进行介绍。

    sourceforge地址:https://sourceforge.net/projects/zenglox/files  (该版本位于zenglOX_v1.0.0的文件夹,该文件夹里包含了和上面Dropbox里一样的7个文件)

    有关zenglOX的源码编译及gdb调试的方法,请参考zenglOX v0.0.1里的文章。

    上一个v0.0.11的版本实现了execve系统调用,并且可以通过该系统调用来加载ELF可执行格式的文件(例如ram disk里的cpuid程式),所以当前的v1.0.0的版本就可以创建出各种ELF可执行文件出来,例如,shell(命令行程式),ls(可以显示出ram disk里包含的文件列表的程式)等等。

    我们可以先将Dropbox或sourceforge里的zenglOX.iso镜像直接挂接到VirtualBox里(或者别的类似的虚拟机软件里),来简单的查看下v1.0.0版本中各种工具的基本使用方法:

图1

    先如上图所示,在VirtualBox里新建一个虚拟电脑,名称填写zenglOX(也可以自定义别的名字),类型选择Other,版本也是Other,然后选择下一步:
 
图2

    内存保持默认设置的大小,我这里的默认大小为64M,在zenglOX实际运行的过程中,目前只会用到16M的内存,所以如果你要自定义内存大小的话,自定义的大小不要低于16M。

    选择好内存大小后,点击下一步按钮:
 
图3

    虚拟硬盘界面选择"现在创建虚拟硬盘",然后点击创建按钮,进入下一步:
 
图4

    在上图显示的虚拟硬盘文件类型界面,选择默认的VDI类型,目前zenglOX还用不到虚拟硬盘,因为所有数据都存储在ram disk里,选择好虚拟硬盘文件类型后,继续点击下一步按钮:
 
图5

    在上图显示的界面里选择默认的动态分配,然后继续进入下一步:
 
图6
 
    在图6所示的界面,选择虚拟硬盘文件需要存储到哪个位置,我这里选择的是"F:\zenglOX\zenglOX.vdi",虚拟硬盘大小则保持默认的大小即可,然后点击该界面的创建按钮,这样,VirtualBox就会创建一个名为zenglOX的虚拟电脑了。

    接下来,我们就需要将zenglOX.iso的镜像文件挂载到zenglOX的虚拟电脑上:
 
图7
 
    右键单击zenglOX虚拟电脑的图标,在弹出菜单里选择第一项设置,进入下面的设置界面:
 
图8
 
    在设置界面,先点击左侧的存储按钮,然后在右侧的存储界面,一开始没有盘片,这样我们就需要选择一个ISO镜像作为虚拟光驱里的盘片:
 
图9
   
   
图10
 
    根据上面两图,选择你的zenglOX.iso的文件位置,选择好后,设置界面就可以看到该镜像文件了:
 
图11
 
    设置好ISO镜像文件后,就可以点击设置界面的确定按钮来结束设置,接着就可以启动zenglOX了:
 
图12
 
    启动后,就可以看到grub的引导画面:
 
图13
 
    和v0.0.11版的区别在于,v1.0.0版本的grub多了一个5秒的超时时间,这样如果什么键都不按的情况下,过5秒就会自动进入zenglOX了,当然也可以手动按回车键进入zenglOX(不过有时候按键在grub下偶尔失灵的时候,超时时间就会很有用了)。

    进入zenglOX后,会先显示出一些简单的内核信息,然后就会显示出shell的命令提示符了:
 
图14
 
    上面显示出了zenglOX的版本号信息,然后通过"you can input some command ...."的一串英文告诉用户,目前v1.0.0版本可以输入一些像ls,ps,cat之类的命令,虽然这些命令看起来和Linux里的命令很像,但也只是名字差不多而已,至于命令的显示结果,参数,以及实现的源代码都是完全不一样的,因为zenglOX的系统调用与Linux的完全不一样,所有的工具都必须重头写一遍。

    下面我们就来看下ls程式和ps程式的用法:
 
图15
 
    ls程式会将ram disk里存储的文件的文件名都显示出来,ls程式目前还没有任何附加的参数,从上图ls程式输出的结果可以看到,当前的ram disk里一共有9个文件,licence.txt是普通的文本文件(非ELF格式,不能在shell里直接运行,但是可以通过cat程式来显示其内容),其余的cpuid,uname之类的都是可以直接在shell里运行的ELF可执行文件。

    ps程式在不使用任何参数的情况下,可以将当前正在运行的任务按顺序给显示出来,内核在启动时,会先创建一个system first task即系统的第一个任务,该任务没有任何参数信息,所以上图显示的ID为1的任务对应的args为空字符串,在内核创建了第一个任务后,该任务会通过execve系统调用创建一个shell任务,就是上图ID为2的任务,该任务会读取ram disk里的shell文件,并将该文件的代码段拷贝到ELF格式指定的虚拟内存位置,并转到该内存位置去执行shell程式,上图里ID为3的任务,就是用户在shell中输入的ps程式对应的任务。

    另外,在ps显示的任务列表里,还有个status字段,该字段表示任务的状态,目前任务一共有4种可能的状态:wait(等待状态),running(正在运行状态),finish(结束状态)以及zombie(僵死状态)。最后还有一个ParentID字段,表示该任务的父任务的ID值,内核的第一个任务不存在父任务,所以第一个任务就没有ParentID字段。

    由于采用了任务ID的重利用机制,所以任务的ID号是可以重利用的,例如ps程式执行结束后,下一个将要执行的任务就可以使用之前ps使用过的ID号。

    ps程式目前只有一个可用的参数即"-m",当提供-m参数时,就会看到上图所示的内存信息,其中,total memory表示总共可用的内存大小,目前版本一共可用的内存大小是16M,即便你在虚拟机里分配再多的内存,目前也只会使用16M,在真实机子里也是一样的,不管真实机子的实际物理内存是多少,都只会使用16M。

    usage memory表示已使用的物理内存的大小,已使用的物理内存包括ram disk在内。

    free memory表示剩余的可供分配的物理内存的大小,当剩余物理内存为0时,再需要分配内存时,内核就会显示出错误信息,并让电脑陷入无限循环。

    kheap部分显示的是已分配的内核堆的大小等信息,现阶段,用户态程式与内核是共用一个大的内核堆的,但并不表示用户态程式可以随便修改内核堆里的内存数据,用户态程式只有通过syscall_umalloc系统调用分配到的堆内存才可以进行写入操作,对其余的内核堆部分进行写入操作会产生分页错误。

    kheap hole number表示内核堆里的hole数目,有关hole的作用请参考之前的zenglOX v0.0.6 使用Heap(堆)动态分配和释放内存里的文章。

    下面我们再来看下cpuid,uname和cat程式的用法:
 
图16
 
    cpuid程式在上一节已经讲解过,就是将处理器的供应商ID字符串信息给显示出来,如果是Intel处理器,则会显示"GenuineIntel",我的台式机是AMD处理器,所以VirtualBox里显示的就是"AuthenticAMD"。

    uname程式可以显示出当前zenglOX的版本号信息,如上图显示的"zenglOX v1.0.0"。

    cat程式则可以将某文件的内容输出显示到屏幕上。例如上图的cat licence.txt命令就将licence.txt文件里的内容给显示了出来。licence.txt是一个简单的许可文件,里面声明了zenglOX是一个基于GPLV3的自由软件程式,然后还有些作者信息,官网信息。

    在shell里还可以再运行shell,如下图所示:
 
图17
 
    上图在ls命令后面,又输入了一个shell secondshell的命令,从ps的输出可以看到,当前系统就有两个shell在运行了,一个是初始任务创建的ID为2的不带任何参数的shell,另一个就是用户创建的带了一个secondshell参数的ID为3的shell 。

    另外,通过ps -m命令也可以看到,usage memory也在原来的基础上增加了12K字节,这是因为新创建的shell任务会在内存里分配新的物理内存来存放自己的程式,同时新任务也会有自己独立的用户栈和内核栈,这些都需要进行内存分配操作,还可以看到kheap hole number数目也增加了,这是因为新创建的任务在堆里分配block时,block会对hole产生一定的拆分作用。

    当使用exit命令(该命令是shell里内建的命令,并非ram disk里的ELF可执行文件)退出当前的shell时,内存的使用情况又会还原到原来的状态:
 
图18
 
    在exit退出用户创建的shell后,ps显示的任务列表和ps -m显示的内存使用情况又回到最开始的状态了。

    最后,我们还可以使用reboot来重启系统,或者使用shutdown来直接关机,不过shutdown关机由于涉及到ACPI(Advanced Configuration and Power Management Interface即高级配置和电源管理接口)等东东,ACPI本身就是一个复杂庞大的东东,不同的主板都不太一样,还涉及到主板驱动问题,所以目前还只能在bochs或virtualBox之类的虚拟机里起作用,在真实的电脑上是不会起作用的,reboot程式则在虚拟机和真实机子上都可以正常执行。

   下面我们来看下,如何利用grub4dos和grubinst来将U盘做成启动盘,同时可以通过U盘来启动zenglOX。

[zengl pagebreak]

    先将U盘插入电脑,然后将Dropbox或sourceforge里下载下来的grubinst-1.1-bin-w32-2008-01-01.zip进行解压,解压后得到一个grubinst-1.1-bin-w32-2008-01-01目录,该目录内的文件如下图所示:
 

图19

    将U盘做成启动盘,主要需要用到上图显示的grubinst_gui.exe程序,运行该程序(Win7下请使用管理员权限来运行):
 

图20

    在该程序里,磁盘列表中需要选择U盘对应的磁盘(可以通过磁盘大小来进行判断),分区列表中则选择"整个磁盘(MBR)",选项部分,勾选"不保存原来MBR","启动时不搜索软盘"以及"不引导原来MBR"这三个选项。

    然后点击安装按钮,就会弹出一个命令提示框:
 

图21

    上面提示MBR安装成功,这样,U盘就可以引导grub4dos了,接下来就需要将grub4dos相关的文件放入U盘根目录,将Dropbox或sourceforge下载下来的grub4dos-0.4.4.zip进行解压,得到的grub4dos-0.4.4目录中的文件如下:
 

图22

    我们只需要里面的grldr文件,将该文件放入U盘根目录,再将Dropbox或sourceforge上下载下来的menu.lst,zenglOX.bin以及initrd.img这三个文件也放入U盘根目录,这样U盘里的文件情况如下:
 

图23

    此时的U盘就变为启动盘了,并且可以通过grub4dos来引导zenglOX了,这里U盘使用的menu.lst文件和grub4dos自带的menu.lst文件的内容是不同的,Dropbox下载下来的供U盘使用的menu.lst文件的内容如下:

# This is a sample menu.lst file. You should make some changes to it.
# The old install method of booting via the stage-files has been removed.
# Please install GRLDR boot strap code to MBR with the bootlace.com
# utility under DOS/Win9x or Linux.


color blue/green yellow/red white/magenta white/magenta
timeout 10
default /default

default 0
timeout 10
#zenglOX
title zenglOX
root (hd0,0)
kernel --type=multiboot /zenglOX.bin
module /initrd.img

title reboot
reboot

title halt
halt
 

    在该文件里,通过kernel --type=multiboot /zenglOX.bin选项将U盘根目录中的zenglOX.bin设置为多启动标准的内核(这样只要选择了zenglOX的启动项就可以加载zenglOX.bin到内存里),并通过module /initrd.img选项将initrd.img设置为多启动内核的模块文件(作为zenglOX的ram disk),grub在加载zenglOX.bin到内存后,还会将initrd.img加载到内存中。

    设置好U盘后,重启电脑,在BIOS里将U盘设置为第一启动项,这样就可以从U盘启动系统,U盘启动时的grub界面如下:


图24

    直接按回车键,或者等待10秒超时,就可以看到zenglOX v1.0.0的命令行界面了,命令行界面前面已经讲解过,这里就不再说了,在真机上,除了shutdown程式不能用外,其余的程式,包括reboot重启程式都可以正常使用。

    v1.0.0版本的源代码主要是围绕shell之类的ELF可执行程式的需求来添加系统调用,shell之类的程式的源代码都存储在build_initrd_img目录内:
 

图25

    上图显示的shell.c是shell程式的源代码文件,ls.c是ls程式的源代码文件,等等。

    在之前的zenglOX v0.0.1的文章里,介绍过bochs加gdb的调试方法,默认情况下,使用gdb -q zenglOX.bin来调试时,gdb只会加载zenglOX.bin里的内核调试信息,如果想调试shell之类的程式的话,可以在gdb中使用add-symbol-file命令:
 

图26

    上图里通过add-symbol-file build_initrd_img/shell 0x8048054命令将shell程式的调试信息与线性地址0x8048054对应起来,0x8048054是shell的ELF格式里定义的.text代码段的起始线性地址,可以通过readelf命令来查看到:

zenglOX_v1.0.0/build_initrd_img$ readelf -a shell
........................................
........................................
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        08048054 000054 000849 00  AX  0   0  4
  [ 2] .rodata           PROGBITS        0804889d 00089d 00001e 00   A  0   0  1
  [ 3] .eh_frame         PROGBITS        080488bc 0008bc 000518 00   A  0   0  4
........................................
........................................


    使用add-symbol-file添加调试信息后,还需要指定shell.c之类的源代码文件的位置,可以通过上面图26所示的dir build_initrd_img/命令来指定这些程式的源代码所在的目录位置。

    添加了调试信息和添加了源代码位置后,就可以通过b命令在shell.c源代码文件中下断点了,在gdb中就可以同时调试zenglOX的内核代码与shell之类的用户态程式了。

    在开发v1.0.0版本时,遇到的最大的麻烦就是TLB(translation lookaside buffer),这是CPU对页目录,页表的一种缓存机制,也就是说当你修改了页目录及页表里的内容时,这些修改并不一定会生效,因为CPU只会查看缓存的页目录和页表信息,必须通过重新写入CR3控制寄存器的方式,才能更新这种缓存,另外,在更新TLB缓存时,很有可能会清空页表项对应的物理内存里的内容,所以在修改了页目录和页表里的内容后,必须先更新TLB缓存,然后再对内存进行写入操作,如果先写入数据,再更新TLB缓存的话,就很有可能会被处理器清空掉之前写入的数据,所以,在写内核代码时,如果不考虑TLB的因素的话,调试时,即便有gdb调试器,也很难找出BUG的真正原因。

    所以在zlox_elf.c文件里,zlox_load_elf函数在修改了页目录及页表信息后,就会通过一段内联汇编代码来刷新TLB:

ZLOX_UINT32 zlox_load_elf(ZLOX_ELF32_EHDR *hdr,ZLOX_UINT8 * buf)
{
..................................................
..................................................
	ZLOX_PAGE * tmp_page;
	ZLOX_UINT32 table_idx;
	for(i = 0; i < hdr->e_shnum; i++)
	{
		if(shdr[i].sh_type == ZLOX_SHT_PROGBITS)
		{
			tmpSheader = &shdr[i];
			if(tmpSheader->sh_addr == ZLOX_NULL)
				continue;
			for( j = tmpSheader->sh_addr;
				j < (tmpSheader->sh_addr + tmpSheader->sh_size);
				(j = ((j + 0x1000) & 0xFFFFF000)))
			{
				tmp_page = zlox_get_page(j, 1, current_directory);
				table_idx = (j/0x1000)/ 1024;
				if(tmp_page->frame == 0)
					zlox_alloc_frame( tmp_page, 0 , 1 );
				else if((current_directory->tablesPhysical[table_idx] & 0x2) == 0)
					zlox_page_copy(j);
			}
		}
	}

	// Flush the TLB(translation lookaside buffer) by reading and writing the page directory address again.
	ZLOX_UINT32 pd_addr;
	asm volatile("mov %%cr3, %0" : "=r" (pd_addr));
	asm volatile("mov %0, %%cr3" : : "r" (pd_addr));

	for(i = 0; i < hdr->e_shnum; i++)
	{
		if(shdr[i].sh_type == ZLOX_SHT_PROGBITS)
		{
			tmpSheader = &shdr[i];
			if(tmpSheader->sh_addr == ZLOX_NULL)
				continue;

			zlox_memcpy((ZLOX_UINT8 *)tmpSheader->sh_addr,
					(ZLOX_UINT8 *)(buf + tmpSheader->sh_offset),tmpSheader->sh_size);
		}
	}
..................................................
..................................................
}


    上面代码里,在为ELF可执行文件分配物理内存和修改对应的页表信息后,就通过asm volatile("mov %%cr3, %0" : "=r" (pd_addr));和asm volatile("mov %0, %%cr3" : : "r" (pd_addr));这两条内联汇编代码来刷新TLB,这两条代码就是将CR3里的数据读取出来,再写回去,在写入的过程中,就可以刷新TLB了。接着就可以对所分配的内存进行写入操作了。

    在zlox_kheap.c文件中的zlox_umalloc与zlox_ufree函数里,因为修改了页表项里的字段,所以也需要刷新TLB:

// 给用户态程式使用的分配堆函数
ZLOX_UINT32 zlox_umalloc(ZLOX_UINT32 sz)
{
	ZLOX_PAGE * page;
	ZLOX_UINT32 ret = zlox_kmalloc_int(sz, 0, 0);
	ZLOX_UINT32 i;

	for(i=ret; i < ret + sz ;i+=0x1000)
	{
		page = zlox_get_page(i, 0, kernel_directory);
		if(page->rw == 0)
			page->rw = 1;
	}

	// Flush the TLB(translation lookaside buffer) by reading and writing the page directory address again.
	ZLOX_UINT32 pd_addr;
	asm volatile("mov %%cr3, %0" : "=r" (pd_addr));
	asm volatile("mov %0, %%cr3" : : "r" (pd_addr));

	return ret;
}

// 给用户态程式使用的释放堆函数
ZLOX_VOID zlox_ufree(ZLOX_VOID *p)
{
	ZLOX_PAGE * page;
	ZLOX_KHP_HEADER * header = (ZLOX_KHP_HEADER *)((ZLOX_UINT32)p - sizeof(ZLOX_KHP_HEADER));
	ZLOX_UINT32 sz = header->size - sizeof(ZLOX_KHP_HEADER) - sizeof(ZLOX_KHP_FOOTER);
	ZLOX_UINT32 i;
	for(i=(ZLOX_UINT32)p; i < (ZLOX_UINT32)p + sz ;i+=0x1000)
	{
		page = zlox_get_page(i, 0, kernel_directory);
		if(page->rw == 1)
			page->rw = 0;
	}

	// Flush the TLB(translation lookaside buffer) by reading and writing the page directory address again.
	ZLOX_UINT32 pd_addr;
	asm volatile("mov %%cr3, %0" : "=r" (pd_addr));
	asm volatile("mov %0, %%cr3" : : "r" (pd_addr));

	zlox_free(p, kheap);
}


    上面的函数在修改了页表项里的rw(读写字段)后,需要通过同样的读写CR3控制寄存器的方式来刷新TLB,在bochs下,上面两个函数不刷新TLB也可以运行,不过在virtualBox中不刷新TLB的话,就会出现用户态程式对只读内存进行写入操作的分页错误,因为TLB缓存中对应页表里的读写字段并没改变(至少在virtualBox里是这样的),所以保险起见,还是在修改了页目录或页表里的项目后,就对TLB进行刷新操作。

当前版本可用的系统调用:

[zengl pagebreak]

当前版本可用的系统调用:

    你可以在zlox_syscall.c里查看到当前可用的系统调用如下:

ZLOX_DEFN_SYSCALL1(monitor_write, ZLOX_SYSCALL_MONITOR_WRITE, const char*);
ZLOX_DEFN_SYSCALL1(monitor_write_hex, ZLOX_SYSCALL_MONITOR_WRITE_HEX, ZLOX_UINT32);
ZLOX_DEFN_SYSCALL1(monitor_write_dec, ZLOX_SYSCALL_MONITOR_WRITE_DEC, ZLOX_UINT32);
ZLOX_DEFN_SYSCALL1(monitor_put, ZLOX_SYSCALL_MONITOR_PUT, char);
ZLOX_DEFN_SYSCALL1(execve, ZLOX_SYSCALL_EXECVE, const char*);
ZLOX_DEFN_SYSCALL3(get_tskmsg,ZLOX_SYSCALL_GET_TSKMSG,void *,void *,ZLOX_BOOL);
ZLOX_DEFN_SYSCALL1(wait, ZLOX_SYSCALL_WAIT, void *);
ZLOX_DEFN_SYSCALL1(set_input_focus, ZLOX_SYSCALL_SET_INPUT_FOCUS, void *);
ZLOX_DEFN_SYSCALL0(get_currentTask,ZLOX_SYSCALL_GET_CURRENT_TASK);
ZLOX_DEFN_SYSCALL1(exit, ZLOX_SYSCALL_EXIT, int);
ZLOX_DEFN_SYSCALL1(finish, ZLOX_SYSCALL_FINISH, void *);
ZLOX_DEFN_SYSCALL1(get_args, ZLOX_SYSCALL_GET_ARGS, void *);
ZLOX_DEFN_SYSCALL1(get_init_esp, ZLOX_SYSCALL_GET_INIT_ESP, void *);
ZLOX_DEFN_SYSCALL1(umalloc, ZLOX_SYSCALL_UMALLOC, int);
ZLOX_DEFN_SYSCALL1(ufree, ZLOX_SYSCALL_UFREE, void *);
ZLOX_DEFN_SYSCALL4(read_fs,ZLOX_SYSCALL_READ_FS,void *,int,int,void *);
ZLOX_DEFN_SYSCALL2(readdir_fs,ZLOX_SYSCALL_READDIR_FS,void *,int);
ZLOX_DEFN_SYSCALL2(finddir_fs,ZLOX_SYSCALL_FINDDIR_FS,void *,void *);
ZLOX_DEFN_SYSCALL0(get_fs_root,ZLOX_SYSCALL_GET_FS_ROOT);
ZLOX_DEFN_SYSCALL2(get_frame_info,ZLOX_SYSCALL_GET_FRAME_INFO,void **,void *);
ZLOX_DEFN_SYSCALL0(get_kheap,ZLOX_SYSCALL_GET_KHEAP);
ZLOX_DEFN_SYSCALL3(get_version,ZLOX_SYSCALL_GET_VERSION,void *,void *,void *);
ZLOX_DEFN_SYSCALL0(reboot,ZLOX_SYSCALL_REBOOT);
ZLOX_DEFN_SYSCALL0(shutdown,ZLOX_SYSCALL_SHUTDOWN);

static ZLOX_VOID * syscalls[ZLOX_SYSCALL_NUMBER] =
{
	&zlox_monitor_write,
	&zlox_monitor_write_hex,
	&zlox_monitor_write_dec,
	&zlox_monitor_put,
	&zlox_execve,
	&zlox_get_tskmsg,
	&zlox_wait,
	&zlox_set_input_focus,
	&zlox_get_currentTask,
	&zlox_exit,
	&zlox_finish,
	&zlox_get_args,
	&zlox_get_init_esp,
	&zlox_umalloc,
	&zlox_ufree,
	&zlox_read_fs,
	&zlox_readdir_fs,
	&zlox_finddir_fs,
	&zlox_get_fs_root,
	&zlox_get_frame_info,
	&zlox_get_kheap,
	&zlox_get_version,
	&_zlox_reboot,
	&_zlox_shutdown,
};


    上面一共包含了24个系统调用,ZLOX_DEFN_SYSCALL的宏展开后,会得到一个zlox_syscall_开头的系统调用函数的定义,例如:ZLOX_DEFN_SYSCALL1(monitor_write, ZLOX_SYSCALL_MONITOR_WRITE, const char*);的宏展开后就是:

ZLOX_SINT32 zlox_syscall_monitor_write(const char* p1) \
{ \
  ZLOX_SINT32 a; \
  asm volatile("int $0x80" : "=a" (a) : "0" (0), "b" ((ZLOX_SINT32)p1)); \
  return a; \
}


    也就是定义了一个zlox_syscall_monitor_write的函数,内核代码在进入用户态模式后,就可以通过zlox_syscall_monitor_write函数,在int $0x80中断指令下,进入内核态,最后会通过zlox_syscall_handler函数和syscalls数组,调用对应的zlox_monitor_write内核函数,从而将字符串显示到屏幕上。

    在zlox_kernel.c文件里,当内核切换到用户态后,就必须通过系统调用函数来访问内核里的功能:

//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_MULTIBOOT * mboot_ptr, ZLOX_UINT32 initial_stack)
{
..........................................
..........................................
	// 切换到ring 3的用户模式
	zlox_switch_to_user_mode();

	zlox_syscall_monitor_write("I'm in user mode!\n");

	zlox_syscall_monitor_write("welcome to zenglOX v");
	ZLOX_SINT32 major,minor,revision;
	zlox_syscall_get_version(&major,&minor,&revision);
	zlox_syscall_monitor_write_dec(major);
	zlox_syscall_monitor_put('.');
	zlox_syscall_monitor_write_dec(minor);
	zlox_syscall_monitor_put('.');
	zlox_syscall_monitor_write_dec(revision);
	zlox_syscall_monitor_write("! I will execve a shell\n"
					"you can input some command: ls , ps , cat , uname , cpuid , shell , reboot ...\n");

	zlox_syscall_execve("shell");
	//zlox_syscall_execve("cpuid");

	zlox_syscall_wait(current_task);

	ZLOX_SINT32 ret;
	ZLOX_TASK_MSG msg;
	while(ZLOX_TRUE)
	{
		ret = zlox_syscall_get_tskmsg(current_task,&msg,ZLOX_TRUE);
		if(ret != 1)
		{
			zlox_syscall_wait(current_task);
			// 只剩下一个初始任务了,就再创建一个shell
			if(current_task->next == 0)
			{
				zlox_syscall_execve("shell");
				zlox_syscall_wait(current_task);
			}
		}
		else if(msg.type == ZLOX_MT_TASK_FINISH)
		{
			zlox_syscall_finish(msg.finish_task.exit_task);
		}
	}

	return 0;
}


    上面的zlox_kernel_main函数在通过zlox_switch_to_user_mode()函数切换到用户态后,后面的代码就都必须使用zlox_syscall_开头的系统调用函数来访问所需的功能了。

    上面涉及到的系统调用里,zlox_syscall_monitor_write和zlox_syscall_execve已经讲解过了,zlox_syscall_get_version系统调用则是用于获取zenglOX当前的版本号信息的,在zlox_syscall.h头文件里定义了内核的主次版本号信息:

// zlox_syscall.h Defines the interface for and structures relating to the syscall dispatch system.

#ifndef _ZLOX_SYSCALL_H_
#define _ZLOX_SYSCALL_H_

#include "zlox_common.h"

#define ZLOX_SYSCALL_NUMBER 24

#define ZLOX_MAJOR_VERSION 1 //zenglOX 主版本号
#define ZLOX_MINOR_VERSION 0 //zenglOX 子版本号
#define ZLOX_REVISION 0      //zenglOX 修正版本号
..........................................
..........................................


    上面定义了三个宏:ZLOX_MAJOR_VERSION对应zenglOX的主版本号,ZLOX_MINOR_VERSION对应zenglOX的子版本号,ZLOX_REVISION对应zenglOX的修正版本号。

    zlox_syscall_get_version系统调用最后会调用zlox_get_version函数(位于zlox_syscall.c文件里)来获取到上面的三个版本号信息:

ZLOX_SINT32 zlox_get_version(ZLOX_SINT32 * major, ZLOX_SINT32 * minor, ZLOX_SINT32 * revision)
{
	*major = ZLOX_MAJOR_VERSION;
	*minor = ZLOX_MINOR_VERSION;
	*revision = ZLOX_REVISION;
	return 0;
}


    该函数会将三个版本号写入到参数对应的整数里。

    上面的zlox_kernel_main函数在调用zlox_syscall_execve("shell");加载执行shell命令行程式后,就会通过zlox_syscall_wait(current_task);系统调用进入wait(等待状态)。

    zlox_syscall_wait系统调用最终会调用zlox_task.c里的zlox_wait函数来设置任务的状态:

// 将task任务设置为wait等待状态
ZLOX_SINT32 zlox_wait(ZLOX_TASK * task)
{
	// 将任务状态设置为等待状态
	task->status = ZLOX_TS_WAIT;
	// 如果task是当前任务,则进行任务切换
	if(task == current_task)
		zlox_switch_task();
	return 0;
}


    zlox_wait函数会将task任务的status(状态)设置为ZLOX_TS_WAIT,表示当前任务处于等待状态,这样在任务调度时,就不会切换到该任务,只有当task的子任务结束时,子任务才会通过zlox_syscall_exit系统调用来将父任务task给唤醒,zlox_syscall_exit系统调用最终会由zlox_task.c里的zlox_exit函数来完成相关功能:

// 结束当前任务,并向父任务或首任务发送结束消息
ZLOX_SINT32 zlox_exit(ZLOX_SINT32 exit_code)
{
	ZLOX_TASK * parent_task = current_task->parent;
	ZLOX_TASK * notify_task = parent_task;
	ZLOX_TASK_MSG ascii_msg = {0};
	if(parent_task == 0)
		return -1;

	// 如果父任务处于wait等待状态,则将其唤醒,并发送结束消息
	if(parent_task->status == ZLOX_TS_WAIT)
		parent_task->status = ZLOX_TS_RUNNING;
	// 如果父任务已经结束了,则向第一个任务发送结束消息
	else if(parent_task->status == ZLOX_TS_FINISH)
	{
		if(ready_queue->status == ZLOX_TS_WAIT)
			ready_queue->status = ZLOX_TS_RUNNING;
		else if(ready_queue->status == ZLOX_TS_FINISH)
		{
			current_task->status = ZLOX_TS_ZOMBIE; // 如果没有任务可以进行通知的话,则当前任务就变为僵死任务
			return -1;
		}
		notify_task = (ZLOX_TASK *)ready_queue;
	}

	current_task->status = ZLOX_TS_FINISH;
	ascii_msg.type = ZLOX_MT_TASK_FINISH;
	ascii_msg.finish_task.exit_task = (ZLOX_TASK *)current_task;
	ascii_msg.finish_task.exit_code = exit_code;
	zlox_send_tskmsg(notify_task,&ascii_msg);
	zlox_switch_task();
	return 0;
}


    zlox_exit函数里会将parent_task(父任务)的状态重新设置为ZLOX_TS_RUNNING即运行状态,这样在进行任务调度时,就可以切换到父任务去执行了,除了要将父任务唤醒外,还需要通过zlox_send_tskmsg函数向父任务发送一个ZLOX_MT_TASK_FINISH消息(即任务结束消息),在该消息的exit_task字段存储了需要结束的任务结构体的指针值,在消息的exit_code字段,则存储了任务结束时的退出码。

    每个任务都有一个消息队列,与消息队列相关的结构体定义在zlox_task.h头文件中:

typedef enum _ZLOX_MSG_TYPE
{
	ZLOX_MT_KEYBOARD,
	ZLOX_MT_TASK_FINISH,
}ZLOX_MSG_TYPE;

typedef enum _ZLOX_MSG_KB_TYPE
{
	ZLOX_MKT_ASCII,
}ZLOX_MSG_KB_TYPE;

typedef enum _ZLOX_TSK_STATUS
{
	ZLOX_TS_WAIT,
	ZLOX_TS_RUNNING,
	ZLOX_TS_FINISH,
	ZLOX_TS_ZOMBIE,
}ZLOX_TSK_STATUS;

typedef struct _ZLOX_TASK_MSG_KEYBOARD
{
	ZLOX_MSG_KB_TYPE type;
	ZLOX_UINT32 ascii;
}ZLOX_TASK_MSG_KEYBOARD;

typedef struct _ZLOX_TASK ZLOX_TASK;

typedef struct _ZLOX_TASK_MSG_FINISH
{
	ZLOX_TASK * exit_task;
	ZLOX_SINT32 exit_code;
}ZLOX_TASK_MSG_FINISH;

typedef struct _ZLOX_TASK_MSG
{
	ZLOX_MSG_TYPE type;
	ZLOX_TASK_MSG_KEYBOARD keyboard;
	ZLOX_TASK_MSG_FINISH finish_task; // 消息中存储的结束任务的相关信息
}ZLOX_TASK_MSG;

typedef struct _ZLOX_TASK_MSG_LIST
{
	ZLOX_BOOL isInit;
	ZLOX_SINT32 count;
	ZLOX_SINT32 size;
	ZLOX_SINT32 cur;
	ZLOX_SINT32 finish_task_num;
	ZLOX_TASK_MSG * ptr;
}ZLOX_TASK_MSG_LIST;

// This structure defines a 'task' - a process.
struct _ZLOX_TASK
{
	ZLOX_UINT32 sign; // Process sign
	ZLOX_SINT32 id; // Process ID.
	ZLOX_UINT32 esp, ebp; // Stack and base pointers.
	ZLOX_UINT32 eip; // Instruction pointer.
	ZLOX_UINT32 init_esp; // stack top
	ZLOX_PAGE_DIRECTORY * page_directory; // Page directory.
	ZLOX_UINT32 kernel_stack;   // Kernel stack location.
	ZLOX_TASK_MSG_LIST msglist; // task message.
	ZLOX_TSK_STATUS status;	// task status.
	ZLOX_CHAR * args;	// task args
	ZLOX_TASK * next; // The next task in a linked list.
	ZLOX_TASK * prev; // the prev task
	ZLOX_TASK * parent; // parent task
};


    消息队列是一个ZLOX_TASK_MSG_LIST类型的结构体,该结构体管理着一个动态数组,动态数组的指针存储在ptr字段,数组里的每个元素都是一个ZLOX_TASK_MSG消息结构体,目前有两种消息,一种是键盘中断时,发送过来的ASCII码消息(表示按下了哪个键),一种则是ZLOX_MT_TASK_FINISH即任务结束消息,当收到任务结束消息时,当前任务就需要通过zlox_syscall_finish系统调用来将需要结束的任务给清理掉(例如:清理掉该任务的页表结构,释放该任务分配的物理内存,以及释放该任务在内核堆里分配到的block块等)。

    消息队列对应的动态数组是可以动态扩容的,无论是zlox_send_tskmsg发送消息函数,还是zlox_get_tskmsg获取消息函数,最后都会分别调用zlox_push_tskmsg和zlox_pop_tskmsg函数来完成消息队列的压入和弹出操作(这两个函数也都位于zlox_task.c文件里):

// 将消息压入消息列表,消息列表里的消息是采用的先进入的消息先处理的方式
ZLOX_SINT32 zlox_push_tskmsg(ZLOX_TASK_MSG_LIST * msglist , ZLOX_TASK_MSG * msg)
{
	if(!msglist->isInit) // 如果没进行过初始化,则初始化消息列表
	{
		msglist->size = ZLOX_TSK_MSGLIST_SIZE;
		msglist->ptr = (ZLOX_TASK_MSG *)zlox_kmalloc(msglist->size * sizeof(ZLOX_TASK_MSG));
		zlox_memset((ZLOX_UINT8 *)msglist->ptr,0,msglist->size * sizeof(ZLOX_TASK_MSG));		
		msglist->count = 0;
		msglist->cur = 0;
		msglist->isInit = ZLOX_TRUE;
	}
	else if(msglist->count == msglist->size) // 如果消息列表数目达到了当前容量的上限,则对消息列表进行动态扩容
	{
		ZLOX_TASK_MSG * tmp_ptr;
		msglist->size += ZLOX_TSK_MSGLIST_SIZE;
		tmp_ptr = (ZLOX_TASK_MSG *)zlox_kmalloc(msglist->size * sizeof(ZLOX_TASK_MSG));
		zlox_memcpy((ZLOX_UINT8 *)(tmp_ptr + msglist->cur),(ZLOX_UINT8 *)(msglist->ptr + msglist->cur),
				(msglist->count - msglist->cur) * sizeof(ZLOX_TASK_MSG));
		if(msglist->cur > 0)
		{
			zlox_memcpy((ZLOX_UINT8 *)(tmp_ptr + msglist->count),(ZLOX_UINT8 *)msglist->ptr,
				msglist->cur * sizeof(ZLOX_TASK_MSG));
		}
		zlox_kfree(msglist->ptr);
		msglist->ptr = tmp_ptr;
	}
	
	ZLOX_UINT32 index = ((msglist->cur + msglist->count) < msglist->size) ? (msglist->cur + msglist->count) : 
				(msglist->cur + msglist->count - msglist->size);
	msglist->ptr[index] = *msg;
	msglist->count++;
	return 0;
}

// 获取消息列表中的消息,needPop参数表示是否需要将消息弹出消息列表
ZLOX_TASK_MSG * zlox_pop_tskmsg(ZLOX_TASK_MSG_LIST * msglist,ZLOX_BOOL needPop)
{
	ZLOX_TASK_MSG * ret;
	if(msglist->count <= 0)
		return ZLOX_NULL;

	ret = &msglist->ptr[msglist->cur];
	if(needPop)
	{
		msglist->cur = ((msglist->cur + 1) < msglist->size) ? (msglist->cur + 1) : 0;
		msglist->count = (msglist->count - 1) > 0 ? (msglist->count - 1) : 0;
	}
	return ret;
}


    这里需要注意的是,消息队列的工作方式与内存栈的工作方式是不一样的,内存栈里是后压入的数据会先弹出栈,而消息队列里则是先压入队列的消息会先弹出来,这样才能保证先进来的消息能被先处理掉。

    在build_initrd_img目录内也有一个syscall.c文件,里面有ram disk里的ELF可执行程式可以使用的系统调用函数的定义:

// syscall.c Defines the implementation of a system call system.

#include "syscall.h"

DEFN_SYSCALL1(monitor_write, SYSCALL_MONITOR_WRITE, const char*);
DEFN_SYSCALL1(monitor_write_hex, SYSCALL_MONITOR_WRITE_HEX, UINT32);
DEFN_SYSCALL1(monitor_write_dec, SYSCALL_MONITOR_WRITE_DEC, UINT32);
DEFN_SYSCALL1(monitor_put, SYSCALL_MONITOR_PUT, char);
DEFN_SYSCALL1(execve, SYSCALL_EXECVE, const char*);
DEFN_SYSCALL3(get_tskmsg,SYSCALL_GET_TSKMSG,void *,void *,BOOL);
DEFN_SYSCALL1(wait, SYSCALL_WAIT, void *);
DEFN_SYSCALL1(set_input_focus, SYSCALL_SET_INPUT_FOCUS, void *);
DEFN_SYSCALL0(get_currentTask,SYSCALL_GET_CURRENT_TASK);
DEFN_SYSCALL1(exit, SYSCALL_EXIT, int);
DEFN_SYSCALL1(finish, SYSCALL_FINISH, void *);
DEFN_SYSCALL1(get_args, SYSCALL_GET_ARGS, void *);
DEFN_SYSCALL1(get_init_esp, SYSCALL_GET_INIT_ESP, void *);
DEFN_SYSCALL1(umalloc, SYSCALL_UMALLOC, int);
DEFN_SYSCALL1(ufree, SYSCALL_UFREE, void *);
DEFN_SYSCALL4(read_fs,SYSCALL_READ_FS,void *,int,int,void *);
DEFN_SYSCALL2(readdir_fs,SYSCALL_READDIR_FS,void *,int);
DEFN_SYSCALL2(finddir_fs,SYSCALL_FINDDIR_FS,void *,void *);
DEFN_SYSCALL0(get_fs_root,SYSCALL_GET_FS_ROOT);
DEFN_SYSCALL2(get_frame_info,SYSCALL_GET_FRAME_INFO,void **,void *);
DEFN_SYSCALL0(get_kheap,SYSCALL_GET_KHEAP);
DEFN_SYSCALL3(get_version,SYSCALL_GET_VERSION,void *,void *,void *);
DEFN_SYSCALL0(reboot,SYSCALL_REBOOT);
DEFN_SYSCALL0(shutdown,SYSCALL_SHUTDOWN);


    上面的DEFN_SYSCALL1(monitor_write, SYSCALL_MONITOR_WRITE, const char*);宏展开后的情况如下:

SINT32 syscall_monitor_write(const char* p1) \
{ \
  SINT32 a; \
  asm volatile("int $0x80" : "=a" (a) : "0" (0), "b" ((SINT32)p1)); \
  return a; \
}


    可以看到,与内核使用的系统调用相比,只是函数名部分少了开头的zlox_的前缀而已,下面是build_initrd_img目录中shell.c文件里系统调用的使用情况:

// shell.c -- 命令行程式

#include "common.h"
#include "syscall.h"
#include "task.h"

#define MAX_INPUT 100

int main(VOID * task, int argc, char * argv[])
{
	int ret;
	int count = 0;
	char input[MAX_INPUT]={0};
	TASK_MSG msg;

	UNUSED(argc);
	UNUSED(argv);

	/*syscall_monitor_write("zenglOX> ");
	for(int i=0;i < argc;i++)
	{
		syscall_monitor_write(argv[i]);
		syscall_monitor_put(' ');
	}
	syscall_monitor_put('\n');*/

	/*char * test = (char *)syscall_umalloc(50);
	strcpy(test,"hello world\n");
	syscall_monitor_write(test);
	syscall_ufree(test);*/

	syscall_monitor_write("zenglOX> ");
	syscall_set_input_focus(task);
	while(TRUE)
	{
		ret = syscall_get_tskmsg(task,&msg,TRUE);
		if(ret != 1)
			continue;

		if(msg.type == MT_KEYBOARD)
		{
			if(msg.keyboard.type != MKT_ASCII)
				continue;

			if(msg.keyboard.ascii == '\n')
			{
				syscall_monitor_put('\n');
				if(strcmp(input,"exit") == 0)
					return 0;
				else if(syscall_execve(input) == 0)
					syscall_wait(task);
				count = 0;
				syscall_monitor_write("\nzenglOX> ");
				syscall_set_input_focus(task);
				continue;
			}
			else if(msg.keyboard.ascii == '\b')
			{
				if(count > 0)
				{
					syscall_monitor_write("\b \b");
					input[--count] = '\0';
				}
				continue;
			}
			else if(msg.keyboard.ascii == '\t')
				continue;

			if(count < (MAX_INPUT-1))
			{
				input[count++] = msg.keyboard.ascii;
				input[count]='\0';
				syscall_monitor_put((char)msg.keyboard.ascii);
			}
		}
		else if(msg.type == MT_TASK_FINISH)
		{
			syscall_finish(msg.finish_task.exit_task);
		}
	}

	return -1;
}


    上面的main函数里,通过syscall_monitor_write和syscall_monitor_put系统调用来输出显示字符串或字符信息,另外,上面函数还用到了一个syscall_set_input_focus系统调用,该系统调用用于设置输入焦点,只有通过syscall_set_input_focus系统调用将当前任务设置为输入焦点,键盘的中断处理例程才会向当前任务发送按键消息,否则当前任务就不会收到任何按键消息。由于输入焦点的任务只能有一个,所以如果别的任务设置了输入焦点的话,那么当前任务就必须还要设置一次输入焦点,所以,在shell每执行完一个命令后,还会再调用syscall_set_input_focus来重新设置一次输入焦点。

     另外,还有一点需要注意的是,上面的main并非真正的入口函数,对于C程式而言,真正的入口函数是entry.c文件里的entry函数:

// entry.c -- 主入口函数

#include "common.h"
#include "syscall.h"
#include "task.h"

int main(VOID * task, int argc, char * argv[]);

int entry()
{
	VOID * task = (VOID *)syscall_get_currentTask();
	VOID * location = &main;
	char * args = ((char *)syscall_get_init_esp(task)) - 1;
	char * orig_args = (char *)syscall_get_args(task);
	int arg_num = 0;
	int ret;
	int length = strlen(orig_args);
	int i = length - 1;

	if(args >= orig_args)
		reverse_memcpy((UINT8 *)args,(UINT8 *)(orig_args + length),length + 1);
	else
		memcpy((UINT8 *)(args - length),(UINT8 *)orig_args,length + 1);

	args -= length;
	asm volatile("movl %0,%%esp"::"r"((UINT32)args));

	for(;i>=0;i--)
	{
		if(args[i] == ' ' && args[i+1] != ' ' && args[i+1] != '\0')
		{
			arg_num++;
			asm volatile("push %0" :: "r"((UINT32)(args + i + 1)));
		}
	}

	if(args[0] != ' ' && args[0] != '\0')
	{
		arg_num++;
		asm volatile("push %0" :: "r"((UINT32)args));
	}
	
	for(i=0;i < length;i++)
	{
		if(args[i] == ' ')
		{
			args[i] = '\0';
		}
	}

	asm volatile(" \
		movl %esp,%eax; \
		push %eax; \
		");
	
	asm volatile("push %1; \
			push %2; \
			call *%3; \
			" : "=a" (ret) : "b"(arg_num), "c"((UINT32)task), "0" (location));
	
	syscall_exit(ret);
	return 0;
}


    entry函数里会先将任务的参数存储到用户栈的栈底位置,同时为main函数准备argc(即参数的个数),argv(即存储了每个字符串参数的指针的数组),以及task(当前任务的结构体指针),在为main函数准备好用户栈环境后,最后就通过call指令来跳转到main函数去执行,当main函数返回后,最后再由entry函数通过syscall_exit系统调用来退出当前的任务,同时将ret(即main函数的返回值)作为任务的退出码。

    在build_initrd_img目录内的makefile文件里会通过gcc的-e选项来指定entry为C程式的主入口函数:

#makefile for ram disk

......................................
......................................

shell: shell.o syscall.o entry.o common.o
	@echo 'building $@'
	$(CROSS_CC) -Wl,-eentry -o $@ $(CROSS_CLINK_FLAGS) shell.o syscall.o entry.o common.o
......................................
......................................


    至于build_initrd_img目录里的cpuid.s与uname.s之类的汇编程式则还是默认的_start标签处为主入口。

    限于篇幅,只能介绍部分系统调用,其余的系统调用,需要读者自己参考源代码并通过gdb调试的方式来进行分析。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: zenglOX v1.1.0 通过ATAPI读取光盘里的数据

上一篇: zenglOX v0.0.11 ELF format(ELF可执行文件格式)与execve系统调用

相关文章

zenglOX v0.0.6 使用Heap(堆)动态分配和释放内存

zenglOX v2.0.0 E1000系列网卡驱动, PCI驱动, PS/2控制器驱动, 以太网,ARP,IP,UDP,DHCP,ICMP协议, dhcp,arp,ipconf,ping,lspci命令行程式

zenglOX v0.0.8 Multitask(多任务)

zenglOX v0.0.10 Keyboard(获取键盘的输入)

zenglOX v0.0.1 开发自己的操作系统

zenglOX v1.3.0 动态链接库, 固定位置的内核栈, double fault(双误异常检测内核栈溢出)