UP | HOME

保护模式

Table of Contents

内核启动的第一步

在 上一节 中开始接触到内核启动代码,并且分析了初始化部分,最后停在了对 arch/x86/boot/main.c 的 main 函数调用

接下来将继续对内核启动过程的研究:

  • 认识保护模式
  • 如何从实模式进入保护模式
  • 堆和控制台初始化
  • 内存检测,cpu验证,键盘初始化
  • 还有更多

保护模式

在操作系统可以使用Intel 64位CPU的长模式之前,内核必须首先将CPU切换到保护模式运行

什么是保护模式?

保护模式 于1982年被引入到Intel CPU家族,并且从那之后,直到Intel 64出现,保护模式都是Intel CPU的主要运行模式。淘汰实模式的主要原因是因为在实模式下,系统能够访问的内存非常有限

在实模式下,系统最多只能访问1M内存,而且在很多时候,实际能够访问的内存只有640K

保护模式带来了很多的改变,不过主要的改变都集中在 内存管理 方法。在保护模式中,实模式的20位地址线被替换成32位地址线,因此系统可以访问多达 4GB 的地址空间。另外,在保护模式中引入了 内存分页 功能。总之,保护模式提供了2种完全不同的内存管理机制:

  • 段式内存
  • 内存分页

段式内存

在实模式下,一个物理地址是由2个部分组成的:

  • 内存段的基地址
  • 从基地址开始的偏移

使用这2个信息,可以通过下面的公式计算出对应的物理地址:

PhysicalAddress = Segment * 16 + Offset

在保护模式中,内存段的定义和实模式完全不同。在保护模式中,每个内存段不再是64K大小,段的 大小起始位置 是通过一个叫做 段描述符 的数据结构进行描述。所有内存段的段描述符存储在一个叫做 全局描述符表 GDT 的内存结构中。全局描述符表这个内存数据结构在内存中的位置并不是固定的,它的地址保存在一个 特殊寄存器 GDTR

马上将在Linux内核代码中看到全局描述符表的地址是如何被保存到 GDTR 中

具体的汇编代码看起来是这样的:

lgdt gdt

lgdt 汇编代码将把全局描述符表的基地址和大小保存到 GDTR 寄存器 中。GDTR 是一个 48位 的寄存器,这个寄存器中的保存了2部分的内容:

  • 全局描述符表的大小 16位
  • 全局描述符表的基址 32位

全局描述符

像前面说的,全局描述符表包含了所有内存段的描述符。每个段描述符长度是 64位 ,结构如下图描述:

31          24        19      16              7            0
------------------------------------------------------------
|             | |B| |A|       | |   | |0|E|W|A|            |
| BASE 31:24  |G|/|L|V| LIMIT |P|DPL|S|  TYPE | BASE 23:16 | 4
|             | |D| |L| 19:16 | |   | |1|C|R|A|            |
------------------------------------------------------------
|                             |                            |
|        BASE 15:0            |       LIMIT 15:0           | 0
|                             |                            |
------------------------------------------------------------

粗粗一看,上面的结构非常吓人,不过实际上这个结构是非常容易理解的

比如在上图中的 LIMIT 15:0 表示这个数据结构的0到15位保存的是内存段的大小的0到15位

相似的 LIMITE 19:16 表示上述数据结构的16到19位保存的是内存段大小的16到19位

从这个分析中,可以看出每个内存段的大小是通过20位进行描述的

