linux内核连接脚本,linux内核链接脚本详解

3.3 构造Section

在构建了基本的segment后,就可以从输入.o文件中获取感兴趣的section以生成新的section并放入相应的segment。在这里,输入的section称为input

section,生成的新section称为output section。除此之外,有一个重要的链接脚本符号“.”需要了解。”.”是个位置计数器,记录着当前位置在目标文件中的虚拟地址(VMA)。”.”是个自动增加的计数器,当一个output

section生成后,”.”的值自动加上该output section的长度。我们也可以显式的给”.”赋值以改变当前位置的地址,这在内核链接脚本中被大量使用。一个例子可以很好的描述”.”的作用:

. = 0x100000;

_start_addr = .;

.text : { *(.text) }

_end_addr = . ;

这里我们首先给”.”赋了一个初值,将地址指定到0x100000处,并将该值赋给变量_start_addr,它是.text section的起始地址;接着我们生成了一个.text

section,此时”.”自动加上该section的长度,可描述为. = . + SIZEOF(.text);最后将”.”赋值给_end_addr,记录下.text的结束地址。此时”.”的值变成了0x100000 + SIZEOF(.text)。有了”.”的帮助,我们可以灵活的控制目标文件中各个section所在的虚拟地址(VMA)。

3.3.1 Text Segment的构造

内核首先构造的是text segment,该segment又由若干个.text.*节构成,除此之外,它还包含了note segment的内容以及只读数据section。下面的代码完成了这些工作:

SECTIONS

