先按照上一个v2.0.0版本的要求, 编译好bochs,让其支持e1000网卡,将源代码解压后, 执行make和make iso命令, 分别得到initrd.img, zenglOX.bin及zenglOX.iso文件,在root权限下运行startBochs来启动bochs虚拟机。启动好虚拟机后, 先进行分区格式化操作...

    页面导航:
项目下载地址:

    v2.2.0版本的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)

    Dropbox地址:点此进入Dropbox网盘  该版本位于zenglOX_v2.2.0的文件夹中,文件夹里的zip压缩包为源代码,readme.txt为版本的简单说明。

    Google Drive地址:点此进入Google Drive云端硬盘 对应也是zenglOX_v2.2.0的文件夹。

    sourceforge地址:https://sourceforge.net/projects/zenglox/files  对应也是zenglOX_v2.2.0的文件夹。

    Dropbox与Google Drive如果正常途径访问不了,则需要使用代理访问。

readme说明文档:

    以下是网盘中readme.txt的内容(在github的commit message里也是同样的内容),该文档详细的说明了运行步骤,注意事项,以及ee编辑器的使用方法(如果已经阅读过,可以跳过):

zenglOX v2.2.0 ee(easy editor)文本编辑器 C标准库函数 uheap(单独的用户堆空间) atapi驱动BUG zenglfs文件系统BUG 分页BUG 堆算法BUG等修复

先按照上一个v2.0.0版本的要求, 编译好bochs,让其支持e1000网卡,
将源代码解压后, 执行make和make iso命令, 分别得到initrd.img, zenglOX.bin及zenglOX.iso文件,
在root权限下运行startBochs来启动bochs虚拟机, 如果执行失败, 可能是bochsrc.txt配置不正确,
具体原因请参考v2.0.0的官方文章

启动好虚拟机后, 先进行分区格式化操作, 因为当前版本里的ee文本编辑器和它所依赖的
libc.so(包含一些C标准的库函数)的体积都比较大, 没有放在ramdisk里, 而是放在光盘镜像里,
需要通过下面要介绍的isoget命令来获取这两个文件到磁盘中, 然后从磁盘里启动运行。
分区格式化的相关操作, 请参考zenglOX v1.5.0版本对应的官方文章。

(如果是将生成的iso镜像放置到VirtualBox或VMware的虚拟机里, 并且在之前的版本,
你已经格式化了一个分区的话, 最好还是用format命令重新格式化一下, 因为当前版本
对zenglfs文件系统进行了BUG修复, 尽管理论上, 不重新格式化也可以正常使用,
但是重新格式化一下, 不容易出问题)

假设你的分区位于0号ide磁盘设备的1号分区里, 则输入如下命令来分别挂载光盘和磁盘分区:

zenglOX> mount iso
mount iso to [iso] success! you can use "ls iso".....
zenglOX> mount hd 0 1
mount to [hd] success! you can use "ls hd" to see.....

当挂载iso和hd成功后, 就可以使用isoget命令来获取ee编辑器和libc.so库文件到磁盘中:

zenglOX> isoget -all
copy content of iso/EXTRA/EE.;1 to hd/bin/ee success
copy content of iso/EXTRA/LIBC.SO;1 to hd/lib/libc.so success

上面的isoget -all命令执行时, 它会在内部调用file命令将iso的EXTRA目录内的EE.;1文件拷贝
到hd/bin目录内, 对应文件名为ee , 同理通过file命令将iso里的LIBC.SO;1文件拷贝到
hd/lib目录内,对应文件名为libc.so (这里要注意的是光盘镜像里的文件名有自己特殊的文件名格式
,比如都以;1结尾, 详情请参考zenglOX v1.2.0 ISO 9660文件系统对应的官方文章)

现在就可以使用ee编辑器来编辑文本文件了, 你可以给它输入一个文件名参数, 让它
去编辑该文件, 也可以啥参数也不输入, 在没输入任何参数的情况下, 它会将编辑的内容
保存到hd/Notes的默认文件中。

为了能全面的测试ee编辑器的所有功能, 我们下面还是以一个内容较多的文本文件为例来进行说明。

先通过file命令将iso里的测试用的ee.c文件拷贝到hd目录内:

zenglOX> file iso/EE.C;1 hd/ee.c
copy content of iso/EE.C;1 to hd/ee.c success
zenglOX>

上面的命令将iso里的EE.C;1文件拷贝到了hd目录里, 对应文件名为ee.c

下面就用ee来打开该文本文件(文件的内容是ee编辑器在开发过程中的v1.0.0版本的源代码)

zenglOX> ee hd/ee.c

上面命令执行时, shell会在内部自动在ee前面加上hd/bin/ , 因此完整的命令其实是
hd/bin/ee hd/ee.c , shell在当前版本里会自动在hd/bin目录内搜索可执行文件名,
这样, 我们就可以直接输入ee来启动编辑器了。

执行上面命令后, 就可以看到hd/ee.c里的内容了, 其显示界面类似如下所示:


      图1
内容较多, 1000多行, 因此, 这里就不全显示了。
其中第一行由几个部分组成:
1) 文件名(如上面的hd/ee.c),
2) 各种flag标志(例如上面的....tp..)
3) note区域(可以显示行列号等信息), note区域里一开始显示了当前光标的行列号,
   并提示用户可以按F1键来查看帮助信息

按F1键后, 可以看到如下所示的帮助信息:


      图2

从上面的帮助信息里, 可以看到其功能还是很全面的, 虽然ee还不能与vi编辑器相比,
但是对于普通的文本编辑(脚本代码编辑)来说, 该有的都有了。
(ee源于FreeBSD系统, 由于其代码相当标准, 也非常小, 很适合移植, 所以作者就将其
移植到了zenglOX里, 同时对其按键和部分功能做了些调整, 并修复一些BUG)

通过帮助可以看到, ctrl+a组合可以跳转到文件的最后一行, ctrl+A组合可以
跳到文件的第一行。

这里要注意大小写, ctrl后面的控制字符都是case sensitive(区分大小写)的,
你可以通过Caps Lock(大小写锁键)来切换字母的大小写状态,
也可以在按键a为小写时, 通过ctrl+shift+a来达到ctrl+A的效果, 因为shift键可以
临时切换大小写的状态。

在进行复制, 剪切, 块删除操作时, 必须先用ctrl+b组合来设置块的起始位置,
然后在块的结束位置执行ctrl+c(拷贝)或者ctrl+x(剪切)或者ctrl+d(块删除)命令,
这里的块指的是你要操作的一块连续的字符区域(这块区域内可以包含任意字符,包括空格,
回车,换行等)。例如, 你要复制下面两行内容的话:

FILE  *fi = 0, *fo = 0;
MWIN  win, winnext, wincopy; /* current, next, other windows */

