你管这破玩意儿叫操作系统源码(一)

本文为学习操作系统源码 (低并发编程)所作笔记,仅供学习参考,不做任何商业用途,若有侵权,请联系删除。

第一回 | 最开始的两行代码

你管这破玩意叫操作系统源码 | 第一回 最开始的两行代码 (qq.com)

当你按下开机键的那一刻,在主板上提前写死的固件程序 BIOS 会将硬盘中启动区的 512 字节的数据,原封不动复制到内存中的 0x7c00 这个位置,并跳转到那个位置进行执行。

  • 启动区的定义
    • 0盘0道0扇区的512字节的最后两个字节是0x550xaa,便会被BIOS作为一个启动区

在Linux-0.11的最开始的代码中,启动区代码文件位于boot文件夹下。

通过编译,这个 bootsect.s 会被编译成二进制文件,存放在启动区的第一扇区。

bootsect.s

1
2
mov ax, 0x07c0	# x86架构在16位的实模式下可以访问2位的地址线,从而ds得到的短地址左移四位会变为0x7c00,与BIOS加载到内存的地址一致
mov ds, ax # ds为16位段寄存器,内存寻址充当段基址的作用

第二回 | 自己给自己挪个地儿

你管这破玩意叫操作系统源码 | 第二回 自己给自己挪个地儿 (qq.com)

1
2
3
4
5
6
7
8
mov ax,0x07c0
mov ds,ax
mov ax,0x9000
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep movw

经过这些指令后,以下几个寄存器分别被附上了指定的值

  • ds = 0x07c0
  • es = 0x9000
  • cx = 256
  • si = 0
  • di = 0

为这里寄存器赋值都是为下一条指令服务

1
rep movw

rep 表示重复执行后面的指令

movw 表示复制一个字(16bits)

该指令就是不断重复地复制一个字。

  • 重复次数:cx寄存器中的值,256次
  • 从哪复制到哪:ds:si -> es:di
  • 一次复制多少: 复制一个字,16bit

将内存地址 0x7c00 处开始往后的 512 字节的数据,原封不动复制到 0x90000 处

现在,操作系统最开头的代码,已经被挪到了 0x90000 这个位置了。

再往后是一个跳转指令。

1
2
3
4
jmpi go,0x9000
go:
mov ax,cs
mov ds,ax

jmpi 是一个段间跳转指令,表示跳转到 0x9000:go 处执行。

段基址 : 偏移地址,段基址仍然要先左移四位,因此结论就是跳转到 0x90000 + go 这个内存地址处执行。

再说 go,go 就是一个标签,最终编译成机器码的时候会被翻译成一个值,这个值就是 go 这个标签在文件内的偏移地址。

这个偏移地址再加上 0x90000,就刚好是 go 标签后面那段代码 mov ax,cs 此时所在的内存地址了。

第三回 | 做好最最基础的准备工作

你管这破玩意叫操作系统源码 | 第三回 做好最最基础的准备工作 (qq.com)

操作系统的代码最开头的 512 字节的数据,从硬盘的启动区先是被移动到了内存 0x7c00 处,然后又立刻被移动到 0x90000 处,并且跳转到此处往后再稍稍偏移 go 这个标签所代表的偏移地址处。

go标签代码

1
2
3
4
5
go: mov ax,cs  ; cs表示代码段寄存器,cs=0x9000
mov ds,ax ; ds数据段寄存器,ds=0x9000
mov es,ax ; es=0x9000
mov ss,ax ; ss为栈段寄存器,ss=0x9000
mov sp,#0xFF00 ;sp队长指针,栈顶地址从0x9FF00

至此,操作系统的一些最最最最基础的准备工作就做好了。

  1. 将代码从硬盘移到0x90000处;
  2. 数据段寄存器ds和代码段寄存器cs被设置为0x90000;
  3. 栈顶地址被设置为0x9F0000,具体表现为栈段寄存器 ss 为 0x9000,栈基址寄存器 sp 为 0xFF00。栈顶地址远远大于此时代码所在的位置0x90000,栈向下发展比较安全。

扩展

所以本回的代码,正如标题所说,就是做好最最基础的准备工作。但要从更伟大的战略意义上讲,它其实是按照 Intel 手册上要求的,老老实实把这三类段寄存器的值设置好,达到了初步规划内存的目的。

第四回 | 把自己在硬盘里的其他部分也放到内存来

第四回 | 把自己在硬盘里的其他部分也放到内存来 (qq.com)

上一回简单说,就是设置了如何访问数据的数据段,如何访问代码的代码段,以及如何访问栈的栈顶指针,也即初步做了一次内存规划,从 CPU 的角度看,访问内存,就这么三块地方而已。

接着往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
load_setup:
mov dx,#0x0000 ; drive 0, head 0
mov cx,#0x0002 ; sector 2, track 0
mov bx,#0x0200 ; address = 512, in 0x9000
mov ax,#0x0200+4 ; service 2, nr of sectors
int 0x13 ; read it
jnc ok_load_setup ; ok - continue
mov dx,#0x0000
mov ax,#0x0000 ; reset the diskette
int 0x13
jmp load_setup

ok_load_setup:
...
  • int指令,是汇编指令,不是高级语言的整型变量,int 0x13,表示发起0x13号中断,这条指令上面给dx, cx, bx, ax赋值都是作为这个终端程序的参数。
  • 0x13 号中断的处理程序是 BIOS 提前给我们写好的,是读取磁盘的相关功能的函数。

本段代码的作用就是就是将硬盘的第 2 个扇区开始,把数据加载到内存 0x90200 处,共加载 4 个扇区

