内核映像的形成——链接vmlinux

2.2.5 链接vmlinux

当我们前面$(vmlinux-dirs)目标的工作做完后,也就是形成了各目录中的built-in.o文件,那么就会回到$(sort $(vmlinux-init) $(vmlinux-main)) $(vmlinux-lds)所对应的目标中,这个目标是一行空命令,看到699行:

699 vmlinux-init := $(head-y) $(init-y)

700 vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)

701 vmlinux-all  := $(vmlinux-init) $(vmlinux-main)

702 vmlinux-lds  := arch/$(SRCARCH)/kernel/vmlinux.lds

 

我们先来看vmlinux-lds,其值被赋成了arch/x86/kernel/vmlinux.lds,这个文件前面提过了,它是用来链接vmlinux的,后面会详细讲解。vmlinux-init的值是由head-yinit-y变量组成。init-y前面讲过了,head-y在我们的arch/x86/Makefile118具体这里就不再赘述了我们关注的还是思路。vmlinux-main的值,我们看到了core-ylibs-ydrivers-ynet-y都是些熟面孔前面讲得很多了,略过。

 

空命令执行完后,就会来到vmlinux.o目标,来自869行:

869 vmlinux.o: $(modpost-init) $(vmlinux-main) FORCE

870        $(call if_changed_rule,vmlinux-modpost)

 

这个if_changed_rule是我们第一次见到,在scripts/Kbuild.include220行定义:

if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ),                 /

       @set -e;                                                             /

       $(rule_$(1)))

 

这个if_changed_rule的作用就是既检查依赖的更新也检查命令行参数的更新。所以,call函数之后,if_changed_rule的值变为rule_ vmlinux-modpost,它定义在顶层Makefile841行:

841 define rule_vmlinux-modpost

842        :

843        +$(call cmd,vmlinux-modpost)

844        $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost $@

845        $(Q)echo 'cmd_$@ := $(cmd_vmlinux-modpost)' > $(dot-target).cmd

846 endef

 

cmd也来自scripts/Kbuild.include

cmd = @$(echo-cmd) $(cmd_$(1))

用来打印命令,后面还有很长一串依赖,我们就不去管它了。844行,翻译过来就是:

@make –f scripts/Makefile.modpost vmlinux.o

 

Makefile.modpost是什么呢?这里补充一个KBuild的知识。在scripts目录下有如下重要的编译规则文件:

kbuild.include —— 共用的定义。会被所有独立的Makefile包括,Makefile.build

Makefile.build —— 提供编译built-in.o, lib.a等规则的Makefile

Makefile.lib —— 负责归类分析obj-y obj-m和其中的目录subdir-ym

Makefile.host —— 主机程序hostprog编译规则

Makefile.clean —— clean规则

Makefile.headerinst —— 头文件install规则

Makefile.modinst —— 模块install规则

Makefile.modpost —— 模块编译的第二阶段,.o.mod生成<module>.ko

 

所以,Makefile.modpost是代表一种编译规则,负责将相应的.o对象文件编译成.ko文件。由于有个FORCE,所以在这一步,即使之前刚刚生成了vmlinux.o文件(我猜想,KBuild可能规定,最顶层的目录不会生成built-in.o,而是vmlinux.o),也会执行这个命令。vmlinux.o是怎么生成的呢?看看.vmlinux.o.cmd

cmd_vmlinux.o := ld -m elf_i386 -r -o vmlinux.o arch/x86/kernel/head_64.o arch/x86/kernel/head64.o arch/x86/kernel/head.o arch/x86/kernel/init_task.o  init/built-in.o --start-group  usr/built-in.o  arch/x86/built-in.o  kernel/built-in.o  mm/built-in.o  fs/built-in.o  ipc/built-in.o  security/built-in.o  crypto/built-in.o  block/built-in.o  lib/lib.a  arch/x86/lib/lib.a  lib/built-in.o  arch/x86/lib/built-in.o  drivers/built-in.o  sound/built-in.o  firmware/built-in.o  arch/x86/pci/built-in.o  arch/x86/power/built-in.o  arch/x86/video/built-in.o  net/built-in.o --end-group

 

