感谢博客:https://blog.csdn.net/cinmyheart/article/details/39754269
和https://www.jianshu.com/p/cb6ea1921e7a
PC机软盘和硬盘被分成各个扇区(sectors),每个sector为512字节(byte)。每个扇区是磁盘的最小转移的粒度,即每次读和写操作只能是一个或者多个扇区。如果一个分区是启动分区,那么这个分区的第一个扇区是启动扇区(boot sector)。当BIOS找到了启动磁盘时,它会将这个磁盘的512字节的启动扇区加载到内存(0x7c00到0x7dff),然后使用jmp跳转指令,将CS:IP设置为0000:7c00,即将控制权交给boot loader。(上述提到的地址是人为固定,并且标准化的)
在6.828中,仅仅使用了传统的驱动启动的机制,即加载512byte的启动扇区,交由bootloader完成后续启动。
bootloader的源代码由 boot/boot.S 和 boot/main.c 组成(boot.S是汇编语言文件,main.c是C语言文件)。bootloader主要执行下面两项功能:
1. 完成从实模式向保护模式的转变,只有在保护模式才能发挥x86的32位寻址能力。需要明确的是:(段地址:offset)到物理地址的映射规律发生了改变,同时,offsets从16位变为了32位。
2. boot loader 读取从硬盘kernel。(实现细节:)
「 A20地址线:x86系统组成系统总线的电子线路之一,用于传送第21个bit。(从A0命名)
A20 transmit bit 20(21st bit)
从80286有24根地址线,可以寻址到16MB(保护模式),但是CPU启动时,处于实模式,实现向下兼容,以运行8086(实模式)下的程序。
但是80286没有强制A20线在实模式的时候处于0,因此,地址F800:8000不再指向0x00000000,而指向了“正确”的物理地址0x00100000,从而一些实模式下的DOS程序不能正常工作。为了和这些程序保持兼容,IBM决定解决这个问题。
解决这个问题的思路是:在处理器和系统总线中,A20上插入一个逻辑门(logic gate),称之为Gate-A20.
若A20 Gate被打开,则0x100000-0x10FFEF的内存就可以正常访问。否则,就将超过1M的内存取模再次回到了1M以内。
Gate-A20可以被软件使能或者关闭,从而允许或者禁止系统总线得到A20上的信号。当运行实模式的程序时,Gate-A20关闭,当BIOS检查所有硬件(内存,外设,Gate-A20开启,然后在将控制权交给操作系统时候之前关闭。
BIOS设置CS寄存器为0x0,IP寄存器为0x7c00,开始执行bootloader程序
————————————————————————————————————————————————————————————————————
BIOS 检查完硬件后,会加载引导磁盘的第一个引导扇区(512byte)从0x7c00到0x7dff,然后使用一个jmp指令,设置CS:IP为0000:7c00,把控制权交给扇区内的boot loader程序。
(在gdb中设置breakpoint到0x7c00,c命令执行到0x7c00处暂停)
bootloader 执行过程中,一个重要的功能就是从实模式向保护模式的转换,
从中可以看到,[ 0:7c2d] 到 0x7c32 地址的表现形式可以说明寻址能力的改变。
_____________________
在boot.S中:
.set PROT_MODE_CSEG, 0x8 # 内核代码 段选择子
.set PROT_MODE_DSEG, 0x10 # 内核数据 段选择子
.set CR0_PE_ON, 0X1 # 保护模式使能标志位
.globl start
.globl 告诉编译器 start 作为整个程序的入口,start表示的地址,也就是整个程序的的入口地址。
cli 禁止中断
.code16 16位模式下工作
设置段寄存器(DS ES SS 数据段,额外段,栈段)
使能A20地址线(第21条地址线),即Gate-A20打开。
XV6打开A20采用键盘控制器法,与键盘控制器有关的IO接口是0x60 和 0x64,0x64有状态控制功能,0x60是数据端口。
--------
进入保护模式:
GDT:
~内存地址为段地址+段内偏移。实模式下,段地址放在段寄存器中,保护模式下,段寄存器中保存的不再是段地址,而是GDT(global descriptor table)的索引。为了正确的启动保护模式下的段机制,进入保护模式之前首先要创建GDT。
~GDT中每个段对应一个表项,表项中保存了段基址,段大小,访问权限等。从而访问内存时,可以检查访问的合法性,因而称之为保护模式。GDT中保存了很多段描述符,因此称之为全剧描述符表。
三段基地址合起来形成32位基地址
其中:DPL 为内存访问权限等级,占2个bit,一共4个级别,0为最高权限,内核运行在这个层次,3为最低,普通的应用程序运行。
CPU中,有一个专门的寄存器称之为GDTR,用来保存GDT在内存中的位置和GDT的长度。 GDTR一共48位,高32位储存GDT在内存中的位置,低16位用来存GDT有多少个段描述符。16位最大可以表示65536个数,每个段描述符8位,因此可以有8192个段描述符。
CPU同时提供了一个指令,用来吧GDT的地址和长度传给GDTR寄存器,lgdt 代表加载全局描述符表。
下图表示,从实模式转化为保护模式,使用引导GDT使得虚拟地址等同于物理地址
其中,lgdt gdtdesc 中
gdt:
STA_X等在
代码段空间:
数据段空间:
可以看出,基地址都是0x00000000,内存分段都是0xfffff
G: 1 表示 20 位段界限单位是 4KB,最大长度 4GB;
0 表示 20 位段界限单位是 1 字节,最大长度 1MB
因此所使用的内存都是从0开始到4GB结束的全部内存。
E=1说明是代码段,E=0说明是数据段。代码段RW=1代表可读,数据段RW=1代表可读可写。
NOTE:
代码段和数据段都采用了从0到4GB的全部内存寻址,这种内存规划的方法称之为“平坦内存模型”,即便是Linux也是用基于这样的方式规划内存的,并没有真正的分段。这是应为x86的分页机制是基于分段的,Linux选用了更加先进的分页机制管理内存,因此分段这里只是一个形式。
完成段表(GDT)的初始化后,这是控制寄存器,使能保护模式:
无法直接修改cr0寄存器的内容,因此先用一个通用寄存器保存当前cr0寄存器的值,然后用CRO_PE_ON这个宏定义(在程序初始位置)(数值为0x1)和eax寄存器中的值或运算,将这个结果重新送给cr0,从而将cr0的第0位设置成1,即PE=1,使能保护模式!!(cr0的第31位PG=0表示我们只使用分段机制,不实用分页。
之后进入32位:
ljmp语法:ljmp segment offset,同时这是段寄存器和段内偏移(EIP),
x86架构的CPU由于考虑到向后兼容性,使得CPU在开始时处于实模式运行状态,只能使用20条地址线,这在现在肯定是无法满足的,通过设置地址线,可以完全使用所有地址线。具体设置过程主要是通过写端口数据来完成,这里就不阐述了,代码如下:
设置保护模式数据段寄存器(DS ES SS)设置好后,准备执行C代码
首先设置stack,将esp的指向start地址,start是代码开始的地址,代码是向高内存地址放置(从低到高),stack是向低内存增长,这样设置可以让代码和栈反向扩张,互补影响。
call bootmain后会执行bootmain.c中的bootmain()
summary:
boot loader:
1. 禁止接受中断(cli)
2. 设置数据段寄存器
3. 使能A20地址线
4. 建立GDT表,设置GDTR寄存器值(高32位表示GDT表的位置,后16位表示gdt的大小)
5. cr0 末尾置1,使能保护模式
6. 设置保护模式的代码段寄存器和数据段的段寄存器
7. 设置栈顶指针esp,指向start,使其和代码段反向扩张。(跳转bootmain之前就设置了堆栈!
movl $start %esp
这是我们看到的最早的内核栈)
8. 进去bootmain.()
bootmain():
宏定义磁盘扇区大小为512字节,读取第一个磁盘的4kb,判断是否为ELF格式文件,是继续将文件读入内存。
EFL结构体定义:
其中:uint32_t e_entry : program entry point
e_phoff: offset of the program header table
e_phnum: number of program header table
Proghdr(程序头定义):
p_type : segment type
p_offset: beginning of the segment in the file
p_va : where this segment should be placed at
p_memsz: size of the segment in byte
ph为函数头指针,为ELFHDR+offset(e_phoff)
eph 为最后末尾,为ph+program的个数(e_phnum)
读入了原始的磁盘扇区。
至此,kernel的被读入内存,此时bootloader会将控制权转给kernel:
如何找到这个入口地址呢?
通过反汇编kernel镜像
objdump -x ./obj/kern/kernel
可以在kern/entry.S 中得到印证,找到这个代码: