内存管理-19-vmlinux.lds.S分析
基于msm-5.4
一、简介
链接器主要任务是将符号引用解析到符号定义上,将多个目标文件(.o)和库文件合并成为一个可执行文件或者动态链接库,生成符号表,并对程序代码做最后的检查和优化。这个链接脚本在Linux内核里就是 vmlinux.lds.S 文件。
vmlinux.lds.S 编译后会在 out/target 目录下生成一个 vmlinux.lds 文件,这个文件是各种宏展开后的。
二、文件分析
1. SECTIONS
vmlinux.lds.S 文件是一个 "SECTIONS { }" 描述,它是链接脚本的关键命令,用以描述输出文件的内存布局。告诉链接文件如何把输入文件的段映射到输出文件的各个段中,如何将输入段整合为输出段,如何把输出段放入程序地址空间和进程地址空间中。
2. /DISCARD/ 段
这是一个特殊的输出段,被该段引用的任何输入段,将不会出现在输出文件中。
3. .head.text 段
. = KIMAGE_VADDR + TEXT_OFFSET; //0xffffffc010000000 + 0x00080000 .head.text : { _text = .; HEAD_TEXT }
'.' 号是连接脚本中一个特殊的符号,用以表示当前位置计数器。表示将当前位置的地址设置为等号右边的值,此处表示把代码段地址设置为 0xffffffc010080000。
.head.text 是输出段的名称,对应的输入段是 HEAD_TEXT, HEAD_TEXT 定义为 KEEP(*(.head.text))。意思是将所有输入目标文件(.o文件)中的 .head.text 都放入 .head.text 输出段中。
通过 readelf -S vmlinux 来看:
ubuntu:$ readelf -S vmlinux Section Headers: ------------------------------------------------------------------------------------------------------------------------- [Nr] Name Type Address Offset Size EntSize Flags Link Info Align ------------------------------------------------------------------------------------------------------------------------- [ 0] NULL 0000000000000000 00000000 0 0 0 0 0 [ 1] .head.text PROGBITS ffffffc010080000 00010000 0000000000001000 0 AX 0 0 4096 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), p (processor specific)
可以看到每个段的起始地址和占用多大空间,对齐方式,可执行权限等。
4. text代码段
定义代码段,区间为 [_stext, _etext).
.text : { _stext = .; ... } . = ALIGN(SEGMENT_ALIGN); //64k _etext = .;
5. rodata只读数据段
rodata段是一个非常大的段,里面又进行了很多细分。定义只读数据段内容,区间为 [__start_rodata, __end_rodata);
另外,ALIGN(PAGE_SIZE)表示只读数据段的起始地址、结束地址,都需要页对齐。
//vmlinux.lds.S RO_DATA(PAGE_SIZE) //vmlinux.lds.h #define RO_DATA(align) RO_DATA_SECTION(align) //传参align=4096 #define RO_DATA_SECTION(align) \ /* * 1. PCI quirks * 2. Built-in firmware blobs * 3. Kernel symbol table: Normal symbols * 4. Kernel symbol table: GPL-only symbols * 5. Kernel symbol table: Normal unused symbols * 6. Kernel symbol table: GPL-only unused symbols * 7. Kernel symbol table: GPL-future-only symbols * 8. Kernel symbol table: Normal symbols * 9. Kernel symbol table: GPL-only symbols * 10. Kernel symbol table: Normal unused symbols * 11. Kernel symbol table: GPL-only unused symbols * 12. Kernel symbol table: GPL-future-only symbols * 13. Kernel symbol table: strings * 14. __*init sections * 15. Built-in module parameters. * 16. Built-in module versions. */
9. __ex_table 扩展页表段
定义扩展页表段内容,区间为 [__start___ex_table, __stop___ex_table)
//vmlinux.lds.S EXCEPTION_TABLE(8) //vmlinux.lds.h #define EXCEPTION_TABLE(align) \ . = ALIGN(align); \ __ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { \ __start___ex_table = .; \ KEEP(*(__ex_table)) \ __stop___ex_table = .; \ }
10. 页表地址段
. = ALIGN(PAGE_SIZE); idmap_pg_dir = .; . += IDMAP_DIR_SIZE; //IDMAP_PGTABLE_LEVELS * PAGE_SIZE = 3*4K = 12K idmap_pg_end = .; #ifdef CONFIG_UNMAP_KERNEL_AT_EL0 //默认使能 tramp_pg_dir = .; . += PAGE_SIZE; //4K #endif reserved_pg_dir = .; . += PAGE_SIZE; //4K swapper_pg_dir = .; . += PAGE_SIZE; //4K INIT_TEXT_SECTION(8) //.init.text 段, 就是 __init 修饰的函数存放的段
在 .init.text 段之前,会预留一部分虚拟地址空间给一些页表初始化使用。例如,idmap_pg_dir、tramp_pg_dir、reserved_pg_dir、swapper_pg_dir 等。
idmap_pg_dir 是 identity mapping 使用到的页表。
11. init 段
初始化段范围是 [ __init_begin, __init_end),包括了这里的 inittext 段,以及后面 initdata 段,在 kernel_init() 中内核初始化完成,会将这部分内存释放掉,详细看 free_initmem() 函数。
也就是说 init初始化段,并不是Linux内核起始代码段的位置。
. = ALIGN(SEGMENT_ALIGN); //16k __init_begin = .; __inittext_begin = .; INIT_TEXT_SECTION(8) //.init.text 段, 就是 __init 修饰的函数存放的段 __exittext_begin = .; .exit.text : { ARM_EXIT_KEEP(EXIT_TEXT) //*(.exit.text)/*(.text.exit } __exittext_end = .; . = ALIGN(4); .altinstructions : { __alt_instructions = .; *(.altinstructions) __alt_instructions_end = .; } .altinstr_replacement : { *(.altinstr_replacement) } . = ALIGN(PAGE_SIZE); __inittext_end = .; //inittext结束 __initdata_begin = .; initdata开始 .init.data : { INIT_DATA INIT_SETUP(16) INIT_CALLS CON_INITCALL INIT_RAM_FS *(.init.rodata.* .init.bss) /* from the EFI stub */ } .exit.data : { //输出文件中段名 ARM_EXIT_KEEP(EXIT_DATA) //输入目标.o文件中的段名 } PERCPU_SECTION(L1_CACHE_BYTES) .rela.dyn : ALIGN(8) { *(.rela .rela*) } __rela_offset = ABSOLUTE(ADDR(.rela.dyn) - KIMAGE_VADDR); __rela_size = SIZEOF(.rela.dyn); . = ALIGN(SEGMENT_ALIGN); __initdata_end = .; __init_end = .;
释放init段的函数:
//arch/arm64/mm/init.c void free_initmem(void) { free_reserved_area(lm_alias(__init_begin), lm_alias(__init_end), 0, "unused kernel"); unmap_kernel_range((u64)__init_begin, (u64)(__init_end - __init_begin)); }
11.2 inittext 段
链接脚本见"init 段"小节。注意,本小节分析的是 inittext段(自己起的名字),而不是 .init.text 段。
inittext段区间为:[__inittext_begin, __inittext_end),包括:
.init.text *(.exit.text) *(.text.exit)
percpu段竟然也在这个段中。
11.2.1 .init.text 段
通过宏 INIT_TEXT_SECTION() 指定,将目标文件中的 *.init.text, *.init.text.*, *.text.startup 段放入 .init.text 段中。
11.2.2 .exit.text 段
里面存放目标文件的 *(.exit.text), *(.text.exit) 段。
11.3 initdata 段
本节剖析的是 initdata 段(自己起的名字),而不仅仅是 .init.data,initdata 区间为 [__initdata_begin,__initdata_end), 其中包括:
.init.data 段,.exit.data 段,data..percpu 段,.rela.dyn 段。
.init.data 又包括:INIT_DATA, INIT_SETUP, INIT_CALLS, CON_INITCALL, INIT_RAM_FS, .init.rodata.*, .init.bss
注:
INIT_DATA 里面包含众多种类,包括 *.init.rodata, *.init.rodata.* 等等。
INIT_CALLS 区间[__initcall_start, __initcall_end),里面包含0-7的 INIT_CALLS_LEVEL()
initdata 中还有个重要的 .data.percpu 段,通过 PERCPU_SECTION 进行定义。
12. .bss 段
通过宏 BSS_SECTION(0, 0, 0) 定义bss段内容,区间为 [_bss_start, __bss_stop).
13. 镜像结尾段
. = ALIGN(PAGE_SIZE); init_pg_dir = .; . += INIT_DIR_SIZE; init_pg_end = .; __pecoff_data_size = ABSOLUTE(. - __initdata_begin); _end = .;
定义了 init_pg_dir,这是临时页表初始化页表,对于三级页表,会占用 3 个pages 空间。在页表映射完成之后,这部分的内存会被释放掉,变成普通内存供 buddy 使用。
三、代码分析
1. 设置编译到哪个段中
//include/linux/init.h #define __init __section(.init.text) #define __exit __section(.exit.text) static int __init led_init(void) static void __exit led_exit(void)
四、readelf命令
readelf命令在主机和开发板上都存在。
ubuntu: $ readelf --help Usage: readelf <option(s)> elf-file(s) 显示有关 ELF 格式文件内容的信息 选项为: -a --all 相当于:-h -l -S -s -r -d -V -A -I -h --file-header 显示 ELF 文件头 -l --program-headers 显示程序头 --segments --program-headers 的别名 -S --section-headers 显示节的头 --sections --section-headers 的别名 -g --section-groups 显示节组 -t --section-details 显示节详细信息 -e --headers 相当于:-h -l -S -s --syms 显示符号表 ####### 可以对比机器运行状态和vmlinux文件里面的 --symbols --syms 的别名 --dyn-syms 显示动态符号表 -n --notes 显示核心注释(如果存在) -r --relocs 显示重定位(如果存在) -u --unwind 显示展开信息(如果存在) -d --dynamic 显示动态部分(如果存在) -V --version-info 显示版本部分(如果存在) -A --arch-specific 显示体系结构特定信息(如果有) -c --archive-index 显示存档中的符号/文件索引 -D --use-dynamic 显示符号时使用动态部分信息 -x --hex-dump=<number|name> 将 section <number|name> 的内容转储为字节 -p --string-dump=<number|name> 将 section <number|name> 的内容转储为字符串 -R --relocated-dump=<number|name> 将 section <number|name> 的内容转储为重定位字节 -z --decompress 在转储之前解压缩section -w[lLiaprmfFsoRt] 或 --debug-dump[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames, =frames-interp,=str,=loc,=Ranges,=pubtypes,=gdb_index,=trace_info,=trace_abbrev,=trace_aranges,=addr,=cu_index] 显示 DWARF2 调试sections的内容 --dwarf-depth=N 不显示深度为 N 或更大的 DIE --dwarf-start=N 显示以 N 开头、深度相同或更深的 DIE -I --histogram 显示 bucket list 长度的直方图 -W --wide 允许输出宽度超过 80 个字符 @<file> 从 <file> 读取选项 -H --help 显示此信息 -v --version 显示 readelf 的版本号
五、vmlinux.lds.S文件
/* SPDX-License-Identifier: GPL-2.0 */ /* * ld script to make ARM Linux kernel * taken from the i386 version by Russell King * Written by Martin Mares <mj@atrey.karlin.mff.cuni.cz> */ #ifdef CONFIG_QCOM_RTIC #define BSS_FIRST_SECTIONS \ . = ALIGN(PAGE_SIZE); \ KEEP(*(.bss.rtic)) \ . = ALIGN(PAGE_SIZE); #else #define BSS_FIRST_SECTIONS #endif #include <asm-generic/vmlinux.lds.h> #include <asm/cache.h> #include <asm/kernel-pgtable.h> #include <asm/thread_info.h> #include <asm/memory.h> #include <asm/page.h> #include <asm/pgtable.h> #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) //标识是Arm64平台 ENTRY(_text) //指定程序的入口地址是 _text,代码段 _text -- _etext jiffies = jiffies_64; //指定jiffies #define HYPERVISOR_EXTABLE \ . = ALIGN(SZ_8); \ __start___kvm_ex_table = .; \ *(__kvm_ex_table) \ __stop___kvm_ex_table = .; #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); \ __hyp_idmap_text_start = .; \ *(.hyp.idmap.text) \ __hyp_idmap_text_end = .; \ __hyp_text_start = .; \ *(.hyp.text) \ HYPERVISOR_EXTABLE \ __hyp_text_end = .; #define IDMAP_TEXT \ . = ALIGN(SZ_4K); \ __idmap_text_start = .; \ *(.idmap.text) \ __idmap_text_end = .; #ifdef CONFIG_HIBERNATION //默认不使能 #define HIBERNATE_TEXT \ . = ALIGN(SZ_4K); \ __hibernate_exit_text_start = .; \ *(.hibernate_exit.text) \ __hibernate_exit_text_end = .; #else #define HIBERNATE_TEXT #endif #ifdef CONFIG_UNMAP_KERNEL_AT_EL0 #define TRAMP_TEXT \ . = ALIGN(PAGE_SIZE); \ __entry_tramp_text_start = .; \ *(.entry.tramp.text) \ . = ALIGN(PAGE_SIZE); \ __entry_tramp_text_end = .; #else #define TRAMP_TEXT #endif /* * 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 SECTIONS { /* * XXX: The linker does not define how output sections are * assigned to input sections when there are multiple statements * matching the same input section name. There is no documented * order of matching. */ /DISCARD/ : { ARM_EXIT_DISCARD(EXIT_TEXT) ARM_EXIT_DISCARD(EXIT_DATA) EXIT_CALL *(.discard) *(.discard.*) *(.interp .dynamic) *(.dynsym .dynstr .hash .gnu.hash) *(.eh_frame) } . = KIMAGE_VADDR + TEXT_OFFSET; //0xffffffc010000000 + 0x00080000 内核起始虚拟地址+偏移32K的位置(内核镜像前16K预留出来给初始化页表使用) .head.text : { _text = .; HEAD_TEXT } .text : { /* Real text segment */ _stext = .; /* Text and read-only data */ __exception_text_start = .; *(.exception.text) __exception_text_end = .; IRQENTRY_TEXT //定义在vmlinux.lds.h, 存放 *(.irqentry.text) SOFTIRQENTRY_TEXT //定义在vmlinux.lds.h, 存放 *(.softirqentry.text) ENTRY_TEXT //定义在vmlinux.lds.h, 存放 *(.entry.text) TEXT_TEXT //定义在vmlinux.lds.h, 存放 *(.test.*) SCHED_TEXT //定义在vmlinux.lds.h, 存放 *(.sched.text) CPUIDLE_TEXT //定义在vmlinux.lds.h, 存放 *(.cpuidle.text) LOCK_TEXT //定义在vmlinux.lds.h, 存放 *(.spinlock.text) KPROBES_TEXT //定义在vmlinux.lds.h, 存放 *(.kprobes.text) HYPERVISOR_TEXT //定义在本文件中,hypervisor相关 IDMAP_TEXT //定义在本文件中,存放 *(.idmap.text) HIBERNATE_TEXT //默认空实现 TRAMP_TEXT *(.fixup) *(.gnu.warning) . = ALIGN(16); *(.got) /* Global offset table */ } . = ALIGN(SEGMENT_ALIGN); _etext = .; /* End of text section */ RO_DATA(PAGE_SIZE) /* everything from this point to */ EXCEPTION_TABLE(8) /* __init_begin will be marked RO NX */ NOTES . = ALIGN(PAGE_SIZE); idmap_pg_dir = .; . += IDMAP_DIR_SIZE; idmap_pg_end = .; #ifdef CONFIG_UNMAP_KERNEL_AT_EL0 tramp_pg_dir = .; . += PAGE_SIZE; #endif reserved_pg_dir = .; . += PAGE_SIZE; swapper_pg_dir = .; . += PAGE_SIZE; . = ALIGN(SEGMENT_ALIGN); //16k __init_begin = .; __inittext_begin = .; INIT_TEXT_SECTION(8) //.init.text 段, 就是 __init 修饰的函数存放的段 __exittext_begin = .; .exit.text : { ARM_EXIT_KEEP(EXIT_TEXT) } __exittext_end = .; . = ALIGN(4); .altinstructions : { __alt_instructions = .; *(.altinstructions) __alt_instructions_end = .; } .altinstr_replacement : { *(.altinstr_replacement) } . = ALIGN(PAGE_SIZE); __inittext_end = .; __initdata_begin = .; .init.data : { INIT_DATA INIT_SETUP(16) INIT_CALLS CON_INITCALL INIT_RAM_FS *(.init.rodata.* .init.bss) /* from the EFI stub */ } .exit.data : { ARM_EXIT_KEEP(EXIT_DATA) } PERCPU_SECTION(L1_CACHE_BYTES) .rela.dyn : ALIGN(8) { *(.rela .rela*) } __rela_offset = ABSOLUTE(ADDR(.rela.dyn) - KIMAGE_VADDR); __rela_size = SIZEOF(.rela.dyn); #ifdef CONFIG_RELR //默认不使能 .relr.dyn : ALIGN(8) { *(.relr.dyn) } __relr_offset = ABSOLUTE(ADDR(.relr.dyn) - KIMAGE_VADDR); __relr_size = SIZEOF(.relr.dyn); #endif . = ALIGN(SEGMENT_ALIGN); __initdata_end = .; __init_end = .; _data = .; _sdata = .; //数据段,已初始化 RW_DATA_SECTION(L1_CACHE_BYTES, PAGE_SIZE, THREAD_ALIGN) /* * Data written with the MMU off but read with the MMU on requires * cache lines to be invalidated, discarding up to a Cache Writeback * Granule (CWG) of data from the cache. Keep the section that * requires this type of maintenance to be in its own Cache Writeback * Granule (CWG) area so the cache maintenance operations do not * interfere with adjacent data. */ .mmuoff.data.write : ALIGN(SZ_2K) { __mmuoff_data_start = .; *(.mmuoff.data.write) } . = ALIGN(SZ_2K); .mmuoff.data.read : { *(.mmuoff.data.read) __mmuoff_data_end = .; } PECOFF_EDATA_PADDING __pecoff_data_rawsize = ABSOLUTE(. - __initdata_begin); _edata = .; BSS_SECTION(0, 0, 0) //bss段 . = ALIGN(PAGE_SIZE); init_pg_dir = .; . += INIT_DIR_SIZE; init_pg_end = .; __pecoff_data_size = ABSOLUTE(. - __initdata_begin); _end = .; STABS_DEBUG HEAD_SYMBOLS } #include "image-vars.h" /* * The HYP init code and ID map text can't be longer than a page each, * and should not cross a page boundary. */ ASSERT(__hyp_idmap_text_end - (__hyp_idmap_text_start & ~(SZ_4K - 1)) <= SZ_4K, "HYP init code too big or misaligned") ASSERT(__idmap_text_end - (__idmap_text_start & ~(SZ_4K - 1)) <= SZ_4K, "ID map text too big or misaligned") #ifdef CONFIG_HIBERNATION ASSERT(__hibernate_exit_text_end - (__hibernate_exit_text_start & ~(SZ_4K - 1)) <= SZ_4K, "Hibernate exit text too big or misaligned") #endif #ifdef CONFIG_UNMAP_KERNEL_AT_EL0 ASSERT((__entry_tramp_text_end - __entry_tramp_text_start) <= 3*PAGE_SIZE, "Entry trampoline text too big") #endif /* * If padding is applied before .head.text, virt<->phys conversions will fail. */ ASSERT(_text == (KIMAGE_VADDR + TEXT_OFFSET), "HEAD is misaligned")
六、vmlinux.lds 文件
这个是宏展开后的。
七、补充
1. 打印页表相关的值
idmap_pg_dir=0xffffffc01221a000 init_pg_end=0xffffffc0137be000 swapper_pg_dir=0xffffffc01221f000 idmap_pg_dir=0xffffffc01221a000 idmap_pg_end=0xffffffc01221d000 tramp_pg_dir=0xffffffc01221d000
reserved_pg_dir=0xffffffc01221e000 IDMAP_DIR_SIZE=0x3000 PAGE_KERNEL=0x68000000000713
可以看到都是虚拟地址。还可以cat /proc/kallsyms 查看。
2. _text 和 _end
_text 和 _end 两个变量,分别是kernel代码链接的开始和结束地址。编译器的链接地址实际上就是最后代码期望运行的虚拟地址。在KASLR关闭或不生效的情况下就是kernel image需要映射的虚拟地址。当我们编译kernel后,可以根据符号表System.map或/proc/kallsyms文件查看哪些函数被放在哪些段中。
posted on 2024-07-15 20:45 Hello-World3 阅读(187) 评论(0) 编辑 收藏 举报