操作系统启动过程

计算机执行的第一条指令

问题:打开电源后,计算机执行的第一句指令是什么?

对于x86的PC机,刚上电时,有一部分是固化的,基本过程如下:

  1. x86 PC刚开机时CPU处于实模式
  2. 开机时,CS=0xFFFF;IP=0x0000
  3. 寻址0xFFFF0(ROM BIOS映射区---固化在硬件上)
  4. 检查RAM、键盘、显示器、软硬磁盘
  5. 将磁盘0磁道0扇区(引导扇区)读入0x7c00处,也就是将操作系统引导扇区从磁盘中读入到0x7c00处
  6. 设置CS=0x07c0,IP=0x0000,通过寻址,后面将从引导扇区开始执行,也就是从0x7c00处开始执行。

实模式下的寻址CS:IP----->CS左移4位+IP,例如CS=0xFFFF;IP=0x0000则CS左移4位为0xFFFF0,再加上IP,为0xFFFF0,也就是ROM上的BIOS映射区。

image-20210816221601955

0x7c00处存放的代码

0x7c00处存放的代码是从引导扇区读入的512个字节。

  • 引导扇区是启动设备的第一个扇区
  • 硬盘的第一个扇区上存放着开启后执行的第一段可控制的程序。

Linux最前面的部分是由8086汇编语言编写的bootsect.s,它将由BIOS读入到内存绝对地址0x7c00(31KB)处,当它被执行的时候会将自己移动到内存绝对地址0x90000(576KB)处,并把启动设备中后2KB字节代码(boot/setup.s)读入到内存0x90200处,而内核的其他部分system模块被读入到从内存地址0x10000(64KB)开始处,因此从机器加点开始顺序执行如图所示:

image-20210817222121681

引导扇区代码:bootsect.s

引导扇区中的代码是汇编代码。C程序要经过编译,经过编译后会产生不可控制的变化,而汇编中的每一条指令最后都变成了真正的机器指令,可以对它进行完整的控制。

开始设置相关的段地址,如下:

BOOTSEG  = 07c0h;// bootsect的原始地址(是段地址,以下同)
INITSEG  = 9000h;// 将bootsect移到这里
SETUPSEG = 9020h;// setup程序从这里开始
SYSSEG   = 1000h;// system模块加载到10000(64kB)处.

start

entry _start  告知链接程序,程序从start标号开始执行
_start:
	mov	ax,#BOOTSEG
	mov	ds,ax         //这两句将ds段寄存器置为0x07c0
	mov	ax,#INITSEG
	mov	es,ax         //这两句将es段寄存器置为0x9000
	mov	cx,#256       //设置移动计数值 = 256
	sub	si,si         //源地址为ds:si = 0x07c0:0x0000
	sub	di,di         //目的地址es:di = 0x9000:0x0000
	rep               //重复执行并递减cx的值,直到cx=0
	movw              //movs指令,从内存[si]处移动cx个字到[di]处。
	jmpi	go,INITSEG  //段间跳转,标号go是段内偏移地址

jmpi指令之前的几条指令,将0x07c0:0x0000处的256个字移动到0x9000:0x0000处,也就是上面图中从1移动到2.

jmpi go,INITSEG ——> go赋值给ip,INITSEG赋值给cs,通过cs:ip将指令跳转到go处执行

go

go: mov    ax,cs
   mov    ds,ax
   mov    es,ax
! put stack at 0x9ff00.
   mov    ss,ax
   mov    sp,#0xFF00    ! arbitrary value >>512

在这段代码中将es寄存器的值置为cs寄存器的值即INITSEG,0x9000,即现在es=0x9000。

load_setup

load_setup:
   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
   mov    dx,#0x0000
   mov    ax,#0x0000    ! reset the diskette
   int    0x13
   j  load_setup

【小知识】在寄存器中,ah是ax的高8位,al是ax的低8位。

从上述代码中,ah是0x02(读磁盘),al=扇区数量(SETUPLEN=4),ch=0x00(柱面号),cl=0x02(开始扇区 2),dh=0x00(磁盘头),dl=0x00(驱动器号),而es:bx=内存地址。

那么int 0x13中断之前的代码表示从2号扇区开始读取4个扇区,也就正好是setup所在的扇区。

 

 es:bx=0x9000:0x0200,所以内存地址为0x90200.

int 0x13是BIOS中断—读取磁盘的中断,关于中断的内容在下一个章节中介绍。

