写个小操作系统1
0x00 开宗明义
对OS的兴趣一直都有。最近比较强。因为5月份广东红帽杯被血虐,想补强下二进制方面。后来发现自己之前的相关知识深度还不够,继续深入时,好多东西和OS绕不开。所以,在原来的基础上补下OS。写个OS这种标题,有点标题党,只是给自己定的一个目标。最终完成的功能包含以下,不能对比现在的Linux这种大型成熟的OS。
- 启动
- 基本的内存管理
- 多任务调度
- 一个简单的shell
希望写的过程和实验的过程能加深自己对OS的理解。
以前看过Linux2.6的内核代码。启动的过程是,
- 加载器把内核vmlinux,加载入内存,内核启动开始。
- 进入保护模式,开启分页
- 获取主机的信息。内存,cpu,中断。。
- 建立内存管理数据
- 建立第一个进程
- 启动shell
当然很多细节没有展开。
我的想法是和这个流程相似。当然有些实现的细节,可能会有差别。会参考以下内容 - 《自己动手写操作系统》
- 《使用开源软件写操作系统》
- Linux 0.11的源码
这里,会着重从加载开始。有必要学习下汇编。所以,加载器我会自己写,参考的两本书,关于加载器的有些细节,没有说明,我会尽可能把自己已理解的说出来。我还专门买了本 Professional Assembly Language的中译本,来作为At&t汇编的参考。
0x01 加载器
加载器是OS启动的第一步。在使用PC的日常中,会出现磁盘主引导坏掉的情况,电脑会提示,No boot loader found。
这是在Virtualbox里,找不到引导时的提示。结果就是OS启动不了了。
加载器是什么:
加载器是位于磁盘第一个扇区的特殊程序,它由BIOS加载,它负责加载OS的代码到特定位置,然后把执行权交给OS的初始化代码。
BIOS:
Basic Input/Output System。它是固化在主板芯片里的一段代码,也叫固件(Firmware),
BIOS,其实才是电脑接通电源后执行的第一个程序。加电后,CPU会去内存固定的位置取指令执行,BIOS的代码会放在那个位置。它会在内存它的职能如下:
- 上电自检
台式机在启动时,能听到‘滴’的一声,是主板在自检。如果出现异常,常见的是内存金手指接触不良,电脑就无法启动。 - 加载加加载器
这个工作是在自检之后。如果自检正常,BIOS会把启动设备(磁盘,软盘,U盘)第一个磁道加载到内存中,然后,将执行权给这个加载器。 - 系统设置
BIOS会探测系统的硬件配置。所以在BIOS中,会看到内存大小,CPU的频率等信息。 - 中断
BIOS是为加载器的执行提供了环境。没有这个环境的话,加载器无法运行。
软件的运行需要环境的支持。比如,常用的Office软件,是运行在Windows操作系统上的。Windows操作系统,需要基本的内存,CPU,和外围设备的支持,还有驱动程序。
所以说运行环境分为两部分:软件和硬件。
但当电脑刚上电时,主板上所有的芯片,都处于初始状态,此时只有硬件环境,BIOS就是在这样的环境下运行的。
当加载器运行时,已经具备了一定的软件环境了。对于X86主机来说,此时的环境是实地址模式。
可用内存空间为:1M
因为:此时的地址总线宽度为16,一次的寻址空间为2^16, 64kB。Intel使用了内存分段技术,地址总线实际宽度为20,最大内存为1M。分为16个段。
逻辑地址表达形式为:段:偏移
计算物理地址是:段×64KB + 偏移
当加载器运行时,内存可以用的大小有1M了。中断也可以用了,稍微设置下栈也可以用。
我们要写一个加载器,知道以上的信息了。还需要以下信息,加载器是怎么加载到内存的,如何才能开始执行。
- BIOS会把引导扇区512字节的内容加载到0x0000:0x7c00的位置,然后,跳转到此位置,开始执行。没错前面的0x0000就是段地址。0x7c00是段内偏移地址。
定加载器之前,熟悉下执行环境。
我使用的工具是bochs,可以调试OS,很牛的一个模拟器。
我是自己编译的,安装在/opt目录下,不影响系统。
看到了吧。
看参考书里,代码里都有如下的处理:
参考书1:.org 0x7c00
参考书2:在链接脚本里,加的 .=0x7c00
倒是觉得,这样做的原因,两书原因都没有说明白。确实是将代码开始的位置,对齐为0x7c00,但为什么是这样做就能达到效果了呢?
这里其实跟代码的链接和加载过程有关。在常规的OS里,代码编译好之后,生成的可执行文件里指令其实是可以重定位。就是在加载时,加载器会修改程序的入口偏移量。
但是在加载器中,加载器是由BIOS直接加载进内存,里面的指令的偏移量是不会变的。也就是说,默认的话,编译器是把第一条指令偏移量为0,如果其中有变量的话,变量的地址就是绝对的地址。此时如果加载到了0x7c00,但变量的位置地址没有修正的话,这时访问绝对地址,是肯定是访问不到的。
这里拿书中的代码来实验下。
1 设置不加载到0x7c00,同样能显示出字符来。nasm汇编格式
1 | mov ax, cs |
参考书里第一个代码,去掉了开头的org 0x7c00,不引用字符串的地址和调用函数DispStr。修改后的代码,调用int 10h的中断在屏幕上显示一个a,对应的ASCII是97.运行后,屏幕上成功出现了一个字母a。说明理解是对的。
如果想显示一个字符串呢。
先分析下这段代码,
前3行是设置环境的,为什么是这样设置,书中也没有说。前面说过,BIOS加载第一个扇区后,会跳到0x0000:0x7c00去执行,这里的0x0000是指令的段地址。指令的段地址,是放在cs中的。也就是说,刚开始时,只有cs的值是确定的。ds是数据段,因为原来的代码里用到了BootMessage变量,所以要设置ds,es是额外的寄存器,没有用到时,本来是不用设置的。这里我们的代码里没有用到,所以也可以去掉。
经过研究后,可以显示一个字符串的at&t的代码如下:
1 | .code16 |
编译的命令是
1 | as -o hello.o hello.s |
生成的hello.img就是一个虚拟的软盘。加载到bochs里,就能显示出字符串了
有兴趣的话,可以多显示一些。定字符串时的地址使用的是绝对,担心和代码段有冲突,将ds值置为1,使用的内存是
0x0001:0x0000 - 0x0001:0xffff
内存里的变量,严格来说都是数据,Intel实模式下,就是这样的规则。
注意,向地址内传送数据时,movb后的b不可少。少了它编译器就不知道要操作的内存单元是多大的了。
段寄存器
经过把玩实模式的一些实例,对段寄存器也有了更深入的理解。
cs,就是代码段的段地址。它配合ip寄存器,指示了下个指令的位置。跳转时,如果是远跳转的话,cs寄存器的值也是会变的,但其他情况下,我们不会修改这个寄存器。
ds,数据段。这个比较自由。想把数据存在什么位置,都可以直接操作它。极端来说,我们可以设置ds的值和cs同段,把当前的代码修改掉。有点像二进制里的控制流劫持一样。为什么能这样做呢?因为实模式里没有任何保护机制。这也是后面保护模式优势。在编写汇编代码时,程序员要对内存的布局非常清楚,哪段内存是什么样的用途,要不然,自己把自己玩死了。
ss, 栈段。这个段是用来设置栈的。设置了它之后,再设置下sp和bp,就手动建立起来了一个栈。当然程序里可以有多个栈。切换到不同栈时,正确设置ss,sp, bp的值就可以了。
gs,fs,es,额外段。8086提供的备用的段。除了在个别命令中有特别用途,这个程序员可以随便用。