在x86平台上,linux内核用了一个比较复杂的启动协议规范,主要由于历史原因,早期想把内核做成可启动映像,复杂的计算机内存模型和由于实模式DOS成为主流操作系统而影响的.
现在有下列启动协议存在:
旧版内核:只支持zImage和Image.
2.00:增加bzImage和initrd支持,也拥有了一种正规化的方法来实现启动装载器(* boot loader)和内核间的通信。setup.S建造了一块可移动,但是仍旧可写的传统的安装程序加载区域。
2.01:增加了一些溢出警告
2.02:这是新的命令行协议。它降低了常规的内存使用上限(* 见以下MEMORY LAYOUT内存布局介绍)。没有覆盖传统的安装程序区域,因此对于那些使用了来自SMM或者32位BIOS入口地址的EBDA(* Extended BIOS Data Area,拓展BIOS数据域)的系统,这样做可以使得启动更加安全。
2.03:明确了高端的initrd地址可被 bootloader 使用.
2.04:扩展syssize 域到四个字节
2.05:使得保护模式下的 内核可被重定位,增加 relocatable_kernel 和kernel_alignment 域.
2.06:增加一个域存放 boot command line 的大小.
2.07:增加半虚拟化启动协议,在load_flags 中 增加 hardware_subarch 和 hardware_subarch_data 和 KEEP_SEGMENTS 标志.
2.08:增加CRC32校验和ELF格式的有效载荷。增加 payload_offset 和payload_length 域 用于定位有效载荷(payload)
2.09:增加一个64位的物理指针指向setup_data结构体的单链表.
2.10:增加了init_size 和 pref_address 域.
传统的用于早期的Image和bImage的Memory的映射图:
100000
图形接口卡信息和BIOS自身
0A0000 +------------------------+ 从这一下的640K称为基本内存
| Reserved for BIOS | 未使用,被BIOS EBDA保留
09A000 +------------------------+
| Command line |
| Stack/heap | 被实模式下的内核代码所使用
098000 +------------------------+
| kernel setup | 实模式下的内核代码 31.5k
090200 +------------------------+
| kernel boot sector | 历史遗留下来的内核启动扇区 0.5k
090000 +------------------------+
| Protected-mode kernel | 内核镜像的主要部分 512k
010000 +------------------------+
| Boot loader | - 启动扇区的入口位置 0000:7c00
001000 +------------------------+从这以下的4k 全是不可以直接用的
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+
| BIOS use only |
000000 +------------------------+
当使用大内核的时候,保护模式下的内核被重定位到100000高地址,并且实模式块(boot sector ,setuo ,stack/heap)被设计位可重定位在10000到100000间的任何位置. 但由于EDBA越来越大,所以 boot loader 能够触及到的内存上限越低越好.通常 boot loader会使用 12号中断 判断还有多少低内存(10000~~100000)可用.
现在的bzImage内核的内存映像建议为如下方式:
| Protected-mode kernel |
100000 +------------------------+
| I/O memory hole |
0A0000 +------------------------+
| Reserved for BIOS | 尽可能预留更多空闲空间
~ ~
| Command line | (也可置于 X+10000 标记下)
X+10000 +------------------------+
| Stack/heap | 被实模式下的内核代码所使用
X+08000 +------------------------+
| Kernel setup | 实模式下的内核代码
| Kernel boot sector | 历史遗留下来的内核启动扇区
X +------------------------+ 这里的X应尽可能地低
| Boot loader | - 启动扇区的入口位置 0000:7c00
001000 +------------------------+
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+
| BIOS use only |
000000 +------------------------+
... X位置应和boot loader设计允许的上限一样低。
实模式下的内核首部:
加载内核首先加载实模式代码(boot sector 和setup),然后在0x01f1处检查随后的首部,实模式代码可达32K。尽管bloader只加载1K.首部如下:
01F1/1 ALL(1 setup_sects The size of the setup in sectors #boot sector+setup部分所占的扇区数,即占多少个512 ,read
01F2/2 ALL root_flags If set, the root is mounted readonly #不推荐使用,在命令行使用ro 和rw 代替 ,modify
01F4/4 2.04+(2 syssize The size of the 32-bit code in 16-byte paras #2.04版本以前的用低16位
01F8/2 ALL ram_size DO NOT USE - for bootsect.S use only #obsolete
01FA/2 ALL vid_mode Video mode control #视频模式控制,参见特殊命令行选项 ,modify
01FC/2 ALL root_dev Default root device number #obsolete 默认的根设备号 ,被命令行中的 root= 代替
01FE/2 ALL boot_flag 0xAA55 magic number #魔数AA55 ,read
0200/2 2.00+ jump Jump instruction #这个地方有一个Jump指令和一个地址
0202/4 2.00+ header Magic signature "HdrS" #魔数 HdrS =0x53726448 ,read
0206/2 2.00+ version Boot protocol version supported #启动协议号 read
0208/4 2.00+ realmode_swtch Boot loader hook (see below) #参见高级 bootloader HOOKS modify
020C/2 2.00+ start_sys_seg The load-low segment (0x1000) (obsolete) #0x1000
020E/2 2.00+ kernel_version Pointer to kernel version string #内核版本 ,read
0210/1 2.00+ type_of_loader Boot loader identifier #加载器类型GRUB or LILO等
0211/1 2.00+ loadflags Boot protocol option flags #启动协议的选项标志,其实是位掩码,modify
Bit 0 LOADED_HIGH :0:保护模式代码加载到10000 1:保护模式代码加载到100000
Bit 5 QUIET_FLAG :0:打印早期信息 1:不打印早期信息
Bit 6 KEEP_SEGMENTS :0:在进入保护模式的时候重新加载段寄存器 1:不重新加载
Bit 7 CAN_USE_HEAP :0: 1:heap_end_ptr是有效的
0212/2 2.00+ setup_move_size Move to high memory size (used with hooks)
0214/4 2.00+ code32_start Boot loader hook (see below) #保护模式的入口地址.由两个原因会被修改 1:做一个bootloader HOOKS 2:... modify
0218/4 2.00+ ramdisk_image initrd load address (set by boot loader) #initrd加载位置,由 bootloader给定
021C/4 2.00+ ramdisk_size initrd size (set by boot loader) #initrd大小,由bootloader给定
0220/4 2.00+ bootsect_kludge DO NOT USE - for bootsect.S use only
0224/2 2.01+ heap_end_ptr Free memory after setup end #安装setup后的空闲位置 ,stack/heap地址减去实模式代码开始位置【就是堆的底部相对实模式代码开始地方的便宜,因为堆是向下延伸的】,write
0226/1 2.02+(3 ext_loader_ver Extended boot loader version #扩展bootloader版本
0227/1 2.02+(3 ext_loader_type Extended boot loader ID #扩展bootloader 的ID
0228/4 2.02+ cmd_line_ptr 32-bit pointer to the kernel command line #指向内核命令行的32位的指针,如果不设置会被认为不支持2.02+协议,write
022C/4 2.03+ ramdisk_max Highest legal initrd address #限定的合法的最高initrd地址,2.10协议以后可以修改,
0230/4 2.05+ kernel_alignment Physical addr alignment required for kernel #内核需要的物理地址对齐值
0234/1 2.05+ relocatable_kernel Whether kernel is relocatable or not #显示内核是否可以重定位
0235/1 2.10+ min_alignment Minimum alignment, as a power of two
0236/2 N/A pad3 Unused
0238/4 2.06+ cmdline_size Maximum size of the kernel command line #命令行的最大尺寸
023C/4 2.07+ hardware_subarch Hardware subarchitecture
0240/8 2.07+ hardware_subarch_data Subarchitecture-specific data
0248/4 2.08+ payload_offset Offset of kernel payload
024C/4 2.08+ payload_length Length of kernel payload
0250/8 2.09+ setup_data 64-bit physical pointer to linked list of struct setup_data
0258/8 2.10+ pref_address Preferred loading address #代表内核的加载地址,一个可重定位bootloader应该试图在这个地址去加载,一个不可重定位内核会无条件的把自己移动到这个地方并从这个地方开始运行.
0260/4 2.10+ init_size Linear memory required during initialization #初始化阶段要求的线性地址大小.
如果在偏移0x202的位置无法找到魔术数字“HdrS”(0x53726448),那么这个启动协议版本可被归结至“旧版内核”。装载一个旧版本内核,就需要假设以下参数:Image type = zImage , 不支持initrd ,实模式部分的内核将被装载到0x90000 。 否则version域就会包含启动协议版本。
首部域的作用有三:1:bootloader从内核中获取信息‘read’ 2:被bootloader设值 ‘write’ 3:被bootloader读取并且被修改 ‘modify’
内核的运行开始地址计算方法如下:、 if (relocatable_kernel)
runtime_start = align_up(load_address, kernel_alignment)
else
runtime_start = pref_address
内核命令行:
内核命令行是bootloader和内核通信的重要方式.有些也只是bootloader自己用的.最大长度由cmdline_size限定.地址由cmd_line_ptr限定,可以在setup heap和A0000之间的任何地址
实模式下的内存布局:
实模式下要求建一个stack/heap 和 为cmd 分配内存,这些需要在最低1M空间分配,并且由于现在的EDBA比较可观 和 一些老的内核要求将使用90000以上的内存‘即使使用也要避免使用到9a000’,所以尽可能地为heap和cmdline分配较少内存。
协议高于2.02的时候cmd_line可以不使用实模式的64k(X~~~X+10000) 内存,这就可以给stack/heap配足64k,然后在起上面的内存给cmd_line分配内存.
启动配置的例子:
当加载位置低于90000的时候,偏移地址如下:
0x0000-0x7fff Real mode kernel
0x8000-0xdfff Stack and heap
0xe000-0xffff Kernel command line // 最终不超过 A00000
当加载在90000或者协议版本低于 2.01的时候,偏移地址如下:
0x0000-0x7fff Real mode kernel
0x8000-0x97ff Stack and heap
0x9800-0x9fff Kernel command line //不超过9a000
如此的bootloader可象下面这样填写首部域:
unsigned long base_ptr; /* base address for real-mode segment */
if ( setup_sects == 0 ) {
setup_sects = 4; #根据协议规定
}
if ( protocol >= 0x0200 ) {
type_of_loader = <type code>;
if ( loading_initrd ) { #loading_initrd 从哪来的
ramdisk_image = <initrd_address>;
ramdisk_size = <initrd_size>;
}
if ( protocol >= 0x0202 && loadflags & 0x01 )
heap_end = 0xe000;
else
heap_end = 0x9800;
if ( protocol >= 0x0201 ) {
heap_end_ptr = heap_end - 0x200; #为什么减去200 ? 堆的大小是200,剩下的都是栈?
loadflags |= 0x80; /* CAN_USE_HEAP */ #设置可用HEAP
}
if ( protocol >= 0x0202 ) {
cmd_line_ptr = base_ptr + heap_end;
strcpy(cmd_line_ptr, cmdline);
} else {
cmd_line_magic = 0xA33F;
cmd_line_offset = heap_end;
setup_move_size = heap_end + strlen(cmdline)+1;
strcpy(base_ptr+cmd_line_offset, cmdline);
}
} else {
/* Very old kernel */
heap_end = 0x9800;
cmd_line_magic = 0xA33F;
cmd_line_offset = heap_end;
/* A very old kernel MUST have its real-mode code
loaded at 0x90000 */
if ( base_ptr != 0x90000 ) {
/* Copy the real-mode kernel */
memcpy(0x90000, base_ptr, (setup_sects+1)*512);
base_ptr = 0x90000; /* Relocated */
}
strcpy(0x90000+cmd_line_offset, cmdline);
/* It is recommended to clear memory up to the 32K mark */
memset(0x90000 + (setup_sects+1)*512, 0,
(64-(setup_sects+1))*512);
}
加载内核的剩下部分:
32位的内核开始于内核文件的偏移为(setup_sects+1)*512‘当setup_sects=0时,其实为4.’ 处.
is_bzImage = (protocol >= 0x0200) && (loadflags & 0x01);
load_address = is_bzImage ? 0x100000 : 0x10000;
特殊命令行选项:参见Documentation/kernel-parameters.txt
vga=<mode></mode>
这里的<mode>不是一个整数(在C语言表示法中,应是十进制,八进制或者十六进制其中之一),就是“normal” (0xFFFF),“ext”(0xFFFE),“ask”(0xFFFD)中的一个。这个值应被填入vid_mode域,因为他会在命令行被解析前被内核使用。
mem=<size></size>
<size> 是用C语言表示法定义的整形,后面可以追加(大小写不敏感的)K,M,G,T,P或者E(代表<< 10, << 20, << 30, << 40, << 50或者 << 60)。这就指明了内核在内存中的末尾。它影响了initrd可能存放的位置,因为initrd应该被放置在内核末尾的附近。要注意:这个选项同时作用于内核和启动装载器!
initrd=<file></file>
指定装载的initrd,<file>显然是和启动装载器是独立的文件,而且一些启动装载器(比如LILO)甚至不需要这个选项。
另外,有些启动装载器在额定的命令行上添加了以下参数:
BOOT_IMAGE=<file></file>
要加载的启动镜像。同样,<file>也是和bootloader独立的。
auto
内核不需要用户外界干预自行启动。
如果这些参数被启动装载器添加,强烈建议将它们放置在用户指定的或者设置项指定的命令行之前。否则,“init=/bin/sh”跟上auto会让人产生歧义。
运行内核:
内核的入口点在实模式内核段地址的偏移处为20地方.在入口点,ds=es=ss且都指向内核代码的段起始位置(如果加载在90000则为9000).sp指向heap的top位置,同时关中断, 建议将fs=gs=ds=es=ss,如下所示:
/* Note: in the case of the "old" kernel protocol, base_ptr must
be == 0x90000 at this point; see the previous sample code */
seg = base_ptr >> 4; #取得段地址
cli(); /* Enter with interrupts disabled! */ #关中断
/* Set up the real-mode kernel stack */
_SS = seg; #设置实模式下的段寄存器
_SP = heap_end;
_DS = _ES = _FS = _GS = seg;
jmp_far(seg+0x20, 0); /* Run the kernel */ #跳到内核入口处
高级Bootloader HOOK
有的时候bootloader运行在敌对环境,无法获得需要的标准内存.这个时候就需要 HOOK了.这个就是最后的手段.
注意:所有的HOOKS在调用的时候需要保存 esp ebp esi edi 寄存器的值.
realmode_swtch
在进入保护模式前的16位实模式下的远子程序调用.默认的程序会禁用NMI‘NMI (Non Maskable Interrupt)——不可屏蔽中断(即CPU不能屏蔽) ’。
code32_start
进入保护模式后立即跳转到32位平坦模式‘也被称为 unreal mode,Big real mode ,和实模式一样PE=0,’ , 但是在内核解压以前,除了cs没有其他段保证被设置好. 故应该自己将他们设置为BOOT_DS(0x18).
当设置完成HOOK后,应该在bootloader覆盖之前跳转到这个地址.
32位启动协议
对一些新的BIOS,例如EFI,linuxBIOS ,16位实模式的setup代码将不能用,所以定义了32位的启动协议.
在32位启动协议中,加载内核的第一步简历boot parameters (struct boot_params,就是所谓的 ’ZERO PAGE‘),这个内存应该被分配并全部设置为0,然后地址01f1处的 setup 首部将会被填入 boot_params .
除了像16位实模式下设置boot_params一样,32位下还需要设置 zero_page.txt 中定义的域.
设置完首部后,剩下的就和16位的一样加载内核. 然后跳到内核的 入口点 .
在入口处,CPU必须处于页禁止 的保护模式下,描述段选择子_BOOT_CS(0x10) and __BOOT_DS(0x18的GDT 必须被加载,__BOOT_CS必须有执行和读权限,__BOOT_DS必须有读写权限.cs 存放的是__BOOT_CS,ds,es,ss 必须存放的是__BOOT_DS,关中断 ,esi 存放boot_params的地址,ebp,edi,ebx 必须为0.
---------------------------------------------------------------------------------------------------------------
完毕.