下面将对这个数据结构进行仔细分析:

  1. Limit [ 20位 ] 被保存在上述内存结构的 0-1548-51 位。根据上述内存结构中 G位 的设置,这20位内存定义的 内存长度 是不一样的

    下面是一些具体的例子:
    
    如果G = 0, 并且Limit = 0, 那么表示段长度是1 byte
    
    如果G = 1, 并且Limit = 0, 那么表示段长度是4K bytes
    
    如果G = 0,并且Limit = 0xfffff,那么表示段长度是1M bytes
    
    如果G = 1,并且Limit = 0xfffff,那么表示段长度是4G bytes
    
    • 如果 G = 0 , 那么 内存段的长度 是按照 1 byte 进行 增长 的 ,最大的内存段长度将是 1M bytes

      Limit每增加1,段长度增加1 bytes
      
    • 如果 G = 1 , 那么 内存段的长度 是按照 4K bytes 进行 增长 的,最大的内存段长度将是 4G bytes

      Limit每增加1,段长度增加4K bytes 
      
    • 段长度的计算公式:

      base_seg_length * ( LIMIT + 1)
      
  2. Base [ 32-bits ] 被保存在上述地址结构的 16-3132-39 以及 56-63 位。Base定义了 段基址
  3. Type/Attribute ( 40-47 bits ) 定义了内存段的 类型 以及支持的 操作 。上述内存结构的 第43位 决定了内存段是数据段还是代码段:

    |           Type Field        | Descriptor Type | Description
    |-----------------------------|-----------------|------------------
    | Decimal                     |                 |
    |             0    E    W   A |                 |
    | 0           0    0    0   0 | Data            | Read-Only
    | 1           0    0    0   1 | Data            | Read-Only, accessed
    | 2           0    0    1   0 | Data            | Read/Write
    | 3           0    0    1   1 | Data            | Read/Write, accessed
    | 4           0    1    0   0 | Data            | Read-Only, expand-down
    | 5           0    1    0   1 | Data            | Read-Only, expand-down, accessed
    | 6           0    1    1   0 | Data            | Read/Write, expand-down
    | 7           0    1    1   1 | Data            | Read/Write, expand-down, accessed
    |                  C    R   A |                 |
    | 8           1    0    0   0 | Code            | Execute-Only
    | 9           1    0    0   1 | Code            | Execute-Only, accessed
    | 10          1    0    1   0 | Code            | Execute/Read
    | 11          1    0    1   1 | Code            | Execute/Read, accessed
    | 12          1    1    0   0 | Code            | Execute-Only, conforming
    | 14          1    1    0   1 | Code            | Execute-Only, conforming, accessed
    | 13          1    1    1   0 | Code            | Execute/Read, conforming
    | 15          1    1    1   1 | Code            | Execute/Read, conforming, accessed
    
    • 如果 S = 0 ,说明是一个 数据段 ,第 42,41,40位表示的是(E扩展,W可写,A可访问)
      • 如果 E = 0 ,数据段是 向上扩展 数据段 ,反之为 向下扩展数据段

        关于向上扩展和向下扩展数据段,可以参考链接 http://www.sudleyplace.com/dpmione/expanddown.html
        
        在一般情况下,应该是不会使用向下扩展数据段的
        
      • 如果 W = 1 ,说明这个数据段是 可写的 ,否则不可写

        所有数据段都是可读的
        
      • A位 表示该内存段是否 已经被CPU访问
    • 如果 S = 1 就是一个 代码段 , 第42,41,40位表示的是(C一致,R可读,A可访问)
      • 如果 C = 1 ,说明这个代码段 可以被低优先级的代码访问 ,反之如果C = 0,说明只能同优先级的代码段可以访问

        比如 C=1 可以被用户态代码访问
        
      • 如果 R = 1 ,说明该代码段 可读 。代码段是 永远没有写权限
  4. DPL [ 2-bits ] , bit 4546 定义了该段的 优先级 。具体数值是 0-3
  5. P 标志 bit 47 说明该内存段是否已经 存在内存中

    如果P = 0,那么在访问这个内存段的时候将报错
    
  6. AVL 标志 bit 52 这个位在Linux内核中没有被使用
  7. L 标志 bit 53 :只对代码段有意义,如果 L = 1 ,说明该代码段需要 运行64位模式
  8. D/B 标志 bit 54 :根据段描述符描述的是一个可执行代码段、下扩数据段还是一个堆栈段,这个标志具有不同的功能

    • 可执行代码段:此时这个标志称为D标志并用于指出该段中的 指令引用有效地址操作数的默认长度

      • 如果该标志置位,则默认值是 32位地址32位或8位的操作数
      • 如果该标志为0,则默认值是 16位地址16位或8位的操作数
      指令前缀0x66可以用来选择非默认值的操作数大小
      
      前缀0x67可用来选择非默认值的地址大小
      
    • 栈段(由SS寄存器指向的数据段):此时该标志称为 B(Big)标志 ,用于指明 隐含堆栈操作(如PUSH、POP或CALL) 时的 栈指针大小

      • 如果该标志置位,则使用32位栈指针并存放在ESP寄存器中
      • 如果该标志为0,则使用16位栈指针并存放在SP寄存器中
      如果堆栈段被设置成一个下扩数据段,这个B标志也同时指定了堆栈段的上界限
      
    • 下扩数据段:此时该标志称为 B标志 ,用于指明 堆栈段的上界限
      • 如果设置了该标志,则堆栈段的上界限是 0xFFFFFFFF 4GB
      • 如果没有设置该标志,则堆栈段的上界限是 0xFFFF 64KB
    对于32位代码和数据段,这个标志应该总是设置为1
    
    对于16位代码和数据段,这个标志被设置为0
    

