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

计算机上电后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 按下计算机电源键 BIOS bootsect setup head main 结束

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

书上截图

代码分析与理解

这里只放自己不太理解后经梳理的部分代码,要想了解详细注释请参考《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有关的资料
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值