【总结】上述代码,利用BIOS中断INT 0x13将setup模块从磁盘的第二个扇区开始读到0x90200开始处,共读4个扇区。因此setup模块的开始处是0x90200

ok_load_setup

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track

   mov    dl,#0x00
   mov    ax,#0x0800    ! AH=8 is get drive parameters
   int    0x13
   mov    ch,#0x00
   seg cs
   mov    sectors,cx
   mov    ax,#INITSEG
   mov    es,ax   ---这两句将es的值重新设置为0x9000.

! Print some inane message

   mov    ah,#0x03      ! read cursor pos
   xor    bh,bh
   int    0x10       读光标
   
   mov    cx,#24       显示字符串的字符数
   mov    bx,#0x0007    ! page 0, attribute 7 (normal) bh显示页面号,bl=字符属性
   mov    bp,#msg1       es:bp寄存器对指向要显示的字符串
   mov    ax,#0x1301    ! write string, move cursor
   int    0x10        显示字符

! ok, we've written the message, now
! we want to load the system (at 0x10000)

   mov    ax,#SYSSEG
   mov    es,ax     ! segment of 0x010000
   call   read_it   读入system模块,将system模块加载到0x1000开始处,es为输入参数。
   call   kill_motor

【INT 0x10】BIOS中断,用来读光标和显示字符到屏幕上,具体用法后续补充。

msg1

msg1表示屏幕上要显示的字符串。

sectors:
   .word 0  //磁道扇区数

msg1:
   .byte 13,10
   .ascii "Loading system ..."
   .byte 13,10,13,10

那么,我们要想修改开机时屏幕上显示的字符串,就只需要修改msg1和ok_load_setup中有关代码就可以了,相关实验后续补充。

read_it

read_it:
	mov ax,es
	test ax,#0x0fff
die:	jne die			! es must be at 64kB boundary
	xor bx,bx		! bx is starting address within segment
rp_read:
	mov ax,es
	cmp ax,#ENDSEG		! have we loaded all yet?
	jb ok1_read
	ret

boot_flag

有效引导扇区的标志,仅供BIOS中的程序加载引导扇区时识别使用,必须位于引导扇区的最后两个字节中。

boot_flag:
   .word 0xAA55

把操作系统读入内存,打出logo,将setup和system读入后将控制权交给setup,从而来执行setup代码。最终跳转指令如下:

jmpi    0,SETUPSEG   //cs=SETUPSEG,0x9020,ip=0x0000,所以指令跳转到0x90200处执行,也就是setup开始执行的地址。

setup.s

基本过程

​ setup.s是一个操作系统加载程序,主要利用RPM BIOS中断读取机器系统数据,并将数据保持在0x90000开始的位置(覆盖掉bootsect程序所在的地方),这些参数将被内核中相关程序使用。

​ setup程序将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x0000处,然后加载中断描述符表寄存器idtr和全局描述符表寄存器gdtr,以此来进入32位保护模式,并跳转到位于system模块最前面部分的head.s程序继续执行。

​ setup将完成OS启动前的设置。

image-20210817222121681

start

start:

   mov    ax,#INITSEG    
   mov    ds,ax

将ds设置成INITSEG(9000),在setup程序中需要重新设置ds。

mov	ah,#0x03	
xor	bh,bh
int	0x10
mov	[0],dx 间接寻址,数据段DS=0x9000,偏移是0,对应内存的绝对地址是0x9000左移4位+0---->0x90000.也就是把dx寄存器的值保存到内存地址0x90000处。

这4句代码使用了BIOS 10号中断,截取屏幕当前光标位置,并保存在内存0x90000处。控制台初始化程序会到此处取值。

mov ah,#0x88
int    0x15
mov    [2],ax

这段代码表示取扩展内存的大小值(KB),也就表示刚开机的时候需要将操作系统内存大小保存起来,那么这是为什么?

答:操作系统是管理硬件的,当然也会管理内存,要想管理内存,就需要知道内存的大小。机器之间的内存大小也不相同。通过setup的初始化让操作系统知道了内存、硬件等的信息。

利用BIOS 15号中断功能号ah = 0x88取系统所含扩展内存大小并保存到内存0x90002处(和上面0x90000来历一样)。

mov ah,#0x0f
int    0x10
mov    [4],bx    
mov    [6],ax

这段代码用于获取显卡当前显示模式。

后面的代码会分别获取显示方式并取参数、获取第一个硬盘的信息等。

