【linux V0.11】boot
文章目录
- 前言
- 磁盘结构
- 内存布局
- bootsect.s
- 来源
- 内容
- setup.s
- 描述符表
- head.s
记录一下自己第一次看Linux内核的过程,大致也是囫囵吞枣过一遍。
参考书籍《Linux内核完全注释》
前言
磁盘结构
10分钟学懂磁盘的结构(盘片、磁道、扇区、柱面)
名称 | 解释 |
---|---|
Cylinder(柱面) | 相同半径的一组磁道,从 0 开始编号 |
Head(磁头) | 对应盘面(Platter),即上下不同磁盘片的一面,从 0 开始编号 |
Sector(扇区) | 磁道上被划分出的小块,从 1 开始编号 |
由上,可用CHS 地址(柱面号,盘面号,扇区号)来定位任意一个“磁盘块”。
在操作系统或文件系统使用逻辑寻址(如 LBA 或扇区编号)。
在 LBA(Logical Block Addressing):
扇区从 0 开始编号;
(C=0, H=0, S=1) → LBA 0;
(C=0, H=0, S=2) → LBA 1;
以此类推。
所以有时候会有不同的说法,比如第0扇区是逻辑第0扇区,也是(0,0,1)的第一个物理扇区。
内存布局
(查看完整的实模式内存布局)
bootsect.s
来源
BIOS会从磁盘第0扇区加载bootsect到内存0x07C00。
内容
- 将自己从 0x7c00 移动到 0x90000
BIOS 把 bootsect 放在 0x7c00,但这块内存空间太小,而且后续要加载其他模块(如 setup 和内核),所以 bootsect 会把自己移动到更高的地址 - 使用 BIOS 中断加载 setup 模块
bootsect 使用 BIOS 的中断服务(int 0x13)从磁盘第 1~4 扇区上读取下一个模块:setup - 继续使用 BIOS 中断加载 system(Linux 内核)
- 跳转到 setup 模块继续执行
setup.s
- 收集 BIOS 提供的系统信息
- 光标位置 :保存当前屏幕光标位置,存储在 0x90000 ,用于后续终端初始化;
- 内存大小 :通过 BIOS 中断获取扩展内存大小(单位 KB),存入 0x90002 处;
- 显示模式 :获取视频显示信息(模式(如文本模式或图形模式)、窗口宽度等),存入 0x90004 和 0x90006;
- 显卡信息:获取 EGA/VGA 等配置参数,存入偏移 0x90008、0x9000a、0x9000c;
- 硬盘参数 :读取硬盘 0 (存入偏移 0x90080)和 硬盘 1 (存入偏移 0x90090)的参数表;
- 检查是否存在第二个硬盘(hd1),如果硬盘 1 不存在,就清空之前写入的硬盘参数。 ;
- 关闭中断,准备进入保护模式
- 将内核(system)从0x10000 ~ 0x90000移动到0x00000 ~ 0x80000
- 加载 GDT (全局描述符表)和 IDT(中断描述符表)
设置 IDT和 GDT;
准备进入保护模式所需的段机制和中断处理。 - 开启 A20 地址线
开启 A20 地址线,允许访问超过 1MB 的内存;
A20 是第 21 条地址线(从 A0 开始数);
它对应的是物理地址的第 21 位(bit 20) ;
如果 A20 被禁用,那么任何对超过 1MB 地址的访问都会被“回绕”到低地址区(比如 0x100000 会被当作 0x00000); - 重新编程 8259A 中断控制器
重映射中断向量,避免冲突;
把中断映射到 0x20~0x2F;
设置主从 PIC 控制器。 - 切换到保护模式
设置 CR0.PE 位,进入保护模式; - 跳转到system(真正的系统内核起点
描述符表
描述符表由多个8字节长的描述符项组成。
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs) !跳转至cs段8,偏移0处。
我们已经将system模块移动到0x00000开始的地方,所以这里的偏移地址是0。 这里的段值8已经是保护模式下的段选择符了,用于选择描述符表和描述符表项以及所要求的特权级。
段选择符长度为16位(2字节);
位0~1表示请求的特权级0~3,Linux操作系统只用到两级:0级(系统级)和3级(用 户)级;
位2用于选择全局描述符表(0)还是局部描述符表(1);
位3~15是描述符表项的索引,指出选择第几项描述符。
所以段选择符8(0000 0000 0000 1000
)表示请求特权级0、使用全局描述符表中的第1项,该项指出代码的基地址是0,因此这里的跳转指令就会去执行system中的代码。
gdt:/*第0个描述符,不用但得存在*/.word 0,0,0,0 ! /*第1个描述符,系统代码段描述符,这里在gdt表中的偏移量为0x08,当加载代码段寄存器(段选择符)时,使用的是这个偏移值*/.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb).word 0x0000 ! base address=0.word 0x9A00 ! code read/exec.word 0x00C0 ! granularity=4096, 386/*第2个描述符,这里在gdt表中的偏移量是0x10,当加载数据段寄存器(如ds等)时,使用的是这个偏移值。*/.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb).word 0x0000 ! base address=0.word 0x9200 ! data read/write.word 0x00C0 ! granularity=4096, 386idt_48://lidt指令的操作数。6字节。前2字节是idt表限长,后4字节是idt表所处的基地址。.word 0 ! idt limit=0.word 0,0 ! idt base=0Lgdt_48://lgdt指令的操作数.word 0x800 ! 全局表长度为2KB,因为每8B组成一个段描述符项,所以表中共可有256项。.word 512+gdt,0x9 ! 4字节构成的线性地址:0x0009<<16+0x0200+gdt,即0x90200+gdt
head.s
- startup_32是从绝对地址0x00000000开始的,这里也同样是页目录将存在的地方,因此这里的启动代码将被页目录覆盖。
_pg_dir:
startup_32:movl $0x10,%eax #设置段寄存器指向 GDT 中的 0x10 描述符(即内核数据段)。mov %ax,%dsmov %ax,%esmov %ax,%fsmov %ax,%gslss _stack_start,%esp #设置堆栈指针 esp 指向 _stack_start。call setup_idt #调用 setup_idt 和 setup_gdt 初始化中断描述符表和全局描述符表。call setup_gdtmovl $0x10,%eax #重新加载所有段寄存器,确保使用新 GDT 的段选择符。mov %ax,%dsmov %ax,%es mov %ax,%fsmov %ax,%gslss _stack_start,%esp
setup_idt
/* 默认的中断“向量句柄” :-) */
int_msg:.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:pushl %eaxpushl %ecxpushl %edxpush %dspush %espush %fsmovl $0x10,%eax #使ds,es,fs指向gdt表中的数据段mov %ax,%dsmov %ax,%esmov %ax,%fspushl $int_msg #把调用printk函数的参数指针(地址)入栈call _printk #_printk是 printk 编译后模块中的内部表示法popl %eaxpop %fspop %espop %dspopl %edxpopl %ecxpopl %eaxiret
# 中断描述符表中的项虽然也是8B组成,但其格式与全局表中的不同,被称为门描述符(Gate Descriptor)。
# 它的0~1,6~7字节是偏移量,2~3B是选择符,4~5B是一些标志。
setup_idt:lea ignore_int,%edx #将ignore int的偏移值→edx寄存器,这是偏移地址的低16位 ,后面还会用 %dx 来构造完整偏移movl $0x00080000,%eax # 将选择符0x0008置入eax的高16位中。movw %dx,%ax #偏移值的低16位置入eax的低16位中。此时eax含有门描述符低4B的值。movw $0x8E00,%dx #此时edx含有门描述符高4B的值。lea _idt,%edi #将 _idt 的地址加载到 %edi,作为当前写入的目标位置mov $256,%ecx #设置循环次数为 256,因为 IDT 有 256 个条目
rp_sidt: #循环填充整个 IDTmovl %eax,(%edi) #第一次写入 %eax(低32位)到当前位置movl %edx,4(%edi) #第二次写入 %edx(高32位)到当前位置+4addl $8,%edi #然后 %edi 增加 8,移动到下一个条目dec %ecxjne rp_sidt #直到 256 次完成lidt idt_descr #加载 IDT 描述符 ret
setup_gdt
setup_gdt:lgdt gdt_descr #加载 GDT 描述符 ret
- 测试A20地址线是否已经开启
采用的方法是向内存地址0x000000处写入任意一个数值,然后看内存地址0x100000处是否也是这个数值。如果一直相同的话,就一直比较下去,即死循环、死机,表示地址A20线没有选通,结果内核就不能使用1MB以上内存。 - 检查数学协处理器芯片是否存在
方法是修改控制寄存器CR0,在假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在,需要设置CR0中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。 - 将参数压栈,调用 setup_paging,然后跳转到 main()
after_page_tables:pushl $0 # These are the parameters to main :-)pushl $0pushl $0pushl $L6 # return address for main, if it decides to.pushl $_mainjmp setup_paging
L6:jmp L6 # main should never return here, but# just in case, we know what happens.
分页设置函数
通过设置控制寄存器cr0的标志(PG位31)来启动对内存的分页处理功能,并设置各个页表项的内容,以恒等映射前16MB的物理内存。分页器假定不会产生非法的地址映射(即在只有4MB的机器上设置出大于4MB的内存地址)。注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管理函数能直接使用大于1MB的地址。所有“一般”函数仅使用低于1MB的地址空间,或者是使用局部数据空间,地址空间将被映射到其他一些地方去———mm(内存管理程序)会管理这些事的。对于那些有多于16MB内存的机器,代码就在这里,可对它进行修改。实际上,这并不太困难的。通常只需修改一些常数等。我把它设置为16MB,因为我的机器再怎么扩充都不能超过这个界限(当然,我的机器很便宜的)。我已经通过设置某类标志来给出需要改动的地方(搜索“16MB”),但我不能保证做这些改动就行了。
每个页表长为4KB字节,而每个页表项需要4个字节,因此一个页表共可以存放1024个表项,如果一个表项寻址4KB的地址空间,则一个页表就可以寻址4MB的物理内存。页表项的格式为:项的前0~11位存放一些标志,如是否在内存中(P位0)、读写许可(R/W位1)、普通用户还是超级用户使用(U/S位2)、是否修改过(是否脏了)(D位6)等;表项的位12~31是页框地址,用于指出一页内存的物理起始地址。
setup_paging: movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */ #首先对5页内存(1页目录+4页页表)清零。xorl %eax,%eaxxorl %edi,%edi /* pg_dir is at 0x000 */cld;rep;stosl#共有4个页表,所以只需设置4项。页目录项的结构与页表中项的结构一样,4个字节为1项。#″$pg0+7″表示:0x00001007,是页目录表中的第1项。则第1个页表所在的地址=0x00001007&0xfffff000=0x1000;#第1个页表的属性标志=0x00001007&0x00000fff=0x07,表示该页存在、用户可读写。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 /* --------- " " --------- */#填写4个页表中所有项的内容,共有:4(页表)∗1024(项/页表)=4096项(0-0xfff),#即能映射物理内存4096∗4KB=16MB。每项的内容是:当前项所映射的物理内存地址+该页的标志(这里均为7)。#使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项在页表中的位置是1023∗4=4092。#因此最后一页的最后一项的位置就是$pg3+4092。movl $pg3+4092,%edi#最后1项对应物理内存页面的地址是0xfff000,加上属性标志7,即为0xfff007.movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */ std #方向位置位,edi值递减(4B)。
1: stosl /* fill pages backwards - more efficient :-) */subl $0x1000,%eax #每填写好一项,物理地址值减0x1000。jge 1b #如果小于0则说明全填写好了。#设置页目录基址寄存器cr3的值,指向页目录表xorl %eax,%eax /* pg_dir is at 0x0000 */movl %eax,%cr3 /* cr3 - page directory start */movl %cr0,%eax #设置启动使用分页处理(cr0的PG标志,位31)orl $0x80000000,%eax #添上PG标志。movl %eax,%cr0 /* set paging (PG) bit */ret /* this also flushes prefetch-queue */#在改变分页处理标志后,要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。#该返回指令的另一个作用是将堆栈中的main程序的地址弹出,并开始运行/init/main.c程序。#本程序到此真正结束了。
此时system模块在内存中的详细映像如图