计算机上电后Linux内核从引导到启动的过程

2019-04-13 15:08发布

class="markdown_views prism-atom-one-light">

计算机上电后Linux内核从引导到启动的过程

笔记说明:在RedHat9.0上使用bochs 2.1.1进行实验
目录

简单了解下BIOS程序

这里写图片描述 首先我们了解下BIOS程序,它是被固化在计算机主板的一小块ROM芯片里的一段程序,不同的主机板所用的BIOS程序也有所不同。但是就启动部分而言,各种BIOS的基本原理大致相似。为了方便,我用参考书籍中的说法,使用的BIOS程序占用0xFE000-0xFFFFF的地址段,大小8KB。0xFFFF0是BIOS程序的入口地址。总之我们要知道的就是计算机上电后会去执行BIOS程序,然后BIOS程序会把我们自己写的代码从磁盘的第一扇区复制到内存地址为0x07c00的地方。参考链接: 计算机上电启动过程计算机上电之后,CPU会直接去执行内存地址为0xFFFF0处的指令,我们可以用bochs模拟器来调试验证这个结论,如图所示。
计算机上电启动时BIOS自举 下载Linux0.11内核代码1

内核编译连接/组合结构

书上截图

Linux内核启动的大致流程

Created with Raphaël 2.1.2按下计算机电源键BIOSbootsectsetupheadmain结束

启动过程中程序内存映像的位置变化

书上截图

代码分析与理解

这里只放自己不太理解后经梳理的部分代码,要想了解详细注释请参考《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.ssetup.s的代码在压栈操作过程中被覆盖,setup.s的代码 !也即将被加载到内存地址为0x90200处。 总的来说,bootsect模块主要是做了这几件事情,他首先把自己从0x07c00的位置搬运到0x90000的位置并继续之前的汇编指令执行,然后把setup模块从磁盘复制到内存地址为0x90200的地方,因为bootsect的大小为512B,所以这是紧靠bootsect模块的位置,之后再将system模块从磁盘复制到内存地址为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: 以上添加的一些参考链接,其实在赵炯先生的书中也都有提及。

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内核设计的艺术》第二版 新设计团队著
  1. 赵炯先生的官网是www.oldlinux.org,里头收集了许多早期和Linux有关的资料