相信很多同学知道Windows下的可执行程序是*.exe,但是除了*.exe之外,Windows下动态库*.dll和*.lib也是可执行程序。不过,小师妹问沃的是什么是Linux下的可执行程序?
为了不让小师妹伤心,师兄沃再次拿起《程序员的自我修养》第三章肝了一晚上,呕心沥血才完美回答出小师妹的问题,回答让小师妹十分满意,对师兄好感爆棚。好了,吹牛到此为止,正经脸(咳咳咳 ^_^),接下来让我们来看看师兄回答了什么,让小师妹万分满意?
一、目标文件格式
现在PC平台流行的可执行文件格式(executable)主要是Windows下的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种。其中,Windows下可执行文件还包括动态链接库(DLL,Dynamic Linking Library)和静态链接库(lib,Static Linking Library),Linux下动态链接库(*.so)和静态链接库(*.a)以及目标文件object(*.o)。在Windows下可执行文件按PE-COFF格式存储,Linux下按照ELF格式存储。ELF可以理解为是一系列的目标文件或者加上链接库组合而成的文件包。下面是ELF文件类型和说明。
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件 (Relocatable File) | 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归类为这一类 | Linux的*.o文件 Windows的*.obj文件 |
可执行文件 (Executable File) | 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名 | 比如/bin/bash文件 Windows的*.exe文件 |
共享目标文件 (Shared Object File) | 这类文件包含了代码和数据,可以在一下两种情况使用, 一种是连接器可以使用这种文件跟其它的可重定位文件和共享目标文件链接,产生新的目标文件; 另一种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行。 | Linux的*.so,如/lib/glibc-2.5.so Windows的DLL |
核心转存储文件 (Core Dump File) | 当进程意外中职生时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转存储到核心转存储文件 | Linux下的Core dump |
- 小知识:Unix的可执行程序是*.out;COFF主要贡献在目标文件引入“段”机制。
二、目标文件结构
2.1样例代码
我们先给小师妹上一段经典代码,惊呆小师妹一脸。
(注:文本分析的是ubuntu20.04 x86 64位系统下的ELF文件)
#include <iostream>int globalVar1 = 100;
int globalVar2;void fun1(int var)
{std::cout<<var<<std::endl;
}struct St
{St() : var(200) {}~St() {}int getVar() const {return var;}private:int var;
};int main(int arc, char* argv[]) {static int staticVar1 = 666;static int staticVar2;int var1 = 555;int var2;fun1(staticVar1 + staticVar1 + var1 + var2);return 0;
}
2.2生成可执行文件
编译成main.o文件命令如下:(-c表示只编译不链接)
g++ -c main.cpp -o main.o
编译成main文件命令如下:
g++ main.cpp -o main
2.3分析main.o文件
Linux下查看和操作目标文件许多工具,比如hexdump、objdum、objcopy、readelf,本文主要介绍ELF的文件结构,故主要用objdump工具足够,具体文件结构里面每个部分将在下一篇文章结合源码详细分析。
查看main.o内部结构命令
objdump -h main.o
通过图1-1可知,可执行文件结构主要分为几大部分“.text段”、“.data段”、“.bss段”、“.rodata段”、“其它信息段”,即如下结构图:
由结构图1-1可知,可知执行文件main.o主要结构分为文件头header、.text段、.data段、.bss段、其它信息段(.rodata段、.init_arrary段、.comment段、other段等)。其中左侧红色数据位对应“段”的大小,右边蓝色数字表示文件偏移地址。
ELF header:ELF文件头,定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口地址和长度、段表的位置和长度及段的数量等;(具体代码定义参考Linux系统库“/usr/inlcude//elf.h”)
- .text:代码段,存放程序运行时相关需要的代码
- .data:数据段,保存已经初始化了的全局变量、全局静态变量、局部静态变量,比如globalVar1和staticVar1,一共8字节
- .bss:存放的是未初始化的全局变量、全局静态变量、局部静态变量,比如globalVar2和staticVar2,var2一共12(0x0c)字节
- .rodata:只读数据段,由于cout::endl本质是一个"\n"常量字符,大小为1,故带只读数据段大小只有1字节
- .init_arrary:初始化数组段,有类似ctor效果
- .comment:注释段,一般存放注释代码文本段
- .other:其它段包含符号表、gnu版本号等等
2.4段.text、.data和.rodata、.bss段分析
2.4.1代码段.text分析
objdum中的-s可以显示所有段十六进制数,-d可以将所有段包含的指令反汇编,下面敲入命令查看代码如下:
objdump -s -d main.o
显示结果:
tjq@ubuntu:~/eclipse-workspace/Test1/src$ objdump -s -d main.omain.o: 文件格式 elf64-x86-64Contents of section .text:0000 f30f1efa 554889e5 4883ec10 897dfc8b ....UH..H....}..0010 45fc89c6 488d3d00 000000e8 00000000 E...H.=.........0020 4889c248 8b050000 00004889 c64889d7 H..H......H..H..0030 e8000000 0090c9c3 f30f1efa 554889e5 ............UH..0040 4883ec20 897dec48 8975e0c7 45f82b02 H.. .}.H.u..E.+.0050 00008b05 00000000 8d14008b 45f801c2 ............E...0060 8b45fc01 d089c7e8 00000000 b8000000 .E..............0070 00c9c3f3 0f1efa55 4889e548 83ec1089 .......UH..H....0080 7dfc8975 f8837dfc 01753281 7df8ffff }..u..}..u2.}...0090 00007529 488d3d00 000000e8 00000000 ..u)H.=.........00a0 488d1500 00000048 8d350000 0000488b H......H.5....H.00b0 05000000 004889c7 e8000000 0090c9c3 .....H..........00c0 f30f1efa 554889e5 beffff00 00bf0100 ....UH..........00d0 0000e89c ffffff5d c3 .......].
Contents of section .data:0000 64000000 9a020000 d.......
Contents of section .rodata:0000 00 .
Contents of section .init_array:0000 00000000 00000000 ........
Contents of section .comment:0000 00474343 3a202855 62756e74 7520392e .GCC: (Ubuntu 9.0010 332e302d 31377562 756e7475 317e3230 3.0-17ubuntu1~200020 2e303429 20392e33 2e3000 .04) 9.3.0.
Contents of section .note.gnu.property:0000 04000000 10000000 05000000 474e5500 ............GNU.0010 020000c0 04000000 03000000 00000000 ................
Contents of section .eh_frame:0000 14000000 00000000 017a5200 01781001 .........zR..x..0010 1b0c0708 90010000 1c000000 1c000000 ................0020 00000000 38000000 00450e10 8602430d ....8....E....C.0030 066f0c07 08000000 1c000000 3c000000 .o..........<...0040 00000000 3b000000 00450e10 8602430d ....;....E....C.0050 06720c07 08000000 1c000000 5c000000 .r..........\...0060 00000000 4d000000 00450e10 8602430d ....M....E....C.0070 0602440c 07080000 1c000000 7c000000 ..D.........|...0080 00000000 19000000 00450e10 8602430d .........E....C.0090 06500c07 08000000 .P...... Disassembly of section .text:0000000000000000 <_Z4fun1i>:0: f3 0f 1e fa endbr64 4: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 83 ec 10 sub $0x10,%rspc: 89 7d fc mov %edi,-0x4(%rbp)f: 8b 45 fc mov -0x4(%rbp),%eax12: 89 c6 mov %eax,%esi14: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1b <_Z4fun1i+0x1b>1b: e8 00 00 00 00 callq 20 <_Z4fun1i+0x20>20: 48 89 c2 mov %rax,%rdx23: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 2a <_Z4fun1i+0x2a>2a: 48 89 c6 mov %rax,%rsi2d: 48 89 d7 mov %rdx,%rdi30: e8 00 00 00 00 callq 35 <_Z4fun1i+0x35>35: 90 nop36: c9 leaveq 37: c3 retq 0000000000000038 <main>:38: f3 0f 1e fa endbr64 3c: 55 push %rbp3d: 48 89 e5 mov %rsp,%rbp40: 48 83 ec 20 sub $0x20,%rsp44: 89 7d ec mov %edi,-0x14(%rbp)47: 48 89 75 e0 mov %rsi,-0x20(%rbp)4b: c7 45 f8 2b 02 00 00 movl $0x22b,-0x8(%rbp)52: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 58 <main+0x20>58: 8d 14 00 lea (%rax,%rax,1),%edx5b: 8b 45 f8 mov -0x8(%rbp),%eax5e: 01 c2 add %eax,%edx60: 8b 45 fc mov -0x4(%rbp),%eax63: 01 d0 add %edx,%eax65: 89 c7 mov %eax,%edi67: e8 00 00 00 00 callq 6c <main+0x34>6c: b8 00 00 00 00 mov $0x0,%eax71: c9 leaveq 72: c3 retq 0000000000000073 <_Z41__static_initialization_and_destruction_0ii>:73: f3 0f 1e fa endbr64 77: 55 push %rbp78: 48 89 e5 mov %rsp,%rbp7b: 48 83 ec 10 sub $0x10,%rsp7f: 89 7d fc mov %edi,-0x4(%rbp)82: 89 75 f8 mov %esi,-0x8(%rbp)85: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)89: 75 32 jne bd <_Z41__static_initialization_and_destruction_0ii+0x4a>8b: 81 7d f8 ff ff 00 00 cmpl $0xffff,-0x8(%rbp)92: 75 29 jne bd <_Z41__static_initialization_and_destruction_0ii+0x4a>94: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 9b <_Z41__static_initialization_and_destruction_0ii+0x28>9b: e8 00 00 00 00 callq a0 <_Z41__static_initialization_and_destruction_0ii+0x2d>a0: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # a7 <_Z41__static_initialization_and_destruction_0ii+0x34>a7: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # ae <_Z41__static_initialization_and_destruction_0ii+0x3b>ae: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # b5 <_Z41__static_initialization_and_destruction_0ii+0x42>b5: 48 89 c7 mov %rax,%rdib8: e8 00 00 00 00 callq bd <_Z41__static_initialization_and_destruction_0ii+0x4a>bd: 90 nopbe: c9 leaveq bf: c3 retq 00000000000000c0 <_GLOBAL__sub_I_globalVar1>:c0: f3 0f 1e fa endbr64 c4: 55 push %rbpc5: 48 89 e5 mov %rsp,%rbpc8: be ff ff 00 00 mov $0xffff,%esicd: bf 01 00 00 00 mov $0x1,%edid2: e8 9c ff ff ff callq 73 <_Z41__static_initialization_and_destruction_0ii>d7: 5d pop %rbpd8: c3 retq
由上面反汇编代码可知,代码段包含了全局函数void fun1(),主函数main,类代码指令、静态变量globalVar1;.text代码段的前四个字节是f3 0f 1e fa;函数fun1和main的第一条指令为push %rbp,该两函数最后返回指令为retq。
2.4.2.数据段data和只读数据段.rodata分析
执行命令查看数据段指令和反汇编代码
objdump -x -s -d main.o
输出结果:
......
Idx Name Size VMA LMA File off Algn0 .text 000000d9 0000000000000000 0000000000000000 00000040 2**0CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data 00000008 0000000000000000 0000000000000000 0000011c 2**2CONTENTS, ALLOC, LOAD, DATA2 .bss 0000000c 0000000000000000 0000000000000000 00000124 2**2ALLOC3 .rodata 00000001 0000000000000000 0000000000000000 00000124 2**0CONTENTS, ALLOC, LOAD, READONLY, DATA4 .init_array 00000008 0000000000000000 0000000000000000 00000128 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA
......
Contents of section .data:0000 64000000 9a020000 d.......
Contents of section .rodata:0000 00 .
Contents of section .init_array:0000 00000000 00000000 ........
Contents of section .comment:0000 00474343 3a202855 62756e74 7520392e .GCC: (Ubuntu 9.0010 332e302d 31377562 756e7475 317e3230 3.0-17ubuntu1~200020 2e303429 20392e33 2e3000 .04) 9.3.0.
......
从上面代码可以看出.data数据段有两个已经初始化了全局变量值0x00000064、0x0000029a,这两个十六进制数转化为十进制数分别是100和666,刚好和全局变量globalVar1=100和staticVar1=666对的上。
2.4.3未初始化数据段.bss分析
还是通过敲下面命令查看.bss段的十六进制数和反汇编代码
objdump -x -s -d main.o
输出结果显示:
......
Idx Name Size VMA LMA File off Algn0 .text 000000d9 0000000000000000 0000000000000000 00000040 2**0CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data 00000008 0000000000000000 0000000000000000 0000011c 2**2CONTENTS, ALLOC, LOAD, DATA2 .bss 0000000c 0000000000000000 0000000000000000 00000124 2**2ALLOC
......
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 main.cpp
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l O .rodata 0000000000000001 _ZStL19piecewise_construct
0000000000000004 l O .bss 0000000000000001 _ZStL8__ioinit
0000000000000004 l O .data 0000000000000004 _ZZ4mainE10staticVar1
0000000000000008 l O .bss 0000000000000004 _ZZ4mainE10staticVar2
0000000000000073 l F .text 000000000000004d _Z41__static_initialization_and_destruction_0ii
00000000000000c0 l F .text 0000000000000019 _GLOBAL__sub_I_globalVar1
0000000000000000 l d .init_array 0000000000000000 .init_array
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .note.gnu.property 0000000000000000 .note.gnu.property
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g O .data 0000000000000004 globalVar1
0000000000000000 g O .bss 0000000000000004 globalVar2
0000000000000000 g F .text 0000000000000038 _Z4fun1i
0000000000000000 *UND* 0000000000000000 _ZSt4cout
0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 *UND* 0000000000000000 _ZNSolsEi
0000000000000000 *UND* 0000000000000000 _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
0000000000000000 *UND* 0000000000000000 _ZNSolsEPFRSoS_E
0000000000000038 g F .text 000000000000003b main
0000000000000000 *UND* 0000000000000000 _ZNSt8ios_base4InitC1Ev
0000000000000000 *UND* 0000000000000000 .hidden __dso_handle
0000000000000000 *UND* 0000000000000000 _ZNSt8ios_base4InitD1Ev
0000000000000000 *UND* 0000000000000000 __cxa_atexit
......
由上面的符号表可知,未初始化的全局变量globalVar2、局部静态变量staticVar2、类St都在.bss段,而非静态局部变量var2不在.bss段里面,var2非静态局部变量应该在栈内存中,此处没有体现。
2.5总结
ELF文件的结构主要为ELF文件头、代码段、数据段(.data和rodata)、BSS段与程序运行密切相关的其它段,同时还可以自定义段,比如.mp3,通过objcopy工具将该段插入ELF数据段中。
看到这里,小师妹对师兄的回答非常满意,一双勾魂迷妹眼让师兄心肝砰砰乱跳,血流加速,此时师妹似乎下定了什么决心,羞红着脸颊,决定最后问师兄一个问题:“请问ELF头的内容是什么?段又是怎么定义的呢?”
师兄听完后,决定今夜不眠不休,爆了几个肝,也要为小师妹答疑解惑!好了,具体结果,请听下回分解。哈哈哈~~(^_^)
三、参考文献
《程序员的自我修养——编译、装载与库》俞甲子 石凡 潘爱民 著, 电子工业出版社, page:55-95