LINUX/I386启动协议

 
LINUX/I386 启动协议
                    
H. Peter Anvin <hpa@zytor.com>
Last update 2000-07-27
 
    在i386平台,linux内核使用了一种相当复杂的启动方式。这种做法部分归于历史因素,即早期要求内核自身即是一个可启动映象,还有复杂的PC内存模型以及由于实模式DOS作为主流操作系统的逐渐让位而导致PC工业发展的预期。
    目前,linux i386启动协议有四个版本:
    旧内核版本:仅支持zImage/Image。更早期的内核可能不支持(内核)命令行
    协议2.00:       (内核1.3.17)增加了bzImage和initrd支持,还有启动加载程序和内核通信的正式、规范的方式。处理内核映像的重定位由setup.S处理,而传统的配置区默认仍是可写的。
    协议2.01:    (内核1.3.76)增加了堆溢出告警机制
    协议2.02:    (内核2.4.0-test3-pre3)新的内核命令行协议。减少常规内存限制,不会覆盖传统的配置区,这样可以使使用来自于SMM或32位BIOS入口点的EBDA系统安全启动。不推荐但支持zImage
    协议2.03:(内核2.4.18-pre1)明确使启动加载程序可以使用最高可能的initrd地址    
**** 内存布局
    对于内核加载程序,传统的内存映像用于Image或zImage内核,典型的象如下所示:
                         |                          |
        0A0000   +------------------------+
                      | BIOS 保留        |     未用.保留给BIOS EBDA
        09A000   +------------------------+
                      | 堆栈/堆/命令行 |     内核实模式使用
        098000    +------------------------+
                      | 内核配置区       |     内核实模式代码
        090200    +------------------------+
                      | 内核启动扇区    |     内核遗留的启动扇区,启动后则不使用
        090000    +------------------------+
                      | 保护模式内核    |     内核镜像(zImage/Image<未压缩>或bzImage<压缩>)
        010000    +------------------------+
                      | 启动加载程序    |     <-启动扇区入口 0000:7C00
        001000    +------------------------+
                      |保留给MBR/BIOS |
        000800    +------------------------+
                      | 通常用于MBR  |
        000600    +------------------------+
                      | 仅供BIOS使用 |
        000000    +------------------------+
   
    当使用bzImage时,保护模式内核被重定位到0x100000(高内存),内核实模式块(启动扇区、配置区、堆栈/堆)被重定位到0x10000和低内存末端之间的任何地址。不幸的是,在2.00和2.01协议中,命令行仍然要求放置在0x9XXXX内存范围,并且这个内存范围依然会被早期的内核重写。2.02内核解决了这个问题。
    大家都希望保留内核界限,因为低内存的最高点有启动加载程序访问,而且这个最高点必须尽可能低,因为有些新的BIOS需要分配相当大的位于低内存的上部的称为扩展BIOS区域的内存。启动加载程序应该用int 21h系统调用来验证低端内存的可用值。
    不幸的是,如果int 21h返回的内存容量太小,则启动程序不做任何事情,仅仅是向用户报告这个错误。因此,启动加载程序通常设计成尽它可能地花费较少的低端内存空间(多出来的空间供某些新的BIOS使用)。对于zImage或早期的bzImage内核,它们需要把数据写进0x90000段,启动加载程序必须确保不使用0x9A000以上的地址;非常多的BIOS访问这个地址之上则会崩溃。
 
