【操作系统基础】操作系统启动过程

本文参考MOOC哈工大操作系统课程与课件
主要基于Linux 0.11系统展开
”Author:Mayiming“

打开电源后,计算机执行了什么指令?

1. ROM BIOS

  1. x86 PC开机后CPU处于实模式,寻址方式为CS:IP(CS左移4位+IP)
  2. 开机时,初始化的CS=0xFFFF,IP=0x0000,此时指向的地址为0xFFFF0(ROM BIOS映射区,上电后内存中只有此处有代码)
    此时载入的是BIOS程序,还没有进入操作系统的部分,BIOS程序是固化在主板上的一段程序负责基本输入输出、系统设置信息、开机后自检程序和系统自启动程序。其主要功能是为计算机提供最底层的、最直接的硬件设置和控制。目前大多烧录在可擦写ROM中,因此也可以进行BIOS升级。
  3. 此时通过0xFFFF0此处的BIOS代码检查RAM、键盘、鼠标、磁盘等硬件和IO设备(此处执行不通过则不会执行操作系统,代表硬件有问题)
  4. 然后读取磁盘0磁道0扇区位置的数据到0x7c00处(从磁盘读取到内存0x7c00处),磁盘0磁道0扇区(一个扇区512字节)处的数据即为引导扇区,存放着操作系统的引导程序
  5. 设置CS=0x07c0IP=0x0000即指向地址0x7c00
  6. 读入引导扇区里的代码之后,我们就离开了BIOS,操作系统的故事就从这里开始了
    在这里插入图片描述

2. bootsect.s

下面解析离开BIOS后,首先执行的bootsect.s汇编代码

.globl begtext,begdata,begbss,endtext,enddata,endbss
.text //文本段
begtext:
.data //数据段
begdata:
.bss //未初始化数据段
begbss:
entry start //关键字entry告诉链接器“程序入口”
start:
mov ax, #BOOTSEG mov ds, ax // 设置ax=0x07c0,ds=ax
mov ax, #INITSEG mov es, ax //  设置ax=0x9000,es=ax
mov cx, #256 //  设置cx=256 (十进制256)
sub si, si sub di, di //  si,di归零
rep movw //  rep movw 重复执行移动直到cx归零,即移动256个16位字(也就是512字节) 源地址ds:si,目的地址es:di
jmpi go, INITSEG // jmpi go, INITSET 间接跳转,意思就是跳转到go标签处的代码处
// BOOTSEG = 0x07c0
// INITSEG = 0x9000
// SETUPSEG = 0x9020

上述代码将0x07c00处的512字节也就是引导扇区传送到0x90000
此时start下面的这部分代码已经是移动到了0x90000处了

从上一段代码跳转到go处:

go: mov ax,cs //cs=0x9000
mov ds,ax mov es,ax mov ss,ax mov sp,#0xff00 // 设置ds=0x9000,es=0x9000,ss=0x9000,sp=0xff00
load_setup: //载入setup模块
mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200 // 设置dx=0x0000,cx=0x0002,bx=0x0200,ax=0x0200+setup长度
mov ax,#0x0200+SETUPLEN int 0x13 //BIOS中断
jnc ok_load_setup
mov dx,#0x0000
mov ax,#0x0000 //复位
int 0x13
jmp load_setup //重读
// SETUPLEN=4

上述代码从驱动器0的柱面0磁头号0扇区号2开始读取SETUPLEN = 4个扇区,放到内存地址0x90200后面(也就是引导扇区的512字节后面)
在这里插入图片描述
INT 0x13中断解释:
ax寄存器高八位为ah,低八位为al,此时ah=0x02代表读磁盘,al为setup长度,代表读取的扇区个数
同样cx分为chcldx分为dhdl,此时读取的磁盘的位置为柱面号(磁道号)ch = 0开始扇区为cl = 2磁头号为dh = 0驱动器号为dl = 0,读取到的内存地址为es:bx=0x9000:0x0200=0x902000x900000x90200之间刚好512个字节)
INT 0x08 参数参考

载入setup模块的代码后,执行bootsect.s最后一段代码
boot扇区的代码最后跳转到Ok_load_setup

