内存管理-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编辑  收藏  举报

导航