**** 实模式内核头部
    在接下来内容中,以及无论内核启动顺序的任何环节,一个扇区都代表着512字节。它与实际介质的扇区大小互不相干(实际介质中扇区大小可以是任意字节,只是平常都认为是512bytes)。
    加载linux内核的第一步是加载实模式代码(启动扇区和配置代码),然后检测下面的头部(在内核镜像的偏移0x01f1处)。实模式代码总计可达32kb,所以启动加载程序通常选择加载前两个扇区(1k)然后检查启动扇区大小。
    头部类似如下所示:
    偏移/大小       协议        名称                      含义
   
    01F1/1           ALL        setup_sects            扇区中配置的大小
    01F2/2           ALL        root_flags              如果置位,则root被只读挂载
    01F4/2           ALL        syssize                  仅bootsect.S使用
    01F6/2           ALL        swap_dev                     未用,作废
    01F8/2           ALL        ram_size               仅bootsect.S使用
    01FA/2           ALL        vid_mode                     视频模式控制
    01FC/2           ALL        root_dev               缺省的root设备数
    01FE/2           ALL        boot_flag               启动扇区魔数0xAA55
    0200/2           2.00+      jump                     跳转指令
    0202/4           2.00+      header                   签名魔数 "HdrS"
    0206/2           2.00+      version                  支持的启动协议版本
    0208/4           2.00+      realmode_swtch     启动加载程序钩子(见下文)
    020C/2           2.00+      start_sys               加载在低内存的段地址(即0x1000)(已作废)
    020E/2           2.00+      kernel_version        内核版本字符串指针
    0210/1           2.00+      type_of_loader       启动加载程序标识符
    0211/1            2.00+      loadflags               启动协议选项标识
    0212/2           2.00+      setup_move_size    移动到高内存的尺寸(和钩子配合使用)
    0214/4           2.00+      code32_start          启动加载程序钩子(见下文)
    0218/4           2.00+      ramdisk_image       initrd加载地址(由启动加载程序设置)
    021C/4           2.00+      ramdisk_size          initrd尺寸大小(由启动加载程序设置)
    0220/4           2.00+      bootsect_kludge     仅供bootsect.S使用
    0224/2           2.01+      heap_end_ptr        配置结束后的可用空闲内存
    0226/2           N/A        pad1                            未用,保留
    0228/4           2.02+      cmd_line_ptr          指向内核命令行的32位指针
    022C/4           2.03+      initrd_addr_max     最高非法initrd地址,减一即为合法地址
 
    为了向后兼容,若setup_sects域中包含了0,则其实际值是4。
    如果在偏址0x202处没有发现魔数"HdrS"(0x53726448),则认为启动协议版本为旧版。加载旧版的内核,则默认下面的参数:
       映像类型为zImage
       不支持initrd
       实模式代码必须定位在0x90000
    否则,version域包含了启动协议的版本号。如:如果是2.01版本协议则此域为0x0201 。当设置头部的域时,你应该只使用所使用协议版本支持的域。
    kernel_version域如果被设置为非零值,则它包含指向人可识别的带非终结符的内核版本数字符串的指针,且小于0x200。这可用于向用户显示内核版本。这个值应该小于(0x200*setup_sects).例如:如果此值设置为0x1c00,则内核版本串就会在内核文件的偏移0x1e00处找到。当且仅当setup_sects域所包含的值大于等于14时这个值才是有效的。
    绝大多数启动加载程序简单的将内核直接加载到目的地址,这样的启动加载程序不必填满头部中绝大多数域,但下面的域不管怎样都要填写:
    vid_mode
        请参考下面"特殊命令行选项"小节
    type_of_loader
            如果你的启动加载程序已经指派了id(见下表),则这里键入0xTV,此处T代表启动加载程序的标识符,V代表版本号,否则这里键入0xFF。
        指派的启动加载程序的id:
               0 LILO
                     1 Loadlin
                     2 bootsect-loader
                     3 SYSLINUX
                     4 EtherBoot
                     5 ELILO
                     7 GRuB
                     8 U-BOOT
       如果你需要分配启动加载程序id,请与<hpa@zytor.com>联系
    loadflags, heap_end_ptr:
            如果是2.01或更高协议版本,则键入配置堆的偏移极限到heap_end_ptr中并设置loadflags的0x80位(CAN_USE_HEAP)。heap_end_ptr似乎与配置开始有关(偏址为0x0200)
    setup_move_size:
            当使用2.00或2.01协议时,实模式内核并不是加载到0x90000,它在加载过程中后来被移到此处。除了实模式内核自身如果你想移动其它的数据,则填充此域。
    ramdisk_image, ramdisk_size:
    如果你的启动加载程序加载了初始化的ramdisk(initrd),则将ramdisk_image设置为指向ramdisk数据的32位指针,并将ramdisk_size设置为ramdisk数据的大小。
            initrd应该被放在尽可能高的内存部分,否则它可能被早期的内核初始化程序覆盖。但不管怎样,它都要放在initrd_addr_max域所规定的地址,且至少4k字节对齐。
    cmd_line_ptr:
             如果协议版本是2.02或更高版本,则此域是一个指向内核命令行的32位指针。内核命令行能够放在0xA0000与配置的末端之间的任何地方。即使你的内核不支持命令行,也要填充域,这种情况下你可以使它指向空字符串。(更好的做法是设置它为"auto")。如果此域为0,则内核认为你的启动加载程序不支持2.02以上启动协议。
    ramdisk_max:
             最大的地址可能被initrd内容占用。对于2.02或更早的启动协议,此字段是不会出现的,且最大地址是0x37FFFFFF。 (这个地址被定义为最高安全地址,因此如果你的ramdisk是严格的131072字节长,则此域是0x37FFFFFF,你可以从0x37FE0000启动你的ramdisk)
**** 内核命令行
    内核命令行已经变成启动加载程序和内核通信的最重要的方式,它的有些选项已经与内核启动程序的选项相对应。可以参考下面的"特殊的命令行选项"小节。
    内核命令行是非终结字符串且加上最后的终结符最长可达255个字符。
    如果启动协议是2.02后以后版本,内核命令行将根据下面的协议开始:
    在偏址0x0020(字),cmd_line_magic填入魔数0xA33F
    在偏址0x0022(字),cmd_line_offset填入内核命令行的偏移(相对于实模式内核的开始处)。
    内核命令行必须在setup_move_size所规定的内存区域,因此你可能需要校正这个域。