Ok_load_setup: //载入setup模块
mov dl,#0x00 mov ax,#0x0800 //ah=8获得磁盘参数
int 0x13 mov ch,#0x00 mov sectors,cx 
mov ah,#0x03 xor bh,bh int 0x10 //读光标
mov cx,#24 mov bx,#0x0007 // cx=24为显示字符长度 bx=7是显示属性
mov bp,#msg1 mov ax,#1301 int 0x10 //显示msg1位置的字符 Loading system...
mov ax,#SYSSEG //SYSSEG=0x1000 
mov es,ax 
call read_it //读入system模块
jmpi 0,SETUPSEG // 转入0x9020:0x0000 执行setup.s
// bootsect.s中的数据 //在文件末尾
// sectors: .word 0 //磁道扇区数
// msg1:.byte 13,10
// .ascii “Loading system...”
// .byte 13,10,13,10

上述代码获取磁盘参数放到sectors,读取光标,显示加载系统字符,读入system模块,最后jmpi跳转到setup模块

总结一下bootsect.s代码的功能就是:

  1. 0x07c00处的512字节也就是引导扇区传送到0x90000
  2. 从驱动器0的柱面0磁头号0扇区号2开始读取SETUPLEN = 4个扇区(setup模块),放到内存地址0x90200后面(也就是boot扇区的512字节后面)
  3. 获取磁盘参数放到sectors,读取光标,显示加载系统字符,读入system模块,最后jmpi跳转到setup模块

下面单独解析一下读入system模块的call read_it
system模块可能很大,要跨越磁道
jb代表jump below,此时应该是为了防止程序跳到system

read_it: mov ax,es cmp ax,#ENDSEG jb ok1_read // ax=es,ax与#ENDSEG比较 ENGSEG代表镜像结束的位置(具体与镜像大小SYSSIZE有关)
ret // 函数返回 return
ok1_read:
mov ax,sectors // 设置ax为磁盘扇区数目
sub ax,sread //sread是当前磁道已读扇区数, ax = ax - sread 等于未读扇区数
call read_track //读磁道...
// ENDSEG=SYSSEG+SYSSIZE
// SYSSIZE=0x8000 //该变量可根据
// Image大小设定(编译操作系统时)

上述这段代码把system整体读取到内存中

3. setup模块

至此bootsect.s引导扇区的代码执行完毕,此时需要转入setup模块进行执行。
setup模块作用是完成OS启动前的设置部分

3.1 移动system模块

start: mov ax,#INITSEG mov ds,ax mov ah,#0x03 // #INITSEG=0x9000
xor bh,bh int 0x10 mov [0],dx //取光标位置dx放入内存[0]=0x90000表示间接寻址,段地址ds:偏移地址[0]
mov ah,#0x88 int 0x15 mov [2],ax ... // 获取扩展内存大小ax, 放入[2]=0x90002位置
cli ///不允许中断
mov ax,#0x0000 cld
do_move: mov es,ax add ax,#0x1000
cmp ax,#0x9000 jz end_move
mov ds,ax sub di,di
sub si,si
mov cx,#0x8000
rep // 将system模块移动到0x00000地址
movsw  // 源地址ds:si,目的地址es:di
jmp do_move
// SYSSEG = 0x1000

在这里插入图片描述
因为此时CS:IP最多指向的地址空间为1M(地址位数为20位),2^20bit=1M,所以需要扩展内存。

SYSSET=0x1000system模块的代码在地址0x10000处起始,上述代码将0x10000-0x90000的代码平移到0x00000-0x80000
此时可以解释为何上面要移动0x07c00的代码到0x90000,就是因为这段地址要放system

3.2 转入保护模式

下面CPU要转入保护模式,因为实模式的内存寻址空间太小了,转入保护模式后CS:IP寻址方式会改变,可以寻址32位地址空间(4G),同时INT中断的方式也会发生一些变化。

end_move: mov ax,#SETUPSEG mov ds,ax
lidt idt_48 lgdt gdt_48//设置保护模式下的中断和寻址
进入保护模式的命令...
idt_48:.word 0 .word 0,0 //保护模式中断函数表
gdt_48:.word 0x800 .word 512+gdt,0x9 
gdt: .word 0,0,0,0
.word 0x07FF, 0x0000, 0x9A00, 0x00C0
.word 0x07FF, 0x0000, 0x9200, 0x00C0

