全部博文(44)
分类: LINUX
2008-09-19 20:07:02
前面提到,只读数据被放到了text segment,链接脚本中的RODATA宏完成了这项工作。RODATA创建了大量不同名称的section,它们有些是内置的,有些则是自定义的。创建方式并无特别之处,有了前面的知识,你可以轻易的看懂它们。这里要说的是关于自定义section的第二个例子——内核符号表。
读过LDD的朋友都知道,在module中导出符号给内核其它部分应该使用__ksymtab,我们也经常在内核中看到类似的代码,如:
EXPORT_SYMBOL(boot_cpu_data);
但,内核是怎么做的?符号表如何被创建?如果你看了/path_to_your_kernel_src/include/linux/module.h中EXPORT_SYMBOL的定义,再配合自定义section的知识,很快就能明白内核只是创建了一个名为__ksymtab的自定义section,当调用EXPROT_SYMBOL宏时会生成一个struct kernel_symbol变量记录下函数/数据的名称和地址,最后将这个变量存入__ksymtab section中。RODATA宏的如下代码:
/* Kernel symbol table: __ksymtab : AT(ADDR(__ksymtab) - LOAD_OFFSET) { \ VMLINUX_SYMBOL(__start___ksymtab) = .; \ *(__ksymtab) \ VMLINUX_SYMBOL(__stop___ksymtab) = .; \ } \
将输入文件中的__kysmtab section合并生成新的__ksymtab section,这就是内核最终的导出符号表,同样,__start___ksymtab和__stop___ksymtab记录下了表的起始地址和结束地址。如此一来,动态加载module时内核如何将module中调用的函数替换成相应的地址就不难理解了吧。
链接脚本知识:
或许你已经注意到,上述创建__ksymtab section的代码中,并没有在最后加上:text标明将该section放到text segment。实际上这是链接脚本的一个简化,当没有为section指定segment时,以上一个明确指定的segment为准。例如之前最后一次明确指定segment的__ex_table section指定了text segment,则其后没有指定segment的section也被放到了text segment,直到下一次明确指定segment的section出现为止。
从现在开始,所有的section都归属于data segment。与text segment不同的是,data segment的所有section都是可写的。实际上我已经不需要继续写下去,因为data segment的创建中并没有新奇的链接脚本语法出现。有趣的是,我们发现大量的代码编译后产生的二进制也被放到了data segment(从常理看,它们应该被放到text segment)。这些代码在内核里非常常见,都被__init宏或__initcall宏修饰。内核把它们放到data segment的原因很简单,它们只在初始化阶段有用,一旦进入正常运行阶段,这些代码所在的页面将被回收以作它用。同样会被回收的section还有被__initdata、__setup_param等宏修饰的变量。这些宏会生产名为.init.*或*.init的自定义section,内核在初始化完成后回收它们占用的页面。如何回收这些页面的内容不在本文讨论范围之内,感兴趣的朋友可以grep free_initmem()函数,看看内核在何时调用它。这里我们举自定义section的第三个例子,从技术上来说它和前两个例子并没有什么差别,它较常见于内核代码但多数人不一定了解它的原理,故这里特别提出来说一下。
我们经常在驱动或内核子系统的代码中看到由__initcall、fs_initcall、arch_initcall等类似的宏修饰的函数名,例如:
fs_initcall(acpi_event_init);
很多资料告诉我们这些宏定义了函数的初始化级别,内核会在初始化的不同阶段调用它们,级别从0~7不等。实际上这也是自定义section的应用,原理跟内核符号表的创建一样。寻根究底,这些宏都是由宏__define_initcall生成的,其定义如下:
#define __define_initcall(level,fn,id) \ static initcall_t __initcall_##fn##id __used \ __attribute__((__section__(".initcall" level ".init"))) = fn
其中level参数即0~7(实际是0~7s)共14个级别,这样内核在编译时会生产14个名为”.initcall.(0~7s).init”的section,例如.initcall0.init、.initcall2s.init等。被不同宏修饰的函数被放到对应的section中,例如上例的fs_initcall(acpi_event_init)最终会被放到.initcall5.init。
链接脚本的如下代码:
.initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) { __initcall_start = .; INITCALLS __initcall_end = .; }
生成了initcall表,起始地址和结束地址存在__initcall_start和__initcall_end中。很多资料说根据level参数的值不同,内核在不同阶段调用这些函数。但从代码来看,我认为并非如此,内核只分两个阶段调用,即early initcall阶段和剩余阶段(level = 0~7s)。感兴趣的朋友可以看看上面INITCALLS宏的展开以及do_initcalls()、do_pre_smp_initcalls()两个函数,很容易就能明白。
在构建data segment的最后部分,我们看到如下代码:
.bss : AT(ADDR(.bss) - LOAD_OFFSET) { __init_end = .; __bss_start = .; /* BSS */ *(.bss.page_aligned) *(.bss) . = ALIGN(4); __bss_stop = .; _end = . ; /* This is where the kernel creates the early boot page tables */ . = ALIGN(PAGE_SIZE); pg0 = . ; }
它告诉我们.bss section位于data segment的最后,变量pg0存放的是“Provisional kernel Page Tables”的地址,不熟悉的朋友可以阅读ULK3的2.5.5.1节。
最后,我们列出几个著名的由链接脚本提供的全局变量:
名称 |
描述 |
_text |
text segment的起始地址,也是内核image的起始地址 |
_etext |
内核代码段的结束地址,仅仅是代码段,因为text segment还包含.rodata、exception table、note segment |
_edata |
不好描述具体含义,见下图 |
_end |
内核image的结束地址 |
上述描述不一定准确,实际上你只要在链接脚本中一看它们出现的位置就能很快知道其含义,也可以在合适的位置打印它们的值验证一下,例如setup_arch()函数中。读过ULK的朋友一定想起一副熟悉的图,把它粘贴如下:
图4. 著名的全局变量布局(摘自《Understanding Linux Kernel》)
前面我们提过并非所有输入文件中的section都会出现在目标文件中,对于不感兴趣的section,链接脚本用下列代码抛弃它们。
/* Sections to be discarded */ /DISCARD/ : { *(.exitcall.exit) } |
通过学习内核链接脚本,笔者最大的收获不是了解了内核image的布局,而是通过自定义的section并配合链接脚本来构建动态表的方式(之所以说是动态表,是因为它的长度由加入该section的元素个数决定,并非事先定义好的)。也许你说一个全局的大数组也可以做到,但这样坏处是数组要预定义到最够大,其次它占用的内存在内核运行时释放不掉,最糟糕的是这个数组必须在整个内核空间共享,这样你才能在需要往里添加元素的时候访问到它。这种污染整个名字空间的设计无疑是糟糕的,把工作交给链接器是最好的选择。
最后我建议感兴趣的朋友尝试试试这种方式,把你自定义的section放到data segment,你会发现数据在section中的排列顺序和链接器从输入文件中抽取section的顺序有关。嗯,exception文档提到.text section没有这个问题,还需要研究研究 ……
[1]. ,
[2]. Vendor-specific ELF Note Elements,