刘文学 原创作品转载请注明出处 http://blog.csdn.net/wdxz6547/article/details/50880768 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
前提
本文只讨论 x86 下的 Linux 系统.
预备知识
实模式 real mode: CPU访问的是实际物理地址. 只有 20 位地址总线. 在 80286 之前采用该模式.
保护模式 protected mode :支持虚拟地址, 分页,安全多任务等等, 从 80286 开始出现该模式.
reset vector: CPU 开始运行取的第一个地址. (该文档非常易懂, 建议读)
BIOS 保存了启动顺序
系统启动
当我们按下电脑的开关键, 母板发送一个信号给电源, 电源发现自己供电能力正常, 就会发送一个power good singal给母板, 母板收到该信号启动 CPU. CPU 从寄存器开始取址-执行的循环.
那么寄存器存储的第一个值(即 CS:IP 的值)是什么呢? , 在 x86 系统中该值为:
IP 0xfff0
CS selector 0xf000
CS base 0xffff0000
因此, 地址值为 0xfffffff0, 这个地址被称为 reset vector
这个地址包含一个 jump 指令, 指向 BIOS 的入口地址.
BIOS
BIOS(位于 ROM 而不是 RAM) 启动, 1)初始化和检查硬件 2)根据配置的启动顺序从对应设备读取启动扇区(boot sector). 在有 MBR 分区的硬盘, 启动扇区为第一扇区的前 446 byte. 该扇区的最后两个字节是 0x55, 0xaa 通知 BIOS 该设备是可启动的.
BIOS 完全启动之后将控制器交给 bootloader
Bootloader
Bootloader (如 grub2, syslinux)
BIOS 将控制权转交给启动扇区, 从 boot.img 开始执行, 之后跳转到 Grub2 的 core image. 这个 core image 从 diskboot.img(位于第一个扇区之后, 第一分区之前)
将 core image 剩余部分(GRUB2 的核和处理文件系统的驱动)加载到内存, 加载完剩余的 core image 之后, 开始执行 grub_main
grub_main 初始化 console, 获取模块基地址, 设置 root 设备, 加载和解析 grub 配置, 加载模块等等, 执行完之后, grub_main 转移 grub 到 normal 模块, grub_normal_execute 完成最后的准备阶段, 之后显示可选的操作系统菜单. 当我们选择对应的的选项, 调用 grub_menu_execute_entry 调用 boot 命令启动对应的操作系统.
Bootloader 转换控制权给 kernel, 此时 bootloader 已经将内核加载到内存.
Kernel 启动
首先进入 arch/x86/boot/head.S 的 292 行
.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
其中 0xeb 是 jump 指令, 跳转到 start_of_setup-1f 处.
.section ".entrytext", "ax"
start_of_setup:
# Force %es = %ds
movw %ds, %ax
movw %ax, %es //%es 存储 %ds 相同的地址
cld //将 DF(Direct Flag) 清零
# 建立 Stack
# Apparently some ancient versions of LILO invoked the kernel with %ss != %ds,
# which happened to work by accident for the old code. Recalculate the stack
# pointer if %ss is invalid. Otherwise leave it alone, LOADLIN sets up the
# stack behind its own code, so we can't blindly put it directly past the heap.
movw %ss, %dx # 将 %ss 地址保存到 %dx
cmpw %ax, %dx # %ds - %ss 设置对应的标志位 ZF, SF, CF, OF.
movw %sp, %dx # 保持 %sp 到 %ds
je 2f # -> assume %sp is reasonably set //ZF = 1 即 %ds == %ss 跳转到 2:
# Invalid %ss, make up a new stack
movw $_end, %dx
testb $CAN_USE_HEAP, loadflags
jz 1f
movw heap_end_ptr, %dx
1: addw $STACK_SIZE, %dx
jnc 2f
xorw %dx, %dx # Prevent wraparound
2: # Now %dx should point to the end of our stack space 此时 %ds = %es = %ss
andw $~3, %dx # dword align (might as well...)
jnz 3f # ZF = 0, 跳转到 3:
movw $0xfffc, %dx # Make sure we're not zero
3: movw %ax, %ss #
movzwl %dx, %esp # Clear upper half of %esp
sti # Now we should have a working stack 允许中断
# We will have entered with %cs = %ds+0x20, normalize %cs so
# it is on par with the other segments.
pushw %ds #ds 压栈
pushw $6f #6f 压栈
lretw #取出 %ds 给 %cs, 6:处地址给 ip, 从该地址开始执行程序.
6:
# Check signature at end of setup
cmpl $0x5a5aaa55, setup_sig
jne setup_bad
# BSS 初始化, 将 $__bss_start 到 $end+3 区域清零
# Zero the bss
movw $__bss_start, %di
movw $_end+3, %cx
xorl %eax, %eax
subw %di, %cx
shrw $2, %cx
#repeat %cx times for stosl which store %eax(0) from $__bss_start to $_end+3
rep; stosl
# Jump to C code (should not return)
calll main
其中 start_of_setup 部分做了如下几件事:
- 确保所有分段寄存器的值是相同的
- 如果需要, 建立正确的栈
- 建立 BSS
- 跳到 C 代码的 arch/x86/boot/main.c 的 main 函数
系统启动首先进入实模式(注意是为了向下兼容), 之后进入保护模式
感觉有点乱, 后续修正. 不过要真正理解启动过程, 分析 coreboot 和 grub 的源代码
应该能够建立比较清晰的认识.
从实模式到保护模式
void main(void)
{
/* First, copy the boot header into the "zeropage" */
//将 head.S 中的 hdr 拷贝到 struct boot_params.hdr, 这里 boot_params
//被称为 zeropage
copy_boot_params();
/* Initialize the early-boot console */
console_init();
if (cmdline_find_option_bool("debug"))
puts("early console in setup code\n");
/* End of heap check */
init_heap();
/* Make sure we have all the proper CPU support */
if (validate_cpu()) {
puts("Unable to boot - please use a kernel appropriate "
"for your CPU.\n");
die();
}
/* Tell the BIOS what CPU mode we intend to run in. */
set_bios_mode();
/* Detect memory layout */
detect_memory();
/* Set keyboard repeat rate (why?) and query the lock flags */
keyboard_init();
/* Query MCA information */
query_mca();
/* Query Intel SpeedStep (IST) information */
query_ist();
/* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
/* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
query_edd();
#endif
/* Set the video mode */
set_video();
/* Do the last things and invoke protected mode */
go_to_protected_mode();
}
未完待续
参考
母板
payload
BIOS
Gate-A20
系统启动过程
附录
cmp 指令
/*
unsigned compare
%ds == %ss, ZF=1
%ds != %ss, ZF=0
%ds >= %ss, CF=0
%ds > %ss, ZF=0, CF=0
%ds <= %ss, ZF=1 | CF=1
%ds < %ss, ZF=0, CF=1
singed compare
%ds > %ss, SF = 0, OF = 0
%ds < %ss, SF = 1, OF = 0
%ds < %ss, SF = 0, OF = 1
%ds > %ss, SF = 1, OF = 1
两数同为正,相加,值为负,则说明溢出
两数同为负,相加,值为正,则说明溢出
故有,正正得负则溢出,负负得正则溢出
两数相减,同号,则不溢出
两数为异号,结果与减数符号相同,则溢出。
*/
rep 指令
将 rep 后面的指令执行 cx 次
stosl 指令
将寄存器 %eax 内容存储到内存单元 es:di, 同时修改 di, 指向下一个元素.
lretw 指令
弹出第一个 word 给 ip, 第二个 word 给 cs; 之后执行 cs:ip 指定的地址
内核中的魔数
TODO