Linux对象文件是个啥东东

读书的时候,C语言课程教的是先写C的代码,然后编译出对象文件,最后链接出可执行文件。那么问题来了,为什么一定要先编译出对象文件呢?难道不能直接编译出可执行文件吗?保持一个有疑问的心才能持续的学习,Linux的世界的知识真是太多,也太有趣了。好吧,回归主题。直接编译出可执行文件是完全可以的!而且稍微分析一下对象文件和可执行文件也会发现,他们之间是有很多的相同之处的。那么为何需要对象文件?我个人的理解是,对于大型的软件项目,有对象文件存在更容易对整个项目进行组织和管理。如果说错了,欢迎拍砖。那么继续分析对象文件的内容咯。这篇文章参考了很多深海的小鱼儿的文章 http://www.cnblogs.com/xmphoenix/archive/2011/10/23/2221879.html 里面的内容,表示感谢!

 

目标文件的类型

读书的时候,目标特指.o文件。其实所谓的目标文件包括三个种类:

- 可重定位的对象文件:这个就是我们熟悉的.o文件,或者是被称作中间文件。ko文件也是可重定位的对象文件。可重定位的对象文件是无法直接运行的,那么ko加载到内核后是怎么运行起来的呢?这个以后再说,其实很简单,猜也能猜的到!

- 可执行的对象文件:初中的时候第一次碰MS-DOS,唯一会做的事情就是在各个路径下面寻找exe文件,期望能蹦出个游戏来。后来同学告诉我bat文件也是可以运行的:) 可执行的对象文件是大家最熟悉的了。最常用的ls、cat之类的命令都是可执行的对象文件。题外话,ls的源码有空看看瞧,不是想象的那么简单的哦。

- 可被共享的对象文件:就是so文件,程序运行的时候动态加载的。可被共享的对象文件没有main函数,这是和可执行文件的最大区别。另外不要搞错,运行的时候动态加载可不是运行到特定的库函数才会加载哦,而是一运行起来就加载好了。进程映象中的代码段是只读的,一旦运行起来可不是能随意更改的。运行的时候,动态连接器会处理可执行文件和可被共享的对象文件,生成一个进程映象。

为了表达方便,后面用.o表示可重定位的对象文件,exe表示可执行的对象文件,so表示可被共享的对象文件。

ELF(Executable and Linking Format)

ELF是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用。可以说,ELF是构成众多xNIX系统的基础之一,所以作为嵌入式Linux系统乃至内核驱动程序开发人员,你最好熟悉并掌握它。以上文字完全拷贝自深海的小鱼儿的文章。

格式是什么?格式就是特定文本的组织方式。我们人类可以理解的文本如邮件有邮件的格式,大字报有大字报的格式。ELF格式是link editor和dynamic linker可以理解,并且创建可执行文件和进程映象的构建。注意了,对象文件首先是编译器把我们写的很挫的代码编译出来的。不同的操作系统,不同的芯片对应的编译器都是不同的。因此编译出来的对象文件也是不同的,对应的ELF格式也是有区别的。下面就是ELF格式大致的样子。

ELF 文件头

左边是.o的ELF,右边是exe的ELF。这里面我们重点关注ELF Header即ELF文件头。ELF文件头被固定地放在不同类对象文件的最前面,包含一些数据,用于描述ELF文件中ELF文件头之外的内容。不要把ELF格式和ELF文件头搞混了。我们可以用readelf来分析对象文件,先看看ELF文件头包含了什么信息。

[root@localhost lib]#readelf -h ./libdl-2.12.so
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0xa60 // 程序进入点,第一条要运行的指令的运行时地址
Start of program headers: 52 (bytes into file)
Start of section headers: 16532 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9 // segements的个数
Size of section headers: 40 (bytes)
Number of section headers: 34 // section是的个数
Section header string table index: 33

那什么是所谓 sections 呢?可以说,sections 是在ELF文件里头,用以装载内容数据的最小容器。在ELF文件里面,每一个 sections 内都装载了性质属性都一样的内容,比方:

1) .text section 里装载了可执行代码;

2) .data section 里面装载了被初始化的数据;

3) .bss section 里面装载了未被初始化的数据;

4) 以 .rec 打头的 sections 里面装载了重定位条目;

5) .symtab 或者 .dynsym section 里面装载了符号信息;

6) .strtab 或者 .dynstr section 里面装载了字符串信息;

7) 其他还有为满足不同目的所设置的section,比方满足调试的目的、满足动态链接与加载的目的等等。

