-
-
[原创]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 mode12
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位)
1234/
*
Load new GDT with the
64bit
segments using
32bit
descriptor
*
/
leal gdt(
%
ebp),
%
eax
movl
%
eax, gdt
+
2
(
%
ebp)
lgdt gdt(
%
ebp)
gdt偏移量整体结构如下
1234567891011.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模式
1234/
*
Enable PAE mode
*
/
movl
%
cr4,
%
eax
orl $X86_CR4_PAE,
%
eax
movl
%
eax,
%
cr4
10.初期页表初始化,建立初期的4G启动页表。linux内核使用4级页表,一般建立六个页表,每张表4k
12345/
*
Initialize Page tables to
0
*
/
leal pgtable(
%
ebx),
%
edi
xorl
%
eax,
%
eax
movl $((
4096
*
6
)
/
4
),
%
ecx
rep stosl
其中pgtable定义如下,大小24k
1234.section
".pgtable"
,
"a"
,@nobits
.balign
4096
pgtable:
.fill
6
*
4096
,
1
,
0
开始构建4级页表
1234/
*
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);
查找一个随机地址。12345678910111213141516static 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漏洞挖掘与利用;代码审计。