内核启动程序
检测
通过query_list
函数获取intel_speedstep
信息,检查cpu信息 并将信息储存在boot_params
speedstep技术
通过降低cpu运行主频来达到降低功耗,intel为笔记本cpu开发
通过 query_apm_bios 获取高级电源管理信息,此函数依旧通过int 0x15来使用中断, 但是多添加一个ah
=0x53
来确保APM
的安装,中断处理结束后,函数检查PM
标志位(0x504d),carry 标志位(如果APM
使用,0x0),cx
寄存器(0x02)提供保护模式接口
再次使用0x15
中断,此时使用ax=0x5304
,断开APM
接口 ,连接32位保护模式接口,最后,将boot_params.apm_bios_info
用从BIOS里包含的数据填充完整
只有当CONFIG_APM
或者CONFIG_APM_MODULE
被设置时query_apm_bios
才被使用.
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
最后是 query_edd
函数,从BIOS里检查Enhanced Disk Drive
信息
querry_edd
实现流程
- 从内核的命令行读取
edd
选项,如果关闭,则直接返回 - 如果
EDD
可用,querry_edd
遍历所有的BIOS支持的因i教案,检测edd信息for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) { if (!get_edd_info(devno, &ei) && boot_params.eddbuf_entries < EDDMAXNR) { memcpy(edp, &ei, sizeof ei); edp++; boot_params.eddbuf_entries++; } ... ... ... }
第一个硬件序列号为
0x80
,EDD_MBR_SIG_MAX
marco为16,从edd_info
里获取信息,.get_edd_info
通过0x13
中断ah
=0x41
来检查是否EDD被使用,get_edd_info
再次使用0x13中断,这次
ah
=0x48
si
保存EDD储存数据的缓冲地址
视窗模式的初始化 ,以及向保护模式的转变
视窗模式从set_video
函数开始, arch/x86/boot/video.c (源码)
获取boot_params.hdr
结构体
u16 mode = boot_params.hdr.vid_mode;
vid_mode
在引导装载程序中被填充,同时在引导协议中也能看到对应解释
Offset Proto Name Meaning
/Size
01FA/2 ALL vid_mode Video mode control
vga=<mode>
<mode> here is either an integer (in C notation, either
decimal, octal, or hexadecimal) or one of the strings
"normal" (meaning 0xFFFF), "ext" (meaning 0xFFFE) or "ask"
(meaning 0xFFFD). This value should be entered into the
vid_mode field, as it is used by the kernel before the command
line is parsed.
因此,我们将vga选项加入grub设置文件中,由此将该选项传递给内核命令行
该选项被描述为有不同的值,可以为整形的值(0xffff
)或者被要求参数asked
如果是asked
,在quem运行时如下
选择一个视图模式.接下来深入讨论实现
对于初见内核,我们需要一些其他的东西
内核数据类型
在使用像u64
这样的类型之前,在代码中有其他的数据类型
Type | char | short | int | long | u8 | u16 | u32 | u64 |
---|---|---|---|---|---|---|---|---|
Size | 1 | 2 | 4 | 8 | 1 | 2 | 4 | 8 |
堆处理 API
在获取boot_params.hdr
后,我们看到RESET_HEAP
函数,该函数其实是一个宏定义
#define RESET_HEAP() ((void *)( HEAP = _end ))
还有GET_HEAP
#define GET_HEAP(type, n) \
((type *)__get_heap(sizeof(type),__alignof__(type),(n)))
实现堆定位, 通过向__get_heap
传递三个参数
- 数据类型的大小
- 该数据变量的对齐方式
- n,定位的数量
static inline char *__get_heap(size_t s, size_t a, size_t n)
{
char *tmp;
HEAP = (char *)(((size_t)HEAP+(a-1)) & ~(a-1));
tmp = HEAP;
HEAP += s*n;
return tmp;
}
GET_HEAP使用
saved.data = GET_HEAP(u16, saved.x * saved.y);
在get_heap中 被赋值为_end的HEAP被重新赋值为对齐后的值,对齐用到的变量是参数a,即alignof__(type)
将HEAP
跳转到分配内存区的末尾,返回原来的值
最后是heap_free
static inline bool heap_free(size_t n)
{
return (int)(heap_end - HEAP) >= (int)n;
}
HEAP
值减去heap_end
,如果有足够值,返回1
设置视频模式
正式转入视频模式初始化,在RESET_HEAP
后,store_mode_params
从boot_params.screen_info
中获取video mode的参数.这些定义在头文件include/uapi/linux/screen_info.h中
在store_mode_params
函数,首先调用store_cursor_position
,从cursor
中获取信息,并储存.
store_cursor_position
初始化两个biosreg
变量 AH
值为0x3 启动0x10号bios中断, 成功执行后,,返回视频的行列,被储存在 boot_params.screen_info
结构体的orig_x
和orig_y
中
之后,store_mode_params
检查当前的视频模式,设置视频参数,在BIOS
将控制转给引导向量后,下面的地址为video内存准备
0xB000:0x0000 32 Kb Monochrome Text Video Memory
0xB800:0x0000 32 Kb Color Text Video Memory
如果当前的video mode 是MDA ,HGC ,VGA Monochrome模式我们将video_segment
值放到0xb000
或者是color
模式,放到0xb800
.
在设置视频内存段后,font size需要被存进boot_params.screen_info.orig_video_points
set_fs(0);
font_size = rdfs16(0x485);
boot_params.screen_info.orig_video_points = font_size;
将fs设置为0后,从0x485
获取size,并储存
x = rdfs16(0x44a);
y = (adapter == ADAPTER_CGA) ? 25 : rdfs8(0x484)+1;
从0x44a
处获取列的数量 从0x484
处获取行的数量
存进boot_params.screen_info.orig_video_cols
和boot_params.screen_info.orig_video_lines
中.完成store_mode_params
.
save_screen
函数只是保存了屏幕到堆内存中,该函数保存了所有上一个函数中获取到的信息.并将其放入saved_screen
结构体中
static struct saved_screen {
int x, y;
int curx, cury;
u16 *data;
} saved;
然后检查是否堆空间有足够的释放的空间给它.
if (!heap_free(saved.x*saved.y*sizeof(u16)+512))
return;
如果空间足够 ,分配空间,将saved_screen
储存在里面
下一个调用 probe_cards(0)
arch/x86/boot/video-mode.c
遍历所有的video_cards ,收集这些cards提供的模式参数
for (card = video_cards; card < video_cards_end; card++) {
/* collecting number of modes here */
}
但是video_cred不是在哪里都被声明,因此,每一个视频模式在x86内核中都有一个定义
static __videocard video_vga = {
.card_name = "VGA",
.probe = vga_probe,
.set_mode = vga_set_mode,
};
#define __videocard struct card_info __attribute__((used,section(".videocards")))
card_info
struct card_info {
const char *card_name;
int (*set_mode)(struct mode_info *mode);
int (*probe)(void);
struct mode_info *modes;
int nmodes;
int unsafe;
u16 xmode_first;
u16 xmode_n;
};
在arch/x86/boot/setup.ld 连接脚本里
.videocards : {
video_cards = .;
*(.videocards)
video_cards_end = .;
}
这意味着vide_cards
只是一个内存地址,所有card_info
结构体都被放在该段下.因此可以使用一个循环来遍历所有.
probe_cards
执行后,我们会有一堆像 static __videocard video_vga
一样的结构体填充
在结束probe_cards
后,转向set_video
主循环,
,进入无限循环,试图找到video_mode
带有的set_mode
函数,如果没有将vid_mode=asked
传递给内核命令 或者video_mode
没有被定义则打印出菜单.
set_mode函数在video-mode.c 被定义,只需要一个参数 , 即mode
mode代指视频模式对应的值.
函数检查mode
后,调用raw_set_mode
函数,raw_set_mode
调用被选择的card的set_mode
函数.我们从card_info
结构体中获取到这个函数.
例如vga
static int vga_set_mode(struct mode_info *mode)
{
vga_set_basic_mode();
force_x = mode->x;
force_y = mode->y;
switch (mode->mode) {
case VIDEO_80x25:
break;
case VIDEO_8POINT:
vga_set_8font();
break;
case VIDEO_80x43:
vga_set_80x43();
break;
case VIDEO_80x28:
vga_set_14font();
break;
case VIDEO_80x30:
vga_set_80x30();
break;
case VIDEO_80x34:
vga_set_80x34();
break;
case VIDEO_80x60:
vga_set_80x60();
break;
}
return 0;
}
每个设置视频模式的函数通过设置ah
寄存器的值并使用0x10
BIOS中断.
设定好mode后,将其传递给 boot_params.hdr.vid_mode
.
接下来,vesa_store_edid
调用,这个函数简单地储存EDID
(Extended Display Identification Data)信息.在调用 store_mode_params
后,最终do_restore
设置
屏幕被保存为早期的状态
接下来,进入保护模式
进入保护模式前最后的准备
在arch/x86/boot/main.c下一个进入的函数是go_to_protected_mode
该函数定义在arch/x86/boot/pm.c ,包含了做最后准备时用到的一些函数,
如果realmode_switch_hook
不为空且 NMI禁用,则调用该钩子函数.realmode_switch
函数提供了一个指针指向16位实模式子例程,禁用中断屏蔽,检查钩子后,不可中断屏蔽被禁用
asm volatile("cli");
outb(0x80, 0x70); /* Disable NMI */
io_delay();
首先是内联汇编”cli”,清除IF
,外部中断被阻止.下一行禁用NMI
NMI
中断是一个由外部设备发出给cpu的信号,.在收到这样的信号后cpu暂停当前例程,保存当前状态,把控制流交给中断处理程序.处理完成后恢复原状态,继续执行.
而NMI是必须执行的中断,他们不能被忽略,并且经常用于无法恢复的硬件错误.
回到代码
第二行中,将0x80写为0x70(CMOS地址寄存器) 然后是io_delay,
static inline void io_delay(void)
{
const u16 DELAY_PORT = 0x80;
asm volatile("outb %%al,%0" : : "dN" (DELAY_PORT));
}
向0x80端口输出的数据需要演示1毫秒,因此我们可以卸任盒子到0x80端口,在这个延时期间,钩子函数完成,进入到下一个函数
下一个函数是enable_a20
启用A_20线,该函数定义在arch/x86/boot/a20.c 他尝试使用不同的方法打开A20门.
首先通过a20_test_short
函数检查是否A20线已经可用.由a20_test
实现
static int a20_test(int loops)
{
int ok = 0;
int saved, ctr;
set_fs(0x0000);
set_gs(0xffff);
saved = ctr = rdfs32(A20_TEST_ADDR);
while (loops--) {
wrfs32(++ctr, A20_TEST_ADDR);
io_delay(); /* Serialize and make delay constant */
ok = rdgs32(A20_TEST_ADDR+0x10) ^ ctr;
if (ok)
break;
}
wrfs32(saved, A20_TEST_ADDR);
return ok;
}
将0x0000
放入fs
0xffff
放入GS
然后从通过wrfs32
函数读取fs:A20_TEST_ADDR
值,,延时1ms,从GS寄存器里读取值放到 A20_TEST_ADDR+0x10
里.如果被A20
未被使用,会造成覆盖,否则,A20线已经启用
如果A
线禁用,我们通过不同的方式来启用它,例如调用AH=0x2041
的0x15
BIOS中断,
如果无法成功启用A20线,打印出错误信息,使用函数die
arch/x86/boot/header.S
die:
hlt
jmp die
.size die, .-die
A20被成功启用,调用reset_coprocessor
outb(0, 0xf0);
outb(0, 0xf1);
通过将0
写到0xf0
清除数学处理器 通过将0
写到0xf1
重置
mask_all_interrupts
调用
outb(0xff, 0xa1); /* Mask all interrupts on the secondary PIC */
outb(0xfb, 0x21); /* Mask all but cascade on the primary PIC */
这段代码在二次PIC和主PIC(特别是IRQ2)上掩盖所有的中断
准备完成
建立中断描述符表
setup_idt
函数
static void setup_idt(void)
{
static const struct gdt_ptr null_idt = {0, 0};
asm volatile("lidtl %0" : : "m" (null_idt));
}
设置中断描述符表,现在为止 IDT还没有被安装,但是现在我们加载IDT都是通过lidt
指令,(null_idt)包含IDT的大小和地址,但是现在他们都还是0,null_idt是gdt_ptr结构体类型
struct gdt_ptr {
u16 len;
u32 ptr;
} __attribute__((packed));
包含16位长的len 32位的指针 .__attribute__((packed))
指gdt_ptr
大小是需要的最小值.即,6byte
设置全局描述符表
setup_gdt
函数用于建立GDT,下面是boot_gdt
数组的定义,包含了对三个段的定义
static const u64 boot_gdt[] __attribute__((aligned(16))) = {
[GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
[GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
[GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
};
对于代码段,数据段和TSS段,我们不会使用任务状态段,\
对于该数组有__attribute__((aligned(16)))
描述符,该结构体会按照16byte对齐
例如
#include <stdio.h>
struct aligned {
int a;
}__attribute__((aligned(16)));
struct nonaligned {
int b;
};
int main(void)
{
struct aligned a;
struct nonaligned na;
printf("Not aligned - %zu \n", sizeof(na));
printf("Aligned - %zu \n", sizeof(a));
return 0;
}
$ gcc test.c -o test && test
Not aligned - 4
Aligned - 16
GDT_ENTRY_BOOT_CS
在这里有index-2
GDT_ENTRY_BOOT_DS
是GDT_ENTRY_BOOT_CS + 1
从2开始,因为第一个是强制性的NUll
第二个是未被使用的-1
GDT_ENTRY
是一个宏 用 获取信息构建一个GDT入口
例如,当观察代码段时,GDT_entry
使用以下值,
- base – 0
- limit – 0xfffff
- flags – 0xc09b
段基址为0 最大值为0xfffff 标志位
1100 0000 1001 1011
二进制数据每一位都有相应含义
从左到右依次
- 1 – (G)粒度标志
- 1 – (D) 0 16位段; 1 = 32位段
- 0 – (L) 如果是1 则在64位下
- 0 – (AVL) 允许使用系统软件
- 0000 – 4bit 长 在描述符里19:16bits
- 1 – (P) 段在内存中
- 00 – (DPL) – 权限等级
- 1 – (S) 代码段或者数据段,非系统段
- 101 – 段类型 可执行?/可读?
- 1 – accessed bit
获取GDT长度大小
gdt.len = sizeof(boot_gdt)-1;
获取指向GDT的指针
gdt.ptr = (u32)&boot_gdt + (ds() << 4);
这里将ds()左移4位,(在实模式下)
最后执行lgdtl
加载GDT到GDTR寄存器
asm volatile("lgdtl %0" : : "m" (gdt));
转到保护模式
go_to_protected_mode
函数的最后,加载了IDT和GDT,禁用中断,可以将cpu转入保护模式.最后一步通过 protected_mode_jump
完成,
protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4));
参数1 : 保护模式进入的地址
参数2 : boot_params
地址
第一个参数被放在eax里第二个在edx里,
首先将boot_params
地址放在esi寄存器里,代码段的cs寄存器放入bx
GLOBAL(protected_mode_jump)
movl %edx, %esi # Pointer to boot_params table
xorl %ebx, %ebx
movw %cs, %bx
之后,bx
左移4位,加上标签2处的值((cs << 4) + in_pm32
)
跳转到标签1
shll $4, %ebx
addl %ebx, 2f # Add %ebx to the value stored at label 2
jmp 1f # Short jump to serialize on 386/486
之后lable2处值会被写为(cs << 4) + in_pm32
数据段和TSS段分别放在cx di寄存器内.
movw $__BOOT_DS, %cx
movw $__BOOT_TSS, %di
将PE
放入控制寄存器
movl %cr0, %edx
orb $X86_CR0_PE, %dl
movl %edx, %cr0
长跳转到保护模式
.byte 0x66, 0xea
2: .long in_pm32
.word __BOOT_CS
0x66是允许操作数大小 混合16bit和32bit代码
0xea 跳转的机器码
in_pm32是保护模式下段偏移
__BOOT_CS
我们跳转到的代码段
最终进入保护模式
.code32
.section ".text32","ax"
进入保护模式后,设置数据段
movl %ecx, %ds
movl %ecx, %es
movl %ecx, %fs
movl %ecx, %gs
movl %ecx, %ss
我们曾将cs寄存器设置为$__BOOT_DS
,现在将所有的寄存器设置(除cs外)
为debug需求,设置有效的栈
清空所有的寄存器
xorl %ecx, %ecx
xorl %edx, %edx
xorl %ebx, %ebx
xorl %ebp, %ebp
xorl %edi, %edi
跳转到32位入口点
jmpl *%eax
eax在protected_mode_jump
时已经设置
到此完成转换