前言:
本文是对早期内核的引导启动过程做的分析笔记,这样可以更好的了解内核的启动过程。而现代大部分PC都是靠grub等引导工具引导启动的。x86架构下linux系统引导启动过程,大致分为以下几个阶段:
一.BIOS启动引导阶段
(1)当PC的电源打开后,80x86结构的cpu将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是BIOS的地址。
(2)BIOS的首先进行POST(Power-On Self Test,加电后自检),检测系统中一些关键设备是否存在和能否正常工作,例如内存和显卡等设备。此时显卡还没有初始化,如果发现了一些致命错误,例如没有找到内存或者内存有问题(此时只会检查640K常规内存),BIOS会直接控制喇叭发声来报告错误,声音的长短和次数代表了错误的类型。
(3)然后物理地址0处开始初始化中断向量(注意:这个BIOS的中断向量很重要,后边的很多和硬盘等的交互都是通过此中断向量完成的)。
(4)此后,BIOS将启动设备的第一个扇区(第0磁道第一个扇区被称为MBR,也就是Master Boot Record,即主引导记录,它的大小是512字节,里面存放了预启动信息、分区表信息)读入内存绝对地址0x7C00处,并跳转到这个地方。其实被复制到物理内存0x7C00处的内容就是Boot Loader,对于较早的内核不靠grub启动的,它就是bootsect.S程序,而对于现在PC多数使用grub引导启动的,就是lilo或者grub了。
//我们分析的是不依靠grub启动的较老的内核
二.bootsect.S程序
bootsect.S代码是磁盘引导块程序,驻留在磁盘第一个扇区中(即引导扇区,0磁道,0磁头,扇区大小为512B),相当于后来的grub程序。
1.该程序的主要作用:
(1)bootsect代码执行期间,首先它会将自己移动到内存绝对地址0X90000开始处并继续执行。
(2)把磁盘的第2个扇区开始的4个扇区的setup模块(即setup.S程序)加载到紧跟着bootsect后面位置处,即0x90200地址。由此也可以看出setup最大为4个扇区,2KB。
(3)然后利用BIOS中断0x13取磁盘参数表中当前启动引导磁盘的参数,在屏幕上显示"Loading system..."字符串。
(4)然后再把setup模块后边的system模块(即内核)加载到内存0x10000开始的地方。
(5)确定根文件系统的设备号,并保存于root_dev中(引导块的508地址处)。
(6)最后长跳转到setup程序处(0x90200地址)去执行setup程序。
2.部分代码解析
(1)bootsect程序将自己拷贝到0x9000地址处
start:
mov ax,#BOOTSEG //BOOTSEG=0x7c00,赋值ax寄存器为0x7c00
mov ds,ax //将ds段寄存器置为0x7c00
mov ax,#INITSEG //INITSEG=0x9000
mov es,ax //将es段寄存器置为0x9000
mov cx,#256 //设置移动的计数值为256字(下边是movw),即512B,正好是一个扇区大小,也是bootset程序大小
sub si,si //清空si寄存器
sub di,di //清空di寄存器
rep //重复执行并递减cx的值,直到cx=0
movsw //即movs指令,源地址:ds:si---->目标地址:es:di
jmpi go,INITSEG //段间跳转,标号go是段内偏移地址
(2)读setup模块到0x90200地址处
load_setup:
xor dx,dx //输入参数:设置驱动器号是0,磁头号是0
mov cx,#0x0002 //输入参数:高位00指定磁道号为0,低位02指定是开始扇区号,即第二个扇区
mov bx,#0x0200 //输入参数:bx指定数据缓冲区es:bx,es已经设置为0x9000,所以数据缓冲区地址就是0x90200地址处
mov ax,#0x0200+SETUPLEN //输入参数:高位的02为子命令号,表示读磁盘扇区到内存。低位的04表示需要读出的扇区数量
int 0x13 //利用BIOS中断INT 0x13读扇区内容到指定内存0x90200地址中
jnc ok_load_setup //读取OK,跳转到ok_load_setup!
(3)取当前引导磁盘参数(就是取每磁道扇区数)
ok_load_setup:
xor dl,dl //输入参数dl表示驱动器号为0
mov ah,#0x08 //BIOS 0x13中断的子命令08,表示读取磁盘驱动器参数
int 0x13 //利用BIOS 0x13中断读磁盘驱动器参数
xor ch,ch //ch包含返回的最大磁盘号的低8位,cl返回每磁道扇区数,ch在这清0,那么cx值就为每磁道扇区数(即cl的值)
seg cs //表示下一条的语句在cs段寄存器指定的段中
mov sectors,cx //每磁道扇区数保存到sectors
mov ax,#INITSEG //INITSEG=0x9000
mov es,ax //改变es段寄存器为0x9000
(4)把system模块加载到内存0x10000地址处
mov ax,#SYSSEG //SYSSEG=0x10000
mov es,ax //将es段寄存器设置为0x10000,即存放system的段地址
call read_it //读磁盘上的system模块,es为输入参数
call kill_motor //关闭驱动器马达
call print_nl //光标回车换行
(5)确定根文件系统的设备号
seg cs //下一条指令在cs段寄存器指定的段内执行
mov ax,root_dev //将定义在引导扇区508,509字节处的root_dev赋值给ax寄存器
or ax,ax //将ax寄存器内容或操作,结果保存到ax寄存器中
jne root_defined //若或后ax值不为0,则跳转,即root_dev值已经初始化过。为0表示未初始化,往下执行
seg cs //下一条指令在cs段寄存器指定的段内执行
mov bx,sectors //每磁道扇区数赋值给bx寄存器
mov ax,#0x0208 //代表/dev/ps0,即1.2M软驱
cmp bx,#15 //判断每磁道扇区数是否为15个
je root_defined //若是15个,则ax中的值就是引导驱动器的设备号
mov ax,#0x021c //代表/dev/Ps0,即1.44M软驱
cmp bx,#18 //判断每磁道扇区数是否为18个
je root_defined //若是18个,则ax中的值就是引导驱动器的设备号
undef_root: //若既不是15个也不是18个,则死循环,即死机
jmp undef_root
root_defined:
seg cs
mov root_dev,ax //将ax中保存的设备号赋值给变量root_dev
(6)长跳转到setup程序处(0x90200地址)
jmpi 0,SETUPSEG //长跳转到setup程序处(0x9020:0000)去执行
三、setup.S程序
setup.S程序是一个操作系统加载程序。仍在实模式下运行,主要是设置系统参数,为进入保护模式做准备。
1.它的主要作用是:
(1)利用BIOS中断读取机器系统数据,并将这些数据保存到0x90000开始的位置(即会覆盖掉bootsect程序所在的地方)。
(2)然后将system模块从0x10000-0x8FFFF (当时认为内核模块长度不会大于512K)整块向下移动到内存绝对地址0x00000处。
(3)加载中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR),开启A20地址线。
(4)重新设置两个中断控制芯片8259A,将硬件中断号设置为0x20-0x2F.
(5)设置CPU的控制寄存器CR0,从而进入32位保护模式运行。
(6)跳转到位于system模块的最前面部分的head.S程序继续运行。
2.部分代码解析
(1)利用BIOS中断读取机器系统数据
mov ax,#INITSEG //INITSEG=0x90000
mov ds,ax //将ds段寄存器设置为0x90000
mov ah,#0x88 //利用BIOS中断15,功能号为0x88,读取系统可扩展内存的大小
int 0x15
mov [2],ax //并将读到的系统可扩展内存的大小0x90002处
还有读取显示参数、光标位置、显示模式,取硬盘参数表等分别保存在固定的位置,都是通过BIOS中断实现的,所以不在列出代码。
(2)将system模块整块向下移动到地址0x00000处
mov ax,#0x0000 //
cld //清除操作方向标志位
do_move:
mov es,ax //将es段寄存器设置为0x0000,es:di为目的地址
add ax,#0x1000 //
cmp ax,#0x9000 //是否已经移动完
jz end_move //是则跳转
mov ds,ax //源地址:ds:si(初始为:0x1000:0x0)
sub di,di
sub si,si
mov cx #0x8000 //移动0x8000字,即(从0x10000-0x8ffff)
rep //重复执行并递减cx的值,直到cx=
movsw //移动源地址:ds:si---->目标地址:es:di
jmp do_move
(注意:这里是否把BIOS中断覆盖掉????)
(3)加载IDTR、GDTR
end_move:
mov ax,#SETUPSEG //SETUPSEG=0x90200
mov ds,ax //ds指向本程序setup段
ligt idt_48 //加载IDT寄存器,是一个长度为0的空表,idt在线性空间的32为基地址也设置为0,限长也为0。
lgdt gdt_48 //加载GDT寄存器,设置了3个描述符项,第一项为空,第2项为内核代码段描述符,第3项为内核数据段描述符,并且GDT设置的地址为0x90200+gdt,即在setup程序后边
(4)设置8259A
8259 芯片主片端口为0x20-0x21, 从片端口为0xA0-0xA1.
(5)设置控制寄存器CR0
mov ax,#0x0001 //保护模式位PE,即CR0的第0位置1导致cpu切换到保护模式,并且运行在特权级0中
lmsw ax //加载机器状态字,进入保护模式
(6)跳转到head.S程序
jmpi 0,8 //跳转到cs段偏移0处,即head.S程序。段值8已经是保护模式下的段选择符了,即表示请求特权级0,使用全局描述符表GDT中第二个段描述符,即内核代码段描述符。该描述符指出代码基地址为0,即跳转到了system中的代码。
四、head.S程序
head.S程序在被编译成目标文件后会与内核其他程序一起被链接成system模块,位于system模块的最前面部分。
1.它的主要作用是:
(1)加载各个数据段寄存器。
(2)重新设置中断描述符表IDT,共256项。
(3)重新设置了全局描述符表GDT,只是把段限长设置为16MB。
(4)监测A20地址线是否已经开启。
(5)接着设置管理内存的分页处理机制。
(6)转去执行/init/main.c程序的main()函数。
2.部分代码解析:
(1)加载各个数据段寄存器
startup_32: //这里已经在处于32位运行模式
movl $0x10,%eax //选择内核数据段描述符,并用该描述符设置各个数据段寄存器
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp //设置系统堆栈
call setup_idt //重新设置IDT
call setup_gdt //重新设置GDT
movl $0x10,%eax //由于GDT重新设置过,内核数据段描述符的限长改为了16M,所以要重
mov %ax,%ds //新各个数据段寄存器加载内核数据段描述符
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
(2) 重新设置IDT
setup_idt:
lea ignore_int,%edx //将ignore_int的有效地址值存入edx寄存器
movl $0x00080000,%eax //将段选择符0x0008放入eax的高16位中
movw %dx,%ax //ignore_int的有效地址值的低16位放入eax的低16位中
movw $0x8E00,%dx //设置edx寄存器的低16位为8E00,即表示特权级0,内核代码段存在于内存中
lea _idt,%edi //设置中断描述符表的地址给目标寄存器edi
mov $256,%ecx //共256项
rp_sidt:
movl %eax,(%edi) //低4字节存入中断描述符表中
movl %edx,4(%edi) //低4字节存入中断描述符表中
addl $8,%edi //指向下一个中断描述符表
dec %ecx //256依次递减
jne rp_sidt //没到256项就回去接着设置
lidt idt_descr //设置完之后,加载中断描述符表寄存器
ret
(3)重新设置GDT
setup_dgt:
lgdt gdt_descr //加载全局描述符表寄存器
ret
gdt_descr:
.word 256*8-1
.long _gdt
_gdt:
.quad 0x0000000000000000 //空的全局描述符表项
.quad 0x00c09a0000000fff //0x08,内核代码段限长16M
.quad 0x00c0920000000fff //0x10,内核数据段限长16M
.quad 0x0000000000000000 //空的全局描述符表项
.fill 252,8,0 //预留空间
(4)设置管理内存的分页处理机制
setup_paging:
movl $1024*5,%ecx //对5页内存(1页目录,4页页表)清零
xorl %eax,%eax //eax内容清空
xorl %edi,%edi //页目录从0x0000开始
cld;rep;stosl //eax内容存到es:edi所指的内存处,且edi增4
//以下四项为设置页目录表中的项,_pg_dir地址为0x0000,即页目录地址
movl $pg0+7,_pg_dir //pg0在前边被指定为0x1000,所以第一个页表项设定为0x00001007
movl $pg1+7,_pg_dir+4 //设置页目录中的第二个页表项
movl $pg2+7,_pg_dir+8 //设置页目录中的第三个页表项
movl $pg3+7,_pg_dir+12 //设置页目录中的第四个页表项
//填写4个页表中的页内容
movl $pg3+4092,%edi //edi设定为最后一页页表的最后一项
movl $0xfff007,%eax //最后一项对应的物理内存是0x0xfff000,属性标志为0x07,表示页存在、用户可读写
std //方向位置位
1:stosl //eax内容存到es:edi所指的内存处,且edi增4
subl $0x1000,%eax //物理地址减4K,处理下一页。因为页表中的每一项都是代表一页的。
jge 1b //若小于0表示都已填写页表ok
xorl %eax,%eax //页目录地址在0x0000,所以这里对eax寄存器清空
movl %eax,%cr3 //将页目录表地址存入CR3中
movl %cr0,%eax //
orl $ox80000000,%eax //开启PG标志
movl %eax,%cr0 //表示启动使用分页处理
ret //
(5)转去执行/init/main.c程序的main函数
//现在新的版本的内核初始化程序改名字为start_kernel()
after_page_tables:
push $0 //设置main函数调用参数envp=0
push $0 //设置main函数调用参数argv=0
push $0 //设置main函数调用参数argc=0
push $L6
push $_main //编译程序对main函数的内部表示方法
jmp setup_paging //设置分页结束后,执行ret会将main地址弹出,转去执行main函数
jmp L6 //不会跳转到这里
到此为止,引导启动程序结束,开始转去执行内核初始化程序。