cli        
mov    ax,#0x0000
cld

cli表示从这里不允许中断。

do_move 移动system模块到0x00000处

mov es,ax    //es=0x0000 
add    ax,#0x1000
cmp    ax,#0x9000
jz end_move		//当ax = 0x9000时移动结束
mov    ds,ax  //ds=0x9000   
sub    di,di  //di=0,es:di=0x00000 目的地址
sub    si,si  //si=0,ds:si=0x90000 源地址
mov    cx,#0x8000 //移动0x8000字(64KB字节)
rep				//ds:si ---->es:di
movsw           //每次移动2
jmp    do_move

这段代码的目的:将处于0x10000开始处的system模块移动到0x00000位置,也就是图中4到5,将从0x10000到0x8ffff的内存数据块(512KB)整块的向内存低处移动了0x10000(64KB)的位置。【疑问:为什么要进行移动呢?】

1、从代码实现来看,每次移动2B,每轮重复0x8000次,0x8000*2=0x10000B=64KB,所以共移动8轮。 2、由此可以理解为什么bootsect.s把自己移动到了0x90000? system模块放置在0x10000处,当时假设 system 模块最大长度不会超过 0x80000 (512KB),所以从0x10000到0x8ffff都是预留给system模块的,即其末端不会超过内存地址 0x90000,所以 bootsect.s 会把自己移动到0x90000 开始的地方,并把 setup 加载到它的后面。 3、为什么Load system的时候为什么不一次性放在0x00000处? 因为0x00000处开始放的bios中断向量表。现在bios中断已经不需要了,所以可以覆盖了。

进入保护模式

mov ax,#0x0001 
lmsw   ax    
jmpi   0,8   

在这里开始,寻址方式将不再是实模式,而是保护模式。在实模式下,cs16位,ip16位通过左移4位加上ip的方法,也只有20位,相当于1M的空间,肯定是不够的,现在的内存是4G的,所以不能使用实模式寻址。

从16位切到32位模式来进行工作,也就是切换到保护模式来进行工作,也就是CPU内部对16位模式和32位模式的解释程序不一样。

1、CR0 是系统内的控制寄存器之一。控制寄存器是一些特殊的寄存器,它们可以控制CPU的一些重要特性。0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。

image-20210830221541443

2、lmsw:置处理器状态字。但是只有操作数的低4位被存入CR0,即只有PE,MP,EM和TS被改写,CR0其他位不受影响。此处把cr0的最后一位设置为1,从实模型进入保护模式。 3、为什么有保护模式? 实模式下的寻址方法只能访问1M(20bit)的内存空间,无法满足需要。后来intel有了32位处理器,寻址空间达到4G,保护模式就是32位机。

4、保护模式下的地址翻译

保护模式地址翻译

从图中可以看出,在保护模式下,根据cs在GDT表中的查找的值+ip来进行地址翻译的,CS也被称作选择子,里面放的是查表的索引。

4、jmpi 0, 8:ip=0,cs=8,按照保护模式,取到的段基址其实是0x0000,那么这句话就是跳转到地址为0x00000的地方开始执行,也就是system模块的开始部分。

由于jmpi 0,8会到GDT表中查找值,所以GDT表中必须有内容可以让查找,所以setup还有下面的代码,填写GDT表的表项:

gdt://表项
   .word  0,0,0,0       ! dummy //0 64位

   .word  0x07FF    ! 8Mb - limit=2047 //8(2048*4096=8Mb)
   .word  0x0000    ! base address=0
   .word  0x9A00    ! code read/exec
   .word  0x00C0    ! granularity=4096, 386

   .word  0x07FF    ! 8Mb - limit=2047 (2048*4096=8Mb)
   .word  0x0000    ! base address=0
   .word  0x9200    ! data read/write
   .word  0x00C0    ! granularity=4096, 386

idt_48:
   .word  0        ! idt limit=0
   .word  0,0          ! idt base=0L

gdt_48:
   .word  0x800     ! gdt limit=2048, 256 GDT entries
   .word  512+gdt,0x9    ! gdt base = 0X9xxxx

gdt中的8

保护模式地址翻译

8对应的表项为:

.word 0x07FF .word 0x0000 .word 0x9A00 .word 0x00C0

对应的GDT表项为:

GDT表项

表项中红色的对应的是段基址,那么8对应的段基址为0x00000000,加上ip=0x0,则CS:ip为0x00000.

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值