x86架构下Linux image的组成、早期启动、kexec重启等

Background

这段时间的工作涉及x86 CPU的早期启动,参与了几个与此有关的bugfix,涉及到的问题:

  1. 在没有日志信息的情况下,怎么利用寄存器的状态确定CPU运行到了哪种状态
  2. smp init / shutdown的细节
  3. INIT, SIPI对CPU的影响
  4. kexec-tools载入image时的内存布局
  5. image early bootstrap时的内存布局

construction of AMD64 Linux image

64—bit的x86 Linux,编译会生成这几个文件:

1
2
3
- vmlinux
- arch/x86/boot/compressed/{vmlinux.bin, vmlinux.bin.gz, vmlinux}
- arch/x86/boot/{setup.elf, setup.bin, vmlinux.bin, bzImage}

64-bit的x86 CPU因历史原因,从8086 16-bit的real mode开始启动,CPU复位后执行的第一行代码亦在16-bit的real mode,通过一系列步骤,从real mode转换成protected mode,再进入32-bit mode,最后进入64-bit的long mode。

位于源码目录下的vmlinux,是含有elf header、debuginfo等信息的运行着的kernel的原始文件,它运行在long mode,它由除了arch/x86/boot目录下的x86内核代码编译生成。基于它使用objcopy能够生成arch/x86/boot/compressed/vmlinux.bin,再经过压缩生成arch/x86/boot/compressed/vmlinux.bin.gz,使用mkpiggy生成piggy.S。它的入口在arch/x86/kernel/head_64.S中的startup_64

arch/x86/boot/compressed/目录下的相关代码是将CPU从32-bit的protected mode转换成64-bit的long mode,在这里将会启用分页(paging)机制、解压真正的64-bit内核代码,内核代码的地址随机化(kaslr)也会在这里完成。它的入口在arch/x86/boot/compressed/head_64.S中的startup_32。该目录下的一系列文件(含有piggy.S),会被编译链接成arch/x86/boot/compressed/vmlinux

arch/x86/boot/目录下的代码将CPU从real mode转换成32-bit的protected mode。它的起始地址在arch/x86/boot/header.S中的_start。这里做了些早期资源的探测与初始化,比如内存detect_memory、显示set_video等,这些工作集中在在main.c文件中的main()。该目录下的文件被编译生成arch/x86/boot/{setup.elf, setup.bin},其中setup.bin是由setup.elf经objcopy得到。

Note: $cat <dir>/.<file>.cmd可以看到是怎样生成的,比如$ cat arch/x86/boot/.bzImage.cmd

early boot: process, status and debug option

boot process

具体来讲,kernel的早期启动路径如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# files in arch/x86/boot 
_start (header.S)
-> main (main.c)
-> protected_mode_jump (pmjump.S)
-> 0x66, 0xea __BOOT_CS:in_pm32 (pmjump.S, enter 32-bit protected mode)

# files in arch/x86/boot/compressed
-> startup_32 (head_64.S, enable paging)
-> startup_64 (head_64.S, enter long mode)
-> extract_kernel (misc.c, uncompress kernel to `LOAD_PHYSICAL_ADDR`, default 0x1000000)

# files in arch/x86/kernel
-> startup_64 (head_64.S)
-> secondary_startup_64 (head_64.S)
-> initial_code (head_64.S, here is `x86_64_start_kernel`)
-> x86_64_start_kernel (head64.c)

Note:
real mode至32-bit protected mode的跳转代码这样写的:

1
2
3
4
5
>          # Transition to 32-bit mode
> .byte 0x66, 0xea # ljmpl opcode
> 2: .long in_pm32 # offset
> .word __BOOT_CS # segment
>

这是一个PE = 0的far jump,查阅Intel手册Volume 2 3.2中有关JMP指令的描述知道,在跳转过程中会将%CS__BOOT_CS(0x10),因小端序,所以__BOOT_CS放在了in_pm32的后面,这三行相当于jmp cs:ip

boot status

Linux对x86规定了启动协议[1],规定了早期启动时的寄存器状态与内存布局。struct setup_header定义了与内核启动有关的信息,其作为setup.elf一部分嵌在内核image中,位于_start靠前的512个字节内,在载入内核前由grub等loader动态地修改其值。在16-bit模式下,成员cmd_line_ptr指定了内核启动参数cmdline的起始地址;ramdisk_image是initramfs的起始地址;hdr.code32_start是real mode跳转到protected mode代码的跳转地址,即startup_32的地址。在64-bit模式下,cmd_line_ptr + ext_cmd_line_ptr<<32组成cmdline的地址,ramdisk_image + ext_ramdisk_image<<32组成initramfs的起始地址,其中ext_{cmd_line_ptr, ramdisk_image}struct boot_params的成员。

