在前一部分,我们进入了内核,看到了内核创建代码的初始化部分.并停在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);
对应链接
本文为对英文文章的翻译,加上自己的部分理解,如有不恰当地方,恳求指正,