一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的 section head table(SHT)决定。在SHT中,针对每一个section,都设置有一个条目,用来描述对应的这个section,其内容主要包括该 section 的名称、类型、大小以及在整个ELF文件中的字节偏移位置等等。注意SHT不是在ELF文件头中的,仔细看一下上面的图。

现在看一下SHT的内容

[root@localhost lib]#readelf -S ./libdl-2.12.so
There are 34 section headers, starting at offset 0x4094:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-i NOTE 00000154 000154 000024 00 A 0 0 4
[ 2] .note.ABI-tag NOTE 00000178 000178 000020 00 A 0 0 4
[ 3] .gnu.hash GNU_HASH 00000198 000198 0000b8 04 A 4 0 4
[ 4] .dynsym DYNSYM 00000250 000250 000280 10 A 5 1 4
[ 5] .dynstr STRTAB 000004d0 0004d0 0001d8 00 A 0 0 1
[ 6] .gnu.version VERSYM 000006a8 0006a8 000050 02 A 4 0 2
[ 7] .gnu.version_d VERDEF 000006f8 0006f8 0000c8 00 A 5 6 4
[ 8] .gnu.version_r VERNEED 000007c0 0007c0 000070 00 A 5 2 4
[ 9] .rel.dyn REL 00000830 000830 000070 08 A 4 0 4
[10] .rel.plt REL 000008a0 0008a0 000080 08 A 4 12 4
[11] .init PROGBITS 00000920 000920 000030 00 AX 0 0 4
[12] .plt PROGBITS 00000950 000950 000110 04 AX 0 0 4
[13] .text PROGBITS 00000a60 000a60 001028 00 AX 0 0 16 //代码段
[14] .fini PROGBITS 00001a88 001a88 00001c 00 AX 0 0 4
[15] .rodata PROGBITS 00001aa4 001aa4 0000a3 00 A 0 0 4
[16] .interp PROGBITS 00001b47 001b47 000013 00 A 0 0 1
[17] .eh_frame_hdr PROGBITS 00001b5c 001b5c 0000cc 00 A 0 0 4
[18] .eh_frame PROGBITS 00001c28 001c28 0004a8 00 A 0 0 4
[19] .hash HASH 000020d0 0020d0 0001ac 04 A 4 0 4
[20] .init_array INIT_ARRAY 00003eb0 002eb0 000004 00 WA 0 0 4
[21] .ctors PROGBITS 00003eb4 002eb4 000008 00 WA 0 0 4
[22] .dtors PROGBITS 00003ebc 002ebc 00000c 00 WA 0 0 4
[23] .jcr PROGBITS 00003ec8 002ec8 000004 00 WA 0 0 4
[24] .data.rel.ro PROGBITS 00003ecc 002ecc 000004 00 WA 0 0 4 //初始化过的数据
[25] .dynamic DYNAMIC 00003ed0 002ed0 0000f8 08 WA 5 0 4
[26] .got PROGBITS 00003fc8 002fc8 00002c 04 WA 0 0 4
[27] .got.plt PROGBITS 00003ff4 002ff4 00004c 04 WA 0 0 4
[28] .bss NOBITS 00004040 003040 000034 00 WA 0 0 4 //未初始化的数据
[29] .comment PROGBITS 00000000 003040 00002c 01 MS 0 0 1
[30] .symtab SYMTAB 00000000 00306c 000920 10 31 109 4 //符号表
[31] .strtab STRTAB 00000000 00398c 0005be 00 0 0 1 //字符串表
[32] .gnu_debuglink PROGBITS 00000000 003f4c 000018 00 0 0 4
[33] .shstrtab STRTAB 00000000 003f64 00012d 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

Allocable 的section,是指在运行时,进程(process)需要使用它们,所以它们被加载器加载到内存中去。而与此相反,存在一些non-Allocable 的sections,它们只是被链接器、调试器或者其他类似工具所使用的,而并非参与进程的运行中去的那些 section。所以,实际上,这些 non-Allocable 的section 都可以被我们用 strip 工具从最后的可执行文件中删除掉,删除掉这些sections的可执行文件照样能够运行,只不过你没办法来进行调试之类的事情罢了。

在可重定位文件里面,section承载了大多数被包含的东西,代码、数据、符号信息、重定位信息等等。可重定位对象文件里面的这些sections是作为输入,给链接器那去做链接用的,所以这些 sections 也经常被称做输入 section。链接器在链接可执行文件或动态库的过程中,它会把来自不同可重定位对象文件中的相同名称的 section 合并起来构成同名的 section。接着,它又会把带有相同属性(比方都是只读并可加载的)的 section 都合并成所谓 segments(段)。segments 作为链接器的输出,常被称为输出section。我们开发者可以控制哪些不同.o文件的sections来最后合并构成不同名称的 segments。如何控制呢,就是通过 linker script 来指定。