{

. = LOAD_OFFSET + LOAD_PHYSICAL_ADDR;

phys_startup_32 = startup_32 - LOAD_OFFSET;

.text.head : AT(ADDR(.text.head) - LOAD_OFFSET) {

_text = .;               /* Text and read-only data */

*(.text.head)

} :text = 0x9090

/* read-only */

.text : AT(ADDR(.text) - LOAD_OFFSET) {

. = ALIGN(PAGE_SIZE); /* not really needed, already page aligned */

*(.text.page_aligned)

TEXT_TEXT

SCHED_TEXT

LOCK_TEXT

KPROBES_TEXT

*(.fixup)

*(.gnu.warning)

_etext = .;                     /* End of text section */

} :text = 0x9090

NOTES :text :note

. = ALIGN(16);         /* Exception table */

__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {

__start___ex_table = .;

*(__ex_table)

__stop___ex_table = .;

} :text = 0x9090

RODATA

SECTIONS

{

. = LOAD_OFFSET + LOAD_PHYSICAL_ADDR;

phys_startup_32 = startup_32 - LOAD_OFFSET;

.text.head : AT(ADDR(.text.head) - LOAD_OFFSET) {

_text = .;               /* Text and read-only data */

*(.text.head)

} :text = 0x9090

/* read-only */

.text : AT(ADDR(.text) - LOAD_OFFSET) {

. = ALIGN(PAGE_SIZE); /* not really needed, already page aligned */

*(.text.page_aligned)

TEXT_TEXT

SCHED_TEXT

LOCK_TEXT

KPROBES_TEXT

*(.fixup)

*(.gnu.warning)

_etext = .;                     /* End of text section */

} :text = 0x9090

NOTES :text :note

. = ALIGN(16);         /* Exception table */

__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {

__start___ex_table = .;

*(__ex_table)

__stop___ex_table = .;

} :text = 0x9090

RODATA

首先是SECTIONS关键字,官方的解释是“The SECTIONS command tells the linker how to map input sections into output sections, and how to place the output sections in memory.”,实际上可以把它看成一个描述符,所有的工作要在它的内部完成。就像你在C中定义一个结构体要以struct关键字开头一样。

构造的第一步是为”.”指定初值,之后所有section虚拟地址(VMA)都由该值计算得来(前面我们讲过,生成一个section后”.”的值会自动加上改section的长度)。这里初始值为

LOAD_OFFSET + LOAD_PHYSICAL_ADDR,前者是我们熟知的内核虚拟地址空间起始地址0xC0000000,LOAD_PHYSICAL_ADDR是内核image加载的物理地址,由CONFIG_PHYSICAL_START计算得到。该物理地址是可以指定的,你可以在.config文件中找到它,也可以由make

menuconfig得到,具体解释参考arch/x86/Kconfig文件的PHYSICAL_START条目。对于一般的x86架构,内核被加载到物理地址0x100000处,故”.”的初值为0xC0100000。接着

phys_startup_32 = startup_32 - LOAD_OFFSET;

计算了内核image的入口地址,这在前面已经提到。

开始构造section了。由于使用的语法是固定的,我们只需要了解一个例子,其余的就可举一反三。以第一个section为例:

.text.head : AT(ADDR(.text.head) - LOAD_OFFSET) {

_text = .;               /* Text and read-only data */

*(.text.head)

} :text = 0x9090

.text.head : AT(ADDR(.text.head) - LOAD_OFFSET) {

_text = .;               /* Text and read-only data */

*(.text.head)

} :text = 0x9090

.text.head指定了生成的section的名字,后面的冒号是固定语法。AT关键字前面介绍过,指定该section的加载地址(LMA),它的完整表达是

AT(expression)

括号中expression表达式指定LMA的值。在此例中该表达式由

ADDR(.text.head) - LOAD_OFFSET

计算得到。这里

ADDR(section)

计算section的虚拟地址,故.text.head的加载地址(LMA)是它的物理地址。在大括号内部,_text = .;

定义了一个全局变量,它的值为”.”的当前值,记录了整个text segment的起始地址.。在这里,由于_text变量前还没有任何section被创建,故_text有如下等价关系:

_text = ADDR(.text.head) = . = LOAD_OFFSET + LOAD_PHYSICAL_ADDR;

*(.text.head)完成了具体的section创建工作,”*”代表所有输入的.o文件,括号中的.text.head指定了链接器感兴趣的section名。

*(text.head)

表示从所有输入文件中抽取名为.text.head的section并填充到目标文件的.text.head section中。

: text

指定了新生成section所在的segment,这里冒号后的text是segment名,可见内核的第一个section被放到了text

segment。

= 0x9090

指定section的填充内容。从输入文件中抽取来的section由于代码对齐的缘故,其二进制的存放可能是不连续的,这里指定对section中的空隙用0x9090进行填充。0x90是汇编指令NOP的机器码,故相当于在不连续代码间填充空操作。至此,内核的第一个section就创建好了,它名为.text.head,由输入文件的.text.head

section构成(并非所有文件都有.text.head section,链接器只从具有该section的文件中抽取内容),该section的虚拟地址(VMA)由”.”的值确定,加载地址(LMA)为其物理地址,section中不连续区域产生的间隙由0x9090填充,最后该section被放入了内核的text

segment中。

通过objdump内核,我们可以看到关于该section的最终内容:

Sections:

Idx Name          Size      VMA       LMA       File off  Algn

0 .text.head    00000375  c1000000  01000000  00001000  2**2

CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

…………………………………………………………………………………………..

Disassembly of section .text.head:

c1000000 <_text>:

c1000000:>--f6 86 11 02 00 00 40 >--testb  $0x40,0x211(%esi)

c1000007:>--75 14                >--jne    c100001d <_text>

c1000009:>--0f 01 15 1a e1 4d 01 >--lgdtl  0x14de11a

>--->--->---c100000c: R_386_32>-boot_gdt_descr

c1000010:>--b8 18 00 00 00       >--mov    $0x18,%eax

c1000015:>--8e d8                >--mov    %eax,%ds

…………………………………………………………………………………………………

c10013d5:>--5b                   >--pop    %ebx

c10013d6:>--5e                   >--pop    %esi

c10013d7:>--c9                   >--leave--

c10013d8:>--c3                   >--ret----

c10013d9:>--90                   >--nop----

c10013da:>--90                   >--nop----

c10013db:>--90                   >--nop----

c10013dc:>--90                   >--nop----

Sections:

Idx Name          Size      VMA       LMA       File off  Algn

0 .text.head    00000375  c1000000  01000000  00001000  2**2

CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

…………………………………………………………………………………………..

Disassembly of section .text.head:

c1000000 <_text>:

c1000000:>--f6 86 11 02 00 00 40 >--testb  $0x40,0x211(%esi)

c1000007:>--75 14                >--jne    c100001d <_text>

c1000009:>--0f 01 15 1a e1 4d 01 >--lgdtl  0x14de11a

>--->--->---c100000c: R_386_32>-boot_gdt_descr

c1000010:>--b8 18 00 00 00       >--mov    $0x18,%eax

c1000015:>--8e d8                >--mov    %eax,%ds

…………………………………………………………………………………………………

c10013d5:>--5b                   >--pop    %ebx

c10013d6:>--5e                   >--pop    %esi

c10013d7:>--c9                   >--leave--

c10013d8:>--c3                   >--ret----

c10013d9:>--90                   >--nop----

c10013da:>--90                   >--nop----

c10013db:>--90                   >--nop----

c10013dc:>--90                   >--nop----

c10013dd:>--90                   >--nop----

c10013de:>--90                   >--nop----

c10013df:>--90                   >--nop----

其中最后一部分显示了填充0x9090产生的nop指令。

链接脚本知识:

创建一个section的完整格式是:

section [address] [(type)] : [AT(lma)]

{

output-section-command

output-section-command

...

} [>region] [:phdr :phdr ...] [=fillexp]

其中[address]参数在上例中没有提到,它指定了section的虚拟地址(VMA),如果没有指定该参数及region参数,section的虚拟地址由当前”.”的值确定,正如上例我们看到的一样。Region用于将section分配给通过MEMORY关键字创建的内存描述块,内核链接脚本没使用它,本文也不关注,具体内容详见参考文献1的MEMORY

command一节。

通过这个例子,我们很容易就可以理解text segment中其它section的创建。例如接下来的第二个.text section,它的创建方法和.text.head类似,唯一不同的是这里多了一句:

. = ALIGN(PAGE_SIZE);

ALIGN(exp)关键字计算当前”.”值对齐到exp边界后的地址,即:

ALIGN(exp) = ( . + exp – 1) & ~(exp – 1);

此处在创建.text section前,将”.”对齐到了页边界,从第一个输入section的名字.text.page_aligned就可以看出,输入section的内容是有对齐要求的。内核使用了TEXT_TEXT等宏将不同类型的输入section进行了封装,展开后可以看到它们都是:

*(section_name)

的形式,和我们前面讲的一样,不再多做介绍。

从上面内容可以看出,输入文件中的section有各种各样的名字,如.text.head、.text.page_aligned、.text.hot等,并不是所有的section名都是标准的,绝大部分是内核使用GCC扩展生成的自定义名。举个例子,我们常见的__init宏,展开后如下:

#define __init __attribute__ ((__section__(“.init.text”)))

这里.init.text是个自定义的section,用__init修饰的函数编译后会被放到名为.init.text section中。

自定义的section极大的发挥了链接脚本的作用,让我们可以对代码中的函数、数据进行归类操作,同时还可以完成一些在程序中不易完成的功能。这很容易理解,如果我们都用GCC内置的section,何必要自定义链接脚本,用默认的不就好了。

链接脚本向我们展示了大量的自定义section,本人水平有限,无法一一弄清每个section的用途,但通过几个常见的典型例子,我们可以了解它们的用法。首先就以text segment中的exception

table举例。

3.1.1 Exception Table

此exception table不是用于处理硬件异常的(那是IDT表的工作),但它确实和硬件异常有一点关系,具体来说是和Page Fault有关系。Exception

Table的具体机制在内核文档”Exception”中有详细介绍,你可以在/path_to_your_kernel_src/ Documentation/exception.txt中找到它。这里为了说明问题做一点简要介绍。

我们尊敬的Linus大神为了避免内核在访问用户态地址时进行有效性检查带来的开销(我们总是需要这样的检查,虽然大部分情况下结果是成功的),利用了page fault的处理函数来完成这项任务,这样只有在真正访问了一个坏的用户态地址时检查才会发生。或许你会问:此时检查有什么用?一个例子就很容易说明问题,假设我们有一个函数叫is_user_addr_ok(),用于检查传入的用户态地址是否合法。那么,当地址非法时它能干什么?什么都不能干,仅仅是告诉内核:“这是个非法地址,你不要访问”。这样便带来了个问题,让它在90%的时间里告诉内核:“这是个合法地址,去吧!”是件很无聊的事情。既然该函数对非法地址无能为力,我们干脆就什么都不要干,直到内核真访问到一个非法地址时再告诉调用者:“噢,抱歉,您访问到一个非法地址。”不管用哪种方法,调用者遇到非法地址最终结果都是获得一个错误码,但后者明显省下了对合法地址进行检查的开销。让我们来看看如何用自定义section完成这个任务。

如果你顺着copy_from_user()向下找几层,会看到__get_user_asm宏,该宏展开后可读性太差,我们用下面的伪代码来描述它:

1:      movb (%from),(%to)  /*这里访问用户态地址,当地址非法时会产生一个page fault*/

2:

/*注意,后面的代码在最终的目标文件中不是跟在标号2后的*/

.section .fixup,"ax"

3:      movl $ERROR_CODE,%eax

xorb %dl,%dl

jmp 2b

.section __ex_table,"a"

.align 4

.long 1b,3b

上面的伪代码描述了__get_user_asm宏的用途,它将用户态地址from中的内容拷贝到内核地址to。当from是个非法地址时,会产生page

fault从而执行内核的do_page_fault(),在进行一系列检查处理后fixup_exception()被调用,该函数会调用search_exception_tables()查找exception

table,将EIP设置成对应handler的地址并返回。至此该非法地址造成的错误就交由exception table中的handler处理了。

所有问题的归结到了exception table的建立和错误处理handler的设置。其实上面的伪代码已经告诉我们答案了。首先,标号”1”代表了可能产生page

fault的EIP,当page fault产生时这个地址会被记录在struct pt_regs的ip字段中(不知道的看看do_page_fault()的参数);其次,标号”3”是错误处理handler的地址,很明显,它只是返回了一个错误码(EAX是x86的返回值寄存器)。jmp

2b跳到了产生page fault的指令的下一条指令继续执行。这里

.section .fixup,"ax"

创建了名为.fixup的自定义section,并将整个handler放入其中。标号”1”后的代码是位于.text

section的,故你看到它们在源代码里写在了一起,但在目标文件中去是分开的,它们在不同的section。

好了,我们已经有了会产生错误的代码地址,也有了错误处理handler的地址,

.section __ex_table,"a"

将它们放到了自定义的__ex_table section中(.long 1b,3b),以如下格式存放:

出错地址,处理函数地址

内核用结构体struct exception_table_entry表示该格式,定义如下:

struct exception_table_entry {

unsigned long insn, fixup;

};

很明显,exception table的格式简单,表项的前4个字节是出错地址,后4个字节是处理函数的地址。下图展示了通过exception

table解决一次访问用户态非法地址产生的错误。

34550493_2.jpg

图3.通过execption table处理非法用户态地址访问的过程

怎样,所有的事实都清楚了。当在内核在不同位置调用copy_from_user()时,展开的__get_user_asm宏都会将可能出错的地址和处理函数的地址存入该源文件对应.o文件的__ex_table

section中。链接脚本的如下代码:

__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {

__start___ex_table = .;

*(__ex_table)

__stop___ex_table = .;

} :text = 0x9090

将分散的__ex_table收集起来产生一张完整的exception表,并将表的起始地址和结束地址记录在__start___ex_table和__stop___ex_table两个全局变量中,从而search_exception_tables()函数可以顺利的索引该表。

这种通过自定义section和链接脚本构造表的技巧被大量使用,后面我们还会看到两个例子。在此先告一段落。

3.3.2 Note Segment

前面提到内核image分为三个segment,其中就有note segment,它是包含在text segment中的。NOTE

segment被用于不同的vendor在ELF文件中添加一些标识,让运行这些二进制代码的系统确定能否为该ELF提供其所需要的系统调用接口。它对我们了解内核用处不大,详细内容参见参考文献2。

NOTES :text :note

上面代码中,NOTE是一个宏,展开的格式和构建其它section的格式一样,这里”:text:note”表示把生成的section即加入text

segment又加入note segment。从objdump的内容可以看到后者包含在前者之中,如下:

LOAD off    0x00001000 vaddr 0xc1000000 paddr 0x01000000 align 2**12

filesz 0x004de000 memsz 0x004de000 flags r-x

NOTE off    0x0037b844 vaddr 0xc137a844 paddr 0x0137a844 align 2**2

filesz 0x00000024 memsz 0x00000024 flags ---

LOAD off    0x00001000 vaddr 0xc1000000 paddr 0x01000000 align 2**12

filesz 0x004de000 memsz 0x004de000 flags r-x

NOTE off    0x0037b844 vaddr 0xc137a844 paddr 0x0137a844 align 2**2

filesz 0x00000024 memsz 0x00000024 flags ---

3.3.3 .rodata section的构造

前面提到,只读数据被放到了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: Normal symbols */                \

__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出现为止。

3.3.4 Data Segment的构建

从现在开始,所有的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)

}

4. 做一些尝试

通过学习内核链接脚本,笔者最大的收获不是了解了内核image的布局,而是通过自定义的section并配合链接脚本来构建动态表的方式(之所以说是动态表,是因为它的长度由加入该section的元素个数决定,并非事先定义好的)。也许你说一个全局的大数组也可以做到,但这样坏处是数组要预定义到最够大,其次它占用的内存在内核运行时释放不掉,最糟糕的是这个数组必须在整个内核空间共享,这样你才能在需要往里添加元素的时候访问到它。这种污染整个名字空间的设计无疑是糟糕的,把工作交给链接器是最好的选择。

最后我建议感兴趣的朋友尝试试试这种方式,把你自定义的section放到data segment,你会发现数据在section中的排列顺序和链接器从输入文件中抽取section的顺序有关。嗯,exception文档提到.text section没有这个问题,还需要研究研究 ……

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值