写个小操作系统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。

Alt text

这是在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目录下,不影响系统。

Alt text
Alt text

看到了吧。

看参考书里,代码里都有如下的处理:
参考书1:.org 0x7c00
参考书2:在链接脚本里,加的 .=0x7c00

倒是觉得,这样做的原因,两书原因都没有说明白。确实是将代码开始的位置,对齐为0x7c00,但为什么是这样做就能达到效果了呢?
这里其实跟代码的链接和加载过程有关。在常规的OS里,代码编译好之后,生成的可执行文件里指令其实是可以重定位。就是在加载时,加载器会修改程序的入口偏移量。
但是在加载器中,加载器是由BIOS直接加载进内存,里面的指令的偏移量是不会变的。也就是说,默认的话,编译器是把第一条指令偏移量为0,如果其中有变量的话,变量的地址就是绝对的地址。此时如果加载到了0x7c00,但变量的位置地址没有修正的话,这时访问绝对地址,是肯定是访问不到的。

这里拿书中的代码来实验下。
1 设置不加载到0x7c00,同样能显示出字符来。nasm汇编格式

1
2
3
4
5
6
7
8
9
10
11
    mov ax, cs                                                                     
mov ds, ax
mov es, ax
mov ah, 0x0e
mov al, 97
mov bl, 0x00
int 10h
jmp $

times 510-($-$$) db 0
dw 0xaa55

参考书里第一个代码,去掉了开头的org 0x7c00,不引用字符串的地址和调用函数DispStr。修改后的代码,调用int 10h的中断在屏幕上显示一个a,对应的ASCII是97.运行后,屏幕上成功出现了一个字母a。说明理解是对的。
Alt text

如果想显示一个字符串呢。
先分析下这段代码,
前3行是设置环境的,为什么是这样设置,书中也没有说。前面说过,BIOS加载第一个扇区后,会跳到0x0000:0x7c00去执行,这里的0x0000是指令的段地址。指令的段地址,是放在cs中的。也就是说,刚开始时,只有cs的值是确定的。ds是数据段,因为原来的代码里用到了BootMessage变量,所以要设置ds,es是额外的寄存器,没有用到时,本来是不用设置的。这里我们的代码里没有用到,所以也可以去掉。

经过研究后,可以显示一个字符串的at&t的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.code16
.text
mov $0x1, %ax
mov %ax, %ds
movb $'H', (0x0000)
movb $'e', (0x0001)
movb $'l', (0x0002)
movb $'l', (0x0003)
movb $'o', (0x0004)
movb $',', (0x0005)
movb $'O', (0x0006)
movb $'S', (0x0007)
movb $'w', (0x0008)
mov $0x0201, %dx
mov $0, %bh
mov $0x0, %bp
mov $9, %cx
mov $0x13, %ah
mov $0x01, %bl
mov $0, %al
int $0x10
jmp .

.org 510
.word 0xaa55

编译的命令是

1
2
3
4
as -o hello.o hello.s
ld -o hello.bin --oformat=binary hello.o
dd if=hello.bin of=hello.img bs=512 count=1
dd if=/dev/zero of=hello.img skip=1 seek=1 bs=512 count=2879

生成的hello.img就是一个虚拟的软盘。加载到bochs里,就能显示出字符串了
Alt text
有兴趣的话,可以多显示一些。定字符串时的地址使用的是绝对,担心和代码段有冲突,将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提供的备用的段。除了在个别命令中有特别用途,这个程序员可以随便用。