在linux内核中,arch目录下放置的是关于linux内核所支持的具体架构相关的代码描述文件。其中在具体架构目录下的kernel
目录中都会有一个链接脚本文件。
现代软件工程中,一个大的程序通常都由多个源文件组成,其中包含以高级计算机语言编写的源文件以及汇编语言编写的汇编文件。在编译构建过程中会分别对这些源文件进行汇编或者编译并生成目标文件,这些目录文件包含代码段、数据段、符号表等内容。而链接则是把这些目标文件的代码段、数据段以及符号表等内容收集起来并按照某种格式(例如ELF)组合成一个可执行二进制文件的过程。而这个过程是使用链接器来完成的。
链接器在链接过程中会使用到一个链接脚本文件,该文件用于描述链接的过程,当没有通过“-T”参数指定链接脚本时,链接器会使用内置的链接脚本。
链接脚本控制着如何把输入文件中的段合并到输出文件的段中,以及这些段的地址空间布局等。本质上则是把在编译构建过程中大量的二进制文件(.o文件)合并成一个可执行的二进制文件。
本文以ARM64架构为例,首先贴上链接脚本的完整内容,后面会详细描述。linux内核针对ARM64架构的链接脚本放置于/arch/arm64/kernel/vmlinux.lds.S
文件中:
/*
* ld script to make ARM Linux kernel
* taken from the i386 version by Russell King
* Written by Martin Mares
*/
#include
#include
#include
#include
#include
#include "image.h"
/* .exit.text needed in case of alternative patching */
#define ARM_EXIT_KEEP(x) x
#define ARM_EXIT_DISCARD(x)
OUTPUT_ARCH(aarch64)
ENTRY(_text)
jiffies = jiffies_64;
#define HYPERVISOR_TEXT \
/* \
* Align to 4 KB so that \
* a) the HYP vector table is at its minimum \
* alignment of 2048 bytes \
* b) the HYP init code will not cross a page \
* boundary if its size does not exceed \
* 4 KB (see related ASSERT() below) \
*/ \
. = ALIGN(SZ_4K); \
VMLINUX_SYMBOL(__hyp_idmap_text_start) = .; \
*(.hyp.idmap.text) \
VMLINUX_SYMBOL(__hyp_idmap_text_end) = .; \
VMLINUX_SYMBOL(__hyp_text_start) = .; \
*(.hyp.text) \
VMLINUX_SYMBOL(__hyp_text_end) = .;
/*
* The size of the PE/COFF section that covers the kernel image, which
* runs from stext to _edata, must be a round multiple of the PE/COFF
* FileAlignment, which we set to its minimum value of 0x200. 'stext'
* itself is 4 KB aligned, so padding out _edata to a 0x200 aligned
* boundary should be sufficient.
*/
PECOFF_FILE_ALIGNMENT = 0x200;
#ifdef CONFIG_EFI
#define PECOFF_EDATA_PADDING \
.pecoff_edata_padding : { BYTE(0); . = ALIGN(PECOFF_FILE_ALIGNMENT); }
#else
#define PECOFF_EDATA_PADDING
#endif
#if defined(CONFIG_DEBUG_ALIGN_RODATA)
#define ALIGN_DEBUG_RO . = ALIGN(1<phys conversions will fail.
*/
ASSERT(_text == (PAGE_OFFSET + TEXT_OFFSET), "HEAD is misaligned")
在vmlinux.lds.S
文件的开始处,会使用#include
包含头文件,这一点与C语言类似:
#include
#include
#include
#include
#include
#include "image.h"
在以上列出的头文件中,大多会使用宏定义方式编写特定段的描述内容,用于在vmlinux.lds.S
文件中引用。
OUTPUT_ARCH(aarch64)
语句用于
ENYRY(_text)
语言用于设置程序的入口为_text
,程序执行的第一条指令称为入口点(entry point)。除了这种方式,还有其他的方式:
(1)在GCC工具链的LD命令通过“-e”参数指定入口点。
(2)在链接脚本中通过ENTRY
命令设置入口点。
(3)通过特定符号(例如start
符号)设置入口点。
(4)使用代码段的起始地址。
(5)使用地址0。
在上述五种方式中,链接器会依次尝试来设置入口点,直到成功为止。
接下来设置jiffies
参数值:jiffies = jiffies_64;
,jiffies_64
定义在/kernel/time/timer.c文件中:
接着定义HYPERVISOR_TEXT
代码段:
#define HYPERVISOR_TEXT \
. = ALIGN(SZ_4K); \
VMLINUX_SYMBOL(__hyp_idmap_text_start) = .; \
*(.hyp.idmap.text) \
VMLINUX_SYMBOL(__hyp_idmap_text_end) = .; \
VMLINUX_SYMBOL(__hyp_text_start) = .; \
*(.hyp.text) \
VMLINUX_SYMBOL(__hyp_text_end) = .;
SECTIONS{}
是链接脚本语法中的关键命令,用于描述输出文件的内存布局。SECTIONS
命令告诉链接文件如何把输入文件的段映射到输出文件的各个段中,如何将输入端整合为输出段,如何把输出段放入程序地址空间和进程地址空间中。
在开始之前,先描述两个linux内核中重要的知识点:
(1)在链接脚本中,有一个特殊的符号:“.”,用于表示当前位置计数器。在vmlinux.lds.S
文件中很多地方都会使用到。
(2)在链接脚本中有一个常用的编程技巧:为每个段(或者多个段)设置一些符号,用于标识内存位置的开始和结束,这样便可以在C语言代码中访问每个段(或者多个段)的起始地址和结束地址。
在SECTIONS{}
中最先开始的是:
/DISCARD/ : {
ARM_EXIT_DISCARD(EXIT_TEXT)
ARM_EXIT_DISCARD(EXIT_DATA)
EXIT_CALL
*(.discard)
*(.discard.*)
}
/DISCARD/
是一个特殊的输出段,被该段引用的任何输入段将不会出现在输出文件中。
接着是_text
段:
. = PAGE_OFFSET + TEXT_OFFSET;
.head.text : {
_text = .;
HEAD_TEXT
}
上述. = PAGE_OFFSET + TEXT_OFFSET;
意思是把代码段的链接地址设置为PAGE_OFFSET + TEXT_OFFSET
的计算值。PAGE_OFFSET
表示内核空间和用户空间对虚拟地址空间的划分,TEXT_OFFSET
表示代码段的偏移地址。
.head.text
表示输出段,对应的输入段为HEAD_TEXT,本质为*(.head.text)
。意思是将所有目标文件中的.head.text
段放入.head.text
输出段中。其中_text = .;
用于标识_text
段的开始。
接下来是.text
输出段,本质上是代码段:
.text : { /* Real text segment */
_stext = .; /* Text and read-only data */
__exception_text_start = .;
*(.exception.text)
__exception_text_end = .;
IRQENTRY_TEXT
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
HYPERVISOR_TEXT
*(.fixup)
*(.gnu.warning)
. = ALIGN(16);
*(.got) /* Global offset table */
}
上述代码会汇集目标文件中的多个输入段到.text
中。例如:.exception.text
、.irqentry.text
、.sched.text
、.spinlock.text
、.hyp.idmap.text
等。
接下来则是RO_DATA(PAGE_SIZE)
宏代表的只读数据段,该宏定义非常长(此处不展开)。紧随其后的是异常表段:
#define EXCEPTION_TABLE(align) \
. = ALIGN(align); \
__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___ex_table) = .; \
*(__ex_table) \
VMLINUX_SYMBOL(__stop___ex_table) = .; \
}
接下来放置.notes
段:
#define NOTES \
.notes : AT(ADDR(.notes) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start_notes) = .; \
*(.note.*) \
VMLINUX_SYMBOL(__stop_notes) = .; \
}
上述内容就是text
和rodata
段的定义了,最后以_etext = .
位置计数器结束。
接下来是与初始化相关的段,由[__init_begin , __init_end]
符号标识:
__init_begin = .;
INIT_TEXT_SECTION(8)
.exit.text : {
ARM_EXIT_KEEP(EXIT_TEXT)
}
ALIGN_DEBUG_RO_MIN(16)
.init.data : {
INIT_DATA
INIT_SETUP(16)
INIT_CALLS
CON_INITCALL
SECURITY_INITCALL
INIT_RAM_FS
}
.exit.data : {
ARM_EXIT_KEEP(EXIT_DATA)
}
PERCPU_SECTION(64)
. = ALIGN(PAGE_SIZE);
__init_end = .;
接着是.altinstructions
和.altinstr_replacement
两个输出段。
后面是[_data , _edata]
符号代表的数据相关段:
_data = .;
_sdata = .;
RW_DATA_SECTION(64, PAGE_SIZE, THREAD_SIZE)
PECOFF_EDATA_PADDING
_edata = .;
然后是BSS
相关段:BSS_SECTION(0, 0, 0)
。
最后以_end = .;
标识linux内核的结束。然后还放置了与stab
相关的调试段:
#define STABS_DEBUG \
.stab 0 : { *(.stab) } \
.stabstr 0 : { *(.stabstr) } \
.stab.excl 0 : { *(.stab.excl) } \
.stab.exclstr 0 : { *(.stab.exclstr) } \
.stab.index 0 : { *(.stab.index) } \
.stab.indexstr 0 : { *(.stab.indexstr) } \
.comment 0 : { *(.comment) }
在内存布局的最后会放置HEAD_SYMBOLS
代表的三个符号标志:
#define HEAD_SYMBOLS \
_kernel_size_le = DATA_LE64(_end - _text); \
_kernel_offset_le = DATA_LE64(TEXT_OFFSET); \
_kernel_flags_le = DATA_LE64(__HEAD_FLAGS);
_kernel_offset_le
是镜像从RAM开始加载的偏移量(小端序)。
_kernel_flags_le
是信息标志(小端序)。
_kernel_offset_le
表示linux内核镜像的有效大小(小端序)。
在内核镜像生成过程中,上述三个符号标志代表的值会作为镜像头的一部分输出。
上述内容对linux内核的vmlinux.lds.S
进行了描述,已经知道在内存布局的开始处放置的是.head.text
输出段,这正是linux内核的“头”,对应的输入段为*(.head.text)
。在linux内核源码中,在arch/arm64/kernel/head.S
文件中则描述了.head.text
段:
本文主要描述了linux内核针对ARM64的链接脚本文件vmlinux.lds.S
,寻找linux内核镜像的入口点。不同架构下的vmlinux.lds.S
文件内容大多不同,需要具体查看。
总而言之,linux内核镜像中的组成内容由链接脚本控制,从链接脚本和head.S
启动汇编代码中可以寻找到linux内核镜像的入口点。