前言

内核 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 命令(后面再介绍)下的引用,并不存在于实际文件中;typeflags 自行参考 ELF 相关文档;FILEHDRPHDRS 并没有在内核中使用到,仅做点搬运: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 对应于内核加载时的起始虚拟地址 0xffffffff81000000PHYSICAL_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

Linker Scripts – OSDev Wiki

LD – sourceware.org

[PATCH] x86/vmlinux: Fix linker fill bytes for ld.lld – LKML.ORG

Linux source code (v6.4.8) – bootlin