段选择子

在保护模式下,段寄存器保存的不再是一个内存段的基地址,而是一个称为 段选择子 的结构。每个段描述符都对应一个段选择子。段选择子是一个 16位 的数据结构,下图显示了这个数据结构的内容:

-----------------------------
|       Index    | TI | RPL |
-----------------------------
  • Index : 在GDT中,对应 段描述符的索引号
  • TI : 在 GDT 还是 LDT 中查找对应的段描述符
  • RPL : 请求者优先级

    这个优先级将和段描述符中的优先级协同工作,共同确定访问是否合法
    

在保护模式下,每个段寄存器实际上包含下面2部分内容:

  • 可见部分 : 段选择子
  • 隐藏部分 : 段描述符

保护模式寻址过程

在保护模式中,cpu是通过下面的步骤来找到一个具体的物理地址的:

  1. 代码必须将 相应的段选择子 装入 某个段寄存器
  2. CPU根据 段选择子 从GDT中找到一个 匹配的段描述符 ,然后将段描述符放入段寄存器的隐藏部分
  3. 在没有使用向下扩展段的时候,那么在没有开启分页机制的情况下,这个内存的物理地址就是 基地址 + 偏移
    • 内存段的基地址就是段描述符中的基地址
    • 段描述符的limit + 1就是内存段的长度

linear_address.png

进入保护模式

当代码要从实模式进入保护模式的时候,需要执行下面的操作:

  • 禁止中断发生
  • 使用命令 lgdt 将GDT表装入 GDTR 寄存器
  • 设置CR0寄存器的PE位为1,使CPU进入保护模式
  • 跳转开始执行保护模式代码
在后面的章节中,将看到Linux 内核中完整的转换代码。不过在系统进入保护模式之前,内核有很多的准备工作需要进行

先打开C文件 arch/x86/boot/main.c 这个文件包含了很多的函数,这些函数分别会执行 键盘初始化 , 内存堆初始化 等等操作...

将启动参数拷贝到 zeropage

main函数首先调用了 copy_boot_params(void) 。这个函数将 内核设置信息 拷贝boot_params结构 的相应字段。可以在 arch/x86/include/uapi/asm/bootparam.h 找到 boot_params 结构的定义

  • boot_params结构中包含 struct setup_header hdr 字段。这个结构包含了 linux boot protocol 中定义的相同字段

在内核编译的时候copy_boot_params完成两个工作:

  1. Header.S 中定义的 hdr 结构中的内容拷贝到 boot_params 结构的字段 struct setup_header hdr
  2. 如果内核是通过老的命令行协议运行起来的,那么就更新内核的命令行指针

这里需要注意的是拷贝 hdr 数据结构的 memcpy 函数不是C语言中的函数,而是定义在 copy.S:

GLOBAL(memcpy)
pushw   %si          ;push si to stack
pushw   %di          ;push di to stack
movw    %ax, %di     ;move &boot_param.hdr to di
movw    %dx, %si     ;move &hdr to si
pushw   %cx          ;push cx to stack ( sizeof(hdr) )
shrw    $2, %cx    
rep; movsl           ;copy based on 4 bytes
popw    %cx          ;pop cx
andw    $3, %cx      ;cx = cx % 4
rep; movsb           ;copy based on one byte
popw    %di
popw    %si
retl
ENDPROC(memcpy)