先将光标通过上下左右键或者通过稍候要提到的ctrl+q,ctrl+w,Home之类的按键,
移动到FILE的第一个字符F处, 在该处按下ctrl+b组合(该组合按下时,头部的note区域会显示
Mark Set, press any key to continue的提示),
再将光标移动到第二行的结束位置处, 在光标位于第二行时, 可以用End键来移动到
行尾, 在行尾位置处使用ctrl+c的组合(该组合按下时note区域会显示
Block copied [size:87], press any key to continue 的提示, 在该提示里还包含
了Block块的size尺寸即字符数信息, 你可以通过该信息来粗略的判断一下是否拷贝正确,
如果字符数信息和你要拷贝的内容感觉差距过大的话, 就说明你可能拷贝到了别的你不需要的数据了。

根据提示, 按任意键继续, 这样你就复制好了, 接着将光标移动到你要粘贴的地方,
执行ctrl+v的组合即可。

块剪切和块删除操作都是同理(块删除只执行删除操作, 不需要后续的ctrl+v的粘贴操作)。

这里, 同样要注意控制字符的大小写, 如果你发现某功能按下没反应时, 很可能就是
大小写的问题

通过ctrl+f与ctrl+F组合可以执行字符串的查找操作, 其中ctrl+f组合用于
从当前光标位置处向前查找, ctrl+F组合则进行向后查找。

通过ctrl+e组合可以设置字符串查找时, 是否区分大小写, 当第一次按下ctrl+e组合
时, 头部的flag标志区域里会显示出一个c字符, 头部类似如下显示:
hd/ee.c      ...ctp..
这个tp旁边多出来的c字符表示现在区分大小写, 那么查找操作时就会区分大小写,
再次按下ctrl+e组合时, 该c字符就会变回.(即小数点符号), 这样查找和替换操作时
就不会区分大小写了。

通过ctrl+r与ctrl+R组合可以执行字符串的替换操作, 这里需要注意的是
字符串的替换是根据查找操作来的, 必须先通过ctrl+f之类的组合查找定位到
要替换的字符串, 然后再执行替换操作, ctrl+r组合只执行一次替换操作,
ctrl+R组合则可以将当前光标位置处到文件结尾之间的所有的查找字符串
都进行替换操作(如果执行替换操作时, 光标不位于查找字符串的开头位置时,
note区域会显示Invalid Pos, must search first的提示, 就是告诉用户必须先进行
查找定位的操作)。

通过ctrl+g组合可以跳到指定的行, 输入行号时, 最多只能输入5个数字, 因为后面
会介绍到, ee编辑器有个字符容量限制。

通过ctrl+l组合可以跳到指定的列, 同样最多输入5个数字, 最大列数为10000

这里要注意的是, ee编辑器编辑文本时是不word wrap的, 当一行文本超过
75列时, 多出的文本内容会位于隐藏的视图区域, 需要通过上下左右键或ctrl+q, ctrl+w
之类的按键来移动到该视图区域中。

如果你想让很长的一行文本都位于第一个视图区域的话, 可以将光标移动到该行,
然后按下ctrl+m的组合, 该组合可以对该行文本进行格式化, 也就是通过自动换行,
让他由一行很长的文本, 变为多个小于75个字符的行。

通过ctrl+i组合可以查看一些文本统计信息, 例如总行数, 总字符数, 可容纳的最大
字符数等 (可容纳的最大字符数目前为851968个字符, 当你的文本字符数超过这个
容量时, 就会显示出Main buffer full的错误, 然后退出程式)。

通过ctrl+q和ctrl+w组合可以快速的向前和向后移动, ctrl+q的组合可以让光标向前
移动到下一个tab的边界(一个tab的边界是: 当某个列位置除以tab宽度, 得到余数为1时,
那么该列位置就是一个tab边界),
ctrl+w组合可以向后移动到上一个tab边界处。
(tab的宽度可以在执行ee程式时, 通过-t参数来指定, 下面会提到)。

ctrl+t组合可以切换tab模式, 当文本文件里存在'\t'的tab字符时, 默认就会启用
tab模式, 可以在头部的flag标志区域里看到一个t字符, 类似如下所示:
hd/ee.c     ....tp..
上面的t字符就表示当前处于tab模式, ctrl+t组合可以切换这种模式, 也就是说
如果当前是tab模式, ctrl+t就会取消tab模式, 当取消时, flag标志区域里的t字符就会
变为.(即小数点符号), 所谓tab模式, 是指ee编辑器会在保存文件时, 将位于tab
边界处的连续的空格符压缩为一个'\t'字符, 这里还有一点需要注意的是,
ee编辑器在打开文本文件时, 会自动将文件里的'\t'字符转换为空格符, 之所以这么
做, 是因为只有这样才能有效控制'\t'字符在显示时的tab边界效果, 才不会受到终端
对'\t'字符显示的影响, 在一定程度上也方便文本的编辑。

在非tab模式下, ee编辑器保存文件时, 就不会将tab边界处的连续的空格压缩为tab符,
这样非tab模式下保存的文本里就不会再包含任何\t字符了。

ctrl+s组合可以在不退出ee的情况下保存文件内容, ctrl+s组合保存文件内容时,
会先将文件内容保存到磁盘, 然后再重新执行一次读操作, 这样就可以确保文件
里的'\t'字符能够得到正确的处理。

ctrl+z组合可以退出ee编辑器, 当文本内容被修改时, flag标志区域中会显示
一个|即竖杠字符, 类似如下所示:
hd/ee.c    |...tp..

当ctrl+s保存文件时, 该竖杠字符就会变回.即小数点字符。
ctrl+z组合退出ee时, 如果文件内容被修改过, 则头部note区域会显示
Save file(yes/no/cancel)的提示, 通过输入y来保存文件并退出ee, 通过输入
n来不保存而退出ee, 通过输入c来取消退出操作。

通过F2键, 可以切换头部是否显示行列号信息, 默认一开始是显示行列号的, 在
头部flag标志区域会有个p字符, 表示当前note区域显示行列号, 当按下F2时,
p字符会变为.即小数点字符, 同时note区域的行列号信息会消失, 再次按下F2时,
p字符又会出现, 同时note区域的行列号信息又会显示出来。

Home和End键可以跳到当前行的开头和当前行的结束位置。

上下左右键可以移动光标位置。

PageUp键向上跳转一页。
PageDown键向下跳转一页。

Backspace退格符可以向后删除一个字符, ctrl+Backspace的组合则可以向后退一个
tab边界, 当然退一个tab边界的前提是当前光标移动的方向是空格符组成的情况,
对于非空格的普通字符, ctrl+Backspace与Backspace是一样的效果。
比如你按下了tab键, 向前产生了一个tab边界, 那么ctrl+Backspace就可以向后删掉
该tab边界, 而Backspace则只能一个个字符的删除。

Enter键产生一个新行。

Delete键删除当前光标位置处的字符。

Insert键设置为insert插入模式, 该模式下输入的字符会覆盖掉当前光标处的原来的字符。
插入模式下, flag标志区域里会显示一个o字符, 类似如下所示:
hd/ee.c      |.o.tp..
再次按下Insert可以取消插入模式, o字符就会变回小数点符号

ESC+z的组合也可以退出ee编辑器, 与ctrl+z不同的是, ESC和z不需要同时按下,
ctrl+z的组合则是ctrl键处于按下状态时, 按z键(当然z必须是小写字母状态),
当ESC键被按下时, 头部flag标志区域会出现~字符, 类似如下所示:
hd/ee.c      |...tp~.

再按下z字母时(同样z要是小写状态), 就会在note区域显示类似ctrl+z组合时的
Save file的提示了(前提是文件内容被修改过了, 如果没修改过, 则直接退出ee,
通过前面提到的|即竖杠字符来标识是否被修改了)。

上面提到的ctrl都是left ctrl即左ctrl键, 而非右ctrl键, zenglOX内核里
目前也没有对右ctrl键的任何处理代码。另外像VirtualBox虚拟机会对Right Ctrl
做特殊处理, 因此, 也就没添加右ctrl键的相关代码。

本来想加入alt键的, 但是alt键的释放在bochs里有点问题,
所以就没有alt, 全部都用的left ctrl控制键。

如果ee打开的是一个新文件的话, 在头部flag标志区域里就会显示出*字符,
类似如下所示:
hd/hello     .....p.*

可以通过ee -v命令来查看当前ee的版本信息:
zenglOX> ee -v
ee(easy editor) for zenglOX, ee's version is v1.3.8
zenglOX>

启动ee时, 可以使用-+参数来设置打开文件时, 自动跳到哪一行:
zenglOX> ee -+100 hd/ee.c
执行上面命令后, ee在打开hd/ee.c文件后, 一开始就会自动跳到第100行的第1列。
当你输入的行数超过文件内容的行数时, ee内部会自动设置为最后一行。
(前提是你输入的整数没有超过int的32位范围, 超过范围时就会发生溢出,
溢出情况下, 整数就有可能是正数或负数, 两种情况ee内部都会做处理)

使用-l参数可以设置打开文件时, 自动跳到哪一列:
zenglOX> ee -l10 hd/ee.c
执行上面命令后, ee就会在打开hd/ee.c文件后, 一开始就会自动跳到第1行的第10列。
当你设置的列数超过10000时, ee内部会自动将其设置到10000
(同样ee内部也考虑了输入值的溢出情况)

使用-t参数可以设置ee编辑时的tab边界宽度(默认为8, 最多只能设置到32):
zenglOX> ee -t16 hd/ee.c
执行上面命令后, 在ee编辑器中的tab宽度就会由默认的8变为16 , 如果你设置的值大于32,
则ee会在内部将其调整到32. (同样ee内部也考虑了输入整数溢出时或为负数时的情况)

当然, 上面的-+, -l与-t参数也可以组合在一起使用:
zenglOX> ee -+100 -l10 -t16 hd/ee.c
执行上面命令后, ee编辑器的光标一开始就会位于第100行的第10列, tab宽度为16.

当你的文本内容很大时, 过大的tab宽度会明显增加ee编辑的字符数, 因为\t字符
会按照tab宽度替换为对应数目的空格符, 当超过ee的容量上限时, ee打开文件时
也会有too big content...的警告信息。

这几个参数的顺序无关紧要, 也可以两两组合, 但是有一点需要注意的是
hd/ee.c之类的文件名必须位于所有参数的最后位置。

以上就是ee编辑器的相关用法。

ee编辑器是依赖libc.so库的, 该库里面包含了一些标准的C函数, 例如fopen, fclose
之类的, 和printf相关的函数代码是从Linux内核里提取出来的。
libc.so只包含了ee编辑器所需的库函数, 如果以后开发其他应用, 有需要的话,
会再向libc.so里添加更多的C库函数。
目前,所有C库函数的声明都位于stdlib.h的一个头文件里。
当然libc.so里也包含了一些像initscr, endwin这类的非C标准的库函数。

从v2.2.0版本开始, 每个任务(也可以称作进程), 都有自己独立的用户堆空间,
不再像以前的版本那样, 所有任务与内核都共用一个内核堆空间。
独立的用户堆空间的好处很多, 首先不会与内核堆空间里的数据相冲突。
用户堆空间会在任务结束时自动释放掉, 也就是说如果你使用syscall_umalloc
系统调用分配了一段堆空间的话, 那么即便忘记用syscall_ufree来释放该空间也没
有关系, 因为所有堆空间最后都会被统一释放掉, 可以有效防止内存泄漏。

用户堆空间的起始线性地址为:0xF0001000 , 该值定义在新增的zlox_uheap.h的头文件里。
和用户堆空间相关的函数代码位于新增的zlox_uheap.c文件里。

这样, 每个任务都有自己独立的用户栈, 内核栈, 以及用户堆了。

atapi驱动做了些调整, 取消原来的任务调度方式, 因为任务调度的方式下, 有时候
会收不到中断信号, 这样在读取光盘数据时, 就会一直处于睡眠状态。现在采用和
ata硬盘读写方式相同的poll循环进行状态检测的方式。

对zenglfs文件系统的BUG进行了相关修复, 之前的版本, 当对某文件进行覆盖写入操作时,
文件的原先多出来的数据块并不能得到有效的释放。

对zlox_paging.c文件的zlox_clone_directory函数做调整, 修复分页BUG 。
由于页目录有8K的大小, 而一个8K的虚拟内存空间,
可以对应两个非连续的物理内存页面,
因此简单的用phys(物理内存地址) + offset(偏移量)得到的结果不一定正确,
尤其是在多任务环境下,因此必须用zlox_get_page函数的方式
来得出实际的正确的物理内存地址,
否则,错误的cr3页表地址会让整个系统直接崩溃掉!

最后对一些堆算法BUG进行了处理, 键盘中断也进行了处理, 使之可以接受
F1, F2, Home, End之类的按键, 还可以接受小键盘里的数字输入(当按下小键盘锁时)。

bochs模拟器是比较消耗CPU资源的, 对于调试开发, 首选bochs , 如果是实际的运行
, 当然是首选VirtualBox与VMware了。bochs的好处在于开发硬件驱动时, 你可以
深入到bochs的源代码里去进行调试分析, 在上一个版本开发e1000网卡驱动时,
bochs的源代码就帮了很大的忙。

时间: 2014年10月6日
作者: zenglong
官网: www.zengl.com

readme补充说明及64位编译说明:

    这里需要补充说明的是:ee暂时没有Undo(撤销)和Redo(重做)功能。

    有关zenglOX的gcc交叉编译环境,在ubuntu 64位系统下也搭建和测试通过(作者使用的是UbuntuStudio 12.04.3 amd64的系统进行的测试),因此,如果读者使用的是64位的Linux,应该也可以正常编译和测试zenglOX 。

Makefile文件解析:

    先来看下build_initrd_img/makefile文件,ee编辑器与libc.so的生成规则就定义在该文件中:

#makefile for ram disk

CC = @gcc

CFLAGS = -std=gnu99 -ffreestanding -gdwarf-2 -g3 -Wall -Wextra
DEPS = common.h syscall.h elf.h task.h kheap.h fs.h ata.h fdisk.h format.h vga.h network.h pci.h
AS_TARGET = cpuid uname reboot shutdown
C_TARGET = shell ls ps cat ata mount unmount testoverflow fdisk format file vga lspci isoget
C_NET_TARGET = arp dhcp ipconf ping
C_EXTRA_TARGET = extra/output/ee extra/output/libc.so
C_DEP_LIB = ld.so libcommon.so
C_DEP_NET_LIB = libnetwork.so
INITRD_FILES = licence.txt $(AS_TARGET) $(C_TARGET) $(C_NET_TARGET) $(C_DEP_LIB) $(C_DEP_NET_LIB)

initrd.img:make_initrd.c $(INITRD_FILES) $(C_EXTRA_TARGET)
	@echo 'building initrd.img'
	$(CC) -o make_initrd make_initrd.c $(CFLAGS)
	./make_initrd $(INITRD_FILES)

define AS_make_template
# translation 
$1: $1.o
	@echo "building [email protected]"
	$(CROSS_LD) -o [email protected] $$<
endef

$(foreach l, $(AS_TARGET), \
  $(eval $(call AS_make_template,$(l))) \
)

define C_make_template
# translation 
$1: $1.o $(C_DEP_LIB)
	@echo "building [email protected]"
	$(CROSS_CC) -Wl,-emain -Wl,-dynamic-linker,ld.so -o [email protected] $$< $(CROSS_CLINK_FLAGS) -L. -lcommon
endef

define C_NET_make_template
# translation 
$1: $1.o $(C_DEP_LIB) $(C_DEP_NET_LIB)
	@echo "building [email protected]"
	$(CROSS_CC) -Wl,-emain -Wl,-dynamic-linker,ld.so -o [email protected] $$< $(CROSS_CLINK_FLAGS) -L. -lcommon -lnetwork
endef

$(foreach l, $(C_TARGET), \
  $(eval $(call C_make_template,$(l))) \
)

$(foreach l, $(C_NET_TARGET), \
  $(eval $(call C_NET_make_template,$(l))) \
)

extra/output/ee: extra/ee.c standard/include/stdlib.h common.h syscall.h extra/output/libc.so
	@echo "building [email protected]"
	@mkdir -p extra/output
	$(CROSS_CC) -Wl,-emain -Wl,-dynamic-linker,ld.so -o extra/output/ee extra/ee.c $(CROSS_CLINK_FLAGS) -I standard/include -I. -L. -lcommon -L extra/output -lc

extra/output/libc.so: standard/libc.c standard/printf.c
	@echo "building [email protected]"
	@mkdir -p extra/output
	$(CROSS_CC) -fPIC -Wl,-shared -Wl,-soname,hd/lib/libc.so -o extra/output/libc.so standard/libc.c standard/printf.c $(CROSS_CLINK_FLAGS) -I standard/include -I. -L. -lcommon

libcommon.so:common.o syscall.c
	@echo "building [email protected]"
	$(CROSS_CC) -Wl,-shared -o [email protected] common.o syscall.c $(CROSS_CLINK_FLAGS)

libnetwork.so:network.c network.h common.h libcommon.so
	@echo "building [email protected]"
	$(CROSS_CC) -fPIC -Wl,-shared -o [email protected] network.c $(CROSS_CLINK_FLAGS) -L. -lcommon

ld.so:ld.c ld.s common.h syscall.h elf.h task.h libcommon.so
	@echo "building [email protected]"
	$(CROSS_CC) -fPIC -Wl,-shared -Wl,-edl_main -o [email protected] ld.c ld.s $(CROSS_CLINK_FLAGS) -L. -lcommon

%.o: %.s
	@echo "building [email protected]"
	$(CROSS_AS) -o [email protected] $< $(CROSS_AS_FLAGS)

%.o: %.c $(DEPS)
	@echo "building [email protected]"
	$(CROSS_CC) -fPIC -c -o [email protected] $< $(CROSS_CFLAGS)

clean:
	$(RM) $(RMFLAGS) *.o
	$(RM) $(RMFLAGS) $(AS_TARGET) $(C_TARGET) $(C_NET_TARGET) $(C_DEP_LIB) $(C_DEP_NET_LIB)
	$(RM) $(RMFLAGS) extra/output
	$(RM) $(RMFLAGS) make_initrd
	$(RM) $(RMFLAGS) initrd.img

all: initrd.img


    在makefile里AS_TARGET,C_TARGET,C_NET_TARGET变量定义的值都是存储在ramdisk(内存盘)里的可执行文件。其中,AS_TARGET变量对应的值:cpuid uname reboot shutdown这4个可执行文件的源代码都是汇编程式。C_TARGET对应的可执行文件的源代码都是C程式,C_NET_TARGET对应的可执行文件的源代码也是C程式,只不过arp dhcp ipconf ping这4个程式都是和网络有关的,编译时需要链接libnetwork.so的动态库。

    这3类程式中,每类程式的编译规则都是类似的,因此,可以对这三类程式分别定义一套通用的规则模板,例如,AS_TARGET对应的编译模板为:

define AS_make_template
# translation 
$1: $1.o
	@echo "building [email protected]"
	$(CROSS_LD) -o [email protected] $$<
endef


    上面定义了一个AS_make_template的模板,也可以说是定义了一个AS_make_template的函数,#号开头的语句是注释(例如上面的# translation),$1是稍候call指令调用它时,会传递给该函数的参数。参数以$符开头,传递过来的第一个参数用$1表示,第二个参数用$2表示,以此类推。由于单个$符用于引用参数或者引用变量了,因此,在规则语句里,要表示普通的$字符的话,就需要用两个$符,例如,上面的[email protected],在call展开该函数时,就会被转为[email protected]

    在定义了一个模板后,就可以通过call指令来展开它,例如:

$(call  AS_make_template,cpuid)

    call后面的AS_make_template是定义的模板名,逗号后面的cpuid是参数,call执行后,将返回如下所示的一段字符串:

cpuid: cpuid.o
	@echo "building [email protected]"
	@/mnt/zenglOX/opt/cross/bin/i586-elf-ld -o [email protected] $<


    $1被替换为了cpuid,$(CROSS_LD)被替换为了@/mnt/zenglOX/opt/cross/bin/i586-elf-ld,CROSS_LD是zenglOX根目录的makefile里通过export导出来的,开头的@表示这些命令执行时,只显示结果,而不显示命令信息。

    call只是负责将其展开,返回的只是一段字符串信息,要让这段字符串被解析成makefile里的语法(例如上面的cpuid那段字符串,只有被解析成语法,才能作为规则被执行),就需要使用eval指令,因此,我们可以使用如下代码来定义cpuid规则:

$(eval  $(call  AS_make_template,cpuid))

    上面将call的返回值作为eval的参数,从而让call返回的字符串可以被解析为makefile的语法。

    以上只是定义了一个可执行文件的规则,为了能批量定义AS_TARGET里的4个可执行文件的规则,我们可以使用foreach指令:

$(foreach l, $(AS_TARGET), \
  $(eval $(call AS_make_template,$(l))) \
)


    foreach后面的l是自定义的变量名称,你也可以将上面的语句改写为:

$(foreach arg, $(AS_TARGET), \
  $(eval $(call AS_make_template,$(arg))) \
)


    上面自定义了一个arg变量,该变量可以循环从AS_TARGET的列表里(列表里的每个元素已经通过空格符隔开),获取列表元素,例如foreach第一次执行时,arg会获得AS_TARGET里的第一个列表元素即cpuid,然后在foreach的第三个参数(该参数定义了循环体的执行语句)里,$(arg)就会被替换为cpuid,接着call就会将cpuid作为参数传递给AS_make_template模板,call返回的cpuid的规则字符串,又会作为参数传递给eval ,eval就可以将该字符串解析为cpuid可执行文件的makefile规则了。foreach第二次执行时,arg就会变为AS_TARGET的第二个元素即uname,然后call,eval指令就可以定义出uname的规则,以此类推,直到AS_TARGET列表里的所有值都处理完为止。

    如果想了解foreach,call,eval的具体格式与用法,可以参考官方手册:https://www.gnu.org/software/make/manual/html_node/Functions.html

    上面介绍了AS_TARGET汇编程式相关的makefile规则,至于C_TARGET,C_NET_TARGET的C程式规则,同样也是通过先定义模板,再利用foreach,call,eval指令来批量定义出来的。这些模板和foreach之类的指令都是从v1.3.0版本引入的,之所以在这里进行介绍,是因为在v1.3.0版本的文章里并没有进行这些解释,而模板的定义和使用又是一个难点,在官方手册里只有很少的内容提到过模板,作者也是在 http://stackoverflow.com/questions/9289847/how-to-merge-similar-rules-in-a-makefile 这篇论坛文章里找到的模板的具体示例的(如果该论坛链接打不开,则请通过代理访问)。模板的好处就在于可以批量定义类似的规则。

    和当前版本ee编辑器与libc.so相关的makefile规则如下:

...........................................................
AS_TARGET = cpuid uname reboot shutdown
C_TARGET = shell ls ps cat ata mount unmount testoverflow fdisk format file vga lspci isoget
C_NET_TARGET = arp dhcp ipconf ping
C_EXTRA_TARGET = extra/output/ee extra/output/libc.so

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

extra/output/ee: extra/ee.c standard/include/stdlib.h common.h syscall.h extra/output/libc.so
    @echo "building [email protected]"
    @mkdir -p extra/output
    $(CROSS_CC) -Wl,-emain -Wl,-dynamic-linker,ld.so -o extra/output/ee extra/ee.c $(CROSS_CLINK_FLAGS) -I standard/include -I. -L. -lcommon -L extra/output -lc

extra/output/libc.so: standard/libc.c standard/printf.c
    @echo "building [email protected]"
    @mkdir -p extra/output
    $(CROSS_CC) -fPIC -Wl,-shared -Wl,-soname,hd/lib/libc.so -o extra/output/libc.so standard/libc.c standard/printf.c $(CROSS_CLINK_FLAGS) -I standard/include -I. -L. -lcommon

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

    可以看到,ee可执行文件与libc.so动态库会被生成到extra/output目录中,该目录相对于zenglOX根目录的完整路径为build_initrd_img/extra/output ,一开始在extra目录内只包含ee.c的源文件,并不包含output目录,作者之所以没有在一开始就建立一个output目录,是因为github不会提交空目录,假设按照isodir/boot/grub目录的做法,在该目录里放置一个.gitignore文件的话,确实可以让github提交上去,但是在进行make clean清理操作时,作者希望能将output输出目录里生成的文件都删除掉,这样删除操作就会变得复杂(因为还包含了一个.gitignore文件),因此,一个简单的解决方式就是,最开始不新建output目录,而是在生成ee或者libc.so时,通过mkdir -p extra/output命令新建一个output目录(mkdir的-p参数可以在目录已存在时,跳过新建操作,如果没有-p参数就可能会显示目录已存在的错误信息),这样make clean删除操作时,就可以直接将该目录删除即可,github上也无需提交该目录了。

    由于ee程式调用了libc.so里的库函数,因此,在编译生成ee时,就需要通过 -lc 参数将libc.so链接到可执行文件(gcc的-l参数不需要lib前缀与.so的后缀)。此外,由于libc.so会被生成到extra/output目录中,因此,需要通过-L参数来指明库文件的搜索目录,如上面的 -L extra/output 参数,在前面还有个 -L. (小数点表示当前工作目录),gcc在当前工作目录中没有找到libc.so时,gcc就会到相对于当前工作目录的extra/output目录里去搜索libc.so库文件。

    在编译生成libc.so时,有一个 -Wl,-soname,hd/lib/libc.so 的参数,其中,-Wl用于表示将后面的-soname,hd/lib/libc.so参数传递给ld链接器(gcc生成可执行文件时,默认在最后会自动去调用ld链接器,通过-Wl参数可以向ld传递参数),-soname,hd/lib/libc.so 该链接参数的含义就是将libc.so动态库的SONAME设置为hd/lib/libc.so ,可以在linux下通过readelf命令来查看生成的libc.so的SONAME值:

$ readelf -d build_initrd_img/extra/output/libc.so

Dynamic section at offset 0x34cc contains 15 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libcommon.so]
 0x0000000e (SONAME)                     Library soname: [hd/lib/libc.so]
