x86架构linux内核引导过程分析,学习笔记之:X86架构linux启动过程一:linux引导过程...

前言:

本文是对早期内核的引导启动过程做的分析笔记,这样可以更好的了解内核的启动过程。而现代大部分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                  //不会跳转到这里

到此为止,引导启动程序结束,开始转去执行内核初始化程序。

你可能感兴趣的:(x86架构linux内核引导过程分析,学习笔记之:X86架构linux启动过程一:linux引导过程...)