准确地说,struct boot_params中的地址是相对地址,以image的载入地址作为偏移的起点。启动协议规定了三类boot protocol:16-bit、32-bit、64-bit。对于UEFI等现代BIOS,内核可以直接从32-bit模式启动,startup_32指定了内核的起始地址,为0x0;64-bit的BIOS能够直接启动long mode内核,起始地址是0x200。无论32-bit还是64-bit模式的启动,struct boot_params的所有内容都被预先设置好;16-bit模式仅部分被要求设置,比如cmdline与initramsf。

Note: 这里的0x0, 0x200应该是相对偏移,即若arch/x86/boot/compressed/vmlinux的代码段载入地址为addr,则startup_32地址为addr+0x0startup_64地址为addr+0x200

在内核的启动过程中,能够从寄存器的状态推断出当前运行的kernel处于哪种模式。x86的复位向量是0xfffffff0,指向ROM,执行到内核的real mode时,%cs由bootloader设置,%ds, %es, %ss的值与%cs相同,CR0寄存器的PE位(bit 0)为0。
32-bit的protected mode的%cs__BOOT_CS(0x10),对应的GDT descriptor(从0计数的第2个)D/B(bit 22)是1,L(bit 21)是0,CR0中的PE(bit 0)是1。它有两个起点:一个是写在pmjump.S中的由real mode跳转至,跳转后首先将%ds, %es, %fs, %gs, %ss设置为__BOOT_DS(0x18),设置ltr__BOOT_TSS;另一个是32-bit内核arch/x86/boot/compressed/vmlinux的起始地址startup_32,进入至该地之后%ds, %es, %ss被设置为__BOOT_DS(0x18),跳转long mode之前置ltr__BOOT_TSS。载入新的DGT,其L(bit 21)为1,置CR0的PG(bit 31)为1启用paging后,后跳转到long mode。跳转进入long mode时更新%cs的值为__KERNEL_CS(0x10),之后第一件事是清零%ds, %es, %ss, %fs, %gs

Note: arch/x86/boot/compressed目录下的代码中存在不少BP_前缀的变量,它的定义在编译中生成,定义在arch/x86/kernel/asm-offsets.c中,BPboot_params的缩写,BP_xxxboot_params.xxx

debug option

对于x86早期启动阶段的日志信息,可以使用cmdline earlyprintk=ttyS0 debug控制串口日志信息的打印,与串口相关的配置信息在arch/x86/boot/early_serial_console.c[2]中,在real mode[3]与protected mode[4]中都能看到console_init()的身影。

smp init, kexec reboot

smp init

多核CPU上,第一个启动的CPU称为BSP(bootstrap processor),其他的CPU称为AP(application processor)。自BSP跳转进入arch/x86/kernel/head_64.S中的startup_64后,arch/x86/boot目录下的代码不再使用。BSP唤醒AP的调用路径如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
smp_init (kernel/smp.c)
-> bringup_nonboot_cpus (kernel/cpu.c, sequence wake up AP)
-> cpu_up (kernel/cpu.c)
-> _cpu_up (kernel/cpu.c)
-> cpuhp_up_callbacks (kernel/cpu.c)
-> cpuhp_invoke_callback (kernel/cpu.c)
-> cpuhp_hp_states[CPUHP_BRINGUP_CPU].startup.single (kernel/cpu.c)
-> bringup_cpu (kernel/cpu.c, callback)
-> __cpu_up (arch/x86/include/asm/smp.h)
-> smp_ops.cpu_up (arch/x86/kernel/smp.c)
-> native_cpu_up (arch/x86/kernel/smpboot.c, callback)
-> do_cpu_up (arch/x86/kernel/smpboot.c)
- start_ip = real_mode_header->trampoline_start
- early_gdt_descr.address = (unsigned long)get_cpu_gdt_rw(cpu)
- initial_code = (unsigned long)start_secondary
- initial_stack = idle->thread.sp
-> wakeup_cpu_via_init_nmi (arch/x86/kernel/smpboot.c, for apic has no wakeup_secondary_cpu method)
-> wakeup_secondary_cpu_via_init (arch/x86/kernel/smpboot.c)

AP被BSP通过顺序的INIT INIT SIPI(Startup IPI)三个IPI中断(inter processor interrupts)唤醒,在do_cpu_up中指定了AP的起始地址real_mode_header->trampoline_start。AP的初始化路径如下:

1
2
3
4
5
6
7
8
# files in arch/x86/realmode/rm
trampoline_start (trampoline_64.S, 16-bit real mode)
-> startup_32 (trampoline_64.S, 32-bit protected mode)

# files in arch/x86/kernel
-> startup_64 (head_64.S, 64-bit long mode)
-> secondary_startup_64 (head_64.S, initial_code has been start_secondary)
-> start_secondary (arch/x86/kernel/smpboot.c)

