首页
社区
课程
招聘
[原创]Linux内核基础之boot
2020-10-3 11:18 5932

[原创]Linux内核基础之boot

2020-10-3 11:18
5932

 

title: Linux内核基础学习笔记
date: 2020-07-16 14:57:02
tags:
categories: kernel

cover: https://s1.ax1x.com/2020/07/16/UDSEZ9.jpg

Booting阶段

启动

1.按下电源开关,主板发送信号到电源,电源准备合适的电量,向主板发送备妥信号,启动CPU。

 

2.CPU启动,寄存器复位。

1
2
3
4
5
```
IP          0xfff0
CS selector 0xf000
CS base     0xffff0000
```

3.CPU此时工作在实模式,采用分段管理内存,无分页, Cache、TLB(Translation Lookup Table)、BLB(Branch Target Buffer)这三个部件的内容被清空(Invalidate)。

 

4.根据复位后的cs:ip此时逻辑地址为:0xffff0000:0xfff0即4GB-16字节。指向第一条代码:reset vector。此时这个地址是被映射到ROM中的。

 

5.reset vector包括一个FAR jump跳到BIOS的入口。

 

6.BIOS做初始化与检查。然后寻找引导程序。对于硬盘,找MBR

 

7.BIOS将控制权交给引导程序。一般采用GRUB2引导

 

8.引导完成,调用grub_main初始化,置于normal模式。

 

9.grub_normal_execute显示可用操作系统

 

10.操作系统选定,grub_menu_execute_entry开始执行,它将调用 GRUB 的 boot 命令,来引导被选中的操作系统。

 

11.bootloader完毕,交还控制权给kernel,kernel代码从以下开始执行

1
0x1000 + X + sizeof(KernelBootSector) + 1 //X 是 kernel bootsector 被引导入内存的位置

进入内核前的准备

1.内核设置的起点是arch/x86/boot/header.S中的_start函数开始

1
2
3
4
5
6
7
    .globl    _start
_start:
        # Explicitly enter this as bytes, or the assembler
        # tries to generate a 3-byte jump here, which causes
        # everything else to push off to the wrong offset.
        .byte    0xeb        # short (2-byte) jump
        .byte    start_of_setup-1f

2._start函数做一个短跳转到start_of_setup - 1f

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    .section ".entrytext", "ax"
start_of_setup:
# Force %es = %ds
    movw    %ds, %ax
    movw    %ax, %es
    cld
 
    movw    %ss, %dx
    cmpw    %ax, %dx    # %ds == %ss?
    movw    %sp, %dx
    je    2f        # -> assume %sp is reasonably set
 
    # Invalid %ss, make up a new stack
    movw    $_end, %dx
    testb    $CAN_USE_HEAP, loadflags
    jz    1f
    movw    heap_end_ptr, %dx
1:    addw    $STACK_SIZE, %dx
    jnc    2f
    xorw    %dx, %dx    # Prevent wraparound
 
2:    # Now %dx should point to the end of our stack space
    andw    $~3, %dx    # dword align (might as well...)
    jnz    3f
    movw    $0xfffc, %dx    # Make sure we're not zero
3:    movw    %ax, %ss
    movzwl    %dx, %esp    # Clear upper half of %esp
    sti            # Now we should have a working stack
 
# We will have entered with %cs = %ds+0x20, normalize %cs so
# it is on par with the other segments.
    pushw    %ds
    pushw    $6f
    lretw
6:
 
# Check signature at end of setup
    cmpl    $0x5a5aaa55, setup_sig
    jne    setup_bad
 
# Zero the bss
    movw    $__bss_start, %di
    movw    $_end+3, %cx
    xorl    %eax, %eax
    subw    %di, %cx
    shrw    $2, %cx
    rep; stosl
 
# Jump to C code (should not return)
    calll    main
  • 首先将设置段寄存器(通过%dx)
  • 然后设置堆栈
  • 检查签名
  • 设置bss
  • 跳到main函数执行(c语言)arch/x86/boot/main.c

进入内核

进入保护模式之前的准备

1.main.c首先调用copy_boot_params();拷贝启动参数到boot_params.hdr通过调用:memcpy(&boot_params.hdr, &hdr, sizeof(hdr));

1
2
3
4
5
6
7
8
9
    .globl    hdr