几乎涵盖了最重要,最核心的内核部分,所以make之后,我机器上最后生成的vmlinux.o将近200MB。继续走,再来看到kallsyms.o目标,是在776行定义的

770 ifdef CONFIG_KALLSYMS_EXTRA_PASS

771 last_kallsyms := 3

772 else

773 last_kallsyms := 2

774 endif

776 kallsyms.o := .tmp_kallsyms$(last_kallsyms).o

我们看到前面make menuconfig在主目录中生成的.config126行有

126 CONFIG_KALLSYMS=y

127 CONFIG_KALLSYMS_ALL=y

128 CONFIG_KALLSYMS_EXTRA_PASS=y

 

所以这里last_kallsyms的值为3kallsyms.o变量的值就是.tmp_kallsyms3.o这是一个隐藏文件也是一个make目标。那么,KALLSYMS究竟是个什么东西呢?百度之。在2.6内核中,为了更好地调试内核,引入了kallsyms kallsyms 抽取内核用到的所有函数地址(全局的,静态的)和非栈数据变量地址,生成一个数据小块( data blog ),作为只读数据链接进kernel image 。相当于内核中存在了一份System.map .内核可以根据符号名查出函数的地址。当系统发生oops 时,(不懂oops?好吧,内核崩溃总懂了吧)kallsymsprint_symbol 函数会显示相关信息,以此形式跟踪栈的状态。

 

原来这是一个内核调试选项,主要回去执行752833行的那些依赖目标,我们就不去详细分析它了,只要知道它最后会生成.tmp_vmlinux1.tmp_vmlinux2.tmp_vmlinux3这三个文件就行了。

 

走到这里,vmlinux的所有依赖都完成了,所有的目录也都生成了其对于的built-in.o文件。

849 vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o) FORCE

850 ifdef CONFIG_HEADERS_CHECK

851        $(Q)$(MAKE) -f $(srctree)/Makefile headers_check

852 endif

853 ifdef CONFIG_SAMPLES

854        $(Q)$(MAKE) $(build)=samples

855 endif

856 ifdef CONFIG_BUILD_DOCSRC

857        $(Q)$(MAKE) $(build)=Documentation

858 endif

859        $(call vmlinux-modpost)

860        $(call if_changed_rule,vmlinux__)

861        $(Q)rm -f .old_version

 

CONFIG_HEADERS_CHECK.config里没有设置;CONFIG_SAMPLES设置了,所以854行回去链接samples目录下的built-in.oCONFIG_BUILD_DOCSRC.config里也没有设置,所以直接来到859行,vmlinux-modpost。找遍了,都没有定义这个东西,所以忽略。860行,转变一下if_changed_rule就是“rule_vmlinux__”,所以看到734行:

734 define rule_vmlinux__

735        :

736        $(if $(CONFIG_KALLSYMS),,+$(call cmd,vmlinux_version))

738        $(call cmd,vmlinux__)

739        $(Q)echo 'cmd_$@ := $(cmd_vmlinux__)' > $(@D)/.$(@F).cmd

741        $(Q)$(if $($(quiet)cmd_sysmap),                                      /

742          echo '  $($(quiet)cmd_sysmap)  System.map' &&)                     /

743        $(cmd_sysmap) $@ System.map;                                         /

744        if [ $$? -ne 0 ]; then                                               /

745               rm -f $@;                                                    /

746               /bin/false;                                                  /

747        fi;

748        $(verify_kallsyms)

749  endef

 

还记得@D@F吧,看来前面的预备知识是很有必要的,定义模式规则里面讲了,一个是目录部分,一个是文件部分。当然这里@D是空的,@F自然就是vmlinux。所以上面这一大段Makefile的代码你看不懂没关系,只要知道739行的目的就是把制作vmlinux的最后一步打印出来,并重定向到顶层目录的.vmlinux.cmd文件中。我们来看看这个文件的内容:

cmd_vmlinux := ld -m elf_i386  -o vmlinux -T arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_64.o arch/x86/kernel/head64.o arch/x86/kernel/head.o arch/x86/kernel/init_task.o  init/built-in.o --start-group  usr/built-in.o  arch/x86/built-in.o  kernel/built-in.o  mm/built-in.o  fs/built-in.o  ipc/built-in.o  security/built-in.o  crypto/built-in.o  block/built-in.o  lib/lib.a  arch/x86/lib/lib.a  lib/built-in.o  arch/x86/lib/built-in.o  drivers/built-in.o  sound/built-in.o  firmware/built-in.o  arch/x86/pci/built-in.o  arch/x86/power/built-in.o  arch/x86/video/built-in.o  net/built-in.o --end-group .tmp_kallsyms3.o

 

跟前面制作vmlinux.o的内容差不多,但是,很重要一点,多了一个

-T arch/x86/kernel/vmlinux.lds

.tmp_kallsyms3.o

 

.tmp_kallsyms3.o就是把内核跟踪模块,不去管它了。重点来看第一个,我们看到arch/x86/kernel/head.o处于vmlinux 的最前面,其主要作用就是初始化中断描述符表IDT ,内存页目录表GDT ,把系统从64位段式寻址的保护模式跳到64位页式寻址的保护模式( Enable paging ) .head.S 中首先定义的就是:

.section .text.head"ax"@progbits

ENTRY(startup_64)

starup_64会在稍后的vmlinux.ld中看到。

 

ld 使用了链接脚本:arch/x86/kernel/vmlinux.lds。一般来说普通程序是不需要指定linker script 也不需要关心各个section 的具体位置的。当程序执行时, kernel 中的ELF Loader 会依据ELF Program header 解析可执行文件的各个SECTION,并把它们映射到虚拟地址空间。然而,内核启动时,必须首先确定各个section的具体位置,这就是vmlinux.lds 的作用。

 

注意,vmlinux.lds是个C程序,是在“递归编译各对象”时,生成的。至于是如何生成的,感兴趣的同志可以去研究研究arch/x86/kernel/.vmlinux.lds.cmd文件。生成之后,我们看到它前面大部分是注释,到404行开始才是实际内容:

/* 定义了输出格式和平台*/

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")

OUTPUT_ARCH(i386)

/* 定义phys_startup_32为程序入口点*/

ENTRY(phys_startup_32)

/* jiffesjiffies_64地址相同,也就是说jiffies_64的低32位就是jiffies */

jiffies_64 = jiffies;

/* 指定ELF Program header 可以用objdump -p 查看有关可执行文件的结构的相关知识我们会在相关的博客中补上 */

PHDRS {

 text PT_LOAD FLAGS(5); /* R_E */

 data PT_LOAD FLAGS(7); /* RWE */

 note PT_NOTE FLAGS(0); /* ___ */

}

/* 随后定义输入文件的section如何组织到输出文件的section */

SECTIONS