如果复制成功,则跳转到ok_load_setup标签。

1
2
3
4
5
6
7
ok_load_setup:
...
mov ax,#0x1000
mov es,ax ; segment of 0x10000
call read_it
...
jmpi 0,0x9020

这段代码省略了很多非主逻辑的代码,比如在屏幕上输出 Loading system ... 这个字符串以防止用户等烦了。

剩下的主要代码就都写在这里了,就这么几行,其作用是把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处,和之前的从硬盘捣腾到内存是一个道理。

至此,整个操作系统的全部代码,就已经全部从硬盘中,被搬迁到内存来了。

然后又通过一个熟悉的段间跳转指令 jmpi 0,0x9020,跳转到 0x90200 处,就是硬盘第二个扇区开始处的内容。

整个操作系统的编译过程

通过 Makefilebuild.c 配合完成:

1. 把 bootsect.s 编译成 bootsect 放在硬盘的 1 扇区。

2. 把 setup.s 编译成 setup 放在硬盘的 2~5 扇区。

3. 把剩下的全部代码(head.s 作为开头)编译成 system 放在硬盘的随后 240 个扇区。

所以,我们即将跳转到的内存中的 0x90200 处的代码,就是从硬盘第二个扇区开始处加载到内存的。第二个扇区的最开始处,那也就是 setup.s 文件的第一行代码。

在操作系统刚刚开始建立的时候,那是完全自己安排前前后后的关系,一个字节都不能偏,就是这么强耦合,需要小心翼翼,需要大脑时刻保持清醒,规划好自己写的代码被编译并存储在硬盘的哪个位置,而随后又会被加载到内存的哪个位置,不能错乱。

第五回 | 进入保护模式前的最后一次折腾内存

第五回 | 进入保护模式前的最后一次折腾内存 (qq.com)

操作系统已经完成了各种从硬盘到内存的加载,以及内存到内存的复制。

至此,整个bootsect.s的使命就完成了,之后便跳转到了0x90200这个位置开始执行,这个位置的代码就是setup.s的开头,

1
2
3
4
5
6
7
start:
mov ax,#0x9000 ; this is done in bootsect already, but...
mov ds,ax
mov ah,#0x03 ; read cursor pos
xor bh,bh
int 0x10 ; save it in known place, con_init fetches
mov [0],dx ; it from 0x90000.

int 0x10

  • 触发BIOS提供的显示服务中断处理程序
  • ah寄存器,表示显示服务里具体的读取光标位置功能

int 0x10中断程序执行完毕并返回时,dx寄存器里的之表示光标的位置,具体来说高八位dh存储了行号,低八位存储了列号。

说明:计算机在加电自检后会自动初始化到文字模式,在这种模式下,整个屏幕可以显示25行,每行80个字符,也就是80列。

mov [0], dx就是把这个光标位置存储在[0]这个内存地址处(段基址ds=0x90000)。这里存放着光标的位置,以便之后在初始化控制台的时候用到。

这个指令和平时调用方法没什么区别,只不过这里的寄存器的用法相当于入参和返回值,这里的0x10中断号相当于方法名

这里又应了之前说的一句话,操作系统内核的最开始处处也都是BIOS的调包侠

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
比如获取内存信息。
; Get memory size (extended mem, kB)
mov ah,#0x88
int 0x15
mov [2],ax
获取显卡显示模式。
; Get video-card data:
mov ah,#0x0f
int 0x10
mov [4],bx ; bh = display page
mov [6],ax ; al = video mode, ah = window width
检查显示方式并取参数
; check for EGA/VGA and some config parameters
mov ah,#0x12
mov bl,#0x10
int 0x10
mov [8],ax
mov [10],bx
mov [12],cx
获取第一块硬盘的信息。
; Get hd0 data
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb
获取第二块硬盘的信息。
; Get hd1 data
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46]
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
rep
movsb

最终存储在内存中的信息是什么,在什么位置,

内存地址 长度(字节) 名称
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
cli         ; no interrupts allowed ;

因为后面我们要把原本是 BIOS 写好的中断向量表给覆盖掉,也就是给破坏掉了,写上我们自己的中断向量表,所以这个时候是不允许中断进来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; first we move the system to it's rightful place
mov ax,#0x0000
cld ; 'direction'=0, movs moves forward
do_move:
mov es,ax ; destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax ; source segment
sub di,di
sub si,si
mov cx,#0x8000 repeat times
rep movsw
jmp do_move
; then we load the segment descriptors
end_move:
...

rep movsw同前面的原理一样,也是做了个内存复制操作,最终的结果是,把内存地址 0x10000 处开始往后一直到 0x90000 的内容,统统复制到内存的最开始的 0 位置,大概就是这么个效果。

栈顶地址仍然是 0x9FF00 没有改变。

0x90000 开始往上的位置,原来是 bootsectsetup 程序的代码,现 bootsect 的一部分代码在已经被操作系统为了记录内存、硬盘、显卡等一些临时存放的数据给覆盖了一部分。

内存最开始的 00x80000 这 512K 被 system 模块给占用了,之前讲过,这个 system 模块就是除了 bootsect 和 setup 之外的全部程序链接在一起的结果,可以理解为操作系统的全部

那么现在的内存布局就是这个样子。

接下来,就要进行有点技术含量的工作了,那就是模式的转换,需要从现在的 16 位的实模式转变为之后 32 位的保护模式,这是一项大工程!


你管这破玩意儿叫操作系统源码(一)
https://www.spacezxy.top/2023/02/28/OperatingSystem/Operating-system-source-code/
作者
Xavier ZXY
发布于
2023年2月28日
许可协议