12.1Linux0.11支持的目标文件为了生成内核代码文件,Linux0.11使用了两种编译器。第一种是汇编编译器as86和相应的链接程序ld86。它们专门用于编译和链接运行在实地址模式下的16位内核引导扇区程序bootsect.s和设置程序setup.s。第二种是GNU的汇编器gas和C语言编译器gcc以及相应的链接程序gld。编译器用于为源程序文件产生含有生成的二进制代码和数据的目标文件。链接程序用于对相关的所有目标文件进行组合处理,形成一个可被内核加载执行的目标文件,即可执行文件。下面我们首先简单说明编译器产生的目标文件结构,然后描述链接器如何把需要链接在一起的目标文件模块组合在一起,以生成二进制可执行映像文件或一个大的模块文件。最后说明Linux0.11内核二进制代码文件Image的生成原理和过程。有关目标文件和链接程序的基本工作原理可参见JohnR.Levine著的《Linkers&Loaders》一书,这里仅给出了能够理解编译链接所生成的Linux0.11内核代码文件的信息以及Linux0.11内核所支持的a.out目标文件格式。as86和ld86生成的是MINIX专门的目标文件格式,因为其结构与a.out目标文件格式类似,因此这里就不再说明。为便于描述,这里把编译器生成的目标文件称为目标模块文件(简称模块文件),而把链接程序输出产生的可执行目标文件称为可执行文件。并且把它们都统称为目标文件。2.1.1目标文件格式C语言编译器gcc和汇编器gas编译生成的a.out格式的目标模块文件或链接生成的可执行文件含有7个部分:a)执行头部分(execheader)。执行文件头部分。该部分中含有一些参数(exec结构),是有关目标文件的整体结构信息。例如代码和数据部分的长度、未初始化数据区的长度、对应源程序文件名以及目标文件创建时间等。内核使用这些参数把执行文件加载到内存中并执行,而链接程序(ld)使用这些参数将一些模块文件组合成一个可执行文件。这是目标文件唯一必要的组成部分。b)代码段部分(textsegment)。由编译器或汇编器生成的二进制指令代码和数据信息,含有程序执行时被加载到内存中的指令代码和相关数据。可以以只读形式被加载。c)数据段部分(datasegment)。由编译器或汇编器生成的二进制指令代码和数据信息,这部分含有已经初始化过的数据,总是被加载到可读写的内存中。d)代码重定位部分(textrelocations)。这部分含有供链接程序使用的记录数据。在组合目标模块文件时用于定位代码段中的指针或地址。当链接程序需要改变目标代码的地址时就需要修正和维护这些地方。e)数据重定位部分(datarelocations)。类似于代码重定位部分的作用,但是用于数据段中指针的重定位。f)符号表部分(simboltable)。这部分同样含有供链接程序使用的记录数据。这些记录数据保存着模块文件中定义的全局符号以及需要从其他模块文件中输入的符号,或者是由链接器定义的符号,用于在模块文件之间对命名的变量和函数(符号)进行交叉引用。g)字符串表部分(stringtable)。该部分含有与符号名相对应的字符串。用于调试程序调试目标代码,与链接过程无关。这些信息可包含源程序代码和行号、局部符号以及数据结构描述信息等。在Linux0.11系统中,GNUgcc或gas编译输出的目标模块文件和链接程序所生成的可执行文件都使用了UNIX传统的a.out格式。这是一种被称为汇编与链接输出(Assembly&linkereditoroutput)的目标文件格式。对于具有内存分页机制的系统来说,这是一种简单有效的目标文件格式。a.out格式文件由一个文件头和随后的代码部分(Textsection,也称为文本段)、已初始化数据部分(Datasection,也称为数据段)、重定位信息部分、符号表以及符号名字符串构成,见图1所示。其中代码部分和数据部分通常也被分别称为文本段(代码段)和数据段。2图1a.out格式的目标文件一个指定的目标文件并非一定会包含所有以上信息。由于Linux0.11系统使用了IntelCPU的内存管理功能,因此它会为每个执行程序单独分配一个64MB的地址空间(逻辑地址空间)使用。在这种情况下因为链接器已经把执行文件处理成从一个固定地址开始运行,所以相关的可执行文件中就不再需要重定位信息。目标文件的文件头中含有一个长度为32字节的exec数据结构,通常称为文件头结构或执行头结构。其定义如下所示。有关a.out结构的详细信息请参见include/a.out.h文件后的介绍。structexec{unsignedlonga_magic//执行文件魔数。使用N_MAGIC等宏访问。unsigneda_text//代码长度,字节数。unsigneda_data//数据长度,字节数。unsigneda_bss//文件中的未初始化数据区长度,字节数。unsigneda_syms//文件中的符号表长度,字节数。unsigneda_entry//执行开始地址。unsigneda_trsize//代码重定位信息长度,字节数。unsigneda_drsize//数据重定位信息长度,字节数。}根据a.out文件中头结构魔数字段的值,我们又可把a.out格式的文件分成几种类型。Linux0.11系统使用了其中两种类型:模块目标文件使用了OMAGIC类型的a.out格式,其魔数是0x107(八进制0407)。而执行文件则使用了ZMAGIC类型的a.out格式,魔数是0x10b(八进制0413)。这两种格式的主要区别在于它们对各个部分的存储分配方式上。虽然该结构的总长度只有32字节,但是对于一个ZMAGIC类型的执行文件来说,其文件开始部分却需要专门留出1024字节的空间给头结构使用。除被头结构占用的32个字节以外,其余部分均为0。从1024字节之后才开始放置程序的文本段和数据段等信息。而对于一个OMAGIC类型的.o模块文件来说,文件开始部分的32字节头结构后面紧接着就是代码部分和数据部分。执行头结构中a_text和a_data字段分别指出了后面只读的代码段和可读写数据段的字节长度。a_bss字段指明内核在加载目标文件时数据段后面未初始化数据区域(bss段)的长度。由于Linux在分配内存时会自动对内存清零,因此bss段不需要被包括在模块文件或执行文件中。为了形象地表示目标文件逻辑地具有一个bss段,在后面图示中将使用虚线框来表示目标文件中的bss段。a.out文件头代码部分Textsection数据部分Datasection代码重定位信息Textrelocation数据重定位信息Datarelocation符号表SymbolTable字符串表StringTable3a_entry字段指定了程序开始执行的地址,而a_syms、a_trsize和a_drsize字段则分别说明了数据段后符号表、代码和数据段重定位信息的大小。对于可执行文件来说并不需要符号表和重定位信息,因此除非链接程序为了调试目的而包括了符号信息,执行文件中的这几个字段的值通常为0。Linux0.11系统的模块文件和执行文件都是a.out格式的目标文件,但是只有编译器生成的模块文件中包含用于链接程序的重定位信息。代码段和数据段的重定位信息均有重定位记录(项)构成,每个记录的长度为8字节,其结构如下所示。structrelocation_info{intr_address;//段内需要重定位的地址。unsignedintr_symbolnum:24;//含义与r_extern有关。指定符号表中一个符号或者一个段。unsignedintr_pcrel:1;//1比特。PC相关标志。unsignedintr_length:2;//2比特。指定要被重定位字段长度(2的次方)。unsignedintr_extern:1;//外部标志位。1-以符号的值重定位。0-以段的地址重定位。unsignedintr_pad:4;//没有使用的4个比特位,但最好将它们复位掉。};重定位项的功能有两个。一是当代码段被重定位到一个不同的基地址处时,重定位项则用于指出需要修改的地方。二是在模块文件中存在对未定义符号的引用时,当此未定义符号最终被定义时链接程序就可以使用相应重定位项对符号的值进行修正。由上面重定位记录项的定义可以看出,每个记录项含有模块文件代码部分(代码段)和数据部分(数据段)中需要重定位处长度为4字节的地址以及规定如何具体进行重定位操作的信息。地址字段r_address是指可重定位项从代码段或数据段开始算起的偏移值。2比特的长度字段r_length指出被重定位项的长度,0到3分别表示被重定位项的宽度是1字节、2字节、4字节或8字节。标志位r_pcrel指出被重定位项是一个“PC相关的”的项,即它作为一个相对地址被用于指令当中。外部标志位r_extern控制着r_symbolnum的含义,指明重定位项参考的是段还是一个符号。如果该标志值是0,那么该重定位项是一个普通的重定位项,此时r_symbolnum字段指定是在哪个段中寻址定位。如果该标志是1,那么该重定位项是对一个外部符号的引用,此时r_symbolnum指定目标文件中符号表中的一个符号,需要使用符号的值进行重定位。目标文件的最后一部分是符号表和相关的字符串表。符号表记录项的结构如下所示。structnlist{union{char*n_name;//字符串指针,structnlist*n_next;//或者是指向另一个符号项结构的指针,longn_strx;//或者是符号名称在字符串表中的字节偏移值。}n_un;unsignedcharn_type;//该字节分成3个字段,参见a.out.h文件146-154行。charn_other;//通常不用。shortn_desc;//unsignedlongn_value;//符号的值。};由于GNUgcc编译器允许任意长度的标识符,因此标识符字符串都位于符号表后的字符串表中。每个符号表记录项长度为12字节,其中第一个字段给出了符号名字符串(以null结尾)在字符串表中的偏移位置。类型字段n_type指明了符号的类型。该字段的最后一个比特位用于指明符号是否是外部的(全局的)。如果该位为1的话,那么说明该符号是一个全局符号。链接程序并不需要非全局符号信息,但可供调试程序使用。n_type字段的其余比特位用来指明符号类型。a.out.h头文件中定义了这些类型值常量符号。符号的主要的类型包括:4text、data或bbs指明是本模块文件中定义的符号。此时符号的值是模块中该符号的可重定位地址。abs指明符号是一个绝对的(固定的)不可重定位的符号。符号的值就是该固定值。undef指明是一个本模块文件中未定义的符号。此时符号的值通常是0。但作为一种特殊情况,编译器能够使用一个未定义的符号来要求链接程序为指定的符号名保留一块存储空间。如果一个未定义的外部(全局)符号具有非零值,那么对链接程序而言该值就是程序希望指定符号寻址的存储空间的大小值。在链接操作期间,如果该符号确实没有定义,那么链接程序就会在bss段中为该符号名建立一块存储空间,空间的大小是所有被链接模块中该符号值最大的一个。这个就是bss段中所谓的公共块(Commonblock)定义,主要用于支持未初始化的外部(全局)数据。例如程序中定义的未初始化的数组。如果该符号在任意一个模块中已经被定义了,那么链接程序就会使用该定义而忽略该值。在Linux0.11系统中,我们可以使用objdump命令来查看模块文件或执行文件中文件头结构的具体值。例如,下面列出了hello.o目标文件及其执行文件中文件头的具体值。[/usr/root]#gcc-c-ohello.ohello.c[/usr/root]#gcc-ohellohello.o[/usr/root]#[/usr/root]#objdumpUsage:objdump[-hnrt][+header]