0%

Linux内核镜像头分析

Linux内核镜像头部格式

Linux内核镜像的格式和启动它的bootloader有一定的关系。如果bootloader只是简单的跳到
内核Image的第一条指令开始执行,那么内核Image直接从开始放指令就可以,如果bootloader
对内核Image有一定的要求,一般是Image的头部符合一定的格式,那么内核Image就必须按照
相关的格式排布,这意味着这样的bootloader可以解析内核Image的格式,内核的入口地址也
不一定是内核Image的首地址。

Linux和windows上程序的格式是不一样的,Linux是ELF格式、windows上是PE/COFF格式,这里
说的是应用程序,我们看下Linux内核的Image。编译Linux内核的log可以看见,编译内核时
先生成vmlinux,然后用objcopy生成Image,vmlinux是ELF格式,bootloader需要理解ELF
才能引导Linux内核,所以还要把ELF转变成bootloader可以理解的二进制格式。

如上,如果bootloader直接从第一条指令执行,直接把内核的入口地址放在Image的开始就
可以,现在可以看到opensbi就是这样的简单bootloader。对于有些复杂的bootloader,比如
uboot或者UEFI,它们支持的启动方式更多,有的启动方式对被启动的Image的格式就有约束。

UEFI中使用的EFI启动方式,uboot也支持这种启动方式。这种启动方式一开始在UEFI/windows
中使用,Image head要求是PE/COFF格式的,Linux内核为了支持EFI启动,Image head也要
是PE/COFF格式。

PE/COFF格式的head大概是这样的:
PE/COFF head

其中,开头两个字节的二进制编码必须是0x5A4D,对应的ascii字符就是‘MZ’,0x3C偏移处
是真实PE头在Image中的偏移,PE/COFF的格式为了兼容DOS的格式,在真实PE头前加了如上
的DOS head、DOS STUB。AddressOfEntryPoint处放Image的入口信息,内容是入口地址相对
Image起始的偏移。

下面具体看下riscv内核Image开始部分的代码。

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef CONFIG_EFI
/*
* This instruction decodes to "MZ" ASCII required by UEFI.
*/
c.li s4,-13
j _start_kernel
#else
/* jump to start kernel */
j _start_kernel
/* reserved */
.word 0
#endif

编译好的riscv的Image要么支持EFI启动要么不支持。支持EFI启动的Image一开始放了一条
指令,这条指令的二进制编码就是0x5A4D,后面在DOS head的空间直接放了一条跳转指令,
这样EFI的内核Image也兼容了类似opensbi这种直接从Image第一条指令启动的bootloader,
c.li s4,-13这样的指令不会有什么副作用,第二条指令就直接跳到_start_kernel了,如果
bootloader按照EFI启动,会从PE头里的AddressOfEntryPoint域段得到入口地址,根本不会
去执行j _start_kernel指令。

对于EFI的Image,直接在如下的地方把这个PE头塞进来:

1
2
3
4
5
6
7
8
#ifdef CONFIG_EFI
.word pe_head_start - _start
pe_head_start:

__EFI_PE_HEADER
#else
.word 0
#endif

可以看到宏__EFI_PE_HEADER把PE头加了进来,AddressOfEntryPoint放的是__efistub_efi_pe_entry
的偏移,所以EFI内核的代码执行路径和非EFI内核在这里还是不同的。

riscv上的__efistub_efi_pe_entry是定义在drivers/firmware/efi/libstub/efi-stub-entry.c里
的efi_pe_entry这个函数,在编译的时候给这个函数加上了__efistub_这个前缀:

1
2
3
/* drivers/firmware/efi/libstub/Makefile */
STUBCOPY_FLAGS-$(CONFIG_RISCV) += --prefix-alloc-sections=.init \
--prefix-symbols=__efistub_

opensbi启动内核

如上,opensbi只是简单跳到被启动对象的起始地址。

uboot启动内核

Linux内核已经支持EFI方式启动,当初的内核patch的链接是https://lwn.net/Articles/813589/
这个patch是使用uboot + qemu来测试EFI启动的。

用这个命令可以启动uboot:

1
qemu-system-riscv64 -m 256m -machine virt -nographic -kernel path_to_u-boot/u-boot

启动的效果大概是:

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
51
52
53
54
55
56
57
58
59
60
61
62
OpenSBI v1.1
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|

Platform Name : riscv-virtio,qemu
Platform Features : medeleg
Platform HART Count : 1
Platform IPI Device : aclint-mswi
Platform Timer Device : aclint-mtimer @ 10000000Hz
Platform Console Device : uart8250
Platform HSM Device : ---
Platform Reboot Device : sifive_test
Platform Shutdown Device : sifive_test
Firmware Base : 0x80000000
Firmware Size : 288 KB
Runtime SBI Version : 1.0

Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000002000000-0x000000000200ffff (I)
Domain0 Region01 : 0x0000000080000000-0x000000008007ffff ()
Domain0 Region02 : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x000000008fe00000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes

Boot HART ID : 0
Boot HART Domain : root
Boot HART Priv Version : v1.12
Boot HART Base ISA : rv64imafdch
Boot HART ISA Extensions : time,sstc
Boot HART PMP Count : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count : 16
Boot HART MIDELEG : 0x0000000000001666
Boot HART MEDELEG : 0x0000000000f0b509


U-Boot 2023.01-rc4-00059-g8d6cbf5e6b (Jan 05 2023 - 15:59:10 +0800)

CPU: rv64imafdch_zicsr_zifencei_zihintpause_zba_zbb_zbc_zbs_sstc
Model: riscv-virtio,qemu
DRAM: 256 MiB
Core: 25 devices, 12 uclasses, devicetree: board
Flash: 32 MiB
Loading Environment from nowhere... OK
In: serial@10000000
Out: serial@10000000
Err: serial@10000000
Net: No ethernet found.
Working FDT set to 8f734950
Hit any key to stop autoboot: 0
=>

(todo: 怎么继续启动内核)