操作系统启动:L1级别的探索

发表时间: 2024-02-16 08:26

本章主要介绍Linux0.11的启动过程(开始main函数之前的过程),主要是对bootsect.s、setup.s、head.s三个程序的介绍,硬件环境为Linux0.11所在环境。

本章重点在于了解,而不是纠结在 CPU 上。操作系统的核心不在这里。

1.1 计算机上电

计算机上电后进行了如下过程:

  1. 在计算机开机时, 内存中没有其他程序,只有 BIOS 可执行(BISO是一段固化在内存中的程序,存放在 ROM 中)。CPU 处于实模式状态,即寻址方式和8086一样,寻址范围只有1M。CPU 设置 CS = 0xFFFF; IP = 0x0000,即让 PC 指针指向 ROM BIOS 映射区(计算机初始化过程中会将BIOS代码复制到ROM BIOS 映射区)。CPU 首先从 ROM BIOS 映射区的程序开始执行。
  1. CPU 执行 ROM BIOS 映射区的程序,该程序主要负责检测系统硬件是否正常,并建立中断向量表(这只是供操作系统启动时使用,在操作系统建立完成后会将它覆盖清理,并建立新的中断向量表)
  2. 在 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个字节。


1.2 执行bootsect.s

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行

1.3 执行setup.s

setup.s主要进行了以下工作:

  1. 利用BIOS的中断获取计算机参数(内存大小,磁盘参数、显示器参数等),并将参数存入0X90000开始的位置(将bootsect.s覆盖掉)。我们可以先不管这些参数,等后面用到了再看。关于 setup.s 程序具体读取的系统硬件参数,及其这些参数的存放位置请参考《Linux内核完全剖析——基于0.12内核》。
  2. 将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
  1. 设置gdt和idt。这只是个临时表,之后会重新设置gdt和idt,这里是为了方便cpu进入保护模式时能正确寻址(这里主要是为了保证能跳到system模块执行)。设置8259芯片,重新设置中断向量表。
lidt	idt_48		! load idt with 0,0   !第132行lgdt	gdt_48		! load gdt with whatever appropriate
  1. 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_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执行完后内存的内容如下:

1.5 总结

  • bootsect.s:自举将自己移到0X90000开始的地方;打印开机信息;搬setup.s;搬system模块
  • setup.s:利用BIOS的中断获取计算机参数(内存大小,磁盘参数、显示器参数等);切换到保护模式
  • head.s:初始化GDT表、IDT表、页表、跳到mian()
  • mian():一些init()