x86下 BIOS之后linux内核启动之前
阅读数:156 评论数:0
跳转到新版页面分类
Linux
正文
GRUB
1、BIOS最后会读取引导设备第一个扇区的前 512 字节(MBR),将其读入到内存 0x0000:7C00,并跳转至此处执行。而安装在MBR的一般是GRUB stage1。
2、stage1位于stage1/stage1.S,功能是将0面0道第2扇区上的512字节读到内存中的0x00007000处,然后调用COPY_BUFFER将其复制到0x00008000的位置上,然后跳转到0x00008000处执行,这512字节来自stage2/start.S,作用是加载stage1_5或才stage2。(编译时决定)
/* start.S */
blocklist_default_start:
.long 2 /* 从第3扇区开始*/
blocklist_default_len:
/* 需要读取多少个扇区 */
#ifdef STAGE1_5
.word 0 /* 如果是 STAGE1_5,则不读入 */
#else
.word (STAGE2_SIZE + 511) >> 9 /* 读入 Stage2 所占的所有扇区 */
#endif
blocklist_default_seg:
#ifdef STAGE1_5
.word 0x220 /* 将 stage1.5 加载到 0x2200 */
#else
.word 0x820 /* 将 stage2 加载到 0x8200 */
#endif
3、stage1不具备文件系统识别功能,而stage1_5是具有读取文件系统的能力,而GRUB的核心映像是stage2。
4、stage2将系统切换到保护模式,设置C运行环境,寻找config文件,执行sehll接受用户命令,载入选定的操作系统内核。
5、stage2的入口点是asm.c
#ifdef STAGE1_5
# define ABS(x) ((x) - EXT_C(main) + 0x2200)
#else
# define ABS(x) ((x) - EXT_C(main) + 0x8200)
#endif
6、随后进入stage2.c,执行GRUB的主要功能
(1)cmain()主函数,载入配置文件 menu.lst(GRUB 1)或 grub.cfg(GRUB 2),如果成功载入就进入 run_menu(),显示菜单,进入循环倒计时,如果超时就进入第一个,如果用户按了键就停止倒计时。用户作出选择后,跳转到 boot_entry(),清空屏幕、获取入口,通过 find_command 找到的函数指针调用相应的命令。
(2)如果没有成功载入配置文件,就 enter_cmdline(),也是通过 find_command 调用相应的命令。
x86体系结构相关-内核映像结构
根据 arch/x86/boot/Makefile,bzImage 大内核映像由 setup.elf 和 vmlinux 组成,而 vmlinux 又由 setup.bin 和 vmlinux.bin 组成。vmlinux.bin 会进行压缩存储,变成 vmlinux.bin.gz。因此 bzImage 由 setup.elf、setup.bin、vmlinux.bin.gz 三部分组成。
Line 28: targets := vmlinux.bin setup.bin setup.elf zImage bzImage
Line 29: subdir- := compressed
Line 30:
Line 31: setup-y += a20.o cmdline.o copy.o cpu.o cpucheck.o edd.o
Line 32: setup-y += header.o main.o mca.o memory.o pm.o pmjump.o
Line 33: setup-y += printf.o string.o tty.o video.o video-mode.o version.o
其中setup-y就是setup.elf,其中引用的header.o是从header.S汇编而来的。
大内核情况下的内存分布图:
| vmlinux | 100000 +------------------------+ | setup.elf的setup部分 | 090200 +------------------------+ | setup.elf的启动扇区 | 090000 +------------------------+ | GRUB | 007c00 +------------------------+ | | 000000 +------------------------+
我们先看看用于控制 arch/x86/boot 下代码进行链接的 setup.ld。
ld 文件用于控制 ld 的链接过程:
(1)描述输入文件的各节如何对应到输出文件的各节
(2)控制输入文件各节及符号的内存布局
每个对象文件有一个节(section)列表、一个符号列表,一个符号可以是已定义或未定义的。每个已定义的符号有地址。未定义的符号则要在链接时从其他文件中寻找其定义。
1、指定输出文件格式
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
2、指定目标体系结构
OUTPUT_ARCH(i386)
3、设置入口点
ENTRY(_start)
4、输入文件各节到输出文件的映射
SECTIONS
{
. = 0 // 从 0 开始
.bstext : { *(.bstext) } // 所有输入文件的 .bstext 节组合成输出文件的 .bstext 节
.bsdata : { *(.badata) } // 所有输入文件的 .bsdata 节...
. = 497 // 填充 512 字节的 bootloader( header.S)
.header : { *(.header) }
在每一部分(header、rodata、data、bss、end)之间,对齐 16 字节内存边界:
. = ALIGN(16);
最后用断言保证链接后的目标文件不太大,且偏移量正确。
x86体系结构相关-header.S
start2:
movw %cs, %ax # CS = 0x7c00
movw %ax, %ds # 初始化段寄存器
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp
sti # 开中断
cld # di++, si++
................................
msg_loop: # 打印字符例程
................................
bs_die: # 错误处理例程
.ascii "Direct booting from floppy is no longer supported.\r\n"
.ascii "Please use a boot loader program instead.\r\n"
.ascii "\n"
.ascii "Remove disk and press any key to reboot . . .\r\n"
.byte 0
这段代码编译链接后,会生成 512 字节的 bootsector(setup.elf的启动扇区),其中 .section ".header", "a" 中的变量共 15 字节。注意到 setup.ld (Linker script for the i386 setup code) 中加入了 497 字节的空白,事实上恰好凑够 512 字节。
GRUB 将 setup.elf 读到 0x90000 处,将 vmlinux 读到 0x100000 处,然后跳转到 0x90200 开始执行(恰好跳过了 512 字节的 bootsector),执行最后是跳转到start_of_setup。
# Part 2 of the header, from the old setup.S
................................
# End of setup header #####################################################
上面这两行之间的代码是一个庞大的数据结构,与 include/asm/bootparam.h 中的 struct setup_header 一一对应。这个数据结构定义了启动时所需的默认参数,其中一些参数可以通过命令选项 overwrite。下表列出了一些参数的意义。
名称 | 偏移 | 大小(字节) | 意义 |
---|---|---|---|
root_flags | 0x1f2 | 2 | 根目录是否只读,可用 ro 或 rw 选项指定 |
root_dev | 0x1fc | 2 | 默认的 root 设备,即 /boot 所在目录,可用 root= 选项指定 |
boot_flag | 0x1fe | 2 | 0xAA55,即主引导扇区结束标志 |
header | 0x202 | 4 | HdrS (0x53726448),内核标志 |
version | 0x206 | 2 | 启动协议版本号: major * 64 + minor |
kernel_version | 0x20e | 2 | 内核版本号 |
type_of_loader | 0x210 | 1 | Boot loader ID: Boot loader ID * 64 + Version No.
Boot loader IDs: 0 LILO 1 Loadlin 2 bootsect-loader 3 SYSLINUX 4 EtherBoot 5 ELILO 7 GRuB 8 U-BOOT 9 Xen A Gujin B Qemu |
loadflags | 0x211 | 1 | 启动选项的掩码。
|
code32_start | 0x214 | 4 | 内核解压缩前立即跳转到的 32 位 flat-mode 入口 |
ramdisk_image | 0x218 | 4 | initramfs 的 32 位线性地址 |
cmd_line_ptr | 0x228 | 4 | 内核命令行的 32 位线性地址 |
从start_of_setup,跳转到arch/x86/boot/main.c中,暂时离开汇编,主要作用是初始化boot_params。
include/asm/bootparam.h 中定义的 boot_params 结构体 (即 zeropage) 在此完成初始化。
下面进入 boot/pmjump.S 中的 protected_mode_jump。
29 protected_mode_jump:
30 movl %edx, %esi # Pointer to boot_params table
31
32 xorl %ebx, %ebx
33 movw %cs, %bx # 将实模式的代码段放入 bx
34 shll $4, %ebx # 转换为线性地址
35 addl %ebx, 2f # 将 in_pm32 的实模式地址转换为线性地址
36
37 movw $__BOOT_DS, %cx # ds 段选择子
38 movw $__BOOT_TSS, %di # tss 段选择子
39
40 movl %cr0, %edx
41 orb $X86_CR0_PE, %dl # Protected mode
42 movl %edx, %cr0 # 将 cr0 的0位置0是进入保护模式的标志
43 jmp 1f # Short jump to serialize on 386/486
44 1:
45 # 下面这段作用是跳转到 in_pm32,由于已经在保护模式,所以需要考虑段的问题
46 # Transition to 32-bit mode
47 .byte 0x66, 0xea # ljmpl opcode
48 2: .long in_pm32 # offset
49 .word __BOOT_CS # segment
50
51 .size protected_mode_jump, .-protected_mode_jump
52
53 .code32
54 .type in_pm32, @function
55 in_pm32: # 下面的注释挺清楚,就不翻译了
56 # Set up data segments for flat 32-bit mode
57 movl %ecx, %ds
58 movl %ecx, %es
59 movl %ecx, %fs
60 movl %ecx, %gs
61 movl %ecx, %ss
62 # The 32-bit code sets up its own stack, but this way we do have
63 # a valid stack if some debugging hack wants to use it.
64 addl %ebx, %esp
65
66 # Set up TR to make Intel VT happy
67 ltr %di # 这个比较有意思
68
69 # Clear registers to allow for future extensions to the
70 # 32-bit boot protocol
71 xorl %ecx, %ecx
72 xorl %edx, %edx
73 xorl %ebx, %ebx
74 xorl %ebp, %ebp
75 xorl %edi, %edi
76
77 # Set up LDTR to make Intel VT happy
78 lldt %cx # 又是一个骗 CPU 的东西
79 # eax 是 protected_mode_jump 的第一个参数,即 header.S 中定义的 boot_params.hdr.code32_start,即 vmlinux 的入口地址
80 jmpl *%eax # Jump to the 32-bit entrypoint
81
82 .size in_pm32, .-in_pm32
末尾的 jmpl 指令把我们带入了 vmlinux 的世界。注意到,vmlinux 是压缩存储的,因此内核首先的工作就是把真正的内核解压出来。
根据 Makefile,linux 内核文件有以下几种:
- vmlinux: 原始的 linux 内核
- zImage: 经过 gzip 压缩后的 vmlinux,解压到 640KB 内存位置
- bzImage: 大内核版的 zImage,解压到 1MB 内存位置,现在我们一般都用这个
- vmlinuz: 指向 zImage 或 bzImage 的链接
- initrd: init ram disk,用于引导 vmlinuz
循着 Makefile 的踪迹,我们找到了 arch/x86/boot/compressed/head_32.S,这就是大内核模式下 0x100000 开始的内存内容。
至此,arch/x86/boot 下的流程基本分析完毕。
然后跳转到真正的内核入口是 arch/x86/kernel/head_32.S