....................................
$ 


    ee可执行文件,在链接libc.so成功后,通过readelf命令就也可以看到这个SONAME值:

$ readelf -d build_initrd_img/extra/output/ee

Dynamic section at offset 0x41b4 contains 13 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libcommon.so]
 0x00000001 (NEEDED)                     Shared library: [hd/lib/libc.so]
.............................................
$ 


    可以看到,ee所依赖的动态库里,第一个libcommon.so由于没有设置SONAME值,所以就直接使用的是libcommon.so的动态库名(这样就决定了libcommon.so必须放置在ramdisk里,zenglOX才能搜索和加载成功),而libc.so由于设置了SONAME,因此NEEDED部分就使用的是hd/lib/libc.so的SONAME值,该值指明了libc.so在zenglOX里的完整路径信息,zenglOX里就可以根据完整的路径信息将libc.so从磁盘中加载到内存里,因此,libc.so可以存储到磁盘里(而无需占用ramdisk的内存空间)。

    当然,SONAME在zenglOX里的用法与它本身在Linux系统中的用途有点不太一样,在Linux里,SONAME是用于版本控制的,比如可以将libc.so的SONAME设置为libc.so.1.3.1之类的,这样Linux就会去加载libc.so.1.3.1的版本,在zenglOX里,其实也可以这么做,因为SONAME是可以自定义的,你可以将其设置为路径,也可以加上版本信息等。

    此外,由于ee.c与libc.c的C源代码都需要加载stdlib.h头文件(该头文件里包含了库函数和系统调用等的相关声明),而stdlib.h头文件是存储在standard/include目录中的(这是一个相对路径,完整的路径是build_initrd_img/standard/include),因此,在gcc的编译参数中,需要通过 -I standard/include 参数来指明头文件的搜索目录。

    在生成libc.so时,除了libc.c的C源文件外,还有个printf.c的C源文件,该文件里和printf函数相关的代码,都是从Linux-2.6.37.6的内核里提取出来的。在作者的Slackware系统里,内核源码中printf.c的路径为 /usr/src/linux-2.6.37.6/arch/x86/boot/printf.c 。

    下面再看下zenglOX的源码根目录中,makfile文件所做的改动:

...................................................
export RM = rm
export RMFLAGS = -rvf

DEPS = zlox_common.h zlox_monitor.h zlox_descriptor_tables.h zlox_isr.h zlox_time.h zlox_kheap.h \
		zlox_paging.h zlox_ordered_array.h zlox_initrd.h zlox_fs.h zlox_multiboot.h zlox_task.h \
		zlox_keyboard.h zlox_elf.h zlox_ata.h zlox_iso.h zlox_zenglfs.h zlox_vga.h zlox_pci.h \
		zlox_e1000.h zlox_network.h zlox_ps2.h zlox_uheap.h
OBJS = zlox_boot.o zlox_kernel.o zlox_common.o zlox_monitor.o zlox_descriptor_tables.o \
		zlox_gdt.o zlox_interrupt.o zlox_isr.o zlox_time.o zlox_kheap.o zlox_paging.o \
		zlox_ordered_array.o zlox_initrd.o zlox_fs.o zlox_task.o zlox_process.o zlox_syscall.o \
		zlox_keyboard.o zlox_elf.o zlox_shutdown.o zlox_ata.o zlox_iso.o zlox_zenglfs.o zlox_vga.o \
		zlox_pci.o zlox_e1000.o zlox_network.o zlox_ps2.o zlox_uheap.o

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

