浏览 5770 次
锁定老帖子 主题:Linux 启动协议 v20070523
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
作者 | 正文 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
发表时间:2007-09-20
Linux 启动协议 v20070523作者:H. Peter Anvin [hpa@zytor.com] 译者:Wick [wickmaycry@gmail.com] - *为译者注 - 尊重他人劳动,分享社会成果。转载请保留文章出处和文中所有链接。 ---------- 本文章原作者H. Peter Anvin是瑞典人,作为SYSLINUX的作者,还负责了klibc和NASM等自 由软件项目。 文章对存在的启动协议进行简要概括,对Linux内核分析的起步有很大的帮助,读者也可以 借此理解到纷繁变换的协议给内核代码带来的稍许变化。 由于本人水平有限,如若读者对文中词汇翻译有任何意见或者不同见解,敬请指正。 ---------- 在i386平台下,Linux内核使用了一个相当复杂的启动协议。这要部分归咎于历史原因,因 为早期的内核需要被做成一个可启动镜像,其他原因还包括,复杂的计算机内存模型,由于 实模式DOS作为主流操作系统而改变了计算机工业的预期发展等等。 当前共有以下Linux/i386启动协议存在着:
**** 内存布局使用了Image或者zImage的启动加载器(the kernel loader),其在传统上内存布局大致如下 图所示: | | 0A0000 +------------------------+ | Reserved for BIOS | 未使用,被BIOS EBDA保留 09A000 +------------------------+ | Command line | | Stack/heap | 被实模式下的内核代码所使用 098000 +------------------------+ | kernel setup | 实模式下的内核代码 090200 +------------------------+ | kernel boot sector | 历史遗留下来的内核启动扇区 090000 +------------------------+ | Protected-mode kernel | 内核镜像的主要部分 010000 +------------------------+ | Boot loader | - 启动扇区的入口位置 0000:7c00 001000 +------------------------+ | Reserved for MBR/BIOS | 000800 +------------------------+ | Typically used by MBR | 000600 +------------------------+ | BIOS use only | 000000 +------------------------+ ---------- * 以上的特殊名词翻译 Stack/heap 堆区/栈区 kernel setup 内核安装程序 Boot loader 启动装载器 ---------- 当使用bzImage的时候,保护模式下的内核部分就被重定位到0x100000(高端内存),而实 模式下的内核块(包括启动扇区,安装程序,和堆栈部分)就可以自由定位到0x10000和低 段内存区域的任意位置。不幸的是,在2.00和2.01版启动协议中,0x90000以上的内存区域 仍旧被内核所使用;2.02版启动协议就解决了这个问题。 人们总想让内存上限——启动装载器可触的低端内存的最高点——尽可能的低,越低越好,这是 因为一些新的BIOS已经开始分配相当大的内存区域,这片区域被称为拓展BIOS数据区 (EBDA),位于低端内存的顶端。启动装载器会使用BIOS的0x21号中断来判断有多少低端内 存可以使用。 不幸的是,如果INT 12h报告的内存大小太少,启动装载器将没有任何用处,此时只能向用 户报告错误。因而这个启动加载器根据要求被设计成更小的尺寸。对于zImage或者旧式 bzImage版本内核,它们可能会需要在0x90000段写入数据,这时启动装载器应该确保不会占 用0x9a000以上内存;太多的BIOS破坏了以上原则(* 指BIOS分配太多区域用作EBDA,而导 致低端内存空闲空间过小)。 而现在的bzImage内核已经使用2.02(包括2.02)以后的启动协议,也将建议使用一下内存 布局方式: ~ ~ | 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 +------------------------+ | Boot loader | - 启动扇区的入口位置 0000:7c00 001000 +------------------------+ | Reserved for MBR/BIOS | 000800 +------------------------+ | Typically used by MBR | 000600 +------------------------+ | BIOS use only | 000000 +------------------------+ ... X位置应和boot loader设计允许的上限一样低。 **** 实模式部分的内核头部下面的段落中,还有任何关于内核启动工序的描述中,"一个扇区"意指512个字节。他和实 际使用的底层媒体中的(* 扇区概念)是独立的,不同的。 装载Linux内核的第一步,应该是装载实模式部分的内核代码(包括启动扇区和安装程序代 码),然后检查接着的头部是否位于0x01f1(偏移量)。尽管启动装载器只会选择装载前两 个扇区(1K)并且接着检查启动扇区的大小,但是实模式代码合计可以达到32K。 头部的定义可以如一下情况:
(1) 考虑到向后兼容性,如果setup_sects域是0,那么他的真实值就是4。 (2) 对于2.04以前的协议,syssize前两个字节并未使用,这意味着bzImage的大小无法确定。 如果在偏移0x202的位置无法找到魔术数字“HdrS”(0x53726448),那么这个启动协议版本可 被归结至“旧版内核”。装载一个旧版本内核,就需要假设以下参数: Image type = zImage 不支持initrd 实模式部分的内核将被装载到0x90000 否则,(* 紧接“HdrS”的)“version”域就会包含启动协议版本。 例如,协议版本2.01,“version”域的值为“0x0201”。当在头部设定此域后,你必须确认已 经设定了此协议版本使用到的其他域。 **** 详述头部域的细节情况对于这里的每个域,有些是内核和启动加载器的信息(将在以下标记为“Read”类型),有些 则是将会被启动加载器填写(标记为“Write”),而又有些可能会被启动加载器读取后又更 改(标记为“Modify”)。 通用的启动加载器,其填写的域被标记为(“obligatory”)。将内核加载到非标准化地址的 启动加载器,其填写的域被标记为(“reloc”);而其余的的启动加载器就会忽略这些域。 所有域的字节顺序为从小到大排列(毕竟这是x86)。 Field name(域的名称): setup_secs Type(类型值,* 以上有描述): read Offset/size(偏移/字节数): 0x1f1/1 Protocol(适用协议): ALL 用扇区数表示的安装程序代码大小。如果此域为0,那么真实值即是4。实模式下的代码由 引导扇区(通常为一个扇区大小)和安装程序代码组成。 Field name: root_flags Type: modify (optional) Offset/size: 0x1f2/2 Protocol: ALL 如果这个域非0,根分区将默认为只读。在这里并不赞成使用此域,用命令行的“ro”和 “rw”选项代替吧。 Field name: syssize Type: read Offset/size: 0x1f4/4 (protocol 2.04+) 0x1f4/2 (protocol ALL) Protocol: 2.04+ 保护模式下的内核代码大小,使用两个16位表示。由于2.04以前的协议中此域只有两字节 长度(* 16位),因此如果LOAD_HIGH标志被设定,内核大小就不会被一味的信任。 Field name: ram_size Type: kernel internal Offset/size: 0x1f8/2 Protocol: ALL 此域已经被废弃。 Field name: vid_mode Type: modify (obligatory) Offset/size: 0x1fa/2 请查看 特殊命令行选项 章节。 Field name: root_dev Type: modify (optional) Offset/size: 0x1fc/2 Protocol: ALL 默认的根目录分区所在的设备号。不建议使用此域,使用命令行的“root=”来替代它。 Field name: boot_flag Type: read Offset/size: 0x1fe/2 Protocol: ALL 域的值为0xAA55,这是最接近旧版Linux内核的一个魔术数字。 Field name: jump Type: read Offset/size: 0x200/2 Protocol: 2.00+ 包含一个x86跳转指令,是一个带符号的相对于0x202的偏移量,接着是0xEB。这可以用来 决定头部的大小。 Field name: header Type: read Offset/size: 0x202/4 Protocol: 2.00+ 域的值为“HdrS”(0x53726448). Field name: version Type: read Offset/size: 0x206/2 Protocol: 2.00+ 启动协议的版本号,使用了(整数部分<<8)+ 小数部分的格式。 比如:0x0204代表2.04版本,0x0a11代表了假想的10.17版本。 Field name: readmode_swtch Type: modify (optional) Offset/size: 0x208/4 Protocol: 2.00+ 启动装载器的hook(见以下 高级的启动装载器hook)。 Field name: start_sys Type: read Offset/size: 0x20c/4 Protocol: 2.00+ 低端内存中的系统位置(0x1000)。已废弃。 Field name: kernel_version Type: read Offset/size: 0x20e/2 Protocol: 2.00+ 如果此项非0,那么它就是一个指针,其值为以空值(NULL)结尾的,可识别的内核版本 号的字符串的地址,再减掉0x200。它用来向用户显示内核的版本。这个值应小于(0x200 * setup_sects)。 例如,如果此项被设定为0x1c00,内核版本号的字符串就可能在内核文件中偏移0x1e00的 位置找到。当且仅当“setup_sects”值大于等于15的时候,这个数值合法。如下所示: 0x1c00 < 15 * 0x200 (= 0x1e00) 但是 0x1c00 >= 14 * 0x200 (= 0x1c00) 0x1c00 >> 9 = 14,因此setup_secs最小值是15。 Field name: type_of_loader Type: write (obligatory) Offset/size: 0x210/1 Protocol: 2.00+ 如果你的启动加载器有一个已经给定的id号(见下表),在这里就可输入0xTV,其中,T 是一个启动加载器的标志符,而V是一个版本号。否则,就在这里输入0xff。 可选的启动加载器的id: 0 LILO (0x00为pre-2.00装载器所保留) 1 Loadlin 2 bootsect-loader (0x20,其他0x2V都被保留) 3 SYSLINUX 4 EtherBoot 5 ELILO 7 GRuB 8 U-BOOT 9 Xen A Gujin B Qemu 如果你有一个已用(* 且未被列出)的启动装载器的ID,请联系 [hpa@zytor.com] Field name: loadflags Type: modify (obligatory) Offset/size: 0x211/1 Protocol: 2.00+ 此域是一个比特掩码。 Bit 0 (read): LOADED_HIGH - 如果是0,保护模式部分的代码将被加载到0x10000。 - 如果是1, 保护模式部分的代码将被加载到0x100000。 Bit 7 (write): CAN_USE_HEAP 将此项设置1,可以指明heap_end_ptr定义有效。如果此项清空,一些安装程序的 功能将会被禁用。 Field name: setup_move_size Type: modify (obligatory) Offset/size: 0x212/2 Protocol: 2.00-2.01 当使用2.00或者2.01版协议时,如果实模式部分的内核没有被装载到0x90000,那他将在 加载流程中的靠后部分移动至那里。如果除了实模式代码本身以外,你有另外的数据(例 如,内核命令行)想移动,那么请填写此域。 这块单元是以启动装载器作为开头的几个字节。 如果你的启动协议版本高过2.02(包括2.02),或者实模式代码已经被装载到0x90000,那 么此项可以忽略。 Field name: code32_start Type: modify (optional, reloc) Offset/size: 0x214/4 Protocol: 2.00+ 跳转到保护模式的地址。默认下,这应该是内核所加载的位置,且此项被启动加载器来判 断合适的加载地址。 此域可被用作以下用途:
Field name: ramdisk_image Type: write (obligatory) Offset/size: 0x218/4 Protocol: 2.00+ 用作初始化的ramdisk或者ramfs的32位线性地址。如果没有初始化的ramdisk/ramfs,请 将此位置0。 Field name: ramdisk_size Type: write (obligatory) Offset/size: 0x21c/4 Protocol: 2.00+ 用作初始化的ramdisk或者ramfs的大小。如果没有初始化的ramdisk/ramfs,请 将此位置0。 Field name: bootsect_kludge Type: kernel internal Offset/size: 0x220/4 Protocol: 2.00+ 此域已废弃。 Field name: heap_end_ptr Type: write (obligatory) Offset/size: 0x224/2 Protocol: 2.01+ 将此域设定为安装程序使用的堆区/栈区的位置(指相对于实模式代码的偏移),再减去 0x200。 Field name: cmd_line_ptr Type: write (obligatory) Offset/size: 0x228/4 Protocol: 2.02+ 将此域设定为内核命令行的线性地址。 内核命令行可以被放置在安装程序堆区的末尾和0xA0000之间的任意位置;它不必设在与 实模式代码相同的64K段中。 即便你的启动加载器不支持命令行,也可填写,这时你可以将此地址指向一个空串。如果 此域被置0,内核会假定你的启动装载器不支持2.02以上协议。 Field name: initrd_addr_max Type: read Offset/size: 0x22c/4 Protocol: 2.03+ 你的初始化ramdisk/ramfs内容可用的最高地址。2.02和更老的启动协议并没有定义此域, 且他的最大地址是0x37FFFFFF。(这个地址被定义为最高的安全地址,所以,如果你的 ramdisk正好是131072字节大小,而且此域是0x37FFFFFF,你就可以从0x37FE0000启动你 的ramdisk)。 Field name: kernel_alignment Type: read (reloc) Offset/size: 0x230/4 Protocol: 2.05+ 内核所需要的对齐位(当relocatable_kernel = true时可用)。 Field name: relocatable_kernel Type: read (reloc) Offset/size: 0x234/1 Protocol: 2.05+ 如果此项非0,则保护模式部分的内核可以被加载到满足kernel_alignment的任意位置。 在加载完成后,启动装载器会将code32_start设定为指向加载的代码位置,或者指向启动 加载器的hook。 Field name: cmdline_size Type: read Offset/size: 0x238/4 Protocol: 2.06+ 除去终止符0后的命令行最大长度。意思是,命令行可以包含最大cmdline_size个字符数。 在2.05或更早启动协议版本里,这个最大值是255。 **** 内核命令行如今内核命令行已经成为了启动加载器和内核通信的重要手段。一些命令行选项同样与启动 装载器自身有关,见如下“特殊的命令行选项”。 内核命令行是一个以空值结尾的字符串。他的最大长度被保存到cmdline_size域。在2.06版 协议以前,最大长度是255个字符。太长的字符串会被内核自动截断。 如果启动协议是2.02或者更新版本,内核命令行的地址将由头部的cmd_line_ptr域给定(见 下文)。这个地址可以是安装程序的堆区与0xA0000之间任意地址。 如果启动协议版本低于2.02,就可使用以下的协议来读取内核命令行: 在偏移0x0020(字)位置,“cmd_line_magic”,填入魔术数字 0xA33F 在偏移0x0022(字)位置,“cmd_line_offset”,填入内核命令行(相对于实模式 部分的内核)的偏移地址。 内核命令行必须包含在setup_move_size覆盖的内存范围内,所以你可能需要修改 这个域。 **** 实模式代码的内存布局实模式代码需要建立一个堆区/栈区,也要为内核命令行分配相应内存。这些都需要在实模 式代码可达的低1M空间内完成。 要注意:现在的机器都拥有一个相当大的拓展BIOS数据区(EBDA)。因此建议尽量使用低1M空 间中尽可能小的空间。 不幸的是,在下面的情况下就必须使用0x90000内存段: - 装载一个zImage内核 ((loadflags & 0x01) == 0) - 装载一个使用2.01或更早版本协议的内核。 -> 在2.00和2.01版启动协议中,实模式代码会被加载到另一个地址,但也会被 再次重新分配到0x90000。对于“旧版”内核,实模式代码必须被加载到 0x90000。 当加载到0x90000时,避免使用高于0x9a000的内存。 对于2.02或者更高版本启动协议,命令行不必和实模式下安装程序代码放在同一个64K段中; 因此允许堆区/栈区拥有完整的64K段,且可以在其中存放命令行。 内核命令行既不该被存放在实模式代码以下空间,也不应该放置在高端内存中。 **** 启动设置示例因为是一个设置示例,我们可以假定存在以下实模式内存段的布局: 当装载到低于0x90000的地址时,使用整个段: 0x0000-0x7fff 实模式部分的内核 0x8000-0xdfff 堆区/栈区 0xe000-0xffff 内核命令行 当装载到0x90000处,或者你使用了2.01或者更早版本的协议: 0x0000-0x7fff 实模式部分的内核 0x8000-0x97ff 堆区/栈区 0x9800-0x9fff 内核命令行 这样一个启动装载器应该如此填写头部中的以下域: C 代码
**** 装载内核的剩余部分32位内核代码从内核文件偏移(setup_sects+1)*512的位置开始(同样,如果setup_sects等 于0,其真实值就是4)。 如果是Image/zImage内核,此段代码应该加载到0x10000,而bzImage内核则应该加载到 0x100000位置。 如果使用2.00或更高协议,而且已经设置loadflags的0x01位(LOAD_HIGH),那么内核就被 认作bzImage内核。 C 代码
注意Image/zImage可能在达到512K大小,这样就会使用到整个0x10000-0x90000内存范围。 这意味着,迫切需要这些内核将实模式部分装载到0x90000。bzImage内核则提供了更多的灵 活性。 **** 特殊的命令行选项如果用户键入了启动装载器支持的命令行参数,那么用户可能期望以下命令行选项可以工作。 即便并非所有选项对内核都有意义,他们也不该直接被删除。启动装载器的作者如果需要给 装载器自身提供另外的命令行参数,就应该在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会让人产生歧义。 </file></file></size></mode> **** 启动内核跳转到内核入口地址,内核就开始运行了。其入口处于相对于实模式内核部分的段地址偏移 0x20的位置。这意思是:如果你将实模式内核加载到0x90000,内核入口就应该是9020:0000。 在入口处,我们将ds,es,ss一起指向实模式内核代码的起始位置(如果代码加载到 0x90000,那这个位置应该是0x9000),sp寄存器应该指向堆区的顶端,而且还要禁用中断。 此外,为了防止内核出现bug,建议启动装载器作出设置: fs = gs = ds = es = ss。 对于以上的举例,我们这样来实现: C 代码
如果启动扇区访问的是一个软驱,建议在内核运行前关闭马达。因为内核启动会保持关闭中 断状态,这样就无法关闭驱动器马达,尤其当装载的内核需要加载软驱的时候(* 此时的软 件驱动器状态未知,无法使用)。 **** 高级的启动装载器hook如果启动装载器在一个不利的环境下运行(比如在DOS下运行的LOADIN),就有可能不去遵从标准的内存映射,这样的启动装载器可能使用到了如下的hook。如果是启用了hook,它 会在适当的时间被内核触发。hook应该被认作绝对最后的手段来使用! 醒目:所有的hook在启用时都需要保存%esp,%ebp,%esi和%edi。 realmode_swtch: 在即将进入保护模式之前出发的一个16位实模式段外子程序。默认下这样的程序应 该禁用NMI,所以你的程序也应如此。 code32_start: 在刚刚跃迁到保护模式的时候要跳转到的32位平面模式(flat-mode)例程,但是 此程序应在内核解压缩之前执行。除了CS外其他段地址都不能被确认保存(现在的 内核保存,而老版本没有);你应该自己将他们设置为BOOT_DS。 在完成hook之后,应该在你的启动装载器覆盖hook之前跳转到此域中(* 指 code32_start)(如果条件允许,就执行重定位)。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
发表时间:2007-09-21
现在都干起翻译来了啊 支持!
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||