当我们前面$(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-y和init-y变量组成。init-y前面讲过了,head-y呢,在我们的arch/x86/Makefile的118行,具体这里就不再赘述了,我们关注的还是思路。vmlinux-main的值,我们看到了core-y、libs-y、drivers-y、net-y,都是些熟面孔,前面讲得很多了,略过。
空命令执行完后,就会来到vmlinux.o目标,来自869行:
869 vmlinux.o: $(modpost-init) $(vmlinux-main) FORCE
870 $(call if_changed_rule,vmlinux-modpost)
这个if_changed_rule是我们第一次见到,在scripts/Kbuild.include的220行定义:
if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ), /
@set -e; /
$(rule_$(1)))
这个if_changed_rule的作用就是既检查依赖的更新也检查命令行参数的更新。所以,call函数之后,if_changed_rule的值变为rule_ vmlinux-modpost,它定义在顶层Makefile的841行:
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在主目录中生成的.config的126行有:
126 CONFIG_KALLSYMS=y
127 CONFIG_KALLSYMS_ALL=y
128 CONFIG_KALLSYMS_EXTRA_PASS=y
所以,这里last_kallsyms的值为3,kallsyms.o变量的值就是.tmp_kallsyms3.o,这是一个隐藏文件,也是一个make目标。那么,KALLSYMS究竟是个什么东西呢?百度之。在2.6内核中,为了更好地调试内核,引入了kallsyms 。kallsyms 抽取内核用到的所有函数地址(全局的,静态的)和非栈数据变量地址,生成一个数据小块( data blog ),作为只读数据链接进kernel image 。相当于内核中存在了一份System.map .内核可以根据符号名查出函数的地址。当系统发生oops 时,(不懂oops?好吧,内核崩溃总懂了吧)kallsyms的print_symbol 函数会显示相关信息,以此形式跟踪栈的状态。
原来这是一个内核调试选项,主要回去执行752到833行的那些依赖目标,我们就不去详细分析它了,只要知道它最后会生成.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.o;CONFIG_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)
/* jiffes和jiffies_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地址为当前物理地址是 0x1000000,head.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),用于存放一些debug或fixup的数据,程序无法对其内容进行修改。
然后就是千呼万唤始出来的.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的链接也就结束了。