)
【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载导读本篇文章以hust-open-atom-club/linux-insides-zh仓库中Booting/章节为主线为你完整梳理 Linux 内核从你按下电源键那一刻起到内核镜像最终加载到内存为止的全部启动阶段。你将掌握CPU 复位向量与实模式寻址的来龙去脉、GRUB 等引导程序如何把控制权交给内核、内核早期setup code的堆栈/BSS/堆初始化、视频模式设置、从实模式经保护模式到 64 位长模式的切换以及内核解压与 KASLR 地址随机化的实现细节。读完本章你不仅能看懂arch/x86/boot/下的早期启动代码还能把每个启动阶段与具体寄存器、数据结构和内核源码位置一一对应起来。阅读方法与前置知识本章假设你熟悉基础计算机架构对C语言和x86_64汇编语法特别是 ATT 语法略有了解。你不需要成为内核专家但能读懂短代码片段、识别硬件术语会极大提升阅读收益——因为引导过程的绝大多数细节都以寄存器操作、内存地址和 BIOS 中断的形式呈现。建议的阅读方法是第一次按顺序通读从第一部分依次读到第六部分之后在需要将某个符号或寄存器映射到启动序列中的具体位置时再把对应章节作为参考点逐步回顾。若想在阅读时对照真实代码最好在本地拥有一份 Linux 内核源码可使用 git 克隆获取git clone gitgithub.com:torvalds/linux.git章节内各篇文章提到的关键源码如arch/x86/boot/header.S、arch/x86/boot/main.c、arch/x86/boot/video.c、arch/x86/boot/pm.c、arch/x86/boot/compressed/head_64.S等均可在克隆后的源码树中直接定位研读。术语表读懂引导章节的必备词汇在阅读本章乃至后续其他章节时你会频繁遇到以下专业术语建议先通读一遍建立印象CS、DS、SS、CR0、CR3、CR4、EFER——代表 x86 的段寄存器和控制寄存器。CS代码段、DS数据段、SS堆栈段在实模式与保护模式下扮演完全不同的角色CR0的PE位控制保护模式的开启CR3保存页表基址CR4承载各类特性开关EFERExtended Feature Enable Register则与 64 位长模式的使能直接相关。0x...——表示十六进制值内核启动代码中几乎所有地址与寄存器值都以此形式出现。entry_*和startup_*——早期引导符号的常见前缀例如startup_32、startup_64分别对应 32 位与 64 位入口点。setup code——Linux 内核的早期部分负责执行将内核代码本身加载到内存之前的准备工作。decompressor——setup code中负责把压缩后的内核映像解压到内存的那部分代码。你将学到什么本章明确给出了三个核心学习目标后续六个部分全部围绕它们展开处理器如何从固件BIOS/UEFI和引导加载程序如 GRUB 2一步步到达内核入口点x86_64 处理器的不同运行模式实模式、保护模式、长模式及其切换机制内核本身启动之前的早期setup code如何被加载到内存并开始工作。阅读顺序与六大部分一览本章共六个部分按启动的时间线严格递进部分主题对应文件第一部分从引导程序到内核Booting/linux-bootstrap-1.md第二部分在内核设置代码的第一步Booting/linux-bootstrap-2.md第三部分视频模式初始化和保护模式切换Booting/linux-bootstrap-3.md第四部分切换 64 位模式Booting/linux-bootstrap-4.md第五部分内核解压Booting/linux-bootstrap-5.md第六部分内核地址随机化Booting/linux-bootstrap-6.md下面逐部分展开核心内容。第一部分从引导程序到内核按下电源键之后发生了什么尽管这是一系列关于 Linux 内核的文章第一部分并不从内核代码开始而是从硬件视角出发。按下电源开关后主板向电源发送信号电源回送电源备妥信号Power Good Signal主板随即尝试启动 CPU。CPU 复位所有寄存器并设置预定义值。自 80386 起CPU 复位后寄存器被设置为IP 0xfff0 CS selector 0xf000 CS base 0xffff0000处理器由此开始在实模式下工作。要理解实模式必须先掌握 8086 时代就确立的段式内存管理8086 只有 20 位寻址总线可访问 1MB 地址空间和 16 位寄存器最大寻址0xffff即 64KB。为突破寄存器宽度限制实模式将地址拆为段基址 偏移两部分物理地址计算公式为PhysicalAddress Segment * 16 Offset例如CS:IP 0x2000:0x0010时物理地址为(0x2000 4) 0x0010 0x20010。而 16 位能表达的最大组合0xffff:0xffff计算得到0x10ffef超出 1MB 达 65519 字节——这正是A20 地址线存在的原因在 A20 被禁用时0x10ffef会回卷为0x00ffef。复位向量与 BIOS 启动结合复位后的CS基址与IP值逻辑地址为0xffff0000:0xfff0最终物理地址是0xfffffff0——位于 4GB - 16 字节处即著名的复位向量Reset Vector。这是 CPU 复位后期望执行的第一条指令所在地址它包含一条jump指令通常跳向 BIOS 入口点。文章以 coreboot 源码为例展示了.reset段中的跳转指令opcode0xe9.section .reset, ax, %progbits .code16 .globl _start _start: .byte 0xe9 .int _start16bit - ( . 2 )在实模式下 CPU 只能访问 1MB 地址空间那 CPU 如何访问到0xfffffff0这样的高位地址答案来自 coreboot 的内存映射文档0xFFFE_0000 - 0xFFFF_FFFF这段 128KB 的 ROM 被映射进地址空间因此复位向量处执行的第一条指令来自ROM 而非 RAM。BIOS 寻找可引导设备与 MBRBIOS 完成初始化和硬件检查后会按配置顺序尝试从不同设备寻找引导程序。对硬盘而言BIOS 寻找引导扇区MBR 分区下引导扇区位于第一个扇区512 字节的头 446 字节且扇区末尾必须是魔术字节0x55与0xaaBIOS 据此判定设备可引导。第一部分给出了一个完整的极简引导扇区示例[BITS 16] [ORG 0x7c00] boot: mov al, ! mov ah, 0x0e mov bh, 0x00 mov bl, 0x07 int 0x10 jmp $ times 510-($-$$) db 0 db 0x55 db 0xaa用 NASM 汇编并用 QEMU 直接运行nasm -f bin boot.nasm qemu-system-x86_64 boot该二进制以0x7c00为起始BIOS 约定把引导扇区加载到0x7c00通过int 0x10打印!字符剩余 510 字节用 0 填充最后以0xaa、0x55收尾。运行后 QEMU 屏幕显示如下还可以用objdump查看其二进制转储nasm -f bin boot.nasm objdump -D -b binary -mi386 -Maddr16,data16,intel boot第一部分还给出了实模式下 1MB 地址空间的经典分配表值得牢记0x00000000-0x000003FF是实模式中断向量表0x00007C00-0x00007DFF是引导加载程序区0x000A0000-0x000BFFFF是视频内存VRAM0x000F0000-0x000FFFFF是系统 BIOS 等。GRUB 2 与内核启动协议现实世界中可选的引导程序很多GRUB 2、syslinux 等Linux 内核通过Boot Protocol规定引导程序应如何实现。章节以 GRUB 2 为例BIOS 把控制权交给引导扇区中的boot.img仅占一个扇区只做必要初始化随后跳转到 core image如diskboot.img后者把整个 core image含 GRUB 2 内核代码与文件系统驱动载入内存最终调用grub_main初始化控制台、计算模块基地址、设置 root 设备、读取 grub 配置文件、加载模块最后进入 normal 模式由grub_normal_execute显示操作系统菜单用户选择系统后grub_menu_execute_entry调用boot命令引导所选操作系统。按 boot protocol 要求引导程序必须填充kernel setup header位于 setup code 偏移0x01f1处其定义始于arch/x86/boot/header.S.globl hdr hdr: setup_sects: .byte 0 root_flags: .word ROOT_RDONLY syssize: .long 0 ram_size: .word 0 vid_mode: .word SVGA_MODE root_dev: .word 0 boot_flag: .word 0xAA55引导程序必须填充 boot protocol 中标记为write的字段如type_of_loader这些信息可能来自命令行或由计算得到。内核被引导入内存后内存使用布局如下| Protected-mode kernel | 100000 ------------------------ | I/O memory hole | 0A0000 ------------------------ | Reserved for BIOS | Leave as much as possible unused ~ ~ | Command line | (Can also be below the X10000 mark) X10000 ------------------------ | Stack/heap | For use by the kernel real-mode code. X08000 ------------------------ | Kernel setup | The kernel real-mode code. | Kernel boot sector | The kernel legacy boot sector. X ------------------------ | Boot loader | - Boot sector entry point 0x7C00 001000 ------------------------ | Reserved for MBR/BIOS | 000800 ------------------------ | Typically used by MBR | 000600 ------------------------ | BIOS use only | 000000 ------------------------其中X是内核 boot sector 被加载进内存的位置。示例环境里X 0x10000可通过内存 dump 验证内核设置代码的入口_start内核设置代码的起点是arch/x86/boot/header.S中的_start函数。_start之前的代码其实是内核自带的引导程序——早期内核确实可以如此启动但现代内核中这部分代码已不再启动内核只输出错误信息。直接用qemu-system-x86_64 vmlinuz-...启动时即可看到该错误qemu-system-x86_64 vmlinuz-3.18-generic为了兼容 UEFI 硬件header.S开头定义了 DOS MZ 魔术数字与 PE 头。真正的内核代码从_start开始——其他引导程序GRUB 2 等知道_start位于 MZ 头之后偏移0x200字节处因此会跳过前面位于.bstext段的代码直接跳转至此.globl _start _start: .byte 0xeb .byte start_of_setup-1f 1: // // rest of the header //_start首条指令是短跳转opcode0xeb跳到start_of_setup标号位于.entrytext段其中第一条指令才是内核执行开始后的第一条指令。GRUB 2 跳转到_start时设置state.gs state.fs state.es state.ds state.ss segment; state.cs segment 0x20;当内核被加载到0x10000时fs es ds ss 0x1000、cs 0x1020第一条指令地址为0x10200正好是0x10000起偏移0x200处。从start_of_setup开始的代码需要完成四件事将所有段寄存器值统一、设置堆栈、设置 BSS静态变量区、跳转到main.c。段寄存器设置先让ds与es指向相同地址并用cld清方向标志然后用一个经典技巧统一cspushw %ds pushw $6f lretwlretw会把标号6的地址装入ip、把ds的值装入cs从而让cs与其他段寄存器一致。设置堆栈接下来检查ss寄存器处理三种情况ss 0x10000、ss ! 0x10000且CAN_USE_HEAP置位、ss ! 0x10000且CAN_USE_HEAP未置位。核心代码将sp4 字节对齐若为 0 则设为0xfffc堆栈向下增长64KB 段的最后一个 4 字节地址然后把0x10000写入ss2: andw $~3, %dx jnz 3f movw $0xfffc, %dx 3: movw %ax, %ss movzwl %dx, %esp stiloadflags字段的Bit 7即CAN_USE_HEAP标志置位表示heap_end_ptr值有效。loadflags的全部标志位包括#define LOADED_HIGH (10) #define QUIET_FLAG (15) #define KEEP_SEGMENTS (16) #define CAN_USE_HEAP (17)若CAN_USE_HEAP置位则把heap_end_ptr加上最小堆栈大小STACK_SIZE512 字节得到堆栈顶未置位则只加STACK_SIZE。三种情形下堆栈布局分别如 stack1.png、stack2.png、minimal_stack.png 所示。BSS 段设置执行 C 代码前还要校验 magic 签名并清零 BSS 段。签名校验失败则跳入setup_badcmpl $0x5a5aaa55, setup_sig jne setup_bad随后将 BSS 段全部清零movw $__bss_start, %di movw $_end3, %cx xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stosl即把__bss_start放入di、_end 34 字节对齐放入cx、eax清零计算差值得到 BSS 大小除以 4 后以rep; stosl按 4 字节写零最终效果见 bss.png。至此即可call main进入第一个 C 语言函数定义于arch/x86/boot/main.c。第二部分内核设置代码的第一步第二部分从main()函数开始主线是把启动参数拷贝到 zeropage、初始化控制台、初始化堆、检查 CPU、侦测内存、初始化键盘、查询系统参数。将启动参数拷贝到 boot_paramsmain()首先调用copy_boot_params(void)把内核设置信息拷贝到boot_params结构定义于arch/x86/include/uapi/asm/bootparam.h的struct setup_header hdr字段。该函数完成两件事把header.S中定义的hdr结构拷贝到boot_params.hdr若内核通过旧命令行协议运行则更新命令行指针。注意这里的memcpy不是 C 库函数而是定义在arch/x86/boot/copy.S中的汇编实现采用fastcall调用规则参数经ax、dx、cx寄存器传递GLOBAL(memcpy) pushw %si pushw %di movw %ax, %di ;move boot_param.hdr to di movw %dx, %si ;move hdr to si pushw %cx ;push cx ( sizeof(hdr) ) shrw $2, %cx rep; movsl ;copy based on 4 bytes popw %cx andw $3, %cx ;cx cx % 4 rep; movsb ;copy based on one byte popw %di popw %si retl ENDPROC(memcpy)调用memcpy(boot_params.hdr, hdr, sizeof hdr)时ax指向boot_params.hdr、dx指向hdr、cx为结构大小。实现先以 4 字节为单位复制剩余不足 4 字节的部分再按字节复制。GLOBAL宏定义于arch/x86/include/asm/linkage.h给代码段分配全局可见的名字标签ENDPROC宏定义于include/linux/linkage.h标记汇编函数结束便于静态分析工具识别。控制台初始化console_init定义于arch/x86/boot/early_serial_console.c解析命令行是否包含earlyprintk选项并初始化串口可能的取值包括serial,0x3f8,115200serial,ttyS0,115200ttyS0,115200若命令行含debug选项会出现输出early console in setup code。puts定义于arch/x86/boot/tty.c逐字节调用putcharvoid __attribute__((section(.inittext))) putchar(int ch) { if (ch \n) putchar(\r); bios_putchar(ch); if (early_serial_base ! 0) serial_putchar(ch); }putchar会把\n扩展为\r\n先经bios_putchar用int 0x10AH 0x0e输出到显示器若串口已初始化再输出到串口。bios_putchar使用initregs内部先memset清零biosregs结构填充寄存器。memset同样是copy.S中的汇编实现用imull $0x01010101把单字节值扩展为 4 字节重复模式再配合rep; stosl高效写内存。.inittext段定义在setup.ld链接脚本中。堆初始化init_heap先检查loadflags的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)); } //heap_end heap_end_ptr 512 heap_end (char *)((size_t)boot_params.hdr.heap_end_ptr 0x200);即stack_end esp - STACK_SIZE、heap_end heap_end_ptr 0x200。若heap_end大于stack_end则用stack_end接管——因为在大多数系统中全局堆与堆栈相邻但增长方向相反。堆初始化完成后即可使用GET_HEAP等操作。检查 CPU 类型validate_cpuarch/x86/boot/cpu.c调用check_cpuarch/x86/boot/cpucheck.c获取 CPU 级别并与预设最低级别比较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; }check_cpu还会检查 CPU 标志若为 64 位 CPU 则设置 long mode、检查制造商并据此设置不同的 CPU 选项例如 AMD CPU 若不支持SSESSE2则禁止之。内存分布侦测detect_memoryarch/x86/boot/memory.c通过0xe820获取全部内存分配、0xe801和0x88获取临近内存大小三类 BIOS 接口侦测内存分布。以detect_memory_e820为例它填充biosregs结构initregs(ireg); ireg.ax 0xe820; ireg.cx sizeof buf; ireg.edx SMAP; // 0x534d4150 ireg.di (size_t)buf;其中ax固定为0xe820cx是缓冲区大小edx必须是魔术数字SMAP0x534d4150es:di是缓冲区地址ebx初始为 0。随后循环调用int 0x15把返回的ebx写回寄存器继续下一次调用直到返回的 eflags 含X86_EFLAGS_CF为止。收集到的信息写入e820entry数组每个元素含内存段起始地址、大小与类型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键盘初始化与系统参数查询keyboard_init()先通过int 0x16AH 0x02获取键盘状态并存入boot_params.kbd_status再以AX 0x0305设置按键重复频率。系统参数查询则包括query_mcaarch/x86/boot/mca.c设置AH 0xc0调用int 0x15若 CF 被置位则无 MCAMicro Channel Architecture否则ES:BX指向系统信息表函数把表内容拷贝到boot_params.sys_desc_table。query_ist、query_apm_bios等查询将在后续章节继续介绍。保护模式与段描述符在深入后续部分前必须理解保护模式的内存管理。实模式 20 位地址线在保护模式中被替换为 32 位可访问多达 4GB 地址空间并引入内存分页能力。保护模式下每个内存段的大小和起始位置由段描述符描述所有段描述符存放于全局描述符表GDTGDT 地址保存在 48 位寄存器GDTR中16 位表大小 32 位表基址通过lgdt指令加载。段描述符为 64 位结构核心字段包括Limit20 位——分存于 0-15 与 16-19 位配合G位决定段长度G0时按字节增长最大 1MBG1时按 4KB 增长最大 4GB段长度 粒度 × (LIMIT 1)。Base32 位——分存于 0-15、32-39、56-63 位定义段基址。Type/Attribute40-47 位——S位第 44 位区分系统段与代码/数据段第 43 位区分数据段E扩展方向、W可写、A已访问与代码段C一致、R可读、A已访问。数据段全部可读代码段永远不可写C1时低优先级代码可访问该代码段。DPL2 位——段优先级0-3。P 标志bit 47——段是否存在于内存P0时访问会报错。L 标志bit 53——仅对代码段有意义L1表示该段运行于 64 位模式。D/B 标志bit 54——对可执行代码段称D默认操作数/地址长度 32 位或 16 位对堆栈段称B32 位esp或 16 位sp栈指针对下扩数据段则决定堆栈段上界限0xFFFFFFFF或0xFFFF。保护模式下段寄存器保存的是一种称为段选择子的 16 位结构IndexGDT 中段描述符索引、TI选 GDT 还是 LDT、RPL请求者优先级。CPU 寻址步骤为装入段选择子 → 按选择子从 GDT 取段描述符放入段寄存器隐藏部分 → 非下扩段下物理地址 描述符基址 偏移。线性地址的形成过程见 linear_address.png。从实模式进入保护模式的标准操作是禁止中断 →lgdt装载 GDT → 置CR0.PE 1→ 跳转执行保护模式代码。第三部分视频模式初始化和保护模式切换内核数据类型与堆操作 API第三部分从set_video函数arch/x86/boot/video.c开始它会读取boot_params.hdr.vid_mode。在此之前先认识内核常用数据类型建议阅读内核代码时牢记Typecharshortintlongu8u16u32u64Size12481248内核在boot.h中提供了一套堆操作 APIRESET_HEAP()——((void *)( HEAP _end ))把堆头重置到_end标号内核初始化时堆与栈共享内存空间。GET_HEAP(type, n)——((type *)__get_heap(sizeof(type),__alignof__(type),(n)))按类型大小与对齐要求分配内存。__get_heap(s, a, n)——先按对齐要求调整HEAP把当前HEAP存入tmp再让HEAP s*n预留空间并返回tmp。heap_free(n)——判断heap_end - HEAP n是否成立。设置显示模式set_video首先调用RESET_HEAP()然后调用store_mode_params把显示模式参数写入boot_params.screen_info结构定义于include/uapi/linux/screen_info.h。其内部流程为store_cursor_position通过int 0x10AH 0x3查询光标行列并存入screen_info.orig_x/orig_ystore_video_mode保存当前显示模式到orig_video_mode根据显示模式设置video_segment单色文本内存段0xB000或彩色文本内存段0xB800再通过set_fs(0)配合rdfs16(0x485)读取字体大小存入orig_video_points并从0x44a、0x484读取行列信息存入orig_video_cols/orig_video_lines。随后save_screen把当前屏幕信息屏幕大小、光标位置、字符数据保存到saved_screen结构{ int x, y; int curx, cury; u16 *data; }并检查堆空间是否足够if (!heap_free(saved.x*saved.y*sizeof(u16)512)) return;probe_cards(0)arch/x86/boot/video-mode.c遍历所有显卡驱动for (card video_cards; card video_cards_end; card) { if (card-unsafe unsafe) { if (card-probe) card-nmodes card-probe(); else card-nmodes 0; } }video_cards符号来自arch/x86/boot/setup.ld中定义的.videocards段其中存放所有以__videocard宏即struct card_info __attribute__((used,section(.videocards)))定义的显卡结构例如static __videocard video_vga { .card_name VGA, .probe vga_probe, .set_mode vga_set_mode, };card_info结构含card_name、set_mode、probe、modes、nmodes、unsafe、xmode_first、xmode_n字段。set_video随后进入主循环若vid_mode为ASK_VGA则弹出菜单让用户选择再调用set_mode设置模式失败则提示Undefined video mode number并再次询问for (;;) { if (mode ASK_VGA) mode mode_menu(); if (!set_mode(mode)) break; printf(Undefined video mode number: %x\n, mode); mode ASK_VGA; }set_mode→raw_set_mode遍历card_info调用对应set_mode如vga_set_mode内部按VIDEO_80x25、VIDEO_8POINT、VIDEO_80x43等模式调用vga_set_***函数每个函数最终都是调用int 0x10设置显示模式。设置成功后写回boot_params.hdr.vid_modevesa_store_edid把 EDID 数据写入内存do_restore还原此前保存的屏幕信息。关于vid_mode的传入方式boot protocol 提供了vga命令行选项vgamode mode here is either an integer (in C notation, either decimal, octal, or hexadecimal) or one of the strings normal (meaning 0xFFFF), ext (meaning 0xFFFE) or ask (meaning 0xFFFD). This value should be entered into the vid_mode field, as it is used by the kernel before the command line is parsed.即可以把vgaask或0xFFFD写入 grub 或引导程序配置文件内核启动时便会显示交互式菜单供用户挑选显示模式进入保护模式前的最后准备go_to_protected_modearch/x86/boot/pm.c完成最后的准备工作。首先realmode_switch_hook若存在realmode_swtchhook 则用lcallw远调用之用于 DOS 等宿主环境否则直接禁止中断与 NMIstatic 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(); } }io_delay通过对 I/O 端口0x80写入任意字节获得约 1ms 延时。随后enable_a20arch/x86/boot/a20.c激活 A20 地址线先用a20_test检测是否已激活——把FS设为0x0000、GS设为0xffff在A20_TEST_ADDR写入并读取GS:A20_TEST_ADDR0x10判断是否回卷未激活则尝试多种方法包括 BIOSint 0x15等。若最终失败会调用diearch/x86/boot/header.S中hlt 死循环结束运行。接着reset_coprocessor通过向 I/O 端口0xf0、0xf1写 0 复位数字协处理器mask_all_interrupts屏蔽从中断控制器PIC的所有中断和主 PIC 上除 IRQ2级联中断以外的全部中断outb(0xff, 0xa1); /* Mask all interrupts on the secondary PIC */ outb(0xfb, 0x21); /* Mask all but cascade on the primary PIC */设置 IDT 与 GDT进入保护模式setup_idt用lidtl装载一个空 IDT长度 0使所有中断调用暂时为空static void setup_idt(void) { static const struct gdt_ptr null_idt {0, 0}; asm volatile(lidtl %0 : : m (null_idt)); }gdt_ptr是__attribute__((packed))的 48 位结构16 位长度 32 位指针。setup_gdt则装载启动用 GDTstatic const u64 boot_gdt[] __attribute__((aligned(16))) { [GDT_ENTRY_BOOT_CS] GDT_ENTRY(0xc09b, 0, 0xfffff), [GDT_ENTRY_BOOT_DS] GDT_ENTRY(0xc093, 0, 0xfffff), [GDT_ENTRY_BOOT_TSS] GDT_ENTRY(0x0089, 4096, 103), };这里定义了代码段、数据段和 TSS 段的描述符由于尚未设置任何中断调用TSS 实际不会被使用其存在只是为了满足 Intel 处理器进入保护模式的要求。数组以 16 字节对齐__attribute__((aligned(16)))。准备工作全部完成后内核通过jmpl *%eax跳转到 32 位入口点arch/x86/boot/pmjump.S正式进入保护模式。第四部分切换到 64 位模式第四部分Booting/linux-bootstrap-4.md承接保护模式展示了在保护模式中的最初几步确认 CPU 是否支持长模式Long Mode、SSE 与分页初始化页表最后完成向 64 位长模式的切换。本部分包含大量汇编代码阅读前最好备一本汇编参考书。入口点为arch/x86/boot/pmjump.S的jmpl *%eaxeax寄存器保存 32 位入口点地址。按照 x86 Linux 内核引导协议When using bzImage, the protected-mode kernel was relocated to 0x100000使用 bzImage 时保护模式内核被重定位至0x100000。示例中寄存器状态验证了这一点eax 0x100000 1048576 ecx 0x0 0 edx 0x0 0 ebx 0x0 0 esp 0x1ff5c 0x1ff5c ebp 0x0 0x0 esi 0x14470 83056 edi 0x0 0 eip 0x100000 0x100000 eflags 0x46 [ PF ZF ]本部分随后验证 CPU 能力长模式、SSE、分页支持、初始化页表并最终跳转到 64 位入口点startup_64。切换完成后代码段寄存器加载__KERNEL_CS并执行lret完成远跳转pushl $__KERNEL_CS leal startup_64(%ebp), %eax ... pushl %eax ... lret第五部分内核解压第五部分Booting/linux-bootstrap-5.md涵盖跳进内核代码的最后步骤解压前准备、重定位与直接内核解压。我们在 64 位入口点startup_64源文件arch/x86/boot/compressed/head_64.S开始。进入startup_64后先建立数据段.code64 .org 0x200 ENTRY(startup_64) xorl %eax, %eax movl %eax, %ds movl %eax, %es movl %eax, %ss movl %eax, %fs movl %eax, %gs随后进行解压前的准备工作如初始化解压器环境、处理重定位并调用extract_kernel完成对压缩内核映像的直接解压把真正的内核代码展开到指定物理地址。第六部分内核地址随机化KASLR第六部分Booting/linux-bootstrap-6.md讲述引导过程的高级部分——内核加载地址随机化。Linux 内核的入口点是init/main.c的start_kernel函数它在LOAD_PHYSICAL_ADDR地址开始执行该地址依赖CONFIG_PHYSICAL_START配置选项默认0x1000000config PHYSICAL_START hex Physical address where the kernel is loaded if (EXPERT || CRASH_DUMP) default 0x1000000 ---help--- This gives the physical address where the kernel is loaded.该选项可在内核配置时修改但也可让加载地址取随机值——为此需启用CONFIG_RANDOMIZE_BASE。启用后内核镜像解压和加载的物理地址会被随机化这是出于**地址空间布局随机化ASLR**的安全考量。随机化的最早步骤之一是初始化恒等映射页表identity mapped虚拟地址与物理地址相同若引导加载程序使用 16 位或 32 位引导协议此时已存在页表但若解压器选择页表覆盖范围之外的内存区域就需要建立新页表。随机化从choose_random_location函数开始void choose_random_location(unsigned long input, unsigned long input_size, unsigned long *output, unsigned long output_size, unsigned long *virt_addr)该函数负责在解压前挑选随机的物理与虚拟加载地址为start_kernel的最终运行位置奠定安全基础。内核版本说明按照本仓库章节 README 的标注本章对应Linux kernel v6.19。需要注意的是章节内各篇译文源自较早的上游版本文中多处引用的源码路径与行号对应v3.18时代的内核源码树阅读时如遇到与现行内核源码的细微差异应以最新内核源码树为准并欢迎向仓库提交修正。整个书籍的目录结构可查看 SUMMARY.md书中其余章节如初始化、中断、系统调用、内存管理等同样配有各自的技术主题与源码讲解。小结与下一步至此一条完整的引导链已经清晰电源复位 → 实模式复位向量 → BIOS 自检与 MBR 引导扇区 → GRUB 2 按 boot protocol 填充 setup header → 内核_start/start_of_setup统一段寄存器、设置堆栈与 BSS →main()拷贝启动参数、初始化控制台与堆、校验 CPU、侦测内存、初始化键盘 → 视频模式设置vga选项→ 禁止中断与 NMI、激活 A20、屏蔽 PIC、设置空 IDT 与 boot GDT → 进入保护模式 → 校验长模式能力、初始化页表 → 切换 64 位长模式 → 解压准备与重定位 →extract_kernel解压 → KASLR 随机化加载地址 →start_kernel。每一个环节都对应着 Booting/ 章节中可逐行研读的源码与讲解是理解后续初始化、中断与内存管理章节的坚实起点。赞分享【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载相关推荐按下电源键那一刻发生了什么Linux 内核揭秘linux-insides-zh引导章节 6 大阶段完整解读按下电源键那一刻发生了什么Linux 内核揭秘linux insides zh引导章节 6 大阶段完整解读 按下电源键的那一刻Linux 内核引导过程就Linux内核引导过程解析从开机到内核加载基于0xAX/linux-insides项目Linux内核引导过程解析从开机到内核加载基于0xAX/linux insides项目 引言计算机启动的魔法时刻 当我们按下计算机的电源按钮时一系列精文档教程操作系统新手如何入门 Linux 内核源码Linux 内核揭秘linux-insides-zh3 步阅读法从引导流程到系统调用新手如何入门 Linux 内核源码Linux 内核揭秘linux insides zh3 步阅读法从引导流程到系统调用 刚接触 Linux 内核源码 时上一篇【亲测免费】 QModBus 安装和配置指南下一篇终极Noto Emoji定制指南3步打造专属个性化emoji字体创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考