Linux内核学习笔记之系统启动(一)

本文深入解析了操作系统启动的六个关键步骤,从硬件自动加载硬盘引导扇区内容开始,到最终跳转执行操作系统内核指令。文章通过代码示例详细解释了实地址模式下操作系统引导扇区如何工作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    我们要运行程序,必须先启动操作系统,但是刚开机的时候又是谁运行了操作系统呢?开机的那刻究竟发生了什么?接下来让我们一起来揭开操作系统启动过程的神秘面纱~
    我先引用《Linux内核注释》的一段原话来让大家对开机后发生的情况有段简要的认识,然后结合代码来详细描述这个过程:
    Linux 的最最前面部分是用 8086 汇编语言编写的(boot/bootsect.s),它将由 BIOS 读入到内存绝对地址 0x7C00(31KB)处,当它被执行时就会把自己移到绝对地址 0x90000(576KB)处,并把启动设备中后2kB 字节代码(boot/setup.s)读入到内存 0x90200 处,而内核的其它部分(system 模块)则被读入到从地址 0x10000 开始处,因为当时 system 模块的长度不会超过 0x80000 字节大小(即 512KB),所以它不会覆盖在 0x90000 处开始的 bootsect 和 setup 模块。随后将 system 模块移动到内存起始处,这样 system模块中代码的地址也即等于实际的物理地址。便于对内核代码和数据的操作。图 3.1 清晰地显示出 Linux系统启动时这几个程序或模块在内存中的动态位置。其中,每一竖条框代表某一时刻内存中各程序的映像位置图。在系统加载期间将显示信息"Loading..."。然后控制权将传递给 boot/setup.s 中的代码,这是另一个实模式汇编语言程序。