copy.S 文件中, 可以看到所有的方法都开始于 GLOBAL 宏 定义,而结束于 ENDPROC 宏 定义。可以在 arch/x86/include/asm/linkage.h 中找到 GLOBAL 宏 定义

        #define GLOBAL(name)    \
        .globl name;    \
name:
这个宏给代码段分配了一个名字标签,并且让这个名字全局可用 

可以在 include/linux/linkage.h 中找到 ENDPROC 宏 的定义

#define ENDPROC(name) \
.type name, @function ASM_NL \
END(name)
这个宏通过 END(name) 代码标识了汇编函数的结束,同时将函数名输出,从而静态分析工具可以找到这个函数

memcpy 代码首先将 sidi 寄存器的值 压入堆栈 进行保存,这么做的原因是因为后续的代码将修改 si 和 di 寄存器的值

memcpy 函数(也包括其他定义在copy.s中的其他函数)使用了 fastcall 调用规则

意味着所有的函数调用参数是通过 ax, dx, cx寄存器传入的,而不是传统的通过堆栈传入

因此在使用下面的代码调用 memcpy 函数的时候:

memcpy(&boot_params.hdr, &hdr, sizeof hdr);

函数的参数是这样传递的

  • ax 寄存器:指向 boot_param.hdr 的内存地址
  • dx 寄存器:指向 hdr 的内存地址
  • cx 寄存器:包含 hdr 结构的大小

memcpy 函数在将 si 和 di 寄存器压栈之后:

  1. 将 boot_param.hdr 的地址放入 di 寄存器
  2. 将 hdr 的地址放入 si 寄存器
  3. 将 hdr 数据结构的大小压栈
  4. 接下来代码首先以4个字节为单位,将 si 寄存器指向的内存内容拷贝到 di 寄存器指向的内存
    1. 当剩下的字节数不足4字节的时候,代码将原始的 hdr 数据结构大小出栈放入 cx
    2. 然后对 cx 的值对4求模,接下来就是根据 cx 的值,以字节为单位将 si 寄存器指向的内存内容拷贝到 di 寄存器指向的内存
  5. 当拷贝操作完成之后,将保留的 si 以及 di 寄存器值出栈,函数返回

控制台初始化

在 hdr 结构体被拷贝到 boot_params.hdr 成员之后,系统接下来将进行控制台的初始化

控制台初始化时通过调用 arch/x86/boot/early_serial_console.c 中定义的 console_init 函数实现的。这个函数首先查看命令行参数是否包含 earlyprintk 选项。如果命令行参数包含该选项,那么函数将分析这个选项的内容。得到控制台将使用的 串口信息 ,然后进行串口的初始化。以下是 earlyprintk 选项可能的取值:

  • serial,0x3f8,115200
  • serial,ttyS0,115200
  • ttyS0,115200

当串口初始化成功之后,如果命令行参数包含 debug 选项,将看到如下的输出:

if (cmdline_find_option_bool("debug"))
  puts("early console in setup code\n");

puts 函数定义在 tty.c 。这个函数只是简单的调用 putchar 函数将输入字符串中的内容按字节输出。下面来看看 putchar 函数的实现:

void __attribute__((section(".inittext"))) putchar(int ch)
{
  if (ch == '\n')
    putchar('\r');

  bios_putchar(ch);

  if (early_serial_base != 0)
    serial_putchar(ch);
}

_attribute__((section(".inittext"))) 说明这段代码将被放入 _.inittext 代码段 。关于 .inittext 代码段的定义 可以在 setup.ld 中找到

如果需要输出的字符是 \n ,那么 putchar 函数将调用自己首先输出一个字符 \r

接下来,就调用 bios_putchar 函数将字符输出到显示器(使用bios int10 中断 ):

static void __attribute__((section(".inittext"))) bios_putchar(int ch)
{
  struct biosregs ireg;

  initregs(&ireg);
  ireg.bx = 0x0007;
  ireg.cx = 0x0001;
  ireg.ah = 0x0e;
  ireg.al = ch;
  intcall(0x10, &ireg, NULL);
}

initreg 函数 接受一个 biosregs 结构的地址 作为输入参数,该函数首先调用 memset 函数biosregs 结构体所有成员 清0