Note: 在arch/x86/realmode目录下,前缀pa_BP_类似,它定义在编译时生成的realmode/rm/realmode.lds中,前缀pa_xxx即symbol xxx

BSP在进入do_cpu_up逐个唤醒AP时,与AP之间有类似同步的操作,通过变量cpu_initialized_mask, cpu_callout_mask实现,AP的相关代码在wait_for_master_cpu(arch/x86/kernel/cpu/common.c)中。可为Linux启用dynamic debug 参数dyndbg="file smpboot.c +p; file smp.c +p"获取详细的smp init日志信息。

kexec reboot

Linux提供了两个系统调用kexec_load, kexec_file_load[5],能够令OS不经BIOS重启。kexec-tools[6]利用这两个接口实现了相关功能,参数-l使用的是kexec_load-s使用的是kexec_file_load,他们的区别主要体现在long mode内核的引导代码上:-l的引导阶段代码使用的是kexec-tools源码目录下的purgatory,-q使用的是内核源码中的arch/x86/realmode。此外,kexec_file_load自v5.4起能够通过CONFIG_KEXEC_SIG支持security boot。对64-bit的bzImage,默认情况下,kexec—tools使用的32-bit的启动协议,跳转入口是startup_64

使用-l载入内核,kexec-tools至少会塞入用于内核引导的purgatory、启动协议要求的boot params与cmdline,以及bzImage三段数据,还可以塞入可选的initramfs。kexec_load参数entry指向bzImage入口的物理地址,struct struct kexec_segment中的buf, bufsz指向用户态地址空间及其大小,mem, memsz指向将要载入用户态数据的物理地址。在kexec-tools的代码中,因为purgatory是预先编译好的二进制数据,因此存在很多elf_rel_{get, set}_symbol()的相关代码,他们用于动态更新purgatory诸如bzImage的入口、boot params等数据。kexec-tools -l存在参数--console-serial, --console-vga用于CPU显示运行在purgatory阶段时的相关debug信息。

kexec_load, kexec_file_load的区别仅仅在载入bzImage的方式,一个是以二进制数据方式,一个是通过读取传入的文件名方式,其余步骤皆相同,即相当于把kexec-tools中对bzImage的解析从用户态搬到了内核态、使用的是内核提供的purgatory (arch/x86/realmode)。他们的核心是对struct kimage的填充:control_code_page保存着kexec reboot过程中所需的页表;对-l, -f这类KEXEC_TYPE_DEFAULT类型的bzImage载入,因其目标物理地址空间不存在连续的区域能够载入数据,只能零散地存放在目标区域,因此需要提供一页swap区域用于在kexec reboot时将零散的数据集中起来,这就是swap_page的作用;至于-p参数指定的类型是KEXEC_TYPE_CRASH,它使用的是由内核启动参数crashkernel预留出的一篇区域,因此不需要swap_page

kexec-tools的-e参数用于内核的重启,入口在reboot(LINUX_REBOOT_CMD_KEXEC),在reboot过程中,会触发以此触发device_shutdownmigrate_to_reboot_cpumachine_shutdown,最后调用到machine_kexec (arch/x86/kernel/machine_kexec_64.c),进入relocate_kernel (arch/x86/kernel/relocate_kernel_64.S)后,确保启用paging、protected mode的情况下跳转进入purgatory。在relocate_kernel过程中,swap_pages函数即利用swap page将分散的数据集中起来。

migrate_to_reboot_cpu后的步骤都是BSP的行为,BSP在调用machine_shutdown时令AP进入halt (hlt指令)状态,调用路径如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# BSP
machine_shutdown (arch/x86/kernel/reboot.c)
-> native_machine_shutdown (arch/x86/kernel/reboot.c)
-> stop_other_cpus (x86/include/asm/smp.h)
-> native_stop_other_cpus (arch/x86/kernel/smp.c)
- apic_send_IPI_allbutself(REBOOT_VECTOR) (vec 0xf8)
- apic_send_IPI_allbutself(NMI_VECTOR)

# AP
REBOOT_VECTOR
-> smp_reboot_interrupt (arch/x86/kernel/smp.c)
or sysvec_reboot (from v5.6)
-> stop_this_cpu (arch/x86/kernel/process.c)
-> native_halt (arch/x86/include/asm/irqflags.h)

NMI_VECTOR
-> smp_stop_nmi_callback (arch/x86/kernel/smp.c)
-> stop_this_cpu

x86 initialization

Intel手册Volume 3 9.1 INITIALIZATION OVERVIEW详细地描述了CPU初始化的细节,涵盖power-up, RESET, INIT三类事件的处理器状态、处理器自检、model与stepping信息、第一条指令的执行地址信息。

Reference