一个单独的 segment 通常会包含几个不同的 sections,比方一个可被加载的、只读的segment 通常就会包括可执行代码section .text、只读的数据section .rodata以及给动态链接器使用的符号section .dymsym等等。section 是被链接器使用的,但是 segments 是被加载器所使用的。加载器会将所需要的 segment 加载到内存空间中运行。和用 sections header table 来指定一个可重定位文件中到底有哪些 sections 一样。在一个可执行文件或者动态库中,也需要有一种信息结构来指出包含有哪些 segments。这种信息结构就是 program header table。

我们可以用 readelf -l 来查看可执行文件的程序头表,如下所示:

[root@localhost lib]#readelf -l ./libdl-2.12.so

Elf file type is DYN (Shared object file)
Entry point 0xa60
There are 9 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00120 0x00120 R E 0x4
INTERP 0x001b47 0x00001b47 0x00001b47 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x00000000 0x00000000 0x0227c 0x0227c R E 0x1000
LOAD 0x002eb0 0x00003eb0 0x00003eb0 0x00190 0x001c4 RW 0x1000
DYNAMIC 0x002ed0 0x00003ed0 0x00003ed0 0x000f8 0x000f8 RW 0x4
NOTE 0x000154 0x00000154 0x00000154 0x00044 0x00044 R 0x4
GNU_EH_FRAME 0x001b5c 0x00001b5c 0x00001b5c 0x000cc 0x000cc R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x002eb0 0x00003eb0 0x00003eb0 0x00150 0x00150 R 0x1

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_d .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .interp .eh_frame_hdr .eh_frame .hash
03 .init_array .ctors .dtors .jcr .data.rel.ro .dynamic .got .got.plt .bss
04 .dynamic
05 .note.gnu.build-id .note.ABI-tag
06 .eh_frame_hdr
07
08 .init_array .ctors .dtors .jcr .data.rel.ro .dynamic .got

该结果很明白显示出了哪些 section 映射到哪一个 segment 当中去。这个segment就是程序在内存中运行起来真正的样子。上面类型为PHDR的segment,用来包含程序头表本身。类型为INTERP的segment只包含一个 section,那就是 .interp。在这个section中,包含了动态链接过程中所使用的解释器路径和名称,注意红色的部分。当你在 shell 中敲入一个命令要执行时,内核会帮我们创建一个新的进程,它在往这个新进程的进程空间里面加载进可执行程序的代码段和数据段后,也会加载进动态连接器(在Linux里面通常就是 /lib/ld-linux.so 符号链接所指向的那个程序,它本省就是一个动态库)的代码段和数据。在这之后,内核将控制传递给动态链接库里面的代码。动态连接器接下来负责加载该命令应用程序所需要使用的各种动态库。加载完毕,动态连接器才将控制传递给应用程序的main函数。所以如果程序中某个函数无法在库中找到,程序会很快的返回失败,根本来不及执行到main。

最重要的是三个 segment:代码段,数据段和堆栈段。代码段和数据段的 VirtAddr 列的值分别为 0x00000000 和 0x00003eb0。这是什么意思呢?这是说对应的段要加载在进程虚拟地址空间中的起始地址。虽然在可执行文件中规定了 text segment和 data segment 的起始地址,但是最终,在内存中的这些段的真正起始地址,却可能不是这样的,因为在动态链接器加载这些段的时候,需要考虑到页面对齐的因素。为什么?因为像x86这样的架构,它给内存单元分配读写权限的最小单位是页(page)而不是字节。也就是说,它能规定从某个页开始、连续多少页是只读的。却不能规定从某个页内的哪一个字节开始,连续多少个字节是只读的。因为x86架构中,一个page大小是4k,所以,动态链接器在加载 segment 到虚拟内存中的时候,其真实的起始地址的低12位都是零,也即以 0x1000 对齐。

从程序头表中我们可以看到一个类型为 GNU_STACK 的segment,这是 stack segment。程序头表中的这一项,除了 Flg/Align 两列不为空外, 其他列都为0。这是因为堆栈段在虚拟内存空间中,从哪里开始、占多少字节是由内核说了算的,而不决定于可执行程序。实际上,内核决定把堆栈段放在整个进程地址空间的用户空间的最上面。

深海小雨的文章最后详细介绍了重定位的过程,有兴趣的同学自己看看吧。

 

转载于:https://www.cnblogs.com/CalvinWang/p/5359296.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值