memset(reg, 0, sizeof *reg);
reg->eflags |= X86_EFLAGS_CF;
reg->ds = ds();
reg->es = ds();
reg->fs = fs();
reg->gs = gs();

下面让来看看 memset 函数的实现:

GLOBAL(memset)
pushw   %di
movw    %ax, %di
movzbl  %dl, %eax
imull   $0x01010101,%eax
pushw   %cx
shrw    $2, %cx
rep; stosl
popw    %cx
andw    $3, %cx
rep; stosb
popw    %di
retl
ENDPROC(memset)
memset 函数和 memcpy 函数一样使用了 fastcall 调用规则,因此函数的参数是通过 ax,dx 以及 cx 寄存器传入函数内部的

就像memcpy函数一样,memset 函数:

  1. 一开始将 di 寄存器入栈
  2. 然后将 biosregs 结构的地址从 ax 寄存器拷贝到di寄存器
  3. 接下来,使用 movzbl 指令将 dl 寄存器的内容拷贝到 ax 寄存器的低字节

    这样 ax 寄存器就包含了需要拷贝到 di 寄存器所指向的内存的值
    
  4. 接下来的 imull 指令将 eax 寄存器 的值 乘上 0x01010101

    这么做的原因是代码每次将尝试拷贝4个字节内存的内容。下面让来看一个具体的例子
    
    假设需要将 0x7 这个数值放到内存中,在执行 imull 指令之前,eax 寄存器的值是 0x7
    
    在 imull 指令被执行之后,eax 寄存器的内容变成了 0x07070707(4个字节的 0x7)
    
  5. 在 imull 指令之后,代码使用 rep; stosl 指令将 eax 寄存器的内容拷贝到 es:di 指向的内存

在 bisoregs 结构体被 initregs 函数正确填充之后,bios_putchar 调用中断 0x10 在显示器上输出一个字符

接下来 putchar 函数检查是否初始化了串口,如果串口被初始化了,那么将调用serial_putchar将字符输出到串口

堆初始化

当堆栈和bss段在header.S中被初始化之后 (细节请参考上一节)

内核需要初始化全局堆

全局堆的初始化是通过 init_heap 函数实现的。代码首先检查内核设置头中的 loadflags 是否设置了 CAN_USE_HEAP标志。 如果该标记被设置了,那么代码将计算堆栈的结束地址:

char *stack_end;

//%P1 is (-STACK_SIZE)
if (boot_params.hdr.loadflags & CAN_USE_HEAP) {
  asm("leal %P1(%%esp),%0"
      : "=r" (stack_end) : "i" (-STACK_SIZE));
换言之 stack_end = esp - STACK_SIZE 

在计算了堆栈结束地址之后,代码计算了堆的结束地址:

//heap_end = heap_end_ptr + 512
heap_end = (char *)((size_t)boot_params.hdr.heap_end_ptr + 0x200);

接下来代码判断 heap_end 是否大于 stack_end:

  • 如果条件成立,将 stack_end 设置成 heap_end

    这么做是因为在大部分系统中全局堆和堆栈是相邻的,但是增长方向是相反的
    
到这里为止,全局堆就被正确初始化了,现在就可以使用 GET_HEAP 方法

至于这个函数的实现和使用,将在后续的章节中看到

检查CPU类型

在堆栈初始化之后,内核代码通过调用 arch/x86/boot/cpu.c 提供的 validate_cpu 方法检查CPU级别以确定系统是否能够在当前的CPU上运行。validate_cpu 调用了 check_cpu 方法得到当前系统的CPU级别,并且和系统预设的最低CPU级别进行比较

/*from cpu.c*/
check_cpu(&cpu_level, &req_level, &err_flags);
/*after check_cpu call, req_level = req_level defined in cpucheck.c*/
if (cpu_level < req_level) {
  printf("This kernel requires an %s CPU, ", cpu_name(req_level)); 
  printf("but only detected an %s CPU.\n", cpu_name(cpu_level));
  return -1;
 }
如果不满足条件,则不允许系统运行

除此之外,check_cpu 方法还做了大量的其他检测和设置工作,下面就简单介绍一些:

  1. 检查cpu标志,如果cpu是64位cpu,那么就设置 long mode
  2. 检查CPU的制造商,根据制造商的不同,设置不同的CPU选项

    比如对于AMD出厂的cpu,如果不支持 SSE+SSE2,那么就禁止这些选项
    

内存分布侦测

接下来,内核调用 detect_memory 方法进行内存侦测,以得到系统当前内存的使用分布。该方法使用多种编程接口,包括 0xe820 (获取全部内存分配), 0xe8010x88 (获取临近内存大小),进行内存分布侦测。在这里只介绍 arch/x86/boot/memory.c 中提供的 detect_memory_e820 方法。该方法首先调用 initregs 方法初始化 biosregs 数据结构,然后向该数据结构填入 0xe820 编程接口 所要求的参数:

initregs(&ireg);
ireg.ax  = 0xe820;
ireg.cx  = sizeof buf;
ireg.edx = SMAP;
ireg.di  = (size_t)&buf;
  • ax: 固定为 0xe820
  • cx: 包含 数据缓冲区的大小 ,该缓冲区将包含 系统内存的信息数据
  • edx: 必须是 SMAP 这个魔术数字,就是 0x534d4150
  • es:di : 包含 数据缓冲区的地址
  • ebx: 必须为 0

接下来就是通过一个循环来收集内存信息了。每个循环都开始于一个 0x15 中断 调用,这个中断调用返回 地址分配表中的一项 ,接着程序将返回的 ebx 设置biosregs 数据结构 中,然后进行下一次的 0x15 中断调用

那么循环什么时候结束呢?

直到 0x15 调用返回的 eflags 包含标志 X86_EFLAGS_CF :

intcall(0x15, &ireg, &oreg);
ireg.ebx = oreg.ebx;

在循环结束之后, 整个内存分配 信息将被写入到 e820entry 数组 中,这个数组的每个元素包含下面3个信息:

  • 内存段的起始地址
  • 内存段的大小
  • 内存段的类型

    类型可以是reserved, usable等等
    

可以在 dmesg 输出中看到这个数组的内容:

[    0.000000] e820: BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000003ffdffff] usable
[    0.000000] BIOS-e820: [mem 0x000000003ffe0000-0x000000003fffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved

键盘初始化

接下来内核调用 keyboard_init() 方法进行键盘初始化操作。 首先,方法调用 initregs 初始化寄存器结构,然后调用 0x16 中断来 获取 键盘状态

initregs(&ireg);
ireg.ah = 0x02;     /* Get keyboard status */
intcall(0x16, &ireg, &oreg);
boot_params.kbd_status = oreg.al;

在获取了键盘状态之后,代码再次调用0x16中断来 设置 键盘的 按键检测频率

ireg.ax = 0x0305;   /* Set keyboard repeat rate */
intcall(0x16, &ireg, NULL);

系统参数查询

接下来内核将进行一系列的参数查询

在这里将不深入介绍所有这些查询,将在后续章节中再进行详细介绍

mca

query_mca 方法调用 0x15 中断来获取 机器的型号信息BIOS版本 以及 其他一些硬件相关的属性

int query_mca(void)
{
  struct biosregs ireg, oreg;
  u16 len;

  initregs(&ireg);
  ireg.ah = 0xc0;
  intcall(0x15, &ireg, &oreg);

  if (oreg.eflags & X86_EFLAGS_CF)
    return -1;  /* No MCA present */

  set_fs(oreg.es);
  len = rdfs16(oreg.bx);

  if (len > sizeof(boot_params.sys_desc_table))
    len = sizeof(boot_params.sys_desc_table);

  copy_from_fs(&boot_params.sys_desc_table, oreg.bx, len);
  return 0;
}

这个方法:

  1. 设置 ah 寄存器 的值为 0xc0
  2. 然后调用 0x15 BIOS中断
  3. 中断返回之后代码检查 carry flag
    • 如果它被 置位 ,说明BIOS不支持MCA
    • 如果 CF 被设置成 0 ,那么 ES:BX 指向系统信息表 。这个表的内容如下所示:

      Offset  Size    Description
       00h    WORD    number of bytes following
       02h    BYTE    model (see #00515)
       03h    BYTE    submodel (see #00515)
       04h    BYTE    BIOS revision: 0 for first release, 1 for 2nd, etc.
       05h    BYTE    feature byte 1 (see #00510)
       06h    BYTE    feature byte 2 (see #00511)
       07h    BYTE    feature byte 3 (see #00512)
       08h    BYTE    feature byte 4 (see #00513)
       09h    BYTE    feature byte 5 (see #00514)
      ---AWARD BIOS---
       0Ah  N BYTEs   AWARD copyright notice
      ---Phoenix BIOS---
       0Ah    BYTE    ??? (00h)
       0Bh    BYTE    major version
       0Ch    BYTE    minor version (BCD)
       0Dh  4 BYTEs   ASCIZ string "PTL" (Phoenix Technologies Ltd)
      ---Quadram Quad386---
       0Ah 17 BYTEs   ASCII signature string "Quadram Quad386XT"
      ---Toshiba (Satellite Pro 435CDS at least)---
       0Ah  7 BYTEs   signature "TOSHIBA"
       11h    BYTE    ??? (8h)
       12h    BYTE    ??? (E7h) product ID??? (guess)
       13h  3 BYTEs   "JPN"
      
  4. 接下来代码调用 set_fs 方法,将 es 寄存器 的值写入 fs 寄存器 :

    static inline void set_fs(u16 seg)
    {
      asm volatile("movw %0,%%fs" : : "rm" (seg));
    }
    
    • boot.h 存在很多类似于 set_fs 的方法

      比如 set_gs 
      
  5. 在 query_mca 的最后,代码将 es:bx 指向的内存地址的内容 拷贝boot_params.sys_desc_table

Intel SpeedStep

接下来,内核调用 query_ist 方法获取 Intel SpeedStep 信息。这个方法首先检查CPU类型,然后调用 0x15 中断 获得这个信息并放入 boot_params

APM

内核会调用 query_apm_bios 方法从BIOS获得 高级电源管理 信息。query_apm_bios 也是调用_ 0x15 中断_ ,只不过将 ax 设置成 0x5300 以得到APM设置信息。中断调用返回之后,代码将检查 bx 和 cx 的值:

  • 如果 bx 不是 0x504d ( PM 标记 ),或者 cx 不是 0x02 (0x02,表示支持32位模式),那么代码直接返回错误
  • 否则,将进行下面的步骤
    1. 使用 ax = 0x5304 来调用 0x15 中断,以 断开 APM 接口
    2. 使用 ax = 0x5303 调用 0x15 中断,使用 32位接口 重新连接 APM
    3. 使用 ax = 0x5300 调用 0x15 中断 再次获取 APM设置
    4. 将信息写入 boot_params.apm_bios_info

需要注意的是,只有在 CONFIG_APM 或者 CONFIG_APM_MODULE 被设置的情况下,query_apm_bios 方法才会被调用:

#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif

Disk Drive

最后是 query_edd 方法调用, 这个方法从BIOS中查询 Enhanced Disk Drive 信息。代码检查 内核命令行参数 是否 设置edd 选项

  • 如果 edd选项设置成 off,query_edd 不做任何操作,直接返回
  • 如果EDD被激活了,query_edd 遍历 所有 BIOS支持的硬盘 ,并 获取 相应 硬盘的EDD信息

    • 0x80 是 第一块硬盘
    • EDD_MBR_SIG_MAX 是一个宏,值为16
    for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) {
      if (!get_edd_info(devno, &ei) && boot_params.eddbuf_entries < EDDMAXNR) {
        memcpy(edp, &ei, sizeof ei);
        edp++;
        boot_params.eddbuf_entries++;
      }
      //     ...
      //  ...
      //  ...
    

代码把获得的信息放入数组 edd_info 中:

  • get_edd_info 方法通过调用 0x13 中断 调用(设置 ah = 0x41 ) 来 检查 EDD是否被硬盘支持
    • 如果EDD被支持,代码将再次调用 0x13 中断,在这次调用中 ah = 0x48 ,并且 si 指向一个 数据缓冲区地址
    • 中断调用之后,EDD信息将被保存到 si 指向的缓冲区地址
Next: 进入保护模式 从引导程序到内核 Home:启动引导