0%

Linux内核启动源码与流程分析(一)

从开机到执行linux的main函数过程

开机的时候 80x86的CPU自动进入实模式,从地址0xFFFF0开始自动执行程序代码,这个地址即是ROM-BIOS的地址,BIOS将执行一些系统检测,并在绝对物理地址0初始化BIOS中断向量,然后,将可启动设备的第一个扇区(称磁盘引导扇区,512byte)读入内存绝对地址0x7c00, 并跳转至该处执行,至此,就完成了内核初始化的工作。

bootsect.s

Linux最最前面的程序是bootsect.s,将被BIOS加载到内存绝对地址0x7c00处,他将完成以下操作

  • 将自己移动到0x90000处
  • 将启动设备中后2KB代码(boot/setup.s)读入到0x90200处
  • 将内核其他部分(system)读入到0x10000处
  • 完成以上操作后,还会做一些其他检测工作,不是我们关注的重点,忽略
  • 完成代码加载和一些初始化工作后,执行jmpi 0, SETUPSEG这里SETUPSEG =0x90200,跳到setup.s去执行。

下图前三个场景就是bootsect.s干的

启动引导时内核在内存的位置

setup.s

setup.s是一个操作系统加载程序,他的主要作用是利用BIOS中断读取机器系统数据,并保存到0x90000处,以供内核使用。

所取得的参数和保留的位置如下:

前面bootsect.s将内核代码放置到了0x10000起始处,目的就是为了不把加载到0地址的BIOS中断覆盖掉,setup.s以及完成对BIOS中断的利用,参数也全部保存了起来,那么就可以移动内核代码到地址0处了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    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

完成移动后:

1
2
3
4
5
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

这里加载了两个临时的gdt和idt表,主要看gdt表:

代码段:0x00C0 9A00 0000 07FF,表示段长为8M,基地址为0,可读可执行

数据段:0x00C0 9200 0000 07FF,表示段长为8M,基地址为0,可读可写

代码段与数据段描述符格式

lgdt指令:要求6字节操作数,前2字节为gdt表限长,后四字节为gdt表基地址。以gdt_48为例,.word 0x800即限长为2048个字节,根据一个gdt表占8个字节,这里即有256个gdt表项。.word 512+gdt, 0x9,即0x90200 + gdt,即指向该段程序的gdt表的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gdt:
.word 0,0,0,0 ! dummy

.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_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

来看最后也是最关键的代码:

1
2
3
mov ax,#0x0001      ! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)

lmws将修改CR0寄存器,完成实模式到保护模式的切换,此时我们就无法直接跳转物理地址了,而必须借助段选择符来完成跳转,所以来看这段指令jmpi 0,8,即offset:0,段选择符8(0000 0000 0000 1000),下面是段选择符的数据结构:
那么这里的描述符引索即为:0000 0000 0000 1,TI:0,RPL:0,即选择到了我们第一个gdt表,也即内核代码段,根据段描述符,可知要跳转的地址为物理地址0,此时我们就跳转到了system代码段,并在那里开始执行,而system代码段的头部代码就是head.s,即开始执行heads.s

实模式到保护模式的切换,即寻址方式发生了改变,这是非常重要的一步,实模式下,仅有20位寻址能力,仅有1M,而计算机希望拥有更大的寻址空间,显然20位寻址能力绝对不满足,那怎么切换到更大的寻址模式呢,即切换到保护模式,将寻址能力提升到32位,这个时候内存就变成很大了,那么他是怎么做到这件事的呢?即是改变了指令的解释模式,拿jmpi 0,8举例,这里的8就不在是段了,而是寻找段描述符,利用段描述符去寻找实际的段地址,完成实际的跳转。那么是怎么改变指令的解释模式的呢?即是通过修改CR0寄存器的值。

实模式下:CS左移四位+IP

保护模式下:CS变成了段选择子,来寻找gdt表,寻找到相应的段选择符,从中找到基址再与IP相加,找到对应的实际地址。

head.s

此时内存的样子

head.s程序在编译成目标生成文件后会和内核其他程序一起链接成system模块,位于system模块的头部,这也是被称为是heads的原因,从这里开始,程序就完全运行在保护模式下了,heads.s使用的是AT&T汇编语言。

这段程序实际上处于内存绝对地址0处开始的地方,程序功能也比较单一。

  • 加载各个数据段寄存器
  • 重新设置中断描述符表idt
  • 重新设置全局描述符表gdt
  • 检测A20是否开启
  • 设置内存管理的分页处理机制,放置在绝对内存地址0的地方,会将head.s的部分程序覆盖
  • 最后,利用返回指令将预先放置的在堆栈中的main.c程序入口地址弹出,此时开始运行main.c

此时内存的样子

GDT描述符表

总结

笔记的描述并没有分析太多源码,因为本身做的操作都是比较简单的逻辑,移动代码以及初始化工作,汇编源码我们还是不心力去看去学习了,建议有兴趣研究的同学可以配合linux0.11+《linux内核完全注释》这本书去看,理解源码的过程尽管费时费事,但收获还是颇多的。

-------- 本文结束 感谢阅读 --------