前言
内核 bringup 有些地方没看懂,也没打算看得有多懂。起码基本的语法要了解一下吧
基本背景
链接脚本(Linker Script)可用于定制链接过程,我认为最重要的还是它可以描述一个二进制程序的内存布局,这个二进制程序自然也包括内核映像(kernel image)
ENTRY
ENTRY
描述一个程序的入口。在 x86-64
环境下,vmlinux.lds.S 中定义入口为 phys_startup_64
#ifdef CONFIG_X86_32
OUTPUT_ARCH(i386)
ENTRY(phys_startup_32)
#else
OUTPUT_ARCH(i386:x86-64)
ENTRY(phys_startup_64)
#endif
实际上,Linux 内核的入口应为 startup_64
,但这是一个虚拟地址上的符号,而 phys_startup_64
则是 startup_64
对应的物理地址,也就是说它们在数值意义上相差一个 LOAD_OFFSET
phys_startup_64 = ABSOLUTE(startup_64 - LOAD_OFFSET);
// ...
#ifdef CONFIG_X86_32
#define LOAD_OFFSET __PAGE_OFFSET
#else
#define LOAD_OFFSET __START_KERNEL_map
#endif
// arch/x86/include/asm/page_64_types.h
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
也可以直接在编译好的 vmlinux
上查找对应符号,验证如下:
$ nm vmlinux | grep -w startup_64
ffffffff81000000 T startup_64
$ nm vmlinux | grep -w phys_startup_64
0000000001000000 A phys_startup_64
$ readelf -h vmlinux | grep Entry
Entry point address: 0x1000000
PHDRS
PHDRS
描述各 segment 的具体布局。segment 即使不做声明,linker
也会帮你处理好,除非你需要精确控制每一个布局(既然用到脚本,那么大概率需要精确控制)。语法如下:
PHDRS
{
name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
[ FLAGS ( flags ) ] ;
}
其中,name
仅用做 SECTIONS
命令(后面再介绍)下的引用,并不存在于实际文件中;type
和 flags
自行参考 ELF 相关文档;FILEHDR
和 PHDRS
并没有在内核中使用到,仅做点搬运:The FILEHDR keyword means that the segment should include the ELF file header. The PHDRS keyword means that the segment should include the ELF program headers themselves. If applied to a loadable segment (PT_LOAD), all prior loadable segments must have one of these keywords.
内核实际的布局如下:
PHDRS {
text PT_LOAD FLAGS(5); /* R_E */
data PT_LOAD FLAGS(6); /* RW_ */
#ifdef CONFIG_X86_64
#ifdef CONFIG_SMP
percpu PT_LOAD FLAGS(6); /* RW_ */
#endif
init PT_LOAD FLAGS(7); /* RWE */
#endif
note PT_NOTE FLAGS(0); /* ___ */
}
验证如下:
$ objdump -p vmlinux
vmlinux: file format elf64-x86-64
Program Header:
LOAD off 0x0000000000200000 vaddr 0xffffffff81000000 paddr 0x0000000001000000 align 2**21
filesz 0x00000000018603a0 memsz 0x00000000018603a0 flags r-x
LOAD off 0x0000000001c00000 vaddr 0xffffffff82a00000 paddr 0x0000000002a00000 align 2**21
filesz 0x0000000000932000 memsz 0x0000000000932000 flags rw-
LOAD off 0x0000000002600000 vaddr 0x0000000000000000 paddr 0x0000000003332000 align 2**21
filesz 0x000000000002c8a8 memsz 0x000000000002c8a8 flags rw-
LOAD off 0x000000000275f000 vaddr 0xffffffff8335f000 paddr 0x000000000335f000 align 2**21
filesz 0x000000000028e000 memsz 0x00000000004d1000 flags rwx
NOTE off 0x0000000001a6034c vaddr 0xffffffff8286034c paddr 0x000000000286034c align 2**2
filesz 0x0000000000000054 memsz 0x0000000000000054 flags ---
SECTIONS
SECTIONS
描述各[output] section 的具体布局,即链接时确定 input section 到 output section 的映射关系
内核的实际布局非常长,仅截取开头部分,边看边学:
SECTIONS
{
#ifdef CONFIG_X86_32
. = LOAD_OFFSET + LOAD_PHYSICAL_ADDR;
phys_startup_32 = ABSOLUTE(startup_32 - LOAD_OFFSET);
#else
. = __START_KERNEL;
phys_startup_64 = ABSOLUTE(startup_64 - LOAD_OFFSET);
#endif
/* Text and read-only data */
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
_stext = .;
/* bootstrapping code */
HEAD_TEXT
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
KPROBES_TEXT
SOFTIRQENTRY_TEXT
#ifdef CONFIG_RETPOLINE
__indirect_thunk_start = .;
*(.text.__x86.*)
__indirect_thunk_end = .;
#endif
STATIC_CALL_TEXT
ALIGN_ENTRY_TEXT_BEGIN
ENTRY_TEXT
ALIGN_ENTRY_TEXT_END
*(.gnu.warning)
} :text =0xcccc
/* End of text section, which should occupy whole number of pages */
_etext = .;
. = ALIGN(PAGE_SIZE);
X86_ALIGN_RODATA_BEGIN
RO_DATA(PAGE_SIZE)
X86_ALIGN_RODATA_END
/* Data */
.data : AT(ADDR(.data) - LOAD_OFFSET) {
/* Start of data section */
_sdata = .;
/* init_task */
INIT_TASK_DATA(THREAD_SIZE)
/* ... */
关键字 .
是一个位置计数器(location counter),基本能放在 SECTIONS
内任何(一般符号也能放的)地方,而这个计数器是可赋值的,可以通过这种操作来调整 section 的偏移量:
- 比如这里起始设置了
. = __START_KERNEL
,使得位置计数器发生移动,那么.text
的地址就起始于__START_KERNEL
- 反过来,
_text = .
的赋值操作就是定义一个地址在当前.
的_text
符号 - 更多操作可以看这里
顺手提一下
__START_KERNEL
,代码如下:#define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START) #define __START_KERNEL_map _AC(0xffffffff80000000, UL) #define __PHYSICAL_START ALIGN(CONFIG_PHYSICAL_START, \ CONFIG_PHYSICAL_ALIGN) // kconfig config 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. ...
也就是说
__START_KERNEL
对应于内核加载时的起始虚拟地址0xffffffff81000000
,PHYSICAL_START
确定内核加载时的起始地址0x1000000
(详情见这里,还需要考虑CONFIG_RELOCATABLE
)
关键字 AT
指定 LMA 加载地址(前面的 .
是 VMA 虚拟地址),语法层面上是可选项。如前面所示,内核直接映射的物理地址与虚拟地址相差 LOAD_OFFSET
,不再赘述
关键字 *
是一个通配符,用于匹配任何文件名。也就是说 *(.gnu.warning)
表示所有文件中的所有 .gnu.warning
输入节(input section)
:text
表明 .text
会被加载到特定的 segment 即 text
中;=0xcccc
指如果 section 内部存在空洞,则使用魔数 0xcccc
填充。最近有 patch 改成了 0xcccccccc
,见参考链接
关键字 KEEP
用于保留指定的 section,即使内部没有任何符号。这个没有在上述代码有直接体现,但是有大量的大写符号是内核设置的宏,比如 HEAD_TEXT
宏展开后其实是 KEEP(*(.head.text))
。为什么保留我不太关心,知道 .text
包含 .head.text
就好了,这也确定了启动代码始于 startup_64
完
这篇文章其实是按照 vmlinux.lds.S 的顺序来梳理链接脚本的语法,个人来说基本够用。如果看官还不满足于求知欲的话,也可以翻阅参考链接
References
[PATCH] x86/vmlinux: Fix linker fill bytes for ld.lld – LKML.ORG