本章主要介绍Linux0.11的启动过程(开始main函数之前的过程),主要是对bootsect.s、setup.s、head.s三个程序的介绍,硬件环境为Linux0.11所在环境。
本章重点在于了解,而不是纠结在 CPU 上。操作系统的核心不在这里。
计算机上电后进行了如下过程:
在bios执行结束后,计算机内存中的内容如下图所示:
在计算机启动前,操作系统的程序已经存放在了磁盘之中,Linux0.11内核在磁盘中的分布情况如下图所示:
图中的 system 模块 也就是 Linux0.11 内核的其他部分,如:head.s、main.c 等等。当时的磁盘结构主要是通过磁头数(Heads), 柱面数(Cylinders), 扇区数(Sectors)三个参数读写磁盘信息。其中:
bootsect.s是操作系统的引导程序,是操作系统执行的第一个程序。bootsect.s主要进行了以下工作:
1、 将自己(bootsect.s)搬移到内存0X90000 开始的位置,然后跳转至0X90000+go 处执行程序,bootsect.s 的大小不会超过1个扇区,磁盘的 0扇区只用于存放 bootsect.s。(bootsect.s 的第46至第57行)
_start: !第46行mov ax,#BOOTSEGmov ds,axmov ax,#INITSEGmov es,axmov cx,#256sub si,sisub di,direp 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 parametersint 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,#msg1mov ax,#0x1301 ! write string, move cursorint 0x10
6、将 system 模块的代码从磁盘搬到内存0x10000开始的位置。SYSSIZE = 0x3000 is 0x30000 bytes = 196kB 。Linux0.11中默认内核大小不会超过196KB。
mov ax,#SYSSEG !第107行mov es,ax ! segment of 0x010000call read_it...read_it:...read_track:...mov dx,head !head.s是system模块的第一个程序mov dh,dlmov dl,#0and dx,#0x0100mov ah,#2int 0x13 !这才是正式开始搬运system模块。...
7、 跳转到0x90200处执行,转移到 setup.s 去执行。
!SETUPSEG = 0x9020jmpi 0,SETUPSEG !第139行
setup.s主要进行了以下工作:
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
lidt idt_48 ! load idt with 0,0 !第132行lgdt gdt_48 ! load gdt with whatever appropriate
mov ax,#0x0001 ! protected mode (PE) bit !第189行lmsw ax ! This is it!jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
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_32startup_32: movl .globl idt,gdt,pg_dir,tmp_floppy_area #第15行pg_dir: # 页目录存放的起始位置.globl startup_32startup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fs mov %ax,%gs lss stack_start,%esp # 设置堆栈的位置,当然这只是个暂时的,后面还会修改。 # stack_start 定义在 kernel/sched.c 文件中x10,%eax 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 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. # These are the parameters to main :-) pushl 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. pushl 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. 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执行完后内存的内容如下: