Linux流程分析——从开机那一刻开始(2)

 

在前一部分,我们进入了内核,看到了内核创建代码的初始化部分.并停在main函数第一次调用前

这一部分,我们将会继续探寻内核的创建代码,同时复习

  • 什么是保护模式
  • 如何转换到保护模式
  • 对堆空间和终端的初始化
  • 内存管理 cpu验证 键盘初始化
  • 和其他很多

 

保护模式

在进入intel长模式之前,内核必须将cpu转入保护模式

保护模式在1982年第一次被加入x86体系,是intel长模式出现前的主要intel处理器模式

离开实模式的主要原因是它对地址的的限制,实模式下仅有1mb可用,有时甚至只有640kb

保护模式改变了很多,最主要的改变在于内存管理上.20位地址总线被改为32位.允许4gb的内存寻址.同样页模式也被引入.在下一节会提到

保护模式下内存管理被分为两个基本独立的部分 ,

这里只讲述关于段的内容,页会在下一部分讨论

实模式下地址由两部分组成,

  • 段的基地址
  • 基于段的偏移

在保护模式下段管理机制被完全重写,没有了64kb固定大小的片段,段的大小和地址通过一个新建立的数据结构(Segment Descriptor)段描述符来描述,段描述符被储存在另外一个数据结构全局描述符表(GDT Global Descriptor Table)中

GDT是驻留在内存中的结构, 它没有固定的内存区域,因此他的地址被保存在一个特殊的寄存器(gdtr)中接下来我们会看到GDT如何在内核中被加载.有一个操作将gdt从内存中加载出.

lgdt gdt 将源操作数中的值加载到全局描述符表格寄存器 (GDTR)

GDTR是一个48位寄存器,且有两部分组成

  • 16位全局描述符表大小
  • 32位的地址GDT包含了段描述符,每一个描述符有64位大小,一个通常的描述符结构如下
 63         56         51   48    45           39        32 
------------------------------------------------------------
|             | |B| |A|       | |   | |0|E|W|A|            |
| BASE 31:24  |G|/|L|V| LIMIT |P|DPL|S|  TYPE | BASE 23:16 |
|             | |D| |L| 19:16 | |   | |1|C|R|A|            |
------------------------------------------------------------

 31                         16 15                         0 
------------------------------------------------------------
|                             |                            |
|        BASE 15:0            |       LIMIT 15:0           |
|                             |                            |
------------------------------------------------------------

段大小限制位于描述符首部,占0-15bit 剩下的16-19位于48:51处 ,这些定义了段的长度 这取决于gG(第55位)标志位的内容

  • 如果 G位为0 且段限制为0 段大小为1byte
  • g为1 段限制为 0 ,段大小为4096byte
  • g为0 段限制为 0xfffff 段大小 1mb
  • g为1 段限制为0xfffff 段大小 4g​ 如果g是零,限制被解释为1byte 段最大值为1mb​ g是1 limit被解释为4kb 段最大值为4g 事实上 g位为1时候,limit值被左移12位 有20bit变为32bit 即4g

base[32位],被分为16 – 31bit 32 – 39 bit 56 -63 bit 定义了段的物理开始地址

type /attribute [5bit] 在40-44位定义段的类型以及如何访问

s位在44bit位 标识符类型 0 代表系统段 1代表代码段或者数据段 (栈区和数据段必须可读可写)

为了验证该段是数据还是代码段,我们检验ex位(43bit处) 0 代表数据 否则 代码段

一个段可能是一下几种类型之一

--------------------------------------------------------------------------------------
|           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 |
--------------------------------------------------------------------------------------

数据段 (bit 43)为0 代码段(bit 43)为1 剩下的3位(40,41,42)是 EWA(Expansion Writable Accessible) 可写or CRA(Conforming Readable Accessible)只读.

段寄存器包括在实模式下的段选择器 , 在保护模式下 选择器以一种不同的方式 每一个段描述符与一个16bit段选择器结构体相连

 15             3 2  1     0
-----------------------------
|      Index     | TI | RPL |
-----------------------------

index储存描述符在GDT表里的下标

TI 表明在哪找到描述符 如果TI为0 描述符在全局描述符表里查询否则在本地描述符表里查

RPL 包含 请求者的特权级别

每一个段寄存器有可视和隐藏的部分

可视部分 段选择器存储在这里

隐藏 段描述符在这里 (包括基地址 limit attribute 和一些标志位)

为在保护模式下获取物理地址 下面的步骤

  • 段描述符必须被加载进段寄存器里
  • cpu尝试在GDT偏移里寻找一个段描述符 加载到段寄存器的隐藏部分
  • 如果页不可用 段的线性地址或者说物理地址通过公式 基地址(由前一个包括的描述符得到) + 偏移 得到

示意图

po1

实模式向保护模式的转换算法

  • 禁用中断
  • 通过lgdt指令加载GDT
  • 将CR0(control register 0)设定PE位
  • 跳转到保护模式

接下来,内核完全转向保护模式的转变 在这之前 我们做一些准备

arch/x86/boot/main.c里 ,我们看见一些对键盘初始化,堆初始化等例程.

 

拷贝装载部分进入零号页

在main的主函数调用开始 . 第一个在main函数中被调用的函数 copy_boot_params(void) . 将内核启动头复制到boot_params 结构体中 (definded in arch/x86/include/uapi/asm/bootparam.h)

该结构体包括有’ struct setup_header hdr ‘ 结构体 被bootloader填充 在内核编译创建时间 copy_boot_params做两件事

1.从header.s 拷贝`hdr 到boot_parama` 结构

2 . 更新指向内核命令行的指针,如果内核通过在协议里的命令加载

注意拷贝 hdr通过memcpy函数进行拷贝 定义在copy.s里

GLOBAL(memcpy)
    pushw   %si
    pushw   %di
    movw    %ax, %di
    movw    %dx, %si
    pushw   %cx
    shrw    $2, %cx
    rep; movsl
    popw    %cx
    andw    $3, %cx
    rep; movsb
    popw    %di
    popw    %si
    retl
ENDPROC(memcpy)

copy.s中,我们看见memcpy和其他例程在这里定义,通常以GLOBALENDPROC标志开始和结束.GLOBALarch/x86/include/asm/linkage.h 里被定义,同时定义GLOBAL指令集和相应的标签,ENDPROCinclude/linux/linkage.h 定义,且将name符号作为一个函数名标记,以name的size结束.

memcpy的实现很简单,首先,将sidi的值压入栈中来保存其对应的值.在REALMOD_CFLAGS中,内核创建通过 gcc的-mregparm=3 选项来创建系统,因此函数从ax``dx``cx三个寄存器里获取参数,调用memcpy参数如下

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

因此,根据调用约定

  • ax包含 boot_params的地址
  • adx包含hdr的地址
  • cx包含hdr的大小byte

memcpyboot_params的地址放入di,保存cx在栈上,然后将si地址的值右移2次,拷贝4byte到di里,重新储存hdr的size ,然后按4byte对齐,将剩下的按位拷贝,

然后完成整体的拷贝过程

 

控制台初始化

hdr被拷贝到boot_params.hdr后,下一步是通过coonsole_init函数初始化控制台.这一过程被定义在arch/x86/boot/early_serial_console.c.中

在指令里寻找early_printk选项.如果成功找到,解析串行端口地址并初始化.earlyprintk命令选项可以是下面的几个之一

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

在初始化后,我们看见第一个输出

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

puts定义在 tty.c.puts通过循环使用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段,在

setup.ld.里可以看到

putchar检查输入是否为\n 是 则嵌套使用putchar('\r')在将字符打印到VGA屏幕上.通过调用BIOS的0x10中断

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);
}

这里initregsbiosregs结构体作为参数,并通过memset函数将其设置为0

然后通过寄存器的值填充biosregs

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)

biosregs结构体被memset重置后,bios_putchar通过使用0x10号中断来打印字符.然后检查串行端口是否被初始化,通过serial_putchar写一个字符

 

堆初始化

在栈和bss段都已经被准备好后,内核需要通过init_heap初始化堆空间

首先,init_heap检查loadflag结构体里的CAN_USE_HEAP标志位,

如果被设置则计算栈的尾部

    char *stack_end;

    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 = (char *)((size_t)boot_params.hdr.heap_end_ptr + 0x200);

指向heap_end_ptr+512的地址,最后检查stack_end是否小于heap_end如果是矫正stack_endheap_end使其相等

 

CPU 验证

通过validate_cpu 函数来进行cpu验证

调用 check_cpu 函数,传递cpu等级 和需要的等级为参数,检查内核运行在正确的cpu等级上

check_cpu(&cpu_level, &req_level, &err_flags);
if (cpu_level < req_level) {
    ...
    return -1;
}

调用set_bios_mode在设置代码发现cpu合适的时候,,该函数仅使用在x86_64模式下

static void set_bios_mode(void)
{
#ifdef CONFIG_X86_64
    struct biosregs ireg;

    initregs(&ireg);
    ireg.ax = 0xec00;
    ireg.bx = 2;
    intcall(0x15, &ireg, NULL);
#endif
}

执行0x15号BIOS中断,来告诉cpu使用长模式

 

内存检测

通过detect_memory函数进行内存检测,

detect_memory提供了可用的RAM和CPU.它使用不同的编程接口 如 0xe801 0x88 0xe820 .这里只讨论0xe820

首先detect_memory_e820函数初始化biosreg结构体,填充寄存器为特殊的值

    initregs(&ireg);
    ireg.ax  = 0xe820;
    ireg.cx  = sizeof buf;
    ireg.edx = SMAP;
    ireg.di  = (size_t)&buf;
  • ax 该函数的序号
  • cx 包括这些数据的缓冲区大小
  • edx SMAP魔数
  • es : di 数据缓冲区的地址
  • ebx 0

下一个循环里内存被收集起来,以0x15的BIOS中断引起,写一行地址分配表.为得到下一行,我们再次进行中断(在循环中进行).在这之前,将ebx更新

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

最终,该函数从地址分配表收集数据,将其写到 e820entry数组里

  • 内存段的开始
  • 内存段的大小
  • 内存段的类型

可以在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 function,使用0x16号中断来矫正键盘

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

再调用一次设置重复率和延时

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

 

对应链接

本文为对英文文章的翻译,加上自己的部分理解,如有不恰当地方,恳求指正,
(完)