如何解读vmlinux.lds.S

2019-07-13 04:42发布

vmlinux.lds.S用于对ld的输出进行组版,这个文件的格式在ld.info手册中有详细的说明。vmlinu
x.lds.S的主要目的是对输出文件中段进行排序,并定义相关的符号名,以下是简要注释。
/* ld script to make i386 Linux kernel * Written by Martin Mares ; */ OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386") OUTPUT_ARCH(i386) /* 输出格式 */ ENTRY(_start) /* 定义_start作为入口点 */ SECTIONS { . = PAGE_OFFSET_RAW + 0x100000; /* 定义当前段的偏移量(.代表当前计数器) */ _text = .; /* 定义符号_text为当前位置 */ .text : { /* 定义段.text (": {"是段定义符)*/ *(.text) /* 将所有输入文件中.text段合并到这里 */ *(.fixup) /* 将所有输入文件中的.fixup段合并到这里 */ *(.gnu.warning) /* 将所有输入文件中的.gnu.warning段合并到这里 */ } = 0x9090 /* 合并中的空隙用0x9090填充 */ /* 以下的语法含义可以类推 */ .text.lock : { *(.text.lock) } /* out-of-line lock text */ .rodata : { *(.rodata) } .kstrtab : { *(.kstrtab) } . = ALIGN(16); /* Exception table */ __start___ex_table = .; /* 定义__start_ex_table符号为当前位置 */ __ex_table : { *(__ex_table) } __stop___ex_table = .; __start___ksymtab = .; /* Kernel symbol table */ __ksymtab : { *(__ksymtab) } __stop___ksymtab = .; _etext = .; /* End of text section */ .data : { /* Data */ *(.data) CONSTRUCTORS /* 将C++的构造函数指针段合并到这里 */ } _edata = .; /* End of data section */ . = ALIGN(8192); /* init_task */ .data.init_task : { *(.data.init_task) } . = ALIGN(4096); /* Init code and data */ __init_begin = .; .text.init : { *(.text.init) } .data.init : { *(.data.init) } . = ALIGN(4096); /* 输出计数器在页边界上对齐 */ __init_end = .; . = ALIGN(32); .data.cacheline_aligned : { *(.data.cacheline_aligned) } . = ALIGN(4096); .data.page_aligned : { *(.data.idt) } __bss_start = .; /* BSS */ .bss : { *(.bss) } _end = . ; /* Stabs debugging sections. */ .stab 0 : { *(.stab) } /* 0 是段属性,代表段的起始地址 */ .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) } } 以下是用"objdump --headers vmlinux"得到的组版结果: vmlinux: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 0009ccb8 c0100000 c0100000 00001000 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .text.lock 00000622 c019ccc0 c019ccc0 0009dcc0 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .rodata 0000f4ab c019d2e4 c019d2e4 0009e2e4 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .kstrtab 00002d9c c01ac78f c01ac78f 000ad78f 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 __ex_table 00000ba8 c01af530 c01af530 000b0530 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 __ksymtab 00001870 c01b00d8 c01b00d8 000b10d8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .data 000133c0 c01b1950 c01b1950 000b2950 2**4 CONTENTS, ALLOC, LOAD, DATA 7 .data.init_task 00002000 c01c6000 c01c6000 000c6000 2**2 CONTENTS, ALLOC, LOAD, DATA 8 .text.init 0000868e c01c8000 c01c8000 000c8000 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 9 .data.init 00003220 c01d0690 c01d0690 000d0690 2**2 CONTENTS, ALLOC, LOAD, DATA 10 .data.cacheline_aligned 00001c20 c01d4000 c01d4000 000d4000 2**2 CONTENTS, ALLOC, LOAD, DATA 11 .data.page_aligned 00000800 c01d6000 c01d6000 000d6000 2**2 CONTENTS, ALLOC, LOAD, DATA 12 .bss 0001f324 c01d6800 c01d6800 000d6800 2**4 ALLOC 13 .comment 000011d0 00000000 00000000 000d6800 2**0 CONTENTS, READONLY 14 .note 000011d0 000011d0 000011d0 000d79d0 2**0 CONTENTS, READONLY 更多



vmlinuz自然就是内核了,initrd.img是一个小的映象,包含一个最小的linux系统。通常的步骤是先启动内核,然后内核挂载initrd.img,并执行里面的脚本来进一步挂载各种各样的模块,然后发现真正的root分区,挂载并执行/sbin/init... ...。
initrd.img当然是可选的了,如果没有initrd.img,内核就试图直接挂载root分区。
之所以要有initrd,那是为了启动的时候有更大的灵活性。比如,你把ext3支持编译成模块了。偏偏你的root分区又是ext3的。这下就麻烦了。因为内核需要挂载root分区之后才能加载ext3支持。但是没有ext3支持就没法挂载root分区。initrd就是用来解决这个问题的。
类似的用这个东西还可以做其他的事情,比如从usb盘启动linux也会面临上面类似的问题。用initrd就能搞定了。
甚至,我想在有些嵌入式设备里面都不需要真正的root分区,用initrd就足够搞定一切了。
一位网友写得一篇文章,写得很好,加深了对Linux启动的认识,贴在这里:
内核编译链接过程是依靠vmlinux.lds文件,以arm为例vmlinux.lds文件位于kernel/arch/arm/vmlinux.lds,但是该文件是由vmlinux-armv.lds.in生成的,根据编译选项的不同源文件还可以是vmlinux-armo.lds.in,vmlinux-armv-xip.lds.in。
vmlinux-armv.lds的生成过程在kernel/arch/arm/Makefile中
LDSCRIPT     = arch/arm/vmlinux-armv.lds.in
arch/arm/vmlinux.lds: arch/arm/Makefile $(LDSCRIPT) /
 $(wildcard include/config/cpu/32.h) /
 $(wildcard include/config/cpu/26.h) /
 $(wildcard include/config/arch
        _stext = .;
        __init_begin = .;
            *(.text.init)
        __proc_info_begin = .;
            *(.proc.info)
        __proc_info_end = .;
        __arch_info_begin = .;
            *(.arch.info)
        __arch_info_end = .;
        __tagtable_begin = .;
            *(.taglist)
        __tagtable_end = .;
            *(.data.init)
        . = ALIGN(16);
        __setup_start = .;
            *(.setup.init)
        __setup_end = .;
        __initcall_start = .;
            *(.initcall.init)
        __initcall_end = .;
        . = ALIGN(4096);
        __init_end = .;
    }
   
其中TEXTADDR就是内核启动的虚拟地址,定义在kernel/arch/arm/Makefile中:
ifeq ($(CONFIG_CPU_32),y)
PROCESSOR    = armv
TEXTADDR     = 0xC0008000
LDSCRIPT     = arch/arm/vmlinux-armv.lds.in
endif
需要注意的是这里是虚拟地址而不是物理地址。
一般情况下都在生成vmlinux后,再对内核进行压缩成为zImage,压缩的目录是kernel/arch/arm/boot。
下载到flash中的是压缩后的zImage文件,zImage是由压缩后的vmlinux和解压缩程序组成,如下图所示:
            |-----------------|/    |-----------------|
            |                 | /   |                 |
            |                 |  /  | decompress code |
            |     vmlinux     |   / |-----------------|    zImage
            |                 |    /|                 |
            |                 |     |                 |
            |                 |     |                 |   
            |                 |     |                 |
            |                 |    /|-----------------|
            |                 |   /
            |                 |  /
            |                 | /
            |-----------------|/
           
zImage链接脚本也叫做vmlinux.lds,位于kernel/arch/arm/boot/compressed。
是由同一目录下的vmlinux.lds.in文件生成的,内容如下:
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
 {
   . = LOAD_ADDR;
   _load_addr = .;
 
   . = TEXT_START;
   _text = .;
 
   .text : {
     _start = .;
    
其中LOAD_ADDR就是zImage中解压缩代码的ram偏移地址,TEXT_START是内核ram启动的偏移地址,这个地址是物理地址。
在kernel/arch/arm/boot/Makefile文件中定义了:
ZTEXTADDR   =0
ZRELADDR     = 0xa0008000
ZTEXTADDR就是解压缩代码的ram偏移地址,ZRELADDR是内核ram启动的偏移地址,这里看到指定ZTEXTADDR的地址为0,
明显是不正确的,因为我的平台上的ram起始地址是0xa0000000,在Makefile文件中看到了对该地址设置的几行注释:
# We now have a PIC decompressor implementation.  Decompressors running
# from RAM should not define ZTEXTADDR.  Decompressors running directly
# from ROM or Flash must define ZTEXTADDR (preferably via the config)
他的意识是如果是在ram中进行解压缩时,不用指定它在ram中的运行地址,如果是在flash中就必须指定他的地址。所以这里将ZTEXTADDR指定为0,也就是没有真正指定地址。
在kernel/arch/arm/boot/compressed/Makefile文件有一行脚本:
SEDFLAGS    = s/TEXT_START/$(ZTEXTADDR)/;s/LOAD_ADDR/$(ZRELADDR)/;s/BSS_START/$(ZBSSADDR)/
使得TEXT_START = ZTEXTADDR,LOAD_ADDR = ZRELADDR。
这样vmlinux.lds的生成过程如下:
vmlinux.lds:    vmlinux.lds.in Makefile $(TOPDIR)/arch/$(ARCH)/boot/Makefile $(TOPDIR)/.config
 @sed "$(SEDFLAGS)" < vmlinux.lds.in > $@
 
以上就是我对内核启动地址的分析,总结一下内核启动地址的设置:
1、设置kernel/arch/arm/Makefile文件中的
   TEXTADDR     = 0xC0008000
   内核启动的虚拟地址
2、设置kernel/arch/arm/boot/Makefile文件中的
   ZRELADDR     = 0xa0008000
   内核启动的物理地址
   如果需要从flash中启动还需要设置
   ZTEXTADDR地址。