下面我将针对上图的1~6号步骤结合Linux代码进行详解,这节都是汇编,所以讲解起来可能很枯燥,如果你只是想知道这个时候操作系统做了什么,可以不必详读每步的内容,只需看标签上的总结和上面的图3.1基本上就可以了解了(学习资料传送门

  • 步骤<1>硬件自动加载硬盘第一个扇区内容到内存
    开机后谁运行了操作系统?其实开机后第一个运行的不是我们的Linux操作系统,而是主板里面一开始就已经存在的一个微型操作系统—BOIS,这是一个固化在主板的ROM里面,并且是建立在系统硬件基础上的操作系统,提供直接操作硬件的BOIS调用(最初级的系统调用)的操作系统,而它完成了启动Linux操作系统的第一步加载硬盘的第一个扇区内存绝对地址 0x7C00(31KB)处,而这个扇区就是我们Linux操作系统的引导扇区,里面保存了如何将Linux操作系统载入内存的指令(这些指令保存文件Boot.s中,0.11的内核bootsect.s和setup.s是不区分的)。这一切都是硬件的设定步骤,所以第一步是由硬件自动完成的。
    为什么不直接让BOIS加载操作系统?由于BOIS只会自动加载硬盘的第一个扇区的512字节的内容,而操作系统的大小远远大于这个值,所以才会先加载操作系统自己的加载程序(这个可以很小),然后通过操作系统的加载程序加载操作系统(SYSTEM模块)到内存中。
  • 步骤<2>将引导扇区内容移到0x90000
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. .text  
  2.   
  3. BOOTSEG = 0x07c0  
  4. INITSEG = 0x9000  
  5. SYSSEG  = 0x1000            | system loaded at 0x10000 (65536).  
  6. ENDSEG  = SYSSEG + SYSSIZE  
  7.   
  8. entry start  
  9. start:  
  10.     mov ax,#BOOTSEG  
  11.     mov ds,ax  
  12.     mov ax,#INITSEG  
  13.     mov es,ax  
  14.     mov cx,#256  
  15.     sub si,si  
  16.     sub di,di  
  17.     rep  
  18.     movw  
  19.     jmpi    go,INITSEG  

    这段汇编不是很难,应该不难看出,就是将ds(0x07c0):si指向的内容赋值给es(0x9000):di,一共赋值256字(MOVW)即512字节,然后跳到go这个标签地方,并且指明代码段为CS=INITSEG,即下条指令从INITSEG(0x9000)+offset go开始执行(段内跳转是跳转偏移不是绝对地址),执行的指令就等于现在的go标签后的内容(步骤3)。之所以要移动是因为0x10000-0x90000(实地址模式下地址是等于段寄存器值左移4位加上段内偏移的,所以这边是4个0不是前面段寄存器里面的3个了)等下要放操作系统的内核代码

  • 步骤<3>完成新的段寄存器设置以及打印系统加载提示字符(注意0.11内核和0.12不同,没有setup.s)
[plain]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. go: mov ax,cs  
  2.     mov ds,ax  
  3.     mov es,ax  
  4.     mov ss,ax  
  5.     mov sp,#0x400   | arbitrary value >>512  
  6.   
  7.         ;这边AH=0x03是BIOS调用读取光标位置,返回值CH=光标起始行 DH,DL=行,列     
  8.     mov ah,#0x03    | read cursor pos  
  9.     xor bh,bh  
  10.     int 0x10  
  11.   
  12.     ;这边AH=0x13是BIOS调用显示字符串,具体调用参数可以百度  
  13.     mov cx,#24  
  14.     mov bx,#0x0007  | page 0, attribute 7 (normal)  
  15.     mov bp,#msg1  
  16.     mov ax,#0x1301  | write string, move cursor  
  17.     int 0x10  

    这段主要是通过BOIS调用(这个时候操作系统还未启动,所有系统功能都是通过调用BOIS中断完成INT 0x10)完成打印"Loading system ..."字符串

  • 步骤<4>加载真正的Linux操作系统到内存
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. mov ax,#SYSSEG  
  2. mov es,ax       | segment of 0x010000  
  3. call    read_it  
  4. call    kill_motor  

    完成加载SYSTEM模块到0x10000的工作,关闭软驱马达用于读取其静态参数,有关read_it的流程图我已上传到本节学习资料

  • 步骤<5>把Linux操作系统内核从0x10000移动到0x00000,在设置全局描述符后开启保护模式,并跳到0x0处执行操作系统内核指令
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. mov ah,#0x03    | read cursor pos  
  2. xor bh,bh  
  3. int 0x10        | save it in known place, con_init fetches  
  4. mov [510],dx    | it from 0x90510.  
  5. cli         | no interrupts allowed !  
    把当前光标位置保存到0x90510处,以后会用到,然后关闭中断,准备移动内核
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.     mov ax,#0x0000  
  2.     cld         | 'direction'=0, movs moves forward  
  3. do_move:  
  4.     mov es,ax       | destination segment  
  5.     add ax,#0x1000  
  6.     cmp ax,#0x9000  
  7.     jz  end_move  
  8.     mov ds,ax       | source segment  
  9.     sub di,di  
  10.     sub si,si  
  11.     mov     cx,#0x8000  
  12.     rep  
  13.     movsw  
  14.     j   do_move  
    这边就是把内核从0x90000移动到0x00000过程了,汇编很简单,movsw表示一次拷贝一个字,每次循环0x8000次,就是0x10000字节,如果你熟悉实地址模式的汇编,就应该知道这是一个段的最大长度,即每次拷贝一个段;ds:di=0x0:di能表示的范围0x0~0x10000-1,ds:di=0x1000:di能表示的范围是0x10000~0x20000-1,所以上面每次为段寄存器add ax,0x1000其实是指向下个段,即下一个0x10000字节
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. end_move:  
  2.   
  3.     mov ax,cs       | right, forgot this at first. didn't work :-)  
  4.     mov ds,ax  
  5.     lidt    idt_48      | load idt with 0,0  
  6.     lgdt    gdt_48      | load gdt with whatever appropriate  
  7.   
  8. | that was painless, now we enable A20  
  9.   
  10.     call    empty_8042  
  11.     mov al,#0xD1        | command write  
  12.     out #0x64,al  
  13.     call    empty_8042  
  14.     mov al,#0xDF        | A20 on  
  15.     out #0x60,al  
  16.     call    empty_8042  
    这边设置中断描述符表和全局描述符表(有关全局描述符的内容下节详细介绍,这两张表只有在保护模式下才有用),并开启A20信号线,最初的CPU只能使用20根地址线来寻址,后面CPU的地址线增加了,能寻址更多的范围,但是为了保持向下兼容,所以设置了A20开关,当关闭的时候20比特以上的地址都被清除
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. mov al,#0x11        | initialization sequence  
  2. out #0x20,al        | send it to 8259A-1  
  3. .word   0x00eb,0x00eb       | jmp $+2, jmp $+2  
  4. out #0xA0,al        | and to 8259A-2  
  5. .word   0x00eb,0x00eb  
  6. mov al,#0x20        | start of hardware int's (0x20)  
  7. out #0x21,al  
  8. .word   0x00eb,0x00eb  
  9. mov al,#0x28        | start of hardware int's 2 (0x28)  
  10. out #0xA1,al  
  11. .word   0x00eb,0x00eb  
  12. mov al,#0x04        | 8259-1 is master  
  13. out #0x21,al  
  14. .word   0x00eb,0x00eb  
  15. mov al,#0x02        | 8259-2 is slave  
  16. out #0xA1,al  
  17. .word   0x00eb,0x00eb  
  18. mov al,#0x01        | 8086 mode for both  
  19. out #0x21,al  
  20. .word   0x00eb,0x00eb  
  21. out #0xA1,al  
  22. .word   0x00eb,0x00eb  
  23. mov al,#0xFF        | mask off all interrupts for now  
  24. out #0x21,al  
  25. .word   0x00eb,0x00eb  
  26. out #0xA1,al  
    这边主要是对8259A中断控制器的编程,具体内容可以参见文献或者百度,目前我对研究这个没有太大兴趣,而且这个不影响我们理解操作系统
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. mov ax,#0x0001          | protected mode (PE) bit  
  2.     lmsw    ax      | This is it!  
  3.     jmpi    0,8     | jmp offset 0 of segment 8 (cs)  
  4. gdt:  
  5.         ;gdt[0]  
  6.     .word   0,0,0,0     | dummy  
  7.         ;gdt[1]  
  8.     .word   0x07FF      | 8Mb - limit=2047 (2048*4096=8Mb)  
  9.     .word   0x0000      | base address=0  
  10.     .word   0x9A00      | code read/exec  
  11.     .word   0x00C0      | granularity=4096, 386  
  12.         ;gdt[2]  
  13.     .word   0x07FF      | 8Mb - limit=2047 (2048*4096=8Mb)  
  14.     .word   0x0000      | base address=0  
  15.     .word   0x9200      | data read/write  
  16.     .word   0x00C0      | granularity=4096, 386  
    开启保护模式,并加载状态字,然后取出gdt[1](CS=0000 0000 0000 01000b,高13位为选择子,即gdt数组下标)对应表项的段基地址0,加上偏移0,即跳转到内存地址0:0这个位置开始执行指令,由于SYSTEM模块被移动到内存地址0x0处,所以这边就是要开始执行操作系统的第一条指令了这边简单介绍下实地址模式和保护模式,在实地址模式下一个逻辑地址cs:xx/ds:xx对应的物理地址为cs<<4+xx/ds<<4+xx,而保护模式下,cs/ds变成了选择子,他们只是一个索引,用于指示对应全局描述符表中对应表项(全局描述符表类似数组,选择子类似数组下标,这边还未开启分页模式,所以逻辑地址通过段映射得到的线性地址就是物理地址),全局描述符表具体内容在下节介绍
    原来开机后就是做这些事情啊,是不是很激动自己终于揭开了操作系统的第一层面纱了?感觉自己一下子学到了很多,但是操作系统依然神秘?当然,现在都还没见到操作系统的核心~今天的内容只能算是开胃菜,下节内容更精彩~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值