hdr:
setup_sects:    .byte 0            /* Filled in by build.c */
root_flags:    .word ROOT_RDONLY
syssize:    .long 0            /* Filled in by build.c */
ram_size:    .word 0            /* Obsolete */
vid_mode:    .word SVGA_MODE
root_dev:    .word 0            /* Filled in by build.c */
boot_flag:    .word 0xAA55

2.接下来进行控制台初始化console_init();使用0x10中断输出一些字符。

 

3.接下来做堆初始化init_heap();计算堆的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void init_heap(void)
{
    char *stack_end;
 
    if (boot_params.hdr.loadflags & CAN_USE_HEAP) {
        asm("leal %P1(%%esp),%0"
            : "=r" (stack_end) : "i" (-STACK_SIZE));
 
        heap_end = (char *)
            ((size_t)boot_params.hdr.heap_end_ptr + 0x200);
        if (heap_end > stack_end)
            heap_end = stack_end;
    } else {
        /* Boot protocol 2.00 only, no heap available */
        puts("WARNING: Ancient bootloader, some functionality "
             "may be limited!\n");
    }
}

4.然后调用validate_cpu()检查cpu类型是否与kernel相合适,是否可以正常启动。当不满足cpu等级或一些特定的feature时不许启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int validate_cpu(void)
{
    u32 *err_flags;
    int cpu_level, req_level;
 
    check_cpu(&cpu_level, &req_level, &err_flags);
 
    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;
    }
 
    if (err_flags) {
        puts("This kernel requires the following features "
             "not present on the CPU:\n");
        show_cap_strs(err_flags);
        putchar('\n');
        return -1;
    } else if (check_knl_erratum()) {
        return -1;
    } else {
        return 0;
    }
}
  • 如果是64bits的设置level为64以及long mode
    1
    2
    if (test_bit(X86_FEATURE_LM, cpu.flags))
        cpu.level = 64;
    1
    #define X86_FEATURE_LM            ( 1*32+29) /* Long Mode (x86-64, 64-bit support) */
  • If this is an AMD and we're only missing SSE+SSE2, try to turn them on
  • If this is a VIA C3, we might have to enable CX8 explicitly

5.告诉BIOS我们的cpu模式

1
2
/* Tell the BIOS what CPU mode we intend to run in. */
    set_bios_mode();

6.内存布局探测:detect_memory();调用detect_memory_e820();循环探测,最终信息存在e820_entries;

 

7.键盘初始化:keyboard_init();用0x16中断获取键盘状态。

 

8.一系列系统参数查询:机器型号,BIOS版本,高级电源管理等

 

9.set_video();设置显示模式。

 