initrd:
	@(cd build_initrd_img; make)

cleanrd:
	@(cd build_initrd_img; make clean)

clean:
	$(RM) $(RMFLAGS) *.o
	$(RM) $(RMFLAGS) zenglOX.bin
	$(RM) $(RMFLAGS) zenglOX.iso
	@(cd build_initrd_img; make clean)
	$(RM) $(RMFLAGS) isodir/boot/zenglOX.bin
	$(RM) $(RMFLAGS) isodir/boot/initrd.img
	$(RM) $(RMFLAGS) isodir/extra
	$(RM) $(RMFLAGS) isodir/boot/grub/grub.cfg
	$(RM) $(RMFLAGS) isodir/cpuid

all: zenglOX.bin

iso: zenglOX.bin
	cp zenglOX.bin isodir/boot/zenglOX.bin
	cp build_initrd_img/initrd.img isodir/boot/initrd.img
	cp build_initrd_img/cpuid isodir/cpuid
	mkdir -p isodir/extra
	cp build_initrd_img/extra/output/* isodir/extra/
	cp grub.cfg isodir/boot/grub/grub.cfg
	grub-mkrescue -o zenglOX.iso isodir


    可以看到根目录makefile文件所做的改动并不多,也都不难理解,在最后一个iso目标中,在生成zenglOX.iso之前,会先通过 mkdir -p isodir/extra 命令在isodir目录内创建一个extra目录,接着再通过 cp build_initrd_img/extra/output/* isodir/extra/ 命令,将build_initrd_img/extra/output/目录里生成的ee可执行文件与libc.so库文件拷贝到isodir/extra目录里,该extra目录最后会被存储到zenglOX.iso镜像的根目录(grub-mkrescue命令会自动将isodir目录里的所有子目录与文件都写入到iso镜像)。

    在make clean清理时,就只需通过 $(RM) $(RMFLAGS) isodir/extra 即 rm -rvf isodir/extra 命令将extra目录删除即可,-rvf参数较之前的版本,多了一个 r 参数,该参数可以递归删除子目录和子文件,从而可以将目录及目录里的所有文件都删除掉。

    如果只是对build_initrd_img目录里的源文件做了修改的话,可以只执行 make initrd 命令即可,它会先cd进入build_initrd_img目录,然后执行make操作。

    同理,如果只要清理build_initrd_img目录里生成的文件的话,也只需执行 make cleanrd 命令即可。

    从上面的makefile文件中,还可以看到,当前版本只增加了一个zlox_uheap的模块,该模块是负责分配和释放任务的用户堆空间的,其核心算法与zlox_kheap内核堆模块的算法是一致的。

    以上就是当前版本中两个makefile文件的相关内容。

ee与libc.so调试相关说明:

    限于篇幅,作者这里不会去分析ee.c的源代码,其实该程式的源代码还是不难分析的,因为它不像zenglOX里的其他的ps ,ls之类的程式,ee.c里并没有使用任何系统调用,它都是用的标准的C库函数(除了极个别的initscr,endwin之类的和屏幕操作相关的非C标准的库函数)。

    如果读者想调试ee程式的源代码,可以参考zenglOX v1.0.0版本的官方文章,在该篇文章的第2分页,介绍了如何调试shell程式,调试ee程式的方法与调试shell程式的方法是一样的,都是先在Linux里通过readelf命令得到可执行文件的.text代码段的起始线性地址,然后在gdb远程调试里,通过add-symbol-file命令将ee程式的符号信息导入gdb,再通过dir命令设置ee程式源代码所在的目录,最后就可以在gdb里对ee程式的源代码下断点了,如果断点的线性地址与shell相冲突的话,因为任何两个用户进程都可以具有相同的线性地址(其物理内存地址会被页表映射到不同的位置),可以先在内核的zlox_execve函数处下断点,当shell执行ee程式时,就会在zlox_execve函数处中断下来,此时再在ee.c源文件里下断点,就不会和shell相冲突了(ee执行时,shell处于睡眠状态,不会和ee同时执行,只有ee执行完毕时,shell才会恢复执行)。

    如果要对libc.so之类的动态库文件进行调试的话,就需要先知道该动态库被加载时的起始线性地址,然后用该线性地址加上readelf命令查看到的.text代码段地址,相加的结果才是add-symbol-file命令要用到的线性地址,被加载时的起始线性地址可以通过内核里的zlox_load_shared_library函数来得到,因为所有的动态库都要经过zlox_load_shared_library函数来加载到内存中,因此,我们可以先在该函数处下断点,当该函数的soname即动态库的名称为libc.so之类的你需要调试的库名称时,该函数的第二个参数vaddr对应的值就是动态库被加载时的起始线性地址值。

    另外,还要注意的是,同一个库可能会调用zlox_load_shared_library函数多次,比如:libcommon.so的动态库,它除了被可执行文件依赖外,还被libc.so之类的库所依赖,因此,你会看到两次zlox_load_shared_library函数都是加载libcommon.so,当然只有第一次是真正的加载操作,第二次执行该函数时,内核会先判断libcommon.so是否被加载过了,如果被加载过了,就直接从zlox_load_shared_library函数里返回,因此,只有第一次zlox_load_shared_library函数对应的vaddr值才是真正的起始线性地址。

    例如,在作者的gdb里,当加载libc.so动态库时,其调试情况如下:


图3

    可以看到,当soname为 hd/lib/libc.so 时(之所以为什么是这个soname值,在之前讲解makefile时解释过),其vaddr值为0x80006000,也就是说libc.so被加载时的起始线性地址为0x80006000,接着,我们再在Linux命令行中,通过readelf命令查看下生成的libc.so的.text代码段的起始地址:

$ readelf -a build_initrd_img/extra/output/libc.so 
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x1000
  Start of program headers:          52 (bytes into file)
  Start of section headers:          36736 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         3
  Size of section headers:           40 (bytes)
  Number of section headers:         26
  Section header string table index: 23

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .hash             HASH            00000094 000094 000254 04   A  2   0  4
  [ 2] .dynsym           DYNSYM          000002e8 0002e8 000500 10   A  3   1  4
  [ 3] .dynstr           STRTAB          000007e8 0007e8 0003cd 00   A  0   0  1
  [ 4] .rel.dyn          REL             00000bb8 000bb8 000040 08   A  2   0  4
  [ 5] .rel.plt          REL             00000bf8 000bf8 000150 08   A  2   6  4
  [ 6] .plt              PROGBITS        00000d50 000d50 0002b0 04  AX  0   0 16
  [ 7] .text             PROGBITS        00001000 001000 001ca1 00  AX  0   0  4
..................................................
$


    可以看到.text代码段的起始地址为0x1000,这里需要注意的是:不要使用ELF Header部分的Entry point address(入口地址值),尽管在这里入口地址与起始地址相同,但是很多时候,入口地址并不等于起始地址,你可以用readelf命令查看ee可执行文件,就可以看到,入口地址与.text代码段的起始地址并不相同,这是调试时容易犯的错误。

    0x1000 + 0x80006000 = 0x80007000 这个相加的结果0x80007000就是add-symbol-file命令要使用的线性地址了:


图4

    可以看到,在add-symbol-file加入libc.so的符号信息后,再通过dir命令设置libc.so源代码的目录位置,接着就可以通过b命令在libc.c文件的getch函数处下断点了,当用户在ee编辑器里需要等待用户输入时,getch函数就会马上中断下来,这样就可以对libc.c即libc.so动态库的源文件进行调试了。

单独的用户堆:

   当前版本中,每个任务都有自己独立的用户堆空间,在zlox_task.c文件中,zlox_initialise_tasking函数创建初始化任务和zlox_fork函数创建子任务时,都会通过zlox_create_uheap函数来创建任务对应的用户堆空间:

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

ZLOX_VOID zlox_initialise_tasking()
{
	...................................................
	current_task->id = 1;
	current_task->esp = current_task->ebp = 0;
	current_task->eip = 0;
	current_task->init_esp = 0xE0000000;
	current_task->page_directory = current_directory;
	current_task->heap = zlox_create_uheap(ZLOX_UHEAP_START, ZLOX_UHEAP_START + ZLOX_UHEAP_INITIAL_SIZE, ZLOX_UHEAP_MAX,
						0, 0);
	current_task->next = 0;
	current_task->prev = 0;
	current_task->parent = 0;
	current_task->kernel_stack = 0xF0000000;
	...................................................
}

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

ZLOX_SINT32 zlox_fork()
{
	// We are modifying kernel structures, and so cannot interrupt
	//asm volatile("cli");

	// Take a pointer to this process' task struct for later reference.
	ZLOX_TASK * parent_task = (ZLOX_TASK *)current_task;

	// Clone the address space.
	ZLOX_PAGE_DIRECTORY * directory = zlox_clone_directory(current_directory,0);

	// Create a new process.
	ZLOX_TASK * new_task = (ZLOX_TASK *)zlox_kmalloc(sizeof(ZLOX_TASK));

	new_task->sign = ZLOX_TSK_SIGN;
	new_task->id = zlox_pop_pid(ZLOX_TRUE);
	...................................................

	// We could be the parent or the child here - check.
	if (current_task == parent_task)
	{
		...................................................
	}
	else
	{
		// We are the child.
		current_task->heap = zlox_create_uheap(ZLOX_UHEAP_START, ZLOX_UHEAP_START + ZLOX_UHEAP_INITIAL_SIZE, ZLOX_UHEAP_MAX,
						0, 0);
		return 0;
	}
}

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


    上面的ZLOX_UHEAP_START、ZLOX_UHEAP_INITIAL_SIZE 以及 ZLOX_UHEAP_MAX的宏都是定义在新增的zlox_uheap.h头文件中的,而zlox_create_uheap函数则是定义在新增的zlox_uheap.c文件中:

ZLOX_HEAP * zlox_create_uheap(ZLOX_UINT32 start, ZLOX_UINT32 end_addr, 
							ZLOX_UINT32 max, ZLOX_UINT8 supervisor, ZLOX_UINT8 readonly)
{
	ZLOX_HEAP *heap = (ZLOX_HEAP *)zlox_kmalloc(sizeof(ZLOX_HEAP));

	// All our assumptions are made on startAddress and endAddress being page-aligned.
	ZLOX_ASSERT_UHEAP(start%0x1000 == 0);
	ZLOX_ASSERT_UHEAP(end_addr%0x1000 == 0);

	zlox_pages_alloc(start, (end_addr - start));

	heap->index = zlox_place_ordered_array((ZLOX_VOID *)start, ZLOX_UHEAP_INDEX_SIZE, &zlox_uheap_header_less_than);
	start += sizeof(ZLOX_VPTR) * ZLOX_UHEAP_INDEX_SIZE;
	// Make sure the start address is page-aligned.
	if ((start & 0x00000FFF) != 0)
	{
		start &= 0xFFFFF000;
		start += 0x1000;
	}
	// Write the start, end and max addresses into the heap structure.
	heap->start_address = start;
	heap->end_address = end_addr;
	heap->max_address = max;
	heap->supervisor = supervisor;
	heap->readonly = readonly;
	// We start off with one large hole in the index.
	// 刚开始创建的堆,除了堆头部的index指针数组外,其余部分是一个大的hole,这样zlox_kmalloc函数就可以从该hole里分配到内存
	ZLOX_KHP_HEADER *hole = (ZLOX_KHP_HEADER *)start;
	hole->size = end_addr-start;
	hole->magic = ZLOX_HEAP_MAGIC;
	hole->is_hole = 1;
	ZLOX_KHP_FOOTER *hole_footer = (ZLOX_KHP_FOOTER *)((ZLOX_UINT32)hole + hole->size - sizeof(ZLOX_KHP_FOOTER));
	hole_footer->magic = ZLOX_HEAP_MAGIC;
	hole_footer->header = hole;
	zlox_insert_ordered_array((ZLOX_VPTR)hole, &heap->index);
	return heap;
}


    该函数的第一个参数用于指定用户堆的起始线性地址,第二个参数用于指定用户堆的结束线性地址,第三个参数用于指定用户堆可以扩充的最大线性地址,第四个参数用于指定堆扩容时,扩充的虚拟内存是否是用户可访问的,0表示用户可访问,1表示不可访问,最后一个参数用于指定扩充的虚拟内存是否是用户可写的,0表示可写,1表示只读不可写。此外,该函数与zlox_kheap.c(内核堆模块)里的zlox_create_heap创建内核堆的函数的区别在于,该函数会在内部通过zlox_pages_alloc函数为用户堆分配物理内存页面。

进程的虚拟内存布局:

    之前的zenglOX v0.0.8 Multitask(多任务)的文章里的图1显示了当时v0.0.8版本的内存布局情况,不过后面的版本又对内存布局做了调整(比如当前版本就新增了用户堆),当前版本的虚拟内存布局如下图所示:


图5

    其中,所有任务的内核代码,E1000网卡的内存映射以及内核堆部分都是共用的,即所有任务的这几个共用部分对应的物理内存页面都是相同的。而用户程式、动态链接库、用户栈、内核栈以及用户堆部分都是独立的,也就是每个任务的这几个独立的部分所对应的物理内存都是不同的页面,相互之间是不可见的。

    当前版本的ps命令新增了一个-u参数,可以通过ps -u命令来查看,如下图所示:


图6

    上图的uheap start为0xf0029000,而前面图5里用户堆是从0xf0001000开始的,这是因为0xf0001000到0xf0029000之间的部分是hole指针数组部分,该部分有160K的大小,因此,最多可以存储40960个hole指针。

    此外,图5中所示的E1000网卡的内存映射部分是不占用实际的物理内存页面的,它对应的物理内存地址其实是网卡映射到系统中的,这些映射的内存实际是存储在网卡中的,对这些映射内存的读写操作所产生的效果都是由网卡来决定的。

各种BUG修复:

    在zlox_ata.c文件中,对zlox_atapi_drive_read_capacity函数做了调整:

/* read capacity from the given ide_index into the buffer */
ZLOX_SINT32 zlox_atapi_drive_read_capacity (ZLOX_UINT32 ide_index, ZLOX_UINT8 *buffer)
{
	..................................................
	/* Send ATAPI/SCSI command */
	zlox_outsw (ZLOX_ATA_DATA (bus), (ZLOX_UINT16 *) read_cmd, 6);
	/* Tell the scheduler that this process is using the ATA subsystem. */
	//current_task->status = ZLOX_TS_ATA_WAIT;
	//ata_wait_task = current_task;
	/* Wait for IRQ that says the data is ready. */
	//zlox_switch_task();
	while ((status = zlox_inb (ZLOX_ATA_COMMAND (bus))) & 0x80)     /* BUSY */
		asm volatile ("pause");
	while (!((status = zlox_inb (ZLOX_ATA_COMMAND (bus))) & 0x8) && !(status & 0x1))
		asm volatile ("pause");
	/* DRQ or ERROR set */
	if (status & 0x1) {
		size = -1;
		goto end;
	}
	..................................................
}

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

