本章主要介绍Linux0.11的启动过程(开始main函数之前的过程),主要是对bootsect.s、setup.s、head.s三个程序的介绍,硬件环境为Linux0.11所在环境。
本章重点在于了解,而不是纠结在 cpu 上。操作系统的核心不在这里。
1.1 计算机上电计算机上电后进行了如下过程:
- 在计算机开机时, 内存中没有其他程序,只有 bios 可执行(BISO是一段固化在内存中的程序,存放在 ROM 中)。CPU 处于实模式状态,即寻址方式和8086一样,寻址范围只有1M。CPU 设置 CS = 0xFFFF; IP = 0x0000,即让 PC 指针指向 ROM BIOS 映射区(计算机初始化过程中会将BIOS代码复制到ROM BIOS 映射区)。CPU 首先从 ROM BIOS 映射区的程序开始执行。
- CPU 执行 ROM BIOS 映射区的程序,该程序主要负责检测系统硬件是否正常,并建立中断向量表(这只是供操作系统启动时使用,在操作系统建立完成后会将它覆盖清理,并建立新的中断向量表)
- 在 ROM BIOS 映射区的程序执行的最后,该程序会将操作系统启动程序(bootsect.s)从磁盘第1个扇区(0磁头、0柱面、第1扇区)复制到内存 0X07C00开始的位置,并设置 CS = 0x07c0, IP = 0x0000。最后 CPU 转移到 bootsect.s(0X07C00处)开始执行。至此CUP才正式开始执行“我们自己编写的程序”。
在bios执行结束后,计算机内存中的内容如下图所示:
在计算机启动前,操作系统的程序已经存放在了磁盘之中,Linux0.11内核在磁盘中的分布情况如下图所示:
图中的 system 模块 也就是 Linux0.11 内核的其他部分,如:head.s、main.c 等等。当时的磁盘结构主要是通过磁头数(Heads), 柱面数(Cylinders), 扇区数(sectors)三个参数读写磁盘信息。其中:
- 磁头数(Heads) 表示硬盘总共有几个磁头,也就是有几个盘面, 最大为255(用 8 个二进制位存储);
- 柱面数(Cylinders) 表示硬盘每一面盘片上有几条磁道,最大为1023(用 10个二进制位存储);
- 扇区数(Sectors) 表示每一条磁道上有几个扇区, 最大为 63 (用6个二进制位存储);每个扇区一般是512个字节。
bootsect.s是操作系统的引导程序,是操作系统执行的第一个程序。bootsect.s主要进行了以下工作:
1、 将自己(bootsect.s)搬移到内存0X90000 开始的位置,然后跳转至0X90000 go 处执行程序,bootsect.s 的大小不会超过1个扇区,磁盘的 0扇区只用于存放 bootsect.s。(bootsect.s 的第46至第57行)
_start: !第46行
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw !将自己搬到0X90000处
jmpi go,INITSEG !跳转至0X90000 go 处执行程序
go: mov ax,cs
2、 利用 BIOS 中断 (int 13) 将 setup.s 从磁盘加载到内存0X90200开始的位置,可以看出加载至内存后setup.s依旧紧跟在bootsect.s之后。(bootsect.s第67至第73行)
load_setup: !第67行
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200 SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
3、 获取磁盘的信息,这里不是很重要(bootsect.s第83至第85行)
mov dl,#0x00 !第83行
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
4、检测我们要使用哪一个根文件系统设备(bootsect.s第117至第120行)。如果已经指定了根文件系统所在的设备,那么就直接使用给定的设备。
seg cs !第117行
mov ax,root_dev ! root_dev 在第508、509字节处被定义(在bootsect.s的第250行),其值为0x306,说明根文件系统在第2个磁盘的第1个分区。
cmp ax,#0 ! 若root_dev 不为0,则认为根文件系统所在的设备号已经被定义。
jne root_defined
5、在屏幕上打印"Loading system …"。(bootsect.s第98至第102行)
mov cx,#24 !第98行
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
6、将 system 模块的代码从磁盘搬到内存0x10000开始的位置。SYSSIZE = 0x3000 is 0x30000 bytes = 196kB 。Linux0.11中默认内核大小不会超过196KB。
mov ax,#SYSSEG !第107行
mov es,ax ! segment of 0x010000
call read_it
...
read_it:
...
read_track:
...
mov dx,head !head.s是system模块的第一个程序
mov dh,dl
mov dl,#0
and dx,#0x0100
mov ah,#2
int 0x13 !这才是正式开始搬运system模块。
...
7、 跳转到0x90200处执行,转移到 setup.s 去执行。
!SETUPSEG = 0x9020
jmpi 0,SETUPSEG !第139行
1.3 执行setup.s
setup.s主要进行了以下工作:
- 利用BIOS的中断获取计算机参数(内存大小,磁盘参数、显示器参数等),并将参数存入0X90000开始的位置(将bootsect.s覆盖掉)。我们可以先不管这些参数,等后面用到了再看。关于 setup.s 程序具体读取的系统硬件参数,及其这些参数的存放位置请参考《Linux内核完全剖析——基于0.12内核》。
- 将system模块从内存0x10000 处移到物理内存起始位置 0X0000 0000。这意味着原来的 BIOS 中断向量表被覆盖了。
do_move: !第114行
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
rep
movsw
jmp do_move
- 设置GDT和idt。这只是个临时表,之后会重新设置gdt和idt,这里是为了方便cpu进入保护模式时能正确寻址(这里主要是为了保证能跳到system模块执行)。设置8259芯片,重新设置中断向量表。
lidt idt_48 ! load idt with 0,0 !第132行
lgdt gdt_48 ! load gdt with whatever appropriate
- CPU进入32位的保护模式,程序跳转至物理内存起始位置开始执行(head.s处)
mov ax,#0x0001 ! protected mode (PE) bit !第189行
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
1.4 执行head.s
head.s 里面的内容有点复杂,还没完全弄懂,不过影响不大,现在只需要了解head.s的主要工作即可。
1、设置段选择器。将ds、es、fs等寄存器都赋值为0x10。这里的0x10表示:特权级为0、使用GDT的第2项即数据段描述符。
.globl idt,gdt,pg_dir,tmp_floppy_area #第15行
pg_dir: # 页目录存放的起始位置
.globl startup_32
startup_32:
movl $0x10,?x
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp # 设置堆栈的位置,当然这只是个暂时的,后面还会修改。
# stack_start 定义在 kernel/sched.c 文件中
2、 设置 IDT 和 GDT。
call setup_idt #第 25 行
call setup_gdt
其中 IDT 共有256项且全部填充为 ignore_int 函数的偏移地址,ignore_int是一个只报错误的哑中断子函数。而 GDT 作成如下样子,可以看出这时候还没有设置 LDT :
gdt: .quad 0x0000000000000000 /* NULL descriptor 第0项不使用*/ /*第 236 行*/
.quad 0x00c09a0000000fff /* 16Mb 代码段描述符,其中代码段基地址为0,段的长度为16MB*/
.quad 0x00c0920000000fff /* 16Mb 数据段描述符,其中数据段基地址为0,段的长度为16MB*/
.quad 0x0000000000000000 /* TEMPORARY - don't use 保留,没有使用*/
.fill 252,8,0 /* space for LDT's and TSS's etc */
3、 检测A20线是否开启,检测数字协处理器等。
4、 开启分页机制,设置页目录等。setup_paging 函数就是设置分页机制的。页表从地址 0x0 的位置开始存放,至于页表被设置成什么样子,这里可以先不管,等到学习内存管理章节的时候再回过来看就好。head.s在最后控制程序进入了main函数。程序是利用setup_paging 中的 ret 指令(出栈)进入main函数的。
after_page_tables: # 第137行,在设置好页表后进入main函数。
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $main # 先将main压栈,再利用 setup_paging 里面的 ret 进入 main 函数。
jmp setup_paging # jmp不会进行压栈操作,这里要和call区别一下。
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
进入 main 函数之后会进行一堆初始化,主要是要建立起一些重要的数据结构。
在head.s执行完后内存的内容如下:
- bootsect.s:自举将自己移到0X90000开始的地方;打印开机信息;搬setup.s;搬system模块
- setup.s:利用BIOS的中断获取计算机参数(内存大小,磁盘参数、显示器参数等);切换到保护模式
- head.s:初始化GDT表、IDT表、页表、跳到mian()
- mian():一些init()