**** 简单的启动配置
    举个简单的例子,假定实模式段的布局如下:
           0x0000-0x7FFF     实模式内核
              0x8000-0x8FFF     堆栈和堆
              0x9000-0x90FF     内核命令行
    则启动加载程序应该如下填入头部的下列域:
    unsigned long base_ptr; /* 实模式段的基地址 */
 
       if ( setup_sects == 0 ) {
              setup_sects = 4;
       }
 
       if ( protocol >= 0x0200 ) {
              type_of_loader = <type code>;
              if ( loading_initrd ) {
                     ramdisk_image = <initrd_address>;
                     ramdisk_size = <initrd_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 {
              /* 早期的内核 */
              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;             /* 重定位 */
              }
 
              /* 此处推荐清除内存直到32k标记处*/
              memset(0x90000 + (setup_sects+1)*512, 0,
                     (64-(setup_sects+1))*512);
       }    
**** 加载内核其余部分
    非实模式的内核在内核文件偏移(setup_sects+1)*512处开始(此外,如果setup_sects为0则实际值为4),Image/zImage内核应该被装载到0x10000,而bzImage内核应该被装载到0x100000。
    如果协议大于等于2.00且loadflags域的0x01位(LOAD_HIGH)被置位,则内核为bzImage内核。
           is_bzImage = (protocol >= 0x0200) && (loadflags & 0x01);
              load_address = is_bzImage ? 0x100000 : 0x10000;
    注意:Image/zImage内核可以达到512k尺寸,所以使用了整个0x10000-0x90000内存范围,这意味着对于这些内核而言,将实模式部分加载到0x90000是非常恰当的要求。bzImage内核允许更多的灵活性。        
**** 特殊的命令行选项
    如果内核启动加载程序提供的命令行由用户填入,用户可能希望下面的命令行工作。它们通常不应该从内核命令行删除,即使它们中并非所有的都对内核有意义。对于启动加载程序而言,启动加载程序的作者需要额外的命令行选项,他应该让这些选项在Documentation/kernel-parameters.txt中注册以确保它们不会和现在以及未来的内核实际选项冲突。
    vga=<mode>
                  这里<mode>要麽是一个整数(c语言中的十进制、八进制或十六进制数),要麽是字符串"normal" (代表0xFFFF), "ext"(代表0xFFFE)或"ask"(代表0xFFFD)中的某一个。这个值会填到vid_mode域,它在解析内核命令行之前由内核使用。
    mem=<size>
            <size>是一个C的整数后跟K、M、G(代表左移10、左移20和左移30)。这给内核指定了内存末端,由于initrd被放到内存末端附近,所以这影响着initrd的放置地方。注意,这个选项对内核和启动加载程序都是可选的。        
    initrd=<file>
            initrd应该被加载,则显然<file>意味着启动加载程序是独立的,有些启动加载程序(如LILO)没有这个命令。
       
    另外,有些启动加载程序增加了下面的选项给用户命令行:        
    BOOT_IMAGE=<file>
           启动映像将被加载,同样,显然<file>意味着启动加载程序是独立的。
    auto
           内核没有外部用户干扰而直接被加载
   
    如果启动加载程序增加了这些选项,则我们极力推荐在用户命令行或配置命令行之前定位这些选项。否则,内核将会根据auto选项获得"init=/bin/sh"    
**** 运行内核
    内核开始跳到内核入口点,它被定位在从实模式开始处偏移0x20的地方。即你加载实模式代码在0x90000,则内核入口点就是9020:0000。
    在入口点,ds = es = ss,且它们应该指向内核实模式的开始处(如果内核被加载到0x90000则其值为0x90000)。sp应当被适当的设置,通常指向堆的顶部,并且中断被禁止。此外,内核中为了预防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);   /* 运行内核 */
   
    如果你的启动扇区访问软驱,则推荐在运行内核前切断软驱马达,因为内核启动就禁止了中断,而软驱马达没有切断;特别是当加载的内核把软驱当作可加载模块时。
**** 高级启动时间钩子
    如果启动加载程序运行在一个独特的环境中时(如LOADLIN运行在dos下),遵循标准内存定位需求就变得很重要了,这样,启动加载程序就可以使用下面的钩子。如果设置了这些钩子,则内核在适当的时候调用它们。使用这些钩子可能完全是最后(无奈)的选择。
    重要提示:所有的钩子都需要保存%esp, %ebp, %esi和%edi
    realmode_swtch:
                 在进入保护模式前,内核立即调用16位的实模式远端子程序。程序默认将会禁止NMI(非可屏蔽中断)。因此你的程序可能也应该这么做。
    code32_start:
在切换到保护模式之后,且在解压内核映像之前,内核立即跳转到一个32位平坦模式函数。此时除了CS段寄存器外其它段寄存器都未设置;你应该自己将它们设置为KERNEL_DS (0x18)。
当设置完钩子后,你应该在启动加载程序覆盖此域之前跳转到这个域所设置的地址。
                          
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值