你管这破玩意儿叫操作系统源码(一)
本文为学习操作系统源码 (低并发编程)所作笔记,仅供学习参考,不做任何商业用途,若有侵权,请联系删除。
第一回 | 最开始的两行代码
当你按下开机键的那一刻,在主板上提前写死的固件程序 BIOS 会将硬盘中启动区的 512 字节的数据,原封不动复制到内存中的 0x7c00 这个位置,并跳转到那个位置进行执行。
- 启动区的定义
- 0盘0道0扇区的512字节的最后两个字节是0x55和0xaa,便会被BIOS作为一个启动区
在Linux-0.11的最开始的代码中,启动区代码文件位于boot文件夹下。
通过编译,这个 bootsect.s 会被编译成二进制文件,存放在启动区的第一扇区。
bootsect.s
1 |
|
第二回 | 自己给自己挪个地儿
1 |
|
经过这些指令后,以下几个寄存器分别被附上了指定的值
- ds = 0x07c0
- es = 0x9000
- cx = 256
- si = 0
- di = 0
为这里寄存器赋值都是为下一条指令服务
1 |
|
rep 表示重复执行后面的指令
movw 表示复制一个字(16bits)
该指令就是不断重复地复制一个字。
- 重复次数:cx寄存器中的值,256次
- 从哪复制到哪:ds:si -> es:di
- 一次复制多少: 复制一个字,16bit
将内存地址 0x7c00 处开始往后的 512 字节的数据,原封不动复制到 0x90000 处。
现在,操作系统最开头的代码,已经被挪到了 0x90000 这个位置了。
再往后是一个跳转指令。
1 |
|
jmpi 是一个段间跳转指令,表示跳转到 0x9000:go 处执行。
段基址 : 偏移地址,段基址仍然要先左移四位,因此结论就是跳转到 0x90000 + go 这个内存地址处执行。
再说 go,go 就是一个标签,最终编译成机器码的时候会被翻译成一个值,这个值就是 go 这个标签在文件内的偏移地址。
这个偏移地址再加上 0x90000,就刚好是 go 标签后面那段代码 mov ax,cs 此时所在的内存地址了。
第三回 | 做好最最基础的准备工作
操作系统的代码最开头的 512 字节的数据,从硬盘的启动区先是被移动到了内存 0x7c00 处,然后又立刻被移动到 0x90000 处,并且跳转到此处往后再稍稍偏移 go 这个标签所代表的偏移地址处。
go标签代码
1 |
|
至此,操作系统的一些最最最最基础的准备工作就做好了。
- 将代码从硬盘移到0x90000处;
- 数据段寄存器ds和代码段寄存器cs被设置为0x90000;
- 栈顶地址被设置为0x9F0000,具体表现为栈段寄存器 ss 为 0x9000,栈基址寄存器 sp 为 0xFF00。栈顶地址远远大于此时代码所在的位置0x90000,栈向下发展比较安全。
扩展
所以本回的代码,正如标题所说,就是做好最最基础的准备工作。但要从更伟大的战略意义上讲,它其实是按照 Intel 手册上要求的,老老实实把这三类段寄存器的值设置好,达到了初步规划内存的目的。
第四回 | 把自己在硬盘里的其他部分也放到内存来
上一回简单说,就是设置了如何访问数据的数据段,如何访问代码的代码段,以及如何访问栈的栈顶指针,也即初步做了一次内存规划,从 CPU 的角度看,访问内存,就这么三块地方而已。
接着往下看:
1 |
|
- int指令,是汇编指令,不是高级语言的整型变量,
int 0x13
,表示发起0x13号中断,这条指令上面给dx, cx, bx, ax赋值都是作为这个终端程序的参数。 - 0x13 号中断的处理程序是 BIOS 提前给我们写好的,是读取磁盘的相关功能的函数。
本段代码的作用就是就是将硬盘的第 2 个扇区开始,把数据加载到内存 0x90200 处,共加载 4 个扇区。
如果复制成功,则跳转到ok_load_setup标签。
1 |
|
这段代码省略了很多非主逻辑的代码,比如在屏幕上输出 Loading system ... 这个字符串以防止用户等烦了。
剩下的主要代码就都写在这里了,就这么几行,其作用是把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处,和之前的从硬盘捣腾到内存是一个道理。
至此,整个操作系统的全部代码,就已经全部从硬盘中,被搬迁到内存来了。
然后又通过一个熟悉的段间跳转指令 jmpi 0,0x9020,跳转到 0x90200 处,就是硬盘第二个扇区开始处的内容。
整个操作系统的编译过程
通过 Makefile 和 build.c 配合完成:
1. 把 bootsect.s 编译成 bootsect 放在硬盘的 1 扇区。
2. 把 setup.s 编译成 setup 放在硬盘的 2~5 扇区。
3. 把剩下的全部代码(head.s 作为开头)编译成 system 放在硬盘的随后 240 个扇区。
所以,我们即将跳转到的内存中的 0x90200 处的代码,就是从硬盘第二个扇区开始处加载到内存的。第二个扇区的最开始处,那也就是 setup.s 文件的第一行代码。
在操作系统刚刚开始建立的时候,那是完全自己安排前前后后的关系,一个字节都不能偏,就是这么强耦合,需要小心翼翼,需要大脑时刻保持清醒,规划好自己写的代码被编译并存储在硬盘的哪个位置,而随后又会被加载到内存的哪个位置,不能错乱。
第五回 | 进入保护模式前的最后一次折腾内存
操作系统已经完成了各种从硬盘到内存的加载,以及内存到内存的复制。
至此,整个bootsect.s的使命就完成了,之后便跳转到了0x90200这个位置开始执行,这个位置的代码就是setup.s的开头,
1 |
|
int 0x10
- 触发BIOS提供的显示服务中断处理程序;
- ah寄存器,表示显示服务里具体的读取光标位置功能。
int 0x10中断程序执行完毕并返回时,dx寄存器里的之表示光标的位置,具体来说高八位dh存储了行号,低八位存储了列号。
说明:计算机在加电自检后会自动初始化到文字模式,在这种模式下,整个屏幕可以显示25行,每行80个字符,也就是80列。
mov [0], dx
就是把这个光标位置存储在[0]这个内存地址处(段基址ds=0x90000)。这里存放着光标的位置,以便之后在初始化控制台的时候用到。
这个指令和平时调用方法没什么区别,只不过这里的寄存器的用法相当于入参和返回值,这里的0x10中断号相当于方法名。
这里又应了之前说的一句话,操作系统内核的最开始处处也都是BIOS的调包侠。
1 |
|
最终存储在内存中的信息是什么,在什么位置,
内存地址 | 长度(字节) | 名称 |
---|---|---|
0x90000 | 2 | 光标位置 |
0x90002 | 2 | 扩展内存数 |
0x90004 | 2 | 显示页面 |
0x90006 | 1 | 显示模式 |
0x90007 | 1 | 字符列数 |
0x90008 | 2 | 未知 |
0x9000A | 1 | 显示内存 |
0x9000B | 1 | 显示状态 |
0x9000C | 2 | 显卡特性参数 |
0x9000E | 1 | 屏幕行数 |
0x9000F | 1 | 屏幕列数 |
0x90080 | 16 | 硬盘1参数表 |
0x90090 | 16 | 硬盘2参数表 |
0x901FC | 2 | 根设备号 |
由于之后很快就会用 c 语言进行编程,虽然汇编和 c 语言也可以用变量的形式进行传递数据,但这需要编译器在链接时做一些额外的工作,所以这么多数据更方便的还是双方共同约定一个内存地址,我往这里存,你从这里取,就完事了。这恐怕是最最原始和直观的变量传递的方式了。
继续往下看,
1 |
|
因为后面我们要把原本是 BIOS 写好的中断向量表给覆盖掉,也就是给破坏掉了,写上我们自己的中断向量表,所以这个时候是不允许中断进来的。
1 |
|
rep movsw
同前面的原理一样,也是做了个内存复制操作,最终的结果是,把内存地址
0x10000 处开始往后一直到 0x90000
的内容,统统复制到内存的最开始的 0
位置,大概就是这么个效果。
栈顶地址仍然是 0x9FF00 没有改变。
0x90000 开始往上的位置,原来是 bootsect 和 setup 程序的代码,现 bootsect 的一部分代码在已经被操作系统为了记录内存、硬盘、显卡等一些临时存放的数据给覆盖了一部分。
内存最开始的 0 到 0x80000 这 512K 被 system 模块给占用了,之前讲过,这个 system 模块就是除了 bootsect 和 setup 之外的全部程序链接在一起的结果,可以理解为操作系统的全部。
那么现在的内存布局就是这个样子。
接下来,就要进行有点技术含量的工作了,那就是模式的转换,需要从现在的 16 位的实模式转变为之后 32 位的保护模式,这是一项大工程!