上述代码的功能为写GDT表,保护模式寻址的过程为从GDT表中寻找CS指向的地址,取出表中的地址与IP组合成32位地址。
在这里插入图片描述
在这里插入图片描述

call empty_8042 mov al,#0xD1 out #0x64,al  // 0xD1表示写数据到8042的P2端口
//8042是键盘控制器,其输出端口P2用来控制A20地址线
call empty_8042 mov al,#0xDF out #0x60,al
//选通A20地址线 call empty_8042
初始化8259(中断控制) //一段非常机械化的程序
mov ax,#0x0001 mov cr0,ax // cr0寄存器最低位 置1 
jmpi 0,8 // IP=0, CS=8, 此时是保护模式了,需要根据CS去查GDT表
// 通过查表该地址指向 0 地址

empty_8042:
.word 0x00eb,0x00eb
in al,#0x64 // 读取到al 
test al,#2  // 跳转test
jnz empty_8042
ret

在这里插入图片描述
在这里插入图片描述

4. system模块

system模块(目标代码)中的第一部分代码? head.s
为什么head.ssystem模块的第一段代码,这是由makefile控制的

linux/Makefile:

disk: Image
dd bs=8192 if=Image of=/dev/PS0  // /dev/PS0是软驱
Image: boot/bootsect boot/setup tools/system tools/build
tools/build boot/bootsect boot/setup tools/system > Image
tools/system: boot/head.o init/main.o $(DRIVERS) …
$(LD) boot/head.o init/main.o $(DRIVERS)-o tools/system

4.1 head.s

下面来看head.s做了什么:

stratup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es
mov %as,%fs mov %as,%gs //指向gdt的0x10项(数据段)
lss _stack_start,%esp //设置栈(系统栈)
call setup_idt // 设置idt
call setup_gdt // 设置gdt
xorl %eax,%eax
1:incl %eax
movl %eax,0x000000 cmpl %eax,0x100000
je 1b //0地址处和1M地址处相同(A20没开启),就死循环
jmp after_page_tables //页表,什么东东?
setup_idt: lea ignore_int,%edx 
movl $0x00080000,%eax movw %dx,%ax
lea _idt,%edi movl %eax,(%edi)

在这里插入图片描述

head.s做了一系列设置(堆栈、idtgdt、设置地址线等)执行后需要执行main.c,转入C语言执行

after_page_tables:
pushl $0 pushl $0 pushl $0 pushl $L6 // 将0,0,0,L6压入堆栈
pushl $_main jmp set_paging  // 将_main压入堆栈 跳转到设置页表
L6: jmp L6
setup_paging: 设置页表 ret //设置页表  然后return 

为什么堆栈要压入 0,0,0,L6,_main这样的顺序,和下图C执行函数的堆栈结构有关系。
在这里插入图片描述
在这里插入图片描述

4.2 main(0,0,0)

main(0,0,0)三个参数其实没有用只是为了envp,argv,argc完整性。
下面main函数中执行了一系列的初始化操作,初始化了内存、时间、硬盘、缓冲区等。
在这里插入图片描述

4.3 解析mem_init()

在linux/mm/memory.c中

void mem_init(long start_mem,long end_mem) // start_mem与系统大小有关,end_men参数从3.1中扩展内存获取处得到
{ 
int i;
for(i=0; i<PAGING_PAGES; i++)
mem_map[i] = USED; // 将内存的页表从0开始的地方设置一段为USED, 这一段即为系统所在的地址
i = MAP_NR(start_mem);
end_mem -= start_mem; // end_men-start_men为剩余的内存的大小
end_mem >>= 12; // end_men >>=12,end_men右移12位,代表除以4k, 此时mem_map每一位代表了一个4k的内存页是否被使用
while(end_mem -- > 0)
mem_map[i++] = 0; } // 将除系统地址外的内存页初始化为未使用

通过main.c初始化完成后,操作系统即启动了,main()函数是一个永不返回的函数,会一直执行下去。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值