在前一部分,我们进入了内核,看到了内核创建代码的初始化部分.并停在main函数第一次调用前
这一部分,我们将会继续探寻内核的创建代码,同时复习
- 什么是保护模式
- 如何转换到保护模式
- 对堆空间和终端的初始化
- 内存管理 cpu验证 键盘初始化
- 和其他很多
保护模式
在进入intel长模式之前,内核必须将cpu转入保护模式
保护模式在1982年第一次被加入x86体系,是intel长模式出现前的主要intel处理器模式
离开实模式的主要原因是它对地址的的限制,实模式下仅有1mb可用,有时甚至只有640kb
保护模式改变了很多,最主要的改变在于内存管理上.20位地址总线被改为32位.允许4gb的内存寻址.同样页模式也被引入.在下一节会提到
保护模式下内存管理被分为两个基本独立的部分 ,
- 段
- 页
这里只讲述关于段的内容,页会在下一部分讨论
实模式下地址由两部分组成,
- 段的基地址
- 基于段的偏移
在保护模式下段管理机制被完全重写,没有了64kb固定大小的片段,段的大小和地址通过一个新建立的数据结构(Segment Descriptor)段描述符来描述,段描述符被储存在另外一个数据结构全局描述符表(GDT Global Descriptor Table)中
GDT是驻留在内存中的结构, 它没有固定的内存区域,因此他的地址被保存在一个特殊的寄存器(gdtr)中接下来我们会看到GDT如何在内核中被加载.有一个操作将gdt从内存中加载出.
lgdt gdt 将源操作数中的值加载到全局描述符表格寄存器 (GDTR)
GDTR是一个48位寄存器,且有两部分组成
- 16位全局描述符表大小
- 32位的地址GDT包含了段描述符,每一个描述符有64位大小,一个通常的描述符结构如下
63 56 51 48 45 39 32
------------------------------------------------------------
| | |B| |A| | | | |0|E|W|A| |
| BASE 31:24 |G|/|L|V| LIMIT |P|DPL|S| TYPE | BASE 23:16 |
| | |D| |L| 19:16 | | | |1|C|R|A| |
------------------------------------------------------------
31 16 15 0
------------------------------------------------------------
| | |
| BASE 15:0 | LIMIT 15:0 |
| | |
------------------------------------------------------------
段大小限制位于描述符首部,占0-15bit 剩下的16-19位于48:51处 ,这些定义了段的长度 这取决于gG(第55位)标志位的内容
- 如果 G位为0 且段限制为0 段大小为1byte
- g为1 段限制为 0 ,段大小为4096byte
- g为0 段限制为 0xfffff 段大小 1mb
- g为1 段限制为0xfffff 段大小 4g 如果g是零,限制被解释为1byte 段最大值为1mb g是1 limit被解释为4kb 段最大值为4g 事实上 g位为1时候,limit值被左移12位 有20bit变为32bit 即4g
base[32位],被分为16 – 31bit 32 – 39 bit 56 -63 bit 定义了段的物理开始地址
type /attribute [5bit] 在40-44位定义段的类型以及如何访问
s位在44bit位 标识符类型 0 代表系统段 1代表代码段或者数据段 (栈区和数据段必须可读可写)
为了验证该段是数据还是代码段,我们检验ex位(43bit处) 0 代表数据 否则 代码段
一个段可能是一下几种类型之一
--------------------------------------------------------------------------------------
| Type Field | Descriptor Type | Description |
|-----------------------------|-----------------|------------------------------------|
| Decimal | | |
| 0 E W A | | |
| 0 0 0 0 0 | Data | Read-Only |
| 1 0 0 0 1 | Data | Read-Only, accessed |
| 2 0 0 1 0 | Data | Read/Write |
| 3 0 0 1 1 | Data | Read/Write, accessed |
| 4 0 1 0 0 | Data | Read-Only, expand-down |
| 5 0 1 0 1 | Data | Read-Only, expand-down, accessed |
| 6 0 1 1 0 | Data | Read/Write, expand-down |
| 7 0 1 1 1 | Data | Read/Write, expand-down, accessed |
| C R A | | |
| 8 1 0 0 0 | Code | Execute-Only |
| 9 1 0 0 1 | Code | Execute-Only, accessed |
| 10 1 0 1 0 | Code | Execute/Read |
| 11 1 0 1 1 | Code | Execute/Read, accessed |
| 12 1 1 0 0 | Code | Execute-Only, conforming |
| 14 1 1 0 1 | Code | Execute-Only, conforming, accessed |
| 13 1 1 1 0 | Code | Execute/Read, conforming |
| 15 1 1 1 1 | Code | Execute/Read, conforming, accessed |
--------------------------------------------------------------------------------------
数据段 (bit 43)为0 代码段(bit 43)为1 剩下的3位(40,41,42)是 EWA(Expansion Writable Accessible) 可写or CRA(Conforming Readable Accessible)只读.
段寄存器包括在实模式下的段选择器 , 在保护模式下 选择器以一种不同的方式 每一个段描述符与一个16bit段选择器结构体相连
15 3 2 1 0
-----------------------------
| Index | TI | RPL |
-----------------------------
index储存描述符在GDT表里的下标
TI 表明在哪找到描述符 如果TI为0 描述符在全局描述符表里查询否则在本地描述符表里查
RPL 包含 请求者的特权级别
每一个段寄存器有可视和隐藏的部分
可视部分 段选择器存储在这里
隐藏 段描述符在这里 (包括基地址 limit attribute 和一些标志位)
为在保护模式下获取物理地址 下面的步骤
- 段描述符必须被加载进段寄存器里
- cpu尝试在GDT偏移里寻找一个段描述符 加载到段寄存器的隐藏部分
- 如果页不可用 段的线性地址或者说物理地址通过公式 基地址(由前一个包括的描述符得到) + 偏移 得到
示意图
实模式向保护模式的转换算法
- 禁用中断
- 通过lgdt指令加载GDT
- 将CR0(control register 0)设定PE位
- 跳转到保护模式
接下来,内核完全转向保护模式的转变 在这之前 我们做一些准备
在 arch/x86/boot/main.c里 ,我们看见一些对键盘初始化,堆初始化等例程.
拷贝装载部分进入零号页
在main的主函数调用开始 . 第一个在main函数中被调用的函数 copy_boot_params(void) . 将内核启动头复制到boot_params 结构体中 (definded in arch/x86/include/uapi/asm/bootparam.h)
该结构体包括有’ struct setup_header hdr ‘ 结构体 被bootloader填充 在内核编译创建时间 copy_boot_params做两件事
1.从header.s 拷贝`hdr 到boot_parama` 结构
2 . 更新指向内核命令行的指针,如果内核通过在协议里的命令加载
注意拷贝 hdr通过memcpy函数进行拷贝 定义在copy.s里
GLOBAL(memcpy)
pushw %si
pushw %di
movw %ax, %di
movw %dx, %si
pushw %cx
shrw $2, %cx
rep; movsl
popw %cx
andw $3, %cx
rep; movsb
popw %di
popw %si
retl
ENDPROC(memcpy)
在copy.s中,我们看见memcpy和其他例程在这里定义,通常以GLOBAL和ENDPROC标志开始和结束.GLOBAL在arch/x86/include/asm/linkage.h 里被定义,同时定义GLOBAL指令集和相应的标签,ENDPROC在 include/linux/linkage.h 定义,且将name符号作为一个函数名标记,以name的size结束.
memcpy的实现很简单,首先,将si和di的值压入栈中来保存其对应的值.在REALMOD_CFLAGS中,内核创建通过 gcc的-mregparm=3 选项来创建系统,因此函数从ax``dx``cx三个寄存器里获取参数,调用memcpy参数如下
memcpy(&boot_params.hdr, &hdr, sizeof hdr);
因此,根据调用约定
-
ax包含boot_params的地址 -
adx包含hdr的地址 -
cx包含hdr的大小byte
memcpy将 boot_params的地址放入di,保存cx在栈上,然后将si地址的值右移2次,拷贝4byte到di里,重新储存hdr的size ,然后按4byte对齐,将剩下的按位拷贝,
然后完成整体的拷贝过程
控制台初始化
在hdr被拷贝到boot_params.hdr后,下一步是通过coonsole_init函数初始化控制台.这一过程被定义在arch/x86/boot/early_serial_console.c.中
在指令里寻找early_printk选项.如果成功找到,解析串行端口地址并初始化.earlyprintk命令选项可以是下面的几个之一
- serial,0x3f8,115200
- serial,ttyS0,115200
- ttyS0,115200
在初始化后,我们看见第一个输出
if (cmdline_find_option_bool("debug"))
puts("early console in setup code\n");
puts定义在 tty.c.puts通过循环使用putchar函数输出语句
void __attribute__((section(".inittext"))) putchar(int ch)
{
if (ch == '\n')
putchar('\r');
bios_putchar(ch);
if (early_serial_base != 0)
serial_putchar(ch);
}
__attribute__((section(".inittext")))指该段代码在.inittext段,在
setup.ld.里可以看到
putchar检查输入是否为\n 是 则嵌套使用putchar('\r')在将字符打印到VGA屏幕上.通过调用BIOS的0x10中断
static void __attribute__((section(".inittext"))) bios_putchar(int ch)
{
struct biosregs ireg;
initregs(&ireg);
ireg.bx = 0x0007;
ireg.cx = 0x0001;
ireg.ah = 0x0e;
ireg.al = ch;
intcall(0x10, &ireg, NULL);
}
这里initregs将biosregs结构体作为参数,并通过memset函数将其设置为0
然后通过寄存器的值填充biosregs
memset:的实现 ,
GLOBAL(memset)
pushw %di
movw %ax, %di
movzbl %dl, %eax
imull $0x01010101,%eax
pushw %cx
shrw $2, %cx
rep; stosl
popw %cx
andw $3, %cx
rep; stosb
popw %di
retl
ENDPROC(memset)
在biosregs结构体被memset重置后,bios_putchar通过使用0x10号中断来打印字符.然后检查串行端口是否被初始化,通过serial_putchar写一个字符
堆初始化
在栈和bss段都已经被准备好后,内核需要通过init_heap初始化堆空间
首先,init_heap检查loadflag结构体里的CAN_USE_HEAP标志位,
如果被设置则计算栈的尾部
char *stack_end;
if (boot_params.hdr.loadflags & CAN_USE_HEAP) {
asm("leal %P1(%%esp),%0"
: "=r" (stack_end) : "i" (-STACK_SIZE));
或者说 stack_end = esp - STACK_SIZE;
heap_end的计算结果
heap_end = (char *)((size_t)boot_params.hdr.heap_end_ptr + 0x200);
指向heap_end_ptr+512的地址,最后检查stack_end是否小于heap_end如果是矫正stack_end与heap_end使其相等
CPU 验证
通过validate_cpu 函数来进行cpu验证
调用 check_cpu 函数,传递cpu等级 和需要的等级为参数,检查内核运行在正确的cpu等级上
check_cpu(&cpu_level, &req_level, &err_flags);
if (cpu_level < req_level) {
...
return -1;
}
调用set_bios_mode在设置代码发现cpu合适的时候,,该函数仅使用在x86_64模式下
static void set_bios_mode(void)
{
#ifdef CONFIG_X86_64
struct biosregs ireg;
initregs(&ireg);
ireg.ax = 0xec00;
ireg.bx = 2;
intcall(0x15, &ireg, NULL);
#endif
}
执行0x15号BIOS中断,来告诉cpu使用长模式
内存检测
通过detect_memory函数进行内存检测,
detect_memory提供了可用的RAM和CPU.它使用不同的编程接口 如 0xe801 0x88 0xe820 .这里只讨论0xe820
首先detect_memory_e820函数初始化biosreg结构体,填充寄存器为特殊的值
initregs(&ireg);
ireg.ax = 0xe820;
ireg.cx = sizeof buf;
ireg.edx = SMAP;
ireg.di = (size_t)&buf;
- ax 该函数的序号
- cx 包括这些数据的缓冲区大小
- edx SMAP魔数
- es : di 数据缓冲区的地址
- ebx 0
下一个循环里内存被收集起来,以0x15的BIOS中断引起,写一行地址分配表.为得到下一行,我们再次进行中断(在循环中进行).在这之前,将ebx更新
intcall(0x15, &ireg, &oreg);
ireg.ebx = oreg.ebx;
最终,该函数从地址分配表收集数据,将其写到 e820entry数组里
- 内存段的开始
- 内存段的大小
- 内存段的类型
可以在dmesg的输出中看到如下
[ 0.000000] e820: BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000003ffdffff] usable
[ 0.000000] BIOS-e820: [mem 0x000000003ffe0000-0x000000003fffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
键盘初始化
使用keyboard_init function,使用0x16号中断来矫正键盘
initregs(&ireg);
ireg.ah = 0x02; /* Get keyboard status */
intcall(0x16, &ireg, &oreg);
boot_params.kbd_status = oreg.al;
再调用一次设置重复率和延时
ireg.ax = 0x0305; /* Set keyboard repeat rate */
intcall(0x16, &ireg, NULL);
对应链接
本文为对英文文章的翻译,加上自己的部分理解,如有不恰当地方,恳求指正,









发表评论
您还未登录,请先登录。
登录