计算机上电后Linux内核从引导到启动的过程
笔记说明:在RedHat9.0上使用bochs 2.1.1进行实验
目录
简单了解下BIOS程序
首先我们了解下BIOS程序,它是被固化在计算机主板的一小块ROM芯片里的一段程序,不同的主机板所用的BIOS程序也有所不同。但是就启动部份而言,各种BIOS的基本原理大致相像。为了便捷,我用参考书籍中的说法,使用的BIOS程序占用0xFE000-0xFFFFF的地址段,大小8KB。0xFFFF0是BIOS程序的入口地址。总之我们要知道的就是计算机上电后会去执行BIOS程序,然后BIOS程序会把我们自己写的代码从c盘的第一磁道复制到内存地址为0x07c00的地方。参考链接: 计算机上电启动过程计算机上电以后,CPU会直接去执行内存地址为0xFFFF0处的指令,我们可以用bochs模拟器来调试验证这个推论,如图所示。
下载Linux0.11内核代码
内核编译联接/组合结构
Linux内核启动的大致流程 启动过程中程序显存映像的位置变化
代码剖析与理解
这里只放自己不太理解后经梳理的部份代码,要想了解详尽注释请参考《Linux内核完全注释》。
代码所在目录是linux/boot/
bootsect.s
!
!SYSSIZE是人为设定的上文中内核编译链接/组合结构中system模块在内存中的有限存储空间,以节为单位(1节=16B),
!而且为了保险起见该值不得超过0x7FFF,原因是Linux 0.11版本在设计把system模块从磁盘搬运至内存时,
!仅在内存中留出了从0x10000至0x8FFFF这一段共512KB的内存空间,如果该值太大而内核也足够大,
!就将把0x90000后的可执行代码给覆盖掉。尽管这种情况不太可能会发生,但毕竟初学,还是考虑下。
SYSSIZE = 0x3000
... !此处省略一些代码
jmpi go,INITSEG
!此时的cs已经从0x07c0变成0x9000
go: mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
!因为程序中有堆栈操作,所以就必须要设置一个堆栈指针,由于栈操作中压栈的方向是由高地址到低地址的方向
!所以要是sp的值远远大于512,以防止bootsect.s和setup.s的代码在压栈操作过程中被覆盖,setup.s的代码
!也即将被加载到内存地址为0x90200处。
总的来说,bootsect模块主要是做了这几件事情,他首先把自己从0x07c00的位置搬运到0x90000的位置并继续之前的汇编指令执行新设计团队linux内核设计的艺术:图解linux操作系统架,然后把setup模块从c盘复制到内存地址为0x90200的地方,因为bootsect的大小为512B,所以这是邻近bootsect模块的位置,之后再将system模块从c盘复制到内存地址为0x10000的地方,Linus在这儿给system模块预留了512KB的显存空间,做完这种主要工作后bootsect将接力棒交给了setup。
setup.s
...
!这里的代码都是做一些获取并保存机器系统数据的操作,这里略去了,具体可看源代码。
...
! Get hd0 data
mov ax,#0x0000
mov ds,ax
!4*0x41 = 0x0000:0x104,这里是BIOS的中断向量表的位置里头存放着硬盘参数阵列表的首地址0xf000:0xe401。
!把ds:si赋值为0xf000:0xe401,从上图可知这个地址刚好是BIOS程序占地址段(0xFE000-0xFFFFF)
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb
!BIOS中断向量表和BIOS数据区被完全覆盖,此后,在新的中断服务体系建立之前,
!操作系统将不再具备响应并处理中断的能力。
! 下面是进入保护模式前的准备工作,这个时候BIOS占领的区域(0x00000-0x10000中被BIOS占用的地方)
! 也没什么作用了,于是将system模块向内存低端移动了64K(即system模块的起始位置从0x10000变成了0x00000)
! first we move the system to it is rightful place
mov ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
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
! 完成system模块的搬移之后,就开始设置中断描述符表和全局描述符表
end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
! that was painless, now we enable A20
! 下面开启A20
call empty_8042
mov al,#0xD1 ! command write
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042
A20地址线问题
!下面的代码是对中断重新进行编程的代码
! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.
mov al,#0x11 ! initialization sequence
out #0x20,al ! send it to 8259A-1
.word 0x00eb,0x00eb ! jmp $+2, jmp $+2
out #0xA0,al ! and to 8259A-2
.word 0x00eb,0x00eb
mov al,#0x20 ! start of hardware int's (0x20)
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 ! start of hardware int's 2 (0x28)
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 ! 8259-1 is master
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 ! 8259-2 is slave
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 ! 8086 mode for both
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0xFF ! mask off all interrupts for now
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
8259A编程
! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
!
! Well, now's the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.
!下面两行开启保护模式
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax ! This is it!
!在保护模式下,这条语句的8不能单纯的理解成数字8,而应理解成段选择子,有的书上也管它叫段选择符(占2字节)
!也就是说在实模式下段基址寄存器的作用已经发生了改变,用于选择描述符表和描述符表项以及所要求的特权级
!执行完后就直接跳转到system模块中去执行head.S的代码了。
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
段选择子与段描述符结构
! This routine checks that the keyboard command queue is empty
! No timeout is used - if this hangs there is something wrong with
! the machine, and we probably couldn't proceed anyway.
empty_8042:
.word 0x00eb,0x00eb
in al,#0x64 ! 8042 status port
test al,#2 ! is input buffer full?
jnz empty_8042 ! yes - loop
ret
gdt:
.word 0,0,0,0 ! dummy
为什么全局描述符表GDT的第0项总是一个空描述符,而局部描述符表却不是这样?
!接下来继续定义了两个段描述符,分别用来描述代码段和数据段,
!每个段描述符占据8字节的内存空间,有关段描述符的具体介绍可在赵炯先生的书中找到。
.word 0x07FF ! 8Mb - limit=2047 (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实际上只是一张空表,因目前处于关中断状态不需要调用中断服务程序,反应了一种够用就行的思想。
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 !这里的512是因为setup模块是从0x90200开始运行的
.text
endtext:
.data
enddata:
.bss
endbss:
以上添加的一些参考链接新设计团队linux内核设计的艺术:图解linux操作系统架,其实在赵炯先生的书中也都有提到。
head.s
...
_pg_dir: !在这个地方放置该符号的目的是暗示着这里将会存放页目录表
...
...
!这段代码是用来测试A20是否已经成功打开,不断的把%eax中不同的值送入0x000000地址处
!然后与0x100000地址处的值进行比较,如果相等就陷入死循环,表示A20未选通,内核无法使用1MB以上的内存。
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b
...
fninit fstsw指令
!这两条是80x87协处理器指令
fninit
fstsw %ax
...
!这里是一个比较关键的地方,设计者用了一种模拟call调用返回的方法,提前将main地址压栈了,
!这里的_main是编译程序对main的内部表示方法,事后在执行完setup_paging后,用ret指令将会直接
!跳转到main函数执行,也就是正式进入了内核,在main.c中将会进行内核初始化的操作。
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $_main
jmp setup_paging
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
...
!接下来的过程便是设置页目录项和页表项,然后开启分页机制,最后跳转到main函数执行内核的初识化工作。
.align 2
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl
movl $pg0+7,_pg_dir /* set present bit/user r/w */
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
...
要理解这段代码须要了解一点一些GNU汇编和x86/x86_64 CPU控制寄存器(Control Registers)的知识。
学习过程
一年前我看这本书时,几乎就是看不懂,现在竟忽然能读懂这么点了,于是就想着把自己的学习过程以及对Linux内核代码的理解以博客的方式给记录出来,也便捷自己日后备考学习使用。第一篇写的不是挺好,大部分知识参考资料里都有讲,不过还是希望自己能坚持把这个系列的博客写下去。
参考资料
[1]: 《Linux内核完全注释》 赵炯编绘 点击此处下载
[2]: 《Linux内核设计的艺术》第二版 新设计团队著
赵炯先生的官网是,里头搜集了许多初期和Linux有关的资料