{

/* 起始VMA此时线性地址就是虚拟地址-0xC0000000地址为0xC0100000, startup_32也为0xC0100000 */

        . = 0xC0000000 + ((0x1000000 + (0x400000 - 1)) & ~(0x400000 - 1));

        phys_startup_32 = startup_32 - 0xC0000000;

/* _text地址为当前物理地址是 0x1000000head.o会放到此处*/

.text : AT(ADDR(.text) - 0xC0000000) {

  _text = .;

  /* bootstrapping code */

  *(.head.text)

  . = ALIGN((1 << 12));

  *(.text.page_aligned)

  . = ALIGN(8);

  _stext = .;

  . = ALIGN(8); *(.text.hot) *(.text) *(.ref.text) *(.devinit.text) *(.devexit.text) *(.cpuinit.text) *(.cpuexit.text) *(.text.unlikely)

  . = ALIGN(8); __sched_text_start = .; *(.sched.text) __sched_text_end = .;

  . = ALIGN(8); __lock_text_start = .; *(.spinlock.text) __lock_text_end = .;

  . = ALIGN(8); __kprobes_text_start = .; *(.kprobes.text) __kprobes_text_end = .;

 

  *(.fixup)

  *(.gnu.warning)

  /* End of text section */

  _etext = .;

 } :text = 0x9090  /*合并section中的空隙用0x9090填充*/

 

nm命令查看一下:

[root@localhost linux-2.6.34.1]# nm vmlinux|grep " _text"

c0100000 T _text

[root@ localhost linux-2.6.34.1]# nm vmlinux|grep startup_32

00100000 A phys_startup_32

c0100000 T startup_32

c01000c0 T startup_32_smp

 

要阅读这个.lds文件必须对.lds的语法有一定了解,本着学习知识的原则,还是多多少少地讲一点把。.lds首先最重要的是定义程序的入口,ENTRY(phys_startup_32)指明程序的入口点:phys_startup_32标号;随后.=0xC0100000指明代码段起始虚拟地址;.text : { ....}表示从该位置开始放置所有目标文件的代码段,里面又分几个小区段。又如:

_text = .;

_stext = .;

__sched_text_start = .;

这些是代码段.text又分的几个小区段,设置_text这样的符合作为地址常量

 

再来,每个小区段中,_stext*(.text.hot) *(.text) *(.ref.text) *(.devinit.text) *(.devexit.text) *(.cpuinit.text) *(.cpuexit.text) *(.text.unlikely)意思是将所有输入文件的.text.hot.text.ref.text等段何必到这里。这些段来自哪里说白了就是来自前面看到.vmlinux.cmd文件中的那些built-in.o文件中。

 

再解释一下ALIGN(8)表示.(.current location counter)必需要对齐如果没有这行的话会有隐患我想是总线数据必须以8位对齐以增加效率吧

 

我这里只总结一下最顶层有那些段,详细的大家可以去看一下自己生成的文件。.text段最重要的是_text,表示代码段开始地址;_stext表示实际代码段(除去前面的head部分)起始地址;_etext表示代码段结束地址。代码段结束后,然后是. notes段,即摘要段,主要存放一下内核版本信息。

 

接下来是几个符号地址__ex_table,对应代码里的__ex_table表它看成异常段的入口地址。当异常发生时,内核的异常响应会从这里开始搜索search_exception_table(unsigned long addr)看能不能找到相应的异常处理办法。接下来是.rodata段,只读数据段(read-only data section,  rodata),用于存放一些debugfixup的数据,程序无法对其内容进行修改。

 

然后就是千呼万唤始出来的.data段,内核数据段。跟.text不一样,它没有_data,直接是_sdata打头,_edata结尾。随后是.vsyscall_0.vsyscall_fn.vsyscall_gtod_data.vsyscall_clock

.vsyscall_1.vsyscall_2.vgetcpu_mode.jiffies.vsyscall_3这些段。他们都是一些全局变量,我们最熟悉的莫过于jiffies了,其实vsyscall也很重要,用于快速加载系统调用号到eax寄存器。其他的段我们就不去讨论他们了,感兴趣的可以深入研究一下。

 

接下来是内核初始化相关的段,主要有.init.begin.init.text.init.data等段,最后以.init.end段的__init_end符号结束。接下来还有两个重要的段,.bss段和.brk段,分别以__bss_start开始__bss_stop结束和以__brk_base开始__brk_limit结束。BSS段包含了内核中未初始化全局变量,在内存中 bss段全部置零。BRK段是保留给用户进程通过系统调用brk() 向内核申请的内存空间。最后,以符号_end结束了整个SECTIONS 的工作,vmlinux的链接也就结束了。

你可能感兴趣的:(cmd,header,Build,documentation,makefile,linker)