上一篇文件介绍了从BIOS到BootLoader之间的执行过程,现在已经进入Linux内核的引导过程。本文继续分析Linux在内核引导阶段的执行过程。目前CPU工作在实模式下,我们需要将其切换到保护模式下。
引导程序加载内核镜像后,将控制权转交到内核引导程序,将地址跳转到0x10200(0x1020:0x0000)处,即:arch/x86/boot/header.S文件中_start:位置。现在已经进行Linux内核的世界了。
#header.S#L291
.globl _start
_start:
# Explicitly enter this as bytes, or the assembler
# tries to generate a 3-byte jump here, which causes
# everything else to push off to the wrong offset.
.byte 0xeb # short (2-byte) jump
.byte start_of_setup-1f
1:_start的位置是跳转指令,跳转到start_of_setup-1f位置继续执行。start_of_setup位置的代码如下:
#header.S#L574
.section ".entrytext", "ax"
start_of_setup:
# Force %es = %ds
movw %ds, %ax
movw %ax, %es
cld
...
# Jump to C code (should not return)
calll mainstart_of_setup的功能如下:
- 设置寄存器的值
es = ds; - 根据
ss寄存器和loadflags:CAN_USE_HEAP的状态设置正确的stack; - 检查
setup_sig是否为0x5a5aaa55;如果不正确提示错误; - 将
bss段置零; - 调用
main函数;
可用通过命令objdump --disassemble-all arch/x86/boot/setup.elf >> arch/x86/boot/setup.elf.asm查看对应的汇编代码。
start_of_setup对应的汇编代码如下:
00000268 <start_of_setup>:
268: 8c d8 mov %ds,%eax
26a: 8e c0 mov %eax,%es
26c: fc cld
26d: 8c d2 mov %ss,%edx
26f: 39 c2 cmp %eax,%edx
271: 89 e2 mov %esp,%edx
273: 74 16 je 28b <start_of_setup+0x23>
275: ba d0 58 f6 06 mov $0x6f658d0,%edx
27a: 11 02 adc %eax,(%edx)
27c: 80 74 04 8b 16 xorb $0x16,-0x75(%esp,%eax,1)
281: 24 02 and $0x2,%al
283: 81 c2 00 04 73 02 add $0x2730400,%edx
289: 31 d2 xor %edx,%edx
28b: 83 e2 fc and $0xfffffffc,%edx
28e: 75 03 jne 293 <start_of_setup+0x2b>
290: ba fc ff 8e d0 mov $0xd08efffc,%edx
295: 66 0f b7 e2 movzww %dx,%sp
299: fb sti
29a: 1e push %ds
29b: 68 9f 02 cb 66 push $0x66cb029f
2a0: 81 3e 98 45 55 aa cmpl $0xaa554598,(%esi)
2a6: 5a pop %edx
2a7: 5a pop %edx
2a8: 75 17 jne 2c1 <setup_bad>
2aa: bf a0 45 b9 d3 mov $0xd3b945a0,%edi
2af: 58 pop %eax
2b0: 66 31 c0 xor %ax,%ax
2b3: 29 f9 sub %edi,%ecx
2b5: c1 e9 02 shr $0x2,%ecx
2b8: f3 66 ab rep stos %ax,%es:(%edi)
2bb: 66 e8 90 10 callw 134f <SYSSEG+0x34f>
...经过上一步建立的栈,现在已经可用进行C函数调用。在start_of_setup的最后执行calll main调用main函数,main函数在arch/x86/boot/main.c中实现。main函数初始化计算机中的硬件设备,并为进入保护模式(Protect Mode)建立准备。
main调用的第一个函数是copy_boot_params,改函数做了两件事:
- 拷贝
header.S中hdr信息到boot_params中的struct setup_header hdr; - 处理旧协议下
cmd_line_ptr的地址;
boot_params的定义为struct boot_params boot_params __attribute__((aligned(16)));,可以看到boot_params以16B对齐。
struct boot_params在arch/x86/include/uapi/asm/bootparam.h中定义,struct setup_header hdr对应的是实模式引导头(the real mode kernel header)。
通过在arch/x86/Makefile中定义的REALMODE_CFLAGS,REALMODE_CFLAGS使用了GCC的-mregparm=3选项,使用%ax,%dx,%cx三个寄存器对应函数中的前三个输入参数。
memcpy在arch/x86/boot/copy.S中定义。调用方式如下:
//main.c#L39
memcpy(&boot_params.hdr, &hdr, sizeof(hdr));%ax对应boot_params.hdr的地址;%dx对应hdr的地址;%cx对应hdr的大小;
console_init在arch/x86/boot/early_serial_console.c定义,其功能为:
- 在
command line中找到earlyprintk参数后,初始化对应的控制台(串口的一种)的端口地址(port address)和波特率(baud rate)。earlyprintk支持serial,0x3f8,115200,serial,ttyS0,115200,ttyS0,115200三种选择。 - 未找到
earlyprintk的情况下,初始化uart8250,io,0x3f8,115200n8的控制台;
在控制台初始化、输入、输出等交互时,通过inb/outb进行数据或指令的交互。在控制台初始化完成后,可以看到第一条输出信息:
if (cmdline_find_option_bool("debug"))
puts("early console in setup code\n");puts在arch/x86/boot/tty.c中定义,通过putchar逐字节输出。
void __attribute__((section(".inittext"))) putchar(int ch)
{
if (ch == '\n')
putchar('\r'); /* \n -> \r\n */
bios_putchar(ch);
if (early_serial_base != 0)
serial_putchar(ch);
}其中:
__attribute__((section(".inittext"))指示该代码在.inittext段;bios_putchar通过0x10BIOS调用(intcall(0x10, &ireg, NULL);)将字符打印到屏幕上;serial_putchar通过outb(ch, early_serial_base + TXR);输出字符。
init_heap检查boot_params.hdr.loadflags是否设置了CAN_USE_HEAP标记。
if (boot_params.hdr.loadflags & CAN_USE_HEAP) {
asm("leal %P1(%%esp),%0"
: "=r" (stack_end) : "i" (-STACK_SIZE));
heap_end = (char *)
((size_t)boot_params.hdr.heap_end_ptr + 0x200);
if (heap_end > stack_end)
heap_end = stack_end;
} 换而言之stack_end = %esp - STACK_SIZE,并确保heap_end <= stack_end。
validate_cpu在rch/x86/boot/cpu.c定义,其功能为:
- 调用
check_cpu(arch/x86/boot/cpucheck.c)检查是否为所支持的CPU;如果是不支持的CPU进行提示; check_cpu检查CPU的标记,确保为支持长模式的64位CPU;AMD系列CPU开启SSE+SSE2;Pentium M系列CPU开启PAE等;
set_bios_mode通过0x15BIOS调用告知CPU的模式。
detect_memory在arch/x86/boot/memory.c定义,其功能为:逐步调用detect_memory_e820();,detect_memory_e801();,detect_memory_88();函数获取内存分布。
detect_memory_e820通过0x15BIOS调用获取内存分布情况。struct boot_e820_entry描述内存的分布情况,包括:起始地址(addr),大小(size),类型(type)。
keyboard_init通过0x16BIOS调用获取键盘状态和设置键盘的响应速率(repeat rate)。
query_ist通过0x15BIOS调用获取Intel SpeedStep (IST)。
query_apm_bios函数在#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)内核配置选项开启的情况下调用,在arch/x86/boot/apm.c中实现,通过0x15BIOS调用获取Advanced Power Management。
query_edd在#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)内核配置选项开启的情况下调用,在arch/x86/boot/edd.c中实现。
query_edd从0x80开始获取BIOS支持的硬盘信息。get_edd_info通过0x13BIOS调用获取EDD(Enhanced Disk Drive)。在命令行参数中可以设置EDD的查询方式,包括:skipmbr, skip, off, on四种选项。其中off设置不获取EDD信息。
if (cmdline_find_option("edd", eddarg, sizeof(eddarg)) > 0) {
if (!strcmp(eddarg, "skipmbr") || !strcmp(eddarg, "skip")) {
do_edd = 1;
do_mbr = 0;
}
else if (!strcmp(eddarg, "off"))
do_edd = 0;
else if (!strcmp(eddarg, "on"))
do_edd = 1;
}
...
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++;
}
if (do_mbr && !read_mbr_sig(devno, &ei, mbrptr++))
boot_params.edd_mbr_sig_buf_entries = devno-0x80+1;
}set_video在arch/x86/boot/video.c中实现。
set_video中首先从boot_params.hdr.vid_mode获取视频模式。BootLoader启动时必须填写vid_mode,从Boot Protocol中可以读取改字段说明。
Field name: vid_mode
Type: modify (obligatory)
Offset/size: 0x1fa/2
Please see the section on SPECIAL COMMAND LINE OPTIONS.
命令行参数说明:
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.
QEMU处理命令行中vga=的代码如下,详细信息可参见:https://github.com/qemu/qemu/blob/v6.1.0/hw/i386/x86.c#L936
/* handle vga= parameter */
vmode = strstr(kernel_cmdline, "vga=");
if (vmode) {
unsigned int video_mode;
const char *end;
int ret;
/* skip "vga=" */
vmode += 4;
if (!strncmp(vmode, "normal", 6)) {
video_mode = 0xffff;
} else if (!strncmp(vmode, "ext", 3)) {
video_mode = 0xfffe;
} else if (!strncmp(vmode, "ask", 3)) {
video_mode = 0xfffd;
} else {
ret = qemu_strtoui(vmode, &end, 0, &video_mode);
if (ret != 0 || (*end && *end != ' ')) {
fprintf(stderr, "qemu: invalid 'vga=' kernel parameter.\n");
exit(1);
}
}
stw_p(header + 0x1fa, video_mode);
}我们也可以在arch/x86/include/uapi/asm/boot/h看到相关定义:
/* Internal svga startup constants */
#define NORMAL_VGA 0xffff /* 80x25 mode */
#define EXTENDED_VGA 0xfffe /* 80x50 mode */
#define ASK_VGA 0xfffd /* ask for it at bootup */获取vid_mode后,调用RESET_HEAP();重置堆。其定义在arch/x86/boot/boot.h,代码如下:
#define RESET_HEAP() ((void *)( HEAP = _end ))与堆相关的函数还有如下:
GET_HEAP(type, n)和char *__get_heap(size_t s, size_t a, size_t n)从堆中分配内存;bool heap_free(size_t n)判读堆是否可用;
store_mode_params的功能如下:
- 获取游标信息,
store_cursor_position函数0x10BIOS调用; - 获取视频模式,
store_video_mode函数0x10BIOS调用; - 设置
video_segment。黑白模式(MDA, HGC, or VGA monochrome mode)为0xb000;彩色模式(CGA, EGA, VGA)为0xb800; - 获取字体信息,通过
fs寄存器获取,如:set_fs(0);,rdfs16(0x485);;
save_screen将屏幕信息存储到堆上。调用堆的代码如下:
if (!heap_free(saved.x*saved.y*sizeof(u16)+512))
return; /* Not enough heap to save the screen */
saved.data = GET_HEAP(u16, saved.x*saved.y);probe_cards在arch/x86/boot/video-mode.c中实现。遍历所有的显卡,获取显卡所支持的显示模式:
for (card = video_cards; card < video_cards_end; card++) {
if (card->unsafe == unsafe) {
if (card->probe)
card->nmodes = card->probe();
else
card->nmodes = 0;
}
}video_cards,video_cards_end在arch/x86/boot/video.h声明:
#define __videocard struct card_info __attribute__((used,section(".videocards")))
extern struct card_info video_cards[], video_cards_end[];在arch/x86/boot/setup.ld中.videocards内存段定义:
.videocards : {
video_cards = .;
*(.videocards)
video_cards_end = .;
}每个支持的显示模式(如:vga)定义如下:
static __videocard video_vga = {
.card_name = "VGA",
.probe = vga_probe,
.set_mode = vga_set_mode,
};__videocard是一个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; /* Number of probed modes so far */
int unsafe; /* Probing is unsafe, only do after "scan" */
u16 xmode_first; /* Unprobed modes to try to call anyway */
u16 xmode_n; /* Size of unprobed mode range */
};videocards只是一个内存地址,所有的card_info结构都存放在这个段中,且存放在video_cards和video_cards_end之间,因此可以使用循环来遍历。
card->probe是一个函数地址,可以像正常函数一样调用,如:video_vga.probe指向int vga_probe(),通过0x10BIOS调用检查显卡的显示模式。
在probe_cards执行完成后,进入模式设置。
模式设置是一个循环,如果是用户选择显示模式(mode == ask),显现一个菜单供用户选择。根据选择的模式或现有的mode值,调用set_mode来设置模式。成功设置后退出循环,否则设置模式为用户选择模式(ask),继续选择模式后进行设置。
set_mode在arch/x86/boot/video-mode.c定义。检查mode值并进行转换后,调用raw_set_mode。raw_set_mode循环遍历所有的card_info,并调用card->set_mode(mi)功能。
card->set_mode同样是个函数地址,如:video_vga.probe指向vga_set_mode(struct mode_info *mode),通过0x10BIOS调用设置不同的显示。
在显示模式正确设置后,将最终的显示模式设置到boot_params.hdr.vid_mode。
vesa_store_edid在arch/x86/boot/video-vesa.c中定义。通过0x10BIOS调用,获取并设置EDID。
新显示模式设置后,再次调用store_mode_params存储模式信息;设置了恢复标记后,调用restore_screen恢复之前记录的屏幕信息。
在main函数的最后,调用go_to_protected_mode函数,做最后的准备并切换到保护模式。将在下篇继续分析。
本文描述了BootLoader引导后,Linux内核在实模式下的引导过程,包括:建立C函数调用环境、在实模式下硬件初始化等切换到保护模式前的准备工作。我们将在下一篇中继续分析切换保护模式的过程。
本系列文章翻译自linux-insides,如果你有任何问题或者建议,请联系0xAX或者创建 issue。