L1 操作系统的启动


  本章主要介绍 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 映射区的程序开始执行。

(2)CPU 执行 ROM BIOS 映射区的程序,该程序主要负责检测系统硬件是否正常,并建立中断向量表(这只是供操作系统启动时使用,在操作系统建立完成后会将它覆盖清理,并建立新的中断向量表)

(3)在 ROM BIOS 映射区的程序执行的最后,该程序会将操作系统启动程序(bootsect.s)从磁盘第1个扇区(0磁头、0柱面、第1扇区)复制到内存 0X07C00开始的位置,并设置 CS = 0x07c0, IP = 0x0000。最后 CPU 转移到 bootsect.s(0X07C00处)开始执行。至此CUP才正式开始执行“我们自己编写的程序”。

  在BIOS执行结束后,计算机内存中的内容如图1.1所示:

图1.1 BIOS执行结束后内存中的内容

图1.1 BIOS执行结束后内存中的内容(简图)

在计算机启动前,操作系统的程序已经存放在了磁盘之中,Linux0.11 内核在磁盘中的分布情况如下图所示:
图1.2 操作系统程序在磁盘中的分布

图1.2 操作系统程序在磁盘中的分布(简图)
图中的 system 模块 也就是 Linux0.11 内核的其他部分,如:head.s、main.c 等等。当时的磁盘结构主要是通过磁头数(Heads), 柱面数(Cylinders), 扇区数(Sectors)三个参数读写磁盘信息。其中:
  • 磁头数(Heads) 表示硬盘总共有几个磁头,也就是有几个盘面, 最大为255(用 8 个二进制位存储);
  • 柱面数(Cylinders) 表示硬盘每一面盘片上有几条磁道,最大为1023(用 10个二进制位存储);
  • 扇区数(Sectors) 表示每一条磁道上有几个扇区, 最大为 63 (用6个二进制位存储);每个扇区一般是512个字节。

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,#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 在第508509字节处被定义(在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

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

3、设置gdt和idt。这只是个临时表,之后会重新设置gdt和idt,这里是为了方便cpu进入保护模式时能正确寻址(这里主要是为了保证能跳到system模块执行)。设置8259芯片,重新设置中断向量表。

lidt	idt_48		! load idt with 0,0   !第132行
lgdt	gdt_48		! load gdt with whatever appropriate

4、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)

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,%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 $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执行完后内存的内容如下:
图4.1 执行完head后内存的内容

图4.1 执行完head后内存的内容

参考

[1] 13号中断:https://www.cnblogs.com/coderCaoyu/p/3615220.html
[2] CPU访问外设有两种方式:IO与内存统一编址&IO与内存的独立编址:https://blog.csdn.net/baidu_37973494/article/details/82389577?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
[3]操作系统_哈尔滨工业大学_中国大学MOOC
[4]《Linux内核完全剖析——基于0.12内核》

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值