10.go_to_protected_mode();进入保护模式之前最后的准备(所有的x86 CPU都是在实模式下引导,来确保传统操作系统的兼容性。为了使用保护模式的特性,要由程序主动地切换到保护模式。在现今的电脑上,这种切换通常是操作系统在引导时候完成的第一件任务。当CPU在保护模式下运行时,可以使用虚拟86模式来运行为实模式设计的代码。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void go_to_protected_mode(void)
{
    /* Hook before leaving real mode, also disables interrupts */
    realmode_switch_hook();
 
    /* Enable the A20 gate */
    if (enable_a20()) {
        puts("A20 gate not responding, unable to boot...\n");
        die();
    }
 
    /* Reset coprocessor (IGNNE#) */
    reset_coprocessor();
 
    /* Mask all interrupts in the PIC */
    mask_all_interrupts();
 
    /* Actual transition to protected mode... */
    setup_idt();
    setup_gdt();
    protected_mode_jump(boot_params.hdr.code32_start,
                (u32)&boot_params + (ds() << 4));
}

首先调用realmode_switch_hook();如果发现real_mode的hook函数,那么调用他。else的话清楚中断标志IF(禁止外部中断),然后禁止NMI中断(非可屏蔽中断比如时钟中断)。最后调用io_delay等待操作完成

1
2
3
4
5
6
7
8
9
10
11
12
static void realmode_switch_hook(void)
{
    if (boot_params.hdr.realmode_swtch) {
        asm volatile("lcallw *%0"
                 : : "m" (boot_params.hdr.realmode_swtch)
                 : "eax", "ebx", "ecx", "edx");
    } else {
        asm volatile("cli");
        outb(0x80, 0x70); /* Disable NMI */
        io_delay();
    }
}

然后是enable_a20()激活A20总线,之后使用reset_coprocessor();mask_all_interrupts();复位数字协处理器、屏蔽中断控制器的所有中断、和主中断控制器上除IRQ2以外的所有中断(IRQ2是主中断控制器上的级联中断,所有从中断控制器的中断将通过这个级联中断报告给 CPU )
至此,所有进入保护模式之前的准备工作已经完成,给出整体的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void main(void)
{
    /* First, copy the boot header into the "zeropage" */
    copy_boot_params();
 
    /* Initialize the early-boot console */
    console_init();
    if (cmdline_find_option_bool("debug"))
        puts("early console in setup code\n");
 
    /* End of heap check */
    init_heap();
 
    /* Make sure we have all the proper CPU support */
    if (validate_cpu()) {
        puts("Unable to boot - please use a kernel appropriate "
             "for your CPU.\n");
        die();
    }
 
    /* Tell the BIOS what CPU mode we intend to run in. */
    set_bios_mode();
 
    /* Detect memory layout */
    detect_memory();
 
    /* Set keyboard repeat rate (why?) and query the lock flags */
    keyboard_init();
 
    /* Query Intel SpeedStep (IST) information */
    query_ist();
 
    /* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
    query_apm_bios();
#endif
 
    /* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
    query_edd();
#endif
 
    /* Set the video mode */
    set_video();
 
    /* Do the last things and invoke protected mode */
    go_to_protected_mode();
}

进入保护模式

1
2
3
4
5
/* Actual transition to protected mode... */
    setup_idt();
    setup_gdt();
    protected_mode_jump(boot_params.hdr.code32_start,
                (u32)&boot_params + (ds() << 4));

1.首先是setup_idt();用来设置中断描述符表(IDT)

1
2
3
4
5
6
7
8
/*
 * Set up the IDT
 */
static void setup_idt(void)
{
    static const struct gdt_ptr null_idt = {0, 0};
    asm volatile("lidtl %0" : : "m" (null_idt));
}

lidtl将为空的null_idt载入寄存器IDT

 

2.接下来setup_gdt();来设置全局描述符表即GDT表,其中,定义了boot_gdt[]数组,这个数组中的内容就是我们要传给GDTR的段描述符的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void setup_gdt(void)
{
    static const u64 boot_gdt[] __attribute__((aligned(16))) = {
        /* CS: code, read/execute, 4 GB, base 0 */
        [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
        /* DS: data, read/write, 4 GB, base 0 */
        [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
        /* TSS: 32-bit tss, 104 bytes, base 4096 */
        /* We only have a TSS here to keep Intel VT happy;
           we don't actually use it for anything. */
        [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
    };
    /* Xen HVM incorrectly stores a pointer to the gdt_ptr, instead
       of the gdt_ptr contents.  Thus, make it static so it will
       stay in memory, at least long enough that we switch to the
       proper kernel GDT. */
    static struct gdt_ptr gdt;
 
    gdt.len = sizeof(boot_gdt)-1;
    gdt.ptr = (u32)&boot_gdt + (ds() << 4);
 
    asm volatile("lgdtl %0" : : "m" (gdt));
}

其中GDT_ENTRY是一个宏定义,传入三个参数(标志,基地址,段长度):

1
2
3
4
5
6
#define GDT_ENTRY(flags, base, limit)            \
    ((((base)  & _AC(0xff000000,ULL)) << (56-24)) |    \
     (((flags) & _AC(0x0000f0ff,ULL)) << 40) |    \
     (((limit) & _AC(0x000f0000,ULL)) << (48-16)) |    \
     (((base)  & _AC(0x00ffffff,ULL)) << 16) |    \
     (((limit) & _AC(0x0000ffff,ULL))))

标志字段二进制展开后每一位都有自己的含义。之后获取gdt长度,以及指针,最后载入GDTR寄存器。

 

3.最后调用protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4));完成从实模式到保护模式的跳转。

1
2
3
/* pmjump.S */
void __attribute__((noreturn))
    protected_mode_jump(u32 entrypoint, u32 bootparams);

接受两个参数:保护模式进入地址,bootparams结构的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GLOBAL(protected_mode_jump)
    movl    %edx, %esi        # bootparams地址放入esi
 
    xorl    %ebx, %ebx
    movw    %cs, %bx        # cs放入bx
    shll    $4, %ebx
    addl    %ebx, 2f
    jmp    1f            # Short jump to serialize on 386/486
1:
 
    movw    $__BOOT_DS, %cx
    movw    $__BOOT_TSS, %di
 
    movl    %cr0, %edx        #
    orb    $X86_CR0_PE, %dl    # 设置 CR0 寄存器相应的位使 CPU 进入保护模式:
    movl    %edx, %cr0
 
    # Transition to 32-bit mode
    .byte    0x66, 0xea        # ljmpl opcode
2:    .long    in_pm32            # offset
    .word    __BOOT_CS        # segment
    # 执行长跳转到代码段
ENDPROC(protected_mode_jump)

跳转之后我们就在保护模式下执行以下代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    .code32
    .section ".text32","ax"
GLOBAL(in_pm32)
    # 进入32位保护模式首先重置段寄存器
    movl    %ecx, %ds
    movl    %ecx, %es
    movl    %ecx, %fs
    movl    %ecx, %gs
    movl    %ecx, %ss
    # The 32-bit code sets up its own stack, but this way we do have
    # a valid stack if some debugging hack wants to use it.
    addl    %ebx, %esp
 
    # Set up TR to make Intel VT happy
    ltr    %di
 
    # 清空通用寄存器
    # 32-bit boot protocol
    xorl    %ecx, %ecx
    xorl    %edx, %edx
    xorl    %ebx, %ebx
    xorl    %ebp, %ebp
    xorl    %edi, %edi
 
    # Set up LDTR to make Intel VT happy
    lldt    %cx
 
    jmpl    *%eax            # Jump to the 32-bit entrypoint
ENDPROC(in_pm32)

从32位保护模式向64位长模式切换

1.jmpl *%eax # Jump to the 32-bit entrypoint时eax存储的是32位的入口点。

 

2.到达32位入口点,我们可以看到此时的目录:arch/x86/boot/compressed/head_64.S其中的compressed,因为bzimage 是由 vmlinux + 头文件 + 内核启动代码 被 gzip 压缩之后获得的。之前的属于内核启动代码。而head_64.S做内核解压前的准备。

1
2
3
4
5
    __HEAD
    .code32
ENTRY(startup_32)
...
ENDPROC(startup_32)

其中HEAD代表```#define HEAD .section ".head.text","ax"```也就是说这个段是可执行的。

 

3.首先调用cld清空DF DF与串操作

 

4.通过KEEP_SEGMENTS标记来给段寄存器做正确的赋值。

 

5.计算我们代码和编译运行之间的位置偏差。通过:定义一个标签并且跳转到它,然后把栈顶抛出到一个寄存器中。

1
2
3
4
    leal    (BP_scratch+4)(%esi), %esp
    call    1f
1:    popl    %ebp
    subl    $1b, %ebp

esi指向bootparams结构体,把结构体中scratch+4的地址放进esp制造出一个4字节临时栈。然后通过call 1f。此时1f标签的地址在栈顶,再把他pop到ebp然后ebp-1b就得到了startup_32的加载地址。

 

6.先找到boot_stack_end的实际地址,然后通过call verify_cpu之后test eax中的返回值,验证cpu是否支持长模式和SSE。如果不支持,就hlt掉。

 

7.计算内核解压缩地址,此时ebp中的是startup_32的实际物理地址,当CONFIG_RELOCATABLE打开时,我们将ebp放在ebx中,然后做对齐(2M)然后与$LOAD_PHYSICAL_ADDR(对齐后内核加载位置的物理地址)比较,最后给startup_32加上偏移获得解压缩地址。结束后,ebp包含了加载时的地址,ebx包含了内核解压缩的目标地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef CONFIG_RELOCATABLE
    movl    %ebp, %ebx
    movl    BP_kernel_alignment(%esi), %eax
    decl    %eax
    addl    %eax, %ebx
    notl    %eax
    andl    %eax, %ebx
    cmpl    $LOAD_PHYSICAL_ADDR, %ebx
    jge    1f
#endif
    movl    $LOAD_PHYSICAL_ADDR, %ebx
1:
 
    /* Target address to relocate to for decompression */
    addl    $z_extract_offset, %ebx

8.更新全局描述符表。全局描述符表 保存在 48位 GDTR-全局描述符表寄存器 中,由两个部分组成:

  • GDT大小(16位)
  • GDT基地址(32位)

    1
    2
    3
    4
    /* Load new GDT with the 64bit segments using 32bit descriptor */
    leal    gdt(%ebp), %eax
    movl    %eax, gdt+2(%ebp)
    lgdt    gdt(%ebp)

    gdt偏移量整体结构如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      .data                        /*    位于data段 */
    gdt:
      .word    gdt_end - gdt        /* gdt整体长度 */
      .long    gdt                    /*    gdt基地址 */
      .word    0                   
      .quad    0x0000000000000000    /* NULL descriptor */
      .quad    0x00af9a000000ffff    /* __KERNEL_CS */
      .quad    0x00cf92000000ffff    /* __KERNEL_DS */
      .quad    0x0080890000000000    /* TS descriptor */
      .quad   0x0000000000000000    /* TS continued */
    gdt_end:

    9.打开PAE模式

    1
    2
    3
    4
    /* Enable PAE mode */
    movl    %cr4, %eax
    orl    $X86_CR4_PAE, %eax
    movl    %eax, %cr4

    10.初期页表初始化,建立初期的4G启动页表。linux内核使用4级页表,一般建立六个页表,每张表4k

    1
    2
    3
    4
    5
    /* Initialize Page tables to 0 */
    leal    pgtable(%ebx), %edi
    xorl    %eax, %eax
    movl    $((4096*6)/4), %ecx
    rep    stosl

    其中pgtable定义如下,大小24k

    1
    2
    3
    4
      .section ".pgtable","a",@nobits
      .balign 4096
    pgtable:
      .fill 6*4096, 1, 0

    开始构建4级页表

    1
    2
    3
    4
    /* Build Level 4 */
    leal    pgtable + 0(%ebx), %edi
    leal    0x1007 (%edi), %eax   
    movl    %eax, 0(%edi)

    其中0x1007是四级页表的大小4096+7(页表项标记PRESENT+RW+USER)最后我们把第一个PDP(页目录指针)项地址写入PML4

11.在PDP表(页目录指针表、三级页表)建立4个2级页表(Page Directory)他们都带有PRESENT+RW+USE 标记

1
2
3
4
5
6
7
8
9
    /* Build Level 3 */
    leal    pgtable + 0x1000(%ebx), %edi
    leal    0x1007(%edi), %eax    /* 把第一个 2 级页目录指针表的首项的地址放到 eax 寄存器 */
    movl    $4, %ecx
1:    movl    %eax, 0x00(%edi)
    addl    $0x00001000, %eax
    addl    $8, %edi           /* 计算后面的几个页目录指针项的地址,每个占 8 字节 */
    decl    %ecx
    jnz    1b

12.建立2048个2M页表项

1
2
3
4
5
6
7
8
9
    /* Build Level 2 */
    leal    pgtable + 0x2000(%ebx), %edi
    movl    $0x00000183, %eax    /* 标记位PRESENT + WRITE + MBZ */
    movl    $2048, %ecx
1:    movl    %eax, 0(%edi)
    addl    $0x00200000, %eax
    addl    $8, %edi
    decl    %ecx
    jnz    1b

最终我们拥有了一个4G表,映射了4G大小的内存。

 

13.最后将PML4的地址放入cr3

1
2
3
/* Enable the boot page tables */
leal    pgtable(%ebx), %eax
movl    %eax, %cr3


14.设置MSR中的EFER.LME(Extended Feature Enable Register)标记为 0xC0000080
15.切换向长模式

1
2
3
4
5
6
7
8
9
10
11
12
/*
 * Setup for the jump to 64bit mode
 *
 * When the jump is performend we will be in long mode but
 * in 32bit compatibility mode with EFER.LME = 1, CS.L = 0, CS.D = 1
 * (and in turn EFER.LMA = 1).    To jump into 64bit mode we use
 * the new gdt/idt that has __KERNEL_CS with CS.L = 1.
 * We place all of the values on our mini stack so lret can
 * used to perform that far jump.
 */
pushl    $__KERNEL_CS    //内核代码段地址入栈
leal    startup_64(%ebp), %eax    //startup_up64导入eax

之后打开分页与保护模式

1
2
3
4
5
6
pushl    %eax    //startup_64入栈
/* Enter paged protected Mode, activating Long Mode */
movl    $(X86_CR0_PG | X86_CR0_PE), %eax /* Enable Paging and Protected mode */
movl    %eax, %cr0
/* Jump from 32bit compatibility mode into 64bit mode. */
lret

最后执行lret,因为startup_64已经入栈,这里就跳向了64位模式的起始位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    .code64
    .org 0x200
ENTRY(startup_64)
    /*
     * 64bit entry is 0x200 and it is ABI so immutable!
     * We come here either from startup_32 or directly from a
     * 64bit bootloader.
     * If we come here from a bootloader, kernel(text+data+bss+brk),
     * ramdisk, zero_page, command line could be above 4G.
     * We depend on an identity mapped page table being provided
     * that maps our entire kernel(text+data+bss+brk), zero page
     * and command line.
     */
#ifdef CONFIG_EFI_STUB
    /*
     * The entry point for the PE/COFF executable is efi_pe_entry, so
     * only legacy boot loaders will execute this jmp.
     */
    jmp    preferred_addr

至此正式从32位保护模式进入64位长模式

内核解压

1.进入64位长模式后首先设置段寄存器的值,然后是计算内核编译时的位置和它被加载的位置的差(类似32位)rbp最后包含解压后内核的起始地址。rbx包含用于解压的重定位内核代码的地址。

 

2.设置栈指针与标志寄存器重置

1
2
3
4
5
6
/* Set up the stack */
leaq    boot_stack_end(%rbx), %rsp
 
/* Zero EFLAGS */
pushq    $0
popfq

3.复制压缩的内核到buffer

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
 * Copy the compressed kernel to the end of our buffer
 * where decompression in place becomes safe.
 */
    pushq    %rsi    //保存指向boot_params的指针
    leaq    (_bss-8)(%rip), %rsi
    leaq    (_bss-8)(%rbx), %rdi
    movq    $_bss /* - $startup_32 */, %rcx
    shrq    $3, %rcx
    std
    rep    movsq
    cld                //清除DF
    popq    %rsi

压缩了的代码镜像存放在这份复制了的代码(从startup_32到当前的代码)和解压了的代码之间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Be careful parts of head_64.S assume startup_32 is at
     * address 0.
     */
    . = 0;
    .head.text : {    /*包含startup_32*/
        _head = . ;
        HEAD_TEXT
        _ehead = . ;
    }
    .rodata..compressed : {    /*    包含了压缩了的内核镜像 */
        *(.rodata..compressed)
    }
    .text :    {
        _text = .;     /* 解压代码 */
        *(.text)
        *(.text.*)
        _etext = . ;
    }

最后跳转到.text解压

1
2
3
4
5
/*
 * Jump to the relocated address.
 */
    leaq    relocated(%rbx), %rax
    jmp    *%rax

4.进入.text中首先清空bss节,然后调用:

1
2
3
4
5
6
asmlinkage __visible void *decompress_kernel(void *rmode, memptr heap,
                  unsigned char *input_data,
                  unsigned long input_len,
                  unsigned char *output,
                  unsigned long output_len,
                  unsigned long run_size)

进行内核的解压。

 

5.在解压函数中会初始化控制台,然后拿heap的位置。下一步调用:

1
2
3
4
5
6
7
8
/*
    * The memory hole needed for the kernel is the larger of either
    * the entire decompressed kernel plus relocation table, or the
    * entire decompressed kernel plus .bss and .brk sections.
    */
output = choose_kernel_location(input_data, input_len, output,
                output_len > run_size ? output_len
                                : run_size);

6.choose_kernel_location分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
unsigned char *choose_kernel_location(unsigned char *input,
                      unsigned long input_size,
                      unsigned char *output,
                      unsigned long output_size)
{
    unsigned long choice = (unsigned long)output;
    unsigned long random;
 
#ifdef CONFIG_HIBERNATION
    if (!cmdline_find_option_bool("kaslr")) {
        debug_putstr("KASLR disabled by default...\n");
        goto out;
    }
#else
    if (cmdline_find_option_bool("nokaslr")) {
        debug_putstr("KASLR disabled by cmdline...\n");
        goto out;
    }
#endif
 
    /* Record the various known unsafe memory ranges. */
    mem_avoid_init((unsigned long)input, input_size,
               (unsigned long)output, output_size);
 
    /* Walk e820 and find a random address. */
    random = find_random_addr(choice, output_size);
    if (!random) {
        debug_putstr("KASLR could not find suitable E820 region...\n");
        goto out;
    }
 
    /* Always enforce the minimum. */
    if (random < choice)
        goto out;
 
    choice = random;
out:
    return (unsigned char *)choice;
}
  • 可以看到调用了cmdline_find_option_bool("kaslr")来查找是否打开kaslr,之前做kernel pwn的时候直接用nokaslr关闭随机化原理就是这个。
  • 通过调用find_random_addr(choice, output_size);查找一个随机地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    static unsigned long find_random_addr(unsigned long minimum,
                        unsigned long size)
    {
        int i;
        unsigned long addr;
     
        /* Make sure minimum is aligned. */
        minimum = ALIGN(minimum, CONFIG_PHYSICAL_ALIGN);
     
        /* Verify potential e820 positions, appending to slots list. */
        for (i = 0; i < real_mode->e820_entries; i++) {
            process_e820_entry(&real_mode->e820_map[i], minimum, size);
        }
     
        return slots_fetch_random();
    }

7.返回找到满足要求的随机地址。并且验证地址合法性。然后调用decompress(input_data, input_len, NULL, NULL, output, NULL, error);进行解压,其具体实现取决于选择什么算法。

 

8.最后调用的两个函数是:parse_elf(output);handle_relocations(output, output_len);他们的作用是把解压后的内核移动到正确的位置。Linux内核就像是一个ELF可执行文件,我们进行原地解压,然后移动可加载段到正确的地址。

 

9.parse_elf(output);核心如下:

1
2
3
4
5
6
7
8
memcpy(&ehdr, output, sizeof(ehdr));
if (ehdr.e_ident[EI_MAG0] != ELFMAG0 ||
    ehdr.e_ident[EI_MAG1] != ELFMAG1 ||
    ehdr.e_ident[EI_MAG2] != ELFMAG2 ||
    ehdr.e_ident[EI_MAG3] != ELFMAG3) {
    error("Kernel is not a valid ELF file");
    return;
}

取内核的头,检查ELF签名标志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    memcpy(phdrs, output + ehdr.e_phoff, sizeof(*phdrs) * ehdr.e_phnum);
 
    for (i = 0; i < ehdr.e_phnum; i++) {
        phdr = &phdrs[i];
 
        switch (phdr->p_type) {
        case PT_LOAD:
#ifdef CONFIG_RELOCATABLE
            dest = output;
            dest += (phdr->p_paddr - LOAD_PHYSICAL_ADDR);
#else
            dest = (void *)(phdr->p_paddr);
#endif
            memcpy(dest,
                   output + phdr->p_offset,
                   phdr->p_filesz);
            break;
        default: /* Ignore other PT_* */ break;
        }
    }
 
    free(phdrs);
1
2
3
4
5
6
7
8
10.之后是```handle_relocations(output, output_len);```这个函数的具体实现依赖于```CONFIG_X86_NEED_RELOCS```,它主要是调整内核镜像的地址。
 
11.最后返回output即内核的地址。然后:
 ```c
/*
 * Jump to the decompressed kernel.
 */
    jmp    *%rax

12.解压后的内核部分定义在:arch/x86/kernel/head_64.S注意与arch/x86/boot/compressed/head_64.S是不一样的。

1
2
3
4
5
6
    .text
    __HEAD
    .code64
    .globl startup_64
startup_64:
...

最终的startup_64就是__START_KERNEL;

1
#define _START_KERNEL    (__START_KERNEL_map + __PHYSICAL_START)

即(不考虑kaslr):

  • Linux 内核的默认物理基址:0x1000000
  • Linux 内核的默认虚拟基址: 0xffffffff81000000

至此:我们进入了64位长模式,内核解压与重定位完成,正式启动,进入内核!

参考

Linux 内核揭密
Linux下的lds链接脚本详解
ELF文件格式分析
e820与kernel物理内存映射
Linux四级页表(x64)


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回