static ZLOX_VOID zlox_ata_callback(/*ZLOX_ISR_REGISTERS * regs*/)
{
	if(ata_wait_task != ZLOX_NULL)
	{
		ata_wait_task->status = ZLOX_TS_RUNNING;
		ata_wait_task = ZLOX_NULL;
	}
}


    在zlox_atapi_drive_read_capacity函数里,红色部分是当前版本注释掉的代码,以前的版本在读取光盘里的数据时,会先将当前任务设置为 ZLOX_TS_ATA_WAIT 的等待状态,然后通过zlox_switch_task函数切换到其他任务去执行,当收到磁盘控制器发来的中断信号时,再将当前等待的任务设置为 ZLOX_TS_RUNNING 运行状态,但是有时候系统会收不到这样的中断信号,从而导致这些等待的任务一直处于睡眠状态,因此当前版本就采用while循环检测状态的方式,来避免出现这类的问题。

    当前版本还对zenglfs文件系统的BUG进行了相关修复,首先是zlox_zenglfs.c文件的zlox_zenglfs_get_inode_blk函数中,之前的版本,在对zenglfs_cache_doubly之类的缓存数据进行修改后,并没有及时设置这些缓存的isDirty字段,从而导致这些缓存里被修改的数据无法同步到磁盘中,这样会导致某些写入磁盘的文件数据不完整。当前版本就在必要的地方对这些缓存的isDirty字段进行了设置,以避免此类BUG 。

    另外,还对该文件的zlox_zenglfs_write函数进行了BUG修复:

static ZLOX_UINT32 zlox_zenglfs_write(ZLOX_FS_NODE * node, ZLOX_UINT32 arg_offset, ZLOX_UINT32 size, ZLOX_UINT8 * buffer)
{
	..................................................

	ZLOX_UINT32 tmp_off = 0, /*before_totalblk,*/ blk, lba, i, nwrites = 0;
	ZLOX_BOOL need_memcpy = ZLOX_FALSE;
	if(size % ZLOX_ZLFS_BLK_SIZE != 0)
		need_memcpy = ZLOX_TRUE;
	//before_totalblk = zenglfs_cache_inode.data.totalblk;
	for(i=0;tmp_off < size;i++, tmp_off += ZLOX_ZLFS_BLK_SIZE)
	{
		blk = zlox_zenglfs_get_inode_blk(i, ZLOX_TRUE);
		..................................................
	}
	//if(before_totalblk > zenglfs_cache_inode.data.totalblk)
	for(i=nwrites;(blk =zlox_zenglfs_free_inode_blk(i));i++)
		;

	zenglfs_cache_inode.data.size = size;
	zenglfs_cache_inode.isDirty = ZLOX_TRUE;
	zlox_zenglfs_sync_cache_to_disk();
	return size;
}


    该函数里,将before_totalblk相关的语句都注释掉了,因为当覆盖写入某文件的内容时,如果要写入的数据比原来的文件数据小的话,zenglfs_cache_inode.data.totalblk字段的值并不会发生任何变化,因为zlox_zenglfs_get_inode_blk函数会将已存在的块号直接返回,在这种情况下,if语句里的before_totalblk始终等于totalblk的值,就会不执行zlox_zenglfs_free_inode_blk的释放块函数,导致的结果就是,原来一个40k大小的文件,你以覆盖的方式写入该文件2K的数据,但是它的文件尺寸却没有变为2K,还是原来的40K的大小。因此,将if判断语句注释掉,让该函数始终执行释放块操作,就可以解决这个问题。另外,如果是新建一个文件的话,释放块函数会直接返回,因此不会对新建等操作产生影响。

    当前版本还修复了分页相关的BUG,主要是对zlox_paging.c文件的zlox_clone_directory函数做了些调整:

ZLOX_PAGE_DIRECTORY * zlox_clone_directory(ZLOX_PAGE_DIRECTORY * src , ZLOX_UINT32 needCopy)
{
	ZLOX_UINT32 phys;
	// Make a new page directory and obtain its physical address.
	ZLOX_PAGE_DIRECTORY * dir = (ZLOX_PAGE_DIRECTORY *)zlox_kmalloc_ap(sizeof(ZLOX_PAGE_DIRECTORY), &phys);
	// Ensure that it is blank.
	zlox_memset((ZLOX_UINT8 *)dir, 0, sizeof(ZLOX_PAGE_DIRECTORY));

	// Get the offset of tablesPhysical from the start of the page_directory_t structure.
	//ZLOX_UINT32 offset = (ZLOX_UINT32)dir->tablesPhysical - (ZLOX_UINT32)dir;

	// Then the physical address of dir->tablesPhysical is:
	//dir->physicalAddr = phys + offset;

	// 因为一个8K的虚拟内存空间,可以对应两个非连续的物理内存页面,因此简单的用phys + offset得到的结果不一定正确, 
	// 尤其是在多任务环境下,必须用下面的方法来得出实际的正确的物理内存地址,否则,错误的cr3页表地址会让整个系统直接崩溃掉!
	ZLOX_UINT32 * phy_page = (ZLOX_UINT32 *)zlox_get_page((ZLOX_UINT32)dir->tablesPhysical, 0,
								 current_directory);
	dir->physicalAddr = ((*phy_page) & 0xfffff000);

	...................................................
}


    一个ZLOX_PAGE_DIRECTORY页目录的大小是8K,其结构定义在zlox_paging.h的头文件里:

struct _ZLOX_PAGE_DIRECTORY
{
	/**
	   Array of pointers to pagetables.
	**/
	ZLOX_PAGE_TABLE *tables[1024];
	/**
	   Array of pointers to the pagetables above, but gives their *physical*
	   location, for loading into the CR3 register.
	**/
	ZLOX_UINT32 tablesPhysical[1024];

	/**
	   The physical address of tablesPhysical. This comes into play
	   when we get our kernel heap allocated and the directory
	   may be in a different location in virtual memory.
	**/
	ZLOX_UINT32 physicalAddr;
};


    我们需要将tablesPhysical的物理内存地址写入到cr3寄存器中,在上面zlox_clone_directory函数中,之前版本采用的方式是,直接用zlox_kmalloc_ap函数返回的phys物理地址加上tablesPhysical在结构体中的偏移值来得到tablesPhysical的物理内存地址,但是,对于一个8K的数据结构,它的数据可以分布在两个非连续的物理内存页面中,尤其是在多任务的环境下,因此,采用偏移值的方式得到的物理内存地址就不一定是正确的了,当物理地址错误时,写入cr3寄存器后,会导致整个系统直接崩溃掉。因此必须使用zlox_get_page函数来获取tablesPhysical的实际的正确的物理地址。

新增的系统调用:

    当前版本一共新增了10个系统调用,可以在zlox_syscall.h头文件中查看到这几个系统调用:

ZLOX_DECL_SYSCALL0(monitor_clear)
ZLOX_DECL_SYSCALL1(monitor_set_single, ZLOX_BOOL)
ZLOX_DECL_SYSCALL2(monitor_set_cursor, ZLOX_UINT8, ZLOX_UINT8)
ZLOX_DECL_SYSCALL4(writedir_fs_safe, void * , ZLOX_CHAR *, ZLOX_UINT16 , void *)
ZLOX_DECL_SYSCALL3(readdir_fs_safe, void * , ZLOX_UINT32 , void *)
ZLOX_DECL_SYSCALL3(finddir_fs_safe, void *, ZLOX_CHAR *, void *)
ZLOX_DECL_SYSCALL0(get_control_keys)
ZLOX_DECL_SYSCALL1(release_control_keys, ZLOX_UINT8)
ZLOX_DECL_SYSCALL0(monitor_del_line)
ZLOX_DECL_SYSCALL0(monitor_insert_line)


    其中,monitor开头的系统调用是和显示输出相关的,例如,monitor_clear用于清屏等,这些显示输出相关的系统调用在开发ee编辑器时起到了很大的作用。

    get_control_keys用于获取当前键盘上的shift,ctrl,alt的控制键的按下状态。release_control_keys用于释放掉某个控制键的按下状态(在开发ee编辑器时,暂时没用到)。这两个和按键相关的系统调用的核心代码都位于zlox_keyboard.c文件中:

ZLOX_UINT8 zlox_get_control_keys()
{
	return control_keys;
}

ZLOX_UINT8 zlox_release_control_keys(ZLOX_UINT8 key)
{
	switch(key)
	{
	case ZLOX_CK_SHIFT:
		control_keys &= (0xFF - ZLOX_CK_SHIFT);
		break;
	case ZLOX_CK_ALT:
		control_keys &= (0xFF - ZLOX_CK_ALT);
		break;
	case ZLOX_CK_CTRL:
		control_keys &= (0xFF - ZLOX_CK_CTRL);
		break;
	}
	return control_keys;
}


    最后,writedir_fs_safereaddir_fs_safefinddir_fs_safe这三个系统调用是读写文件节点时,多任务安全的系统调用,所以有个_safe的后缀,另外还有三个之前版本中创建的 writedir_fs、readdir_fs 以及 finddir_fs 三个系统调用,这三个系统调用并非多任务安全的,因为它们返回的ZLOX_FS_NODE文件节点指针,指向的是内核里的某个公用的数据结构,当发生任务切换时,这个公用的数据结构就会被其他任务修改掉。和 writedir_fs_safereaddir_fs_safefinddir_fs_safe 这三个系统调用相关的核心代码位于 zlox_fs.c 文件中:

ZLOX_SINT32 zlox_writedir_fs_safe(ZLOX_FS_NODE * node, ZLOX_CHAR *name, ZLOX_UINT16 type, ZLOX_FS_NODE * output)
{
	if(node == 0)
		return -1;
	if(node->writedir != 0)
	{
		ZLOX_FS_NODE * ret = node->writedir(node, name, type);
		if(ret == ZLOX_NULL)
			return -1;
		if(output == ZLOX_NULL)
			return -1;
		zlox_memcpy((ZLOX_UINT8 *)output, (ZLOX_UINT8 *)ret, sizeof(ZLOX_FS_NODE));
		return 0;
	}
	else
		return -1;
}

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

ZLOX_SINT32 zlox_readdir_fs_safe(ZLOX_FS_NODE *node, ZLOX_UINT32 index, ZLOX_DIRENT * output)
{
	if (node == 0)
		return -1;

	// Is the node a directory, and does it have a callback?
	if ( (node->flags & 0x7) == ZLOX_FS_DIRECTORY &&
		node->readdir != 0 )
	{
		ZLOX_DIRENT * ret = node->readdir(node, index);
		if(ret == ZLOX_NULL)
			return -1;
		if(output == ZLOX_NULL)
			return -1;
		zlox_memcpy((ZLOX_UINT8 *)output, (ZLOX_UINT8 *)ret, sizeof(ZLOX_DIRENT));
		return 0;
	}
	else
		return -1;
}

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

ZLOX_SINT32 zlox_finddir_fs_safe(ZLOX_FS_NODE *node, ZLOX_CHAR *name, ZLOX_FS_NODE * output)
{
	if (node == 0)
		return -1;

	// Is the node a directory, and does it have a callback?
	if ( (node->flags & 0x7) == ZLOX_FS_DIRECTORY &&
		node->finddir != 0 )
	{
		ZLOX_FS_NODE * ret = node->finddir(node, name);
		if(ret == ZLOX_NULL)
			return -1;
		if(output == ZLOX_NULL)
			return -1;
		zlox_memcpy((ZLOX_UINT8 *)output, (ZLOX_UINT8 *)ret, sizeof(ZLOX_FS_NODE));
		return 0;
	}
	else
		return -1;
}


    可以看到这三个核心函数,最终都会通过zlox_memcpy函数将需要返回的ZLOX_FS_NODE结构体里的数据拷贝到用户指定的output缓冲里,这样,即便切换到另一个任务也不会对当前任务的数据结构产生影响了,在系统调用执行过程中,是不会发生任务切换的,因为eflags寄存器里的中断标志位被暂时关闭了,只有当系统调用执行完后,才会发生任务切换,因此,这里的拷贝操作都是多任务安全的。

    以上就是当前版本的相关内容,没有讲解的代码请读者通过gdb调试工具等进行分析。

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

下一篇: zenglOX v2.3.0 移植zengl嵌入式脚本语言

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

相关文章

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

zenglOX v1.0.0 shell(命令行程式及各种小工具)

zenglOX v3.1.0 Sound Blaster 16

zenglOX v0.0.2 VGA输出显示字符串

zenglOX v0.0.9 User Mode(用户模式)

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