Linux内核完全注释 阅读笔记:3.5、Linux 0.11目标文件格式
 2018.10.10    |      Linux内核完全注释    |     AilsonJack    |     暂无评论    |     567 views
By: Ailson Jack
Date: 2018-08-30
个人博客: http://www.only2fire.com/
<p style="text-indent: 2em;">为了生成内核代码文件,Linux 0.11使用了两种编译器。第一种是汇编编译器as86和相应的链接程序(或称为链接器)ld86。它们专门用于编译和链接,运行在实地址模式下的16位内核引导扇区程序bootsect.s和设置程序setup.s。第二种是GNU的汇编器as(gas)和C语言编译器gcc以及相应的链接程序ld。编译器用于为源程序文件产生对应的二进制代码和数据目标文件。链接程序用于对相关的所有目标文件进行组合处理,形成一个可被内核加载执行的目标文件,即可执行文件。<br/></p><p class="artical_littlestyle1">1、目标文件格式</p><p style="text-indent: 2em;">在Linux 0.11系统中,GNU gcc或as编译输出的目标模块文件和链接程序所生成的可执行文件都使用了UNIX传统的a.out格式。目标文件格式示意图如下:</p><p style="text-align:center"><img src="/uploads/AilsonJack/2018.10.10/1539169524601774.png" onclick="preview_image(&#39;/uploads/AilsonJack/2018.10.10/1539169524601774.png&#39;)"/></p><p style="text-indent: 0em;">a.out格式7个区的基本定义和用途如下所述:<br/><span style="color: rgb(0, 112, 192);">执行头部分(exec header)</span>:该部分中含有一些参数(exec结构),是有关目标文件的整体结构信息。例如代码和数据区的长度、未初始化数据区的长度、对应源程序文件名以及目标文件创建时间等。内核使用这些参数把执行文件加载到内存中并执行,而链接程序ld使用这些参数将一些模块文件组合成一个可执行文件。这是目标文件唯一必要的组成部分。<br/><span style="color: rgb(0, 112, 192);">代码区(text segment)</span>:由编译器或汇编器生成的二进制指令代码和数据信息,含有程序执行时被加载到内存中的指令代码和相关数据。可以以只读形式被加载。<br/><span style="color: rgb(0, 112, 192);">数据区(data segment)</span>:由编译器或汇编器生成的二进制指令代码和数据信息,这部分含有已经初始化过的数据,数据区总是被加载到可读写的内存中。<br/><span style="color: rgb(0, 112, 192);">代码重定位部分(text relocations)</span>:这部分含有供链接程序使用的记录数据。在组合目标模块文件时,用于定位代码段中的指针或地址。当链接程序需要改变目标代码的地址时,就需要修正和维护这些地方。<br/><span style="color: rgb(0, 112, 192);">数据重定位部分(data relocations)</span>:类似于代码重定位部分的作用,但是用于数据段中指针的重定位。<br/><span style="color: rgb(0, 112, 192);">符号表部分(symbol table)</span>:这部分同样含有供链接程序使用的记录数据。这些记录数据保存着模块文件中定义的全局符号以及需要从其他模块文件中输入的符号,或者是由链接器定义的符号,用于在模块文件之间对命名的变量和函数(符号)进行交叉引用。<br/><span style="color: rgb(0, 112, 192);">字符串表部分(string table)</span>:该部分含有与符号名相对应的字符串。用于调试程序,调试目标代码,与链接程序无关。这些信息可包含源程序代码和行号、局部符号以及数据结构描述信息等。<br/></p><p style="text-indent: 2em;">目标文件的文件头中含有一个长度为32字节的exec数据结构,通常称为文件头结构或者执行头结构。有关a.out结构的详细信息请参见include/a.out.h文件后的介绍。文件头结构定义如下:<br/></p><pre class="brush:cpp;toolbar:false PrismJs">struct&nbsp;exec{ &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;long&nbsp;a_magic;&nbsp;//执行文件魔数,使用N_MAGIC等宏访问 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;a_text;&nbsp;//代码长度,字节数 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;a_data;&nbsp;//数据长度,字节数 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;a_bss;&nbsp;//文件中的未初始化数据区长度,字节数 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;a_syms;&nbsp;//文件中的符号表长度,字节数 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;a_entry;&nbsp;//执行开始地址 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;a_trsize;&nbsp;//代码重定位信息长度,字节数 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;a_drsize;&nbsp;//数据重定位信息长度,字节数 };</pre><p style="text-indent: 2em;">根据a.out文件中头结构魔数字段的值,我们又可把a.out格式的文件分成几种类型。Linux 0.11系统使用了其中两种类型:模块目标文件使用了<span style="color: rgb(255, 0, 0);">OMAGIC</span>(Old Magic)类型的a.out格式,它指明文件是目标文件或者是不纯的可执行文件,其魔数是0x107(八进制0407)。而执行文件使用了<span style="color: rgb(255, 0, 0);">ZMAGIC</span>类型的a.out格式,它指明文件为需求分页处理(demang-paging,即需求加载load on demand)的可执行文件,其魔数是0x10b(八进制0413)。<span style="color: rgb(0, 112, 192);">这两种格式的主要区别在于它们对各个部分的存储分配方式上。虽然该结构的总长度只有32字节,但是对于一个ZMAGIC类型的执行文件来说,其文件开始部分却需要专门留出1024字节的空间给头结构使用。除头结构占用的32个字节以外,其余部分均为0。从1024字节之后才开始放置程序的正文段和数据段等信息。而对于一个OMAGIC类型的.o模块文件来说,文件开始部分的32字节头结构后面紧接着就是代码区和数据区</span>。<br/></p><p style="text-indent: 2em;">a_entry字段指定了程序代码开始执行的地址。而a_syms、a_trsize和a_drsize字段则分别说明了数据段后符号表、代码和数据段重定位信息的大小。对于可执行文件来说并不需要符号表和重定位信息,因此除非链接程序为了调试目的而包括了符号信息,执行文件中的这几个字段的值通常为0。<br/></p><p style="text-indent: 2em;">Linux 0.11系统的模块文件和执行文件都是a.out格式的目标文件,但是只有编译器生成的模块文件中包含用于链接程序的重定位信息。代码段和数据段的重定位信息均由重定位记录项构成,每个记录的长度为8字节,其结构如下:<br/></p><pre class="brush:cpp;toolbar:false PrismJs">struct&nbsp;relocation_info { &nbsp;&nbsp;&nbsp;&nbsp;int&nbsp;r_address;&nbsp;//段内需要重定位的地址 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;int&nbsp;r_symbolnum:24;&nbsp;//含义与r_extern有关,指定符号表中一个符号或者一个段 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;int&nbsp;r_pcrel:1;&nbsp;//1bit,PC相关的标志 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;int&nbsp;r_length:2;&nbsp;//2bit,指定要被重定位字段长度(2的次方) &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;int&nbsp;r_extern:1;&nbsp;//外部标志位,&nbsp;1:以符号的值重定位,&nbsp;0:以段的地址重定位 &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;int&nbsp;r_pad:4;&nbsp;//没有使用的4个bit,但最好将他们复位 };</pre><p style="text-indent: 2em;">重定位项的功能有两个。一是当代码段被重定位到一个不同的基地址处时,重定位项则用于指出需要修改的地方。二是在模块文件中存在对未定义符号引用时,当此未定义符号最终被定义时链接程序就可以使用相应重定位项对符号的值进行修正。<br/></p><p style="text-indent: 2em;">目标文件的最后部分是符号表和相关的字符串表。符号表记录项的结构如下:<br/></p><pre class="brush:cpp;toolbar:false PrismJs">struct&nbsp;nlist{ &nbsp;&nbsp;&nbsp;union{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;char&nbsp;*n_name;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//字符串指针, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;struct&nbsp;nlist&nbsp;*n_next;&nbsp;//或者是指向另一个符号项结构的指针, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;long&nbsp;n_strx;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//或者是符号名称在字符串表中的字节偏移值 &nbsp;&nbsp;&nbsp;&nbsp;}n_un; &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;char&nbsp;n_type;&nbsp;//该字节分成3个字段,参见a.out.h文件146-154行 &nbsp;&nbsp;&nbsp;&nbsp;char&nbsp;n_other;&nbsp;//通常不用 &nbsp;&nbsp;&nbsp;&nbsp;short&nbsp;n_desc; &nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;long&nbsp;n_value;&nbsp;//符号的值 };</pre><p class="artical_littlestyle2">2、Linux 0.11中的目标文件格式</p><p style="text-indent: 2em;">磁盘上a.out执行文件的各区在进程逻辑地址空间中的对应关系如下图所示:<br/></p><p style="text-align:center"><img src="/uploads/AilsonJack/2018.10.10/1539169524450105.png" onclick="preview_image(&#39;/uploads/AilsonJack/2018.10.10/1539169524450105.png&#39;)"/></p><p style="text-indent: 2em;">Linux 0.11系统中进程的逻辑地址空间大小是64MB。对于ZMAGIC类型的a.out执行文件,它的代码区的长度是内存页面的整数倍。由于Linux 0.11内核使用需求页(Demand-paging)技术,即在一页代码实际要使用的时候才被加载到物理内存页面中,而在进行加载操作的fs/execve()函数中仅仅为其设置了分页机制的页目录项和页表项,因此需求页技术可以加快程序的加载速度。<br/></p><p style="text-indent: 2em;">图中bss是进程的未初始化数据区,用于存放静态的未初始化数据。在开始执行程序时,bss的第一页内存会被设置为全0。图中heap是堆空间区,用于分配进程在执行过程中动态申请的内存空间。<br/></p><p class="artical_littlestyle3">3、链接程序输出</p><p style="text-indent: 2em;">链接程序对输入的一个或多个模块文件以及相关的库函数模块进行处理,最终生成相应的二进制执行文件或者是一个所有模块组合而成的大模块文件。<br/></p><p style="text-indent: 2em;">对于a.out格式的模块文件来说,由于段类型是预先知道的,因此链接程序对a.out格式的模块文件进行存储分配比较容易。例如,对于具有两个输入模块文件和需要链接一个库函数模块的情况,其存储分配情况如下图所示:<br/></p><p style="text-align:center"><img src="/uploads/AilsonJack/2018.10.10/1539169524922498.png" onclick="preview_image(&#39;/uploads/AilsonJack/2018.10.10/1539169524922498.png&#39;)"/></p><p class="artical_littlestyle4">4、链接程序预定义变量</p><p style="text-indent: 2em;">在链接过程中,链接器ld和ld86会使用变量记录下执行程序中每个段的逻辑地址。因此在程序中可以通过访问这几个外部变量来获得程序中段的位置。链接器预定义的外部变量通常至少有etext、_etext、edata、_edata、end和_end。<br/></p><p style="text-indent: 2em;">变量名etext和_etext的地址是程序正文段结束后的第1个地址;edata和_edata的地址是初始化数据区后面的第1个地址;end和_end的地址是未初始化数据区(bss)后的第1个地址位置。带下划线’_’前缀的名称等同于不带下划线的对应名称,它们之间的唯一区别在于ANSI、POSIX等标准中没有定义符号etext、edata和end。<br/></p><p class="artical_littlestyle1">5、System.map文件</p><p style="text-indent: 2em;">在编译内核时,linux/Makefile文件产生的System.map文件就用于存放内核符号表信息。当内核运行出错时,通过System.map文件中的符号表解析,就可以查到一个地址值对应的变量名,或反之。<br/></p><p style="text-indent: 2em;">尽管内核本身实际上不使用System.map,但其他程序,像klogd、lsof、ps以及其他像dosemu等许多软件都需要一个正确的System.map文件。利用该文件,这些程序就可以根据已知的内存地址查找出对应的内核变量名称,便于对内核的调试工作。<br/></p>
欢迎关注博主的公众号呀,精彩内容随时掌握:
热情邀请仔细浏览下博客中的广告,万一有对自己有用或感兴趣的呢。◕ᴗ◕。。
如果这篇文章对你有帮助,记得点赞和关注博主就行了^_^,当然了能够赞赏博主,那就非常感谢啦!
注: 转载请注明出处,谢谢!^_^
暂无评论,要不要来个沙发
发表评论

 
Copyright © 2015~2023  说好一起走   保留所有权利   |  百度统计  蜀ICP备15004292号