1、LINUX/I386 引导协议-H. 彼得安文 最后更新于 2002-01-01在 i386 平台上,Linux 内核使用一个相当复杂的引导协定。演变至此,这一方面是由于历史方面的原因,另一方面是由于早期要求将内核本身做成可引导镜像、复杂的 PC 内存模型,以及因实模式的 DOS 作为主流操作系统的实际消亡而引发的对 PC 工业的预期发生的转变。现在,存在四个版本的 Linux/i386 引导协议。旧内核:只支持 zImage 和 Image。有些很早期的内核甚至不支持命令行。2.00 协议:(1.3.73 内核) 添加了对 bzImage 和 initrd 的支持,以及引导加载程序和内核之
2、间通信的规范方法。尽管传统的 setup 区域仍然假定是可写的, 但是 Setup.S 做成了可 重定位的了。 (译者注:setup 区域会被旧内核的实模式代码重写,比如 0x9XXXX 处的命 令行参数。 )2.01 协议:(1.3.76 内核)添加了堆超出警告。2.02 协议:(2.4.0-test3-pre3 内核)新加的命令行协议。降低了传统的内存顶端地址。禁止 重写传统的 setup 区域,这使得从系统管理模式(SMM)或从 32 位 BIOS 调用的入口地 址那里使用扩展 BIOS 数据区(EBDA)的系统安全引导。zImage 不建议使用,但依然支 持。2.03 协议:(2.4.
3、18-pre1 内核) 显式地给引导加载程序给出 initrd 的最高可能地址。* 内存布局传统的 Image 或 zImage 使用的内核加载器的典型内存布局如下:| |0A0000 +-+| BIOS 保留 | 不要使用。保留给 BIOS EBDA。09A000 +-+| 堆栈/堆/命令行 | 内核实模式代码使用。098000 +-+| 内核 setup | 内核实模式代码。090200 +-+| 内核引导扇区 | 内核遗留引导扇区。090000 +-+| 保护模式内核 | 内核镜像块。010000 +-+| 引导加载程序 | loadflags, heap_end_ptr:如果协议版本是
4、 2.01 或以上,在 heap_end_ptr 里输入 setup 堆的偏移量界限并在loadflags 里设置 0x80 位(CAN_USE_HEAP),heap_end_ptr 从相对于 setup 开始处(0x0200 )看的。setup_move_size: 当使用协议 2.00 or 2.01,如果实模式内核没有加载到 0x90000,它在后面的加载步骤里将被移到那里。如果除了实模式内核本身外你想要移动额外的数据(像内核命令行) ,填写这个字段。ramdisk_image, ramdisk_size:如果引导加载程序已经加载了初始 RAM 盘(initrd) ,设置指向 ramdi
5、sk 数据的 32 位指针到 ramdisk_image,设置 ramdisk 数据的大小到 ramdisk_size。Initrd 通常要加载到尽量高的内存里,要不然,它将被早期内核初始化过程重写。但是,它不能加载到大于由 initrd_addr_max 字段指出的地址,initrd 要至少以 4K 字节的页对齐。cmd_line_ptr:如果协议版本是 2.02 或以上,这是一个指向内核命令行的 32 位指针。内核命令行可以放在 setup 结尾到 0xA0000 之间的任何地方。即使你的引导加载程序不支持命令行也要填写这个字段,这时,你可以把它指向空字符串(或者指向字符串“自动”将更好)
6、 。如果保留该字段在 0 值,内核将认为你的引导加载程序是不支持 2.02 以上的协议。ramdisk_max: (译者注:即 initrd_addr_max 字段)initrd 的内容能占用的最大地址。2.02 或更早的引导协议里没有这个字段,因此,最大地址是 0x37FFFFFF。 (这个地址定义为最高的安全字节的地址,因此,如果你的 ramdisk 恰好是 131072 字节长并且这个字段的值是 0x37FFFFFF,你可以从 0x37FE0000 开始你的ramdisk。 )* 内核命令行内核命令行已成为引导加载程序和内核通信的重要手段。它的一些选项也与引导加载程序本身相关,见下面的“
7、特殊命令行选项”内核命令行是一个零结尾的字符串,加上最后的零字节长度不超过 255 字节。如果引导协议时 2.02 或以上,内核命令行地址由内核头 cmd_line_ptr 字段给出(见前面) 。如果内核版本不是 2.02 或以上,命令行由以下协议输入:在偏移 0x0020 处, “cmd_line_magic”字段(字) ,输入幻数 0xA33F。在偏移 0x0022 处, “cmd_line_offset”字段(字) ,输入内核命令行偏移量(相对于实模式内核开始处) 。内核命令行必须在 setup_move_size 覆盖的内存区以内,所以你可能需要调整这个字段。译者注:命令行的内容有些是
8、针对引导加载程序的,有些是针对内核的,用户输入后,引导加载程序分析,如果是给自己的,就解析使用,如果是给内核的就传递。引导加载程序也可以给内核加入命令行参数* 引导配置举例作为配置举例,假定如下实模式段的布局:0x0000-0x7FFF 实模式内核0x8000-0x8FFF 堆栈和堆0x9000-0x90FF 内核命令行这样的引导加载程序要在内核头里输入下面的字段。unsigned long base_ptr; /* 实模式段基址 */if ( setup_sects = 0 ) setup_sects = 4;if ( protocol = 0x0200 ) type_of_loader =
9、 ;if ( loading_initrd ) ramdisk_image = ;ramdisk_size = ;if ( protocol = 0x0201 ) heap_end_ptr = 0x9000 - 0x200;loadflags |= 0x80; /* CAN_USE_HEAP */if ( protocol = 0x0202 ) cmd_line_ptr = base_ptr + 0x9000; else cmd_line_magic = 0xA33F;cmd_line_offset = 0x9000;setup_move_size = 0x9100; else /* Very
10、 old kernel */cmd_line_magic = 0xA33F;cmd_line_offset = 0x9000;/* 很老的内核必须将它的实模式代码加载到 0x90000 */if ( base_ptr != 0x90000 ) /* 复制实模式内核 */memcpy(0x90000, base_ptr, (setup_sects+1)*512);/* 复制命令行 */memcpy(0x99000, base_ptr+0x9000, 256);base_ptr = 0x90000; /* Relocated */* 建议清零 32K 以内的内存 */memset(0x90000
11、+ (setup_sects+1)*512, 0,(64-(setup_sects+1)*512);* 加载剩下的内核非实模式内核从内核文件偏移(setup_sects+1)*512 处开始(再次,如果setup_setcs=0,真实值是 4) 。如果是 Image/zImage,它应被加载到地址 0x10000 处,如果是 bzImage,加载到 0x100000 处。如果协议=2.00 并且 loadflags 字段的 0x01 位(LOAD_HIGH)置位,内核是 bzImage 的内核:is_bzImage = (protocol = 0x0200) load_address = is
12、_bzImage ? 0x100000 : 0x10000;注意:Image/zImage 内核最大可以有 512K 大小,因此可以使用 0x100000x90000 范围的整个内存。这表示内核实模式部分加载到 0x90000 绝对必需。bzImage 内核则允许更多灵活处理。* 特殊命令行选项如果引导加载程序提供的命令行是由用户输入的,用户可能希望下面的命令行选项有效。正常情况下,即使它们实际上不全对内核有意义,也不能将它们从命令行删除。如果需要给引导加载程序本身添加额外的命令行选项,引导加载程序的作者要在Documentation/kernel-parameters.txt 里注册它们,以
13、确保它们没有和当前现行的以及将来的内核选项相冲突。vga=这里的是整数(用 C 表示法,十进制、八进制或十六进制)或“normal” (表示 0xFFFF) 、 “ext”(表示 0xFFFE) 、 “ask”(表示 0xFFFD)字符串的一种。当命令行被解析之前被内核使用时,该值应当输入 vid_mode 字段。mem=是以 C 语法表示的整数,后面可选跟 K、M 或 G(表示表示要加载 initrd,的含义显然和引导加载程序无关,一些引导加载程序(比如LILO)没有这个命令。另外,一些引导加载程序给用户定义的命令行添加了如下选项:BOOT_IMAGE=要加载的引导镜像,再次,的含义显然和引
14、导加载程序无关。Auto内核加载不需要显式的用户干预。如果引导加载程序添加了这些选项,强烈建议将它们放在前面,放在用户定义或配置定义的命令行之前。要不然, “init=/bin/sh”会混淆“auto”选项。译者注:这些选项是引导加载程序要解析使用的选项,命令行有给内核和/或给引导加载程序的差别。* 运行内核内核是通过跳转到位于从实模式内核开始,段偏移量 0x20 处的内核入口点开始运行的。这表示如果你加载你的实模式内核代码到 0x90000,内核入口点是 0x9020:0000。在入口点,ds=es=ss 应指向实模式内核代码开始处(0x9000 如果代码加载到 0x90000) ,sp 要
15、设置正确,正常应指向堆顶,中断应该关闭。还有,为了防止内核 bug,建议引导加载程序设置 fs=gs=ds=es=ss。上面的例子中,我们应该做:/*注:“旧”内核的情况下,base_ptr 这时必须=0x90000,见上一例子代码。 */seg = base_ptr 4;cli(); /* 以关中断进入! */* 建立实模式内核堆栈 */_SS = seg;_SP = 0x9000; /* 装入 SS 后立即装入 SP! */_DS = _ES = _FS = _GS = seg;jmp_far(seg+0x20, 0); /* 运行内核 */如果你的引导扇区访问软驱,建议内核运行前关掉软驱
16、马达,因为内核引导使中断关断,因此马达不能关掉,特别地当加载的内核把软驱驱动程序当做需要时再加载的模块时更是如此。* 高级引导期钩子如果引导加载程序运行在特别不友好的环境中(像 LOADIN,运行在 DOS 中) ,这将不可能依照标准的内存位置要求,这样的引导加载程序可以使用下面的钩子,如果设置了,在合适的时候会被内核调用。要大致把使用这些钩子当做完全是最后的手段。重要提示:所有的钩子在调用前和调用后必须保持%esp,%ebp,%esi 和%edi 不变。realmode_swtch:这是在刚要进入保护模式之前,发起的一个 16 位实模式的远程子程序调用,默认程序会关掉非可屏蔽中断(NMI) ,所以你的程序也应该这么做。code32_start:这是在刚转入保护模式后,但在内核解压缩之前,要跳转到的一个 32 位平坦地址的程序。除了 CS,还没有段被设置,你要亲自将它们设置为 KERNEL_DS(0x18)。完成你的钩子后,你要跳转到你的引导加载程序重写这个字段之前该字段里的地址处。