您的位置:首页 > 其它

深入理解程序构造

2017-05-27 09:07 239 查看
源代码经过编译器编译后产生的文件叫做目标文件,多个目标文件链接后可以产生可执行文件,所以目标文件除了有些符号和地址没有通过链接来调整,其基本格式与可执行文件相似。


目标文件的格式

目前流行的可执行文件格式(Executable)主要就是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkble Format),都是COFF(Common File format)的变种, 在Linux中目标文件就是常见的中间文件
.o
,对应的在Windows中就是
.obj
文件。由于格式与可执行文件相近,所以基本可以看做一种类型的文件。在Windows下统称为PE-COFF文件格式,在Linux下,统称为ELF文件。

除了可执行文件,包括动态链接库(Windows下的.dll, Linux 下的.so)以及静态链接库(Windows 下的.lib, Linux下的.a)都是按照以上格式存储的,在Windows下的格式都是PE-COFF,Linux下则按照ELF格式存储。唯一不同的是Linux下的静态链接库(.a
文件),它基本上就是把许多目标文件捆绑在一起打包,类似tar命令, 再加上一些索引。

ELF文件标准大概包含了以下四种文件类型:
可重定位文件:主要包含代码和数据,可以被用来链接成可执行文件或者共享目标文件,静态链接库也归类于这一类,包括Linux的.o文件,Windows的.obj文件
可执行文件:包含可以直接执行的程序,比如Linux下的/bin/bash,Windows下的.exe
共享目标文件:主要包含代码和数据,第一种用途可以与其它文件链接生成可重定位或者共享目标文件,再者直接链接到可执行文件,作为进程映象的一部分动态执行。常见的Linux下的.so,Windows下的.dll。
核心转储文件(Core dump):这个格式调试bug时很有用,进程意外终止时产生的,保留程序终止时进程的信息,Linux下的Core dump。

我们可以使用file命令来获取文件的格式。



重定位文件



可执行文件



动态链接库


目标文件内部结构

这节我们以简单的ELF目标文件作为举例:
#include<stdio.h>
int global_var1 = 1;
int global_var2;
void func1(int i)
{
printf("%d\n", i);
}
int main()
{
static int a1 = 85;
static int a2;
int m = 9;
int n;
func1(a1+global_var1+m+n);
return 0;
}


我们默认的平台是32位Intel X86平台
gcc -c cal.c


产生目标文件cal.o

我们可以借助于binutils的工具objdump来查看目标文件内部结构。
$ objdump -h cal.o

cal.o:     文件格式 elf32-i386

节:
Idx Name          Size      VMA       LMA       File off  Algn
0 .text         00000064  00000000  00000000  00000034  2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data         00000008  00000000  00000000  00000098  2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss          00000004  00000000  00000000  000000a0  2**2
ALLOC
3 .rodata       00000004  00000000  00000000  000000a0  2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment      00000035  00000000  00000000  000000a4  2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000  00000000  00000000  000000d9  2**0
CONTENTS, READONLY
6 .eh_frame     00000064  00000000  00000000  000000dc  2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


"-h"就是把ELF文件各个段的基本信息打印出来,也可以到man手册查询更多详细用法。

cal.o



下面我们来分析上面各段:


代码段( .text)

程序源代码编译后的机器指令经常被放在代码段(Code Section)里,代码段常见的名字就是.text或者.code,借助于objdump这个利器,我们可以进一步的分析代码段的内容,-s可以将所有段的内容以十六进制的方式打印出来,-d可以将所有包含的指令反汇编。

下面使用objdump把代码段的内容提取出来:
$ objdump -s -d cal.o

cal.o:     文件格式 elf32-i386

Contents of section .text:
0000 5589e583 ec0883ec 08ff7508 68000000  U.........u.h...
0010 00e8fcff ffff83c4 1090c9c3 8d4c2404  .............L$.
0020 83e4f0ff 71fc5589 e55183ec 14c745f0  ....q.U..Q....E.
0030 09000000 8b150400 0000a100 00000001  ................
0040 c28b45f0 01c28b45 f401d083 ec0c50e8  ..E....E......P.
0050 fcffffff 83c410b8 00000000 8b4dfcc9  .............M..
0060 8d61fcc3                             .a..
Contents of section .data:
0000 01000000 55000000                    ....U...
Contents of section .rodata:
0000 25640a00                             %d..
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520352e  .GCC: (Ubuntu 5.
0010 342e302d 36756275 6e747531 7e31362e  4.0-6ubuntu1~16.
0020 30342e32 2920352e 342e3020 32303136  04.2) 5.4.0 2016
0030 30363039 00                          0609.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 017c0801  .........zR..|..
0010 1b0c0404 88010000 1c000000 1c000000  ................
0020 00000000 1c000000 00410e08 8502420d  .........A....B.
0030 0558c50c 04040000 28000000 3c000000  .X......(...<...
0040 1c000000 48000000 00440c01 00471005  ....H....D...G..
0050 02750043 0f03757c 06750c01 0041c543  .u.C..u|.u...A.C
0060 0c040400                             ....

Disassembly of section .text:

00000000 <func1>:
0:   55                      push   %ebp
1:   89 e5                   mov    %esp,%ebp
3:   83 ec 08                sub    $0x8,%esp
6:   83 ec 08                sub    $0x8,%esp
9:   ff 75 08                pushl  0x8(%ebp)
c:   68 00 00 00 00          push   $0x0
11:   e8 fc ff ff ff          call   12 <func1+0x12>
16:   83 c4 10                add    $0x10,%esp
19:   90                      nop
1a:   c9                      leave
1b:   c3                      ret

0000001c <main>:
1c:   8d 4c 24 04             lea    0x4(%esp),%ecx
20:   83 e4 f0                and    $0xfffffff0,%esp
23:   ff 71 fc                pushl  -0x4(%ecx)
26:   55                      push   %ebp
27:   89 e5                   mov    %esp,%ebp
29:   51                      push   %ecx
2a:   83 ec 14                sub    $0x14,%esp
2d:   c7 45 f0 09 00 00 00    movl   $0x9,-0x10(%ebp)
34:   8b 15 04 00 00 00       mov    0x4,%edx
3a:   a1 00 00 00 00          mov    0x0,%eax
3f:   01 c2                   add    %eax,%edx
41:   8b 45 f0                mov    -0x10(%ebp),%eax
44:   01 c2                   add    %eax,%edx
46:   8b 45 f4                mov    -0xc(%ebp),%eax
49:   01 d0                   add    %edx,%eax
4b:   83 ec 0c                sub    $0xc,%esp
4e:   50                      push   %eax
4f:   e8 fc ff ff ff          call   50 <main+0x34>
54:   83 c4 10                add    $0x10,%esp
57:   b8 00 00 00 00          mov    $0x0,%eax
5c:   8b 4d fc                mov    -0x4(%ebp),%ecx
5f:   c9                      leave
60:   8d 61 fc                lea    -0x4(%ecx),%esp
63:   c3                      ret


看开头一段Contents of section .text就是一十六进制打印出来的内容,最左列是偏移量, 看
0060
那行,只剩下
8d61fcc3
,所以与对照上面一张图,.text段的size是
0x64
字节。最右列是.text段的ASCII码格式,对照下面的反汇编结果,我们可以看到cal.c中的两个函数
func1()
main()
的指令。.text的第一个字节
0x55
就是
func1()
函数的第一条
push
%ebp
指令,最后一个
0xc3
main()
的最后一个指令
ret


数据段和只读数据段(.data & .rodata)

.data段保存的是那些已经初始化的全局静态变量和局部静态变量。代码中的
global_var1
a1
都是这样的变量,每个变量4字节,所以
.data
段的大小为8个字节。

cal.c在调用printf时,内部包含一个字符串常量"%d\n"用来定义格式化输出,它是一种只读数据,所以保存在.rodata段,我们可以看图中.rodata段大小为4字节,内容为25640a00,翻译回来就是
"%d\n"


.rodata段存放的是只读数据,一般程序里面存在只读变量和字符串常量这两种只读类型,单独设置.rodata段有很多好处,支持了C里面的关键字const, 而且操作系统加载程序时自动将只读变量加载到只读存储区,或者映射成只读,这样任何修改操作都会被认为非法操作,保证了程序的安全性。


BSS段(.bss)

.bss段存放的是未初始化的全局变量和局部静态变量。上面代码中的
global_var2
a2
就被存放在.bss段。其实只能说.bss段为他们预留了空间,实际上该段大小只有4个字节,而这两个变量应该占用8个字节。

其实我们可以通过符号表看到,只有a2被放到了
.bss
段,
global_var2
却没有放到任何段,只是一个未定义的“COMMON”符号。其实这与不同的语言和不同的编译器实现有关,有的编译器不把未定义的全局变量放到.bss段,只是保留一个符号,直到链接成可执行文件时才在.bss段分配空间。

有个小例子:
static int x1 = 0;
static int x2 = 1;


x1和x2会被放在什么段呢?
答案是x1被放在.bss段 ,而x2被放在.data段。原因在于x1被初始化为0,相当于没有被初始化,未初始化的都是0,所以这里编译器会把x1优化掉,放在.bss段,因为.bss不占磁盘空间。x2正常的初始化,所以被放到.data段。


其它段

除了以上各段,ELF文件也包含其它段。下表列举了一些常见的段。
常用的段名说明
.rodata1Read Only Data,这种段里存放的是只读数据,比如字符串常量,全局const变量,和".rodata"一样
.comment存放的是编译器版本信息,比如字符串:"GCC:(GUN)4.2.0"
.debug调试信息
.dynamic动态链接信息
.hash符号哈希表
.line调试时的行号表,即源代码行号和编译后指令的对应表
.note额外的编译器信息。比如程序的公司名,发布版本号
.strtabString Table字符串表,用于存储ELF文件中用到的各种字符串
.symtabSymbol Table符号表
.shstrtabSection String Table段名表
,plt .got动态链接的跳转表和全局入口表
.init .finit程序初始化与终结代码段
这些段的名字都是“.”作为前缀,一般系统定义的都是"."开头,如果自己定义的段名则不要以"."开头,容易与系统保留的产生冲突,如果你打开目标文件的段名还有其它一些格式,也许都是以前系统曾经用过的,历史遗留问题。

我们也可以自定义段,GCC提供一个扩展机制可以让我们指定变量所处的段:
__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo()


在全局变量或者函数前加上attribute((section("name")))属性就可以把相应的变量和函数放到以“name”作为段名的段中。


ELF文件结构描述


1. 文件头


上面的例子中我们分析了ELF文件的各个段,位于所有段前面的就是文件头。我们可以使用readelf命令来查看。
$ readelf -h cal.o
ELF 头:
Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别:                              ELF32
数据:                              2 补码,小端序 (little endian)
版本:                              1 (current)
OS/ABI:                            UNIX - System V
ABI 版本:                          0
类型:                              REL (可重定位文件)
系统架构:                          Intel 80386
版本:                              0x1
入口点地址:               0x0
程序头起点:          0 (bytes into file)
Start of section headers:          796 (bytes into file)
标志:             0x0
本头的大小:       52 (字节)
程序头大小:       0 (字节)
Number of program headers:         0
节头大小:         40 (字节)
节头数量:         13
字符串表索引节头: 10


从上面的输出结果可以看到,ELF的文件头中定义了ELF魔数,文件数据存储方式,版本,运行平台,ABI版本,系统架构,硬件平台,入口地址,程序头入口和长度,段表的位置和长度,段的数量等等。

ELF文件头结构和相关常数被定义在"/usr/include/elf.h"里,分为32位和64位版本。我们测试的机器是32位的,包含"Elf32_Ehdr"的数据结构来描述上述输出的ELF头。
typedef struct
{
unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
Elf32_Half    e_type;                 /* Object file type */
Elf32_Half    e_machine;              /* Architecture */
Elf32_Word    e_version;              /* Object file version */
Elf32_Addr    e_entry;                /* Entry point virtual address */
Elf32_Off     e_phoff;                /* Program header table file offset */
Elf32_Off     e_shoff;                /* Section header table file offset */
Elf32_Word    e_flags;                /* Processor-specific flags */
Elf32_Half    e_ehsize;               /* ELF header size in bytes */
Elf32_Half    e_phentsize;            /* Program header table entry size */
Elf32_Half    e_phnum;                /* Program header table entry count */
Elf32_Half    e_shentsize;            /* Section header table entry size */
Elf32_Half    e_shnum;                /* Section header table entry count */
Elf32_Half    e_shstrndx;             /* Section header string table index */
} Elf32_Ehdr;


对比Elf32_Ehdr和之前的ELF头,可以发现很多字段一一对应。不过e_ident这个成员数组对应了“类型”,“数据”,“版本,“OS/ABI”,“ABI版本”这五个参数,剩下的都一一对应。

ELF魔数 从上面的readelf的输出可以看到,Magic有16个字节,对应着Elf32_Ehdr的e_ident这个成员。这个属性被用来标识ELF文件的平台属性。
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00

最开始的4个字节: 所有ELF文件共有的标识码,"0x7F"、"0x45"、"0x4c"、"0x46",其中,"0x7F"对应ASCII中的DEL控制符,后面三个是ELF三个字母的ASCII码。这4个字节又被称为ELF文件的魔数。

基本所有可执行文件开始的几个字节都是魔数:
a.out: 0x01、0x07
PE/COFF: 0x4d,0x5a

这些魔数被操作系统用来确认可执行文件的类型,如果不对就拒绝加载。
第5个字节: 表示ELF的文件类,0x01代表是32位的,如果是0x02则表示64位,
第6个字节: 规定字节序,规定该ELF是大端还是小端的
第7个字节: 规定ELF文件的主版本号,一般都是1,因为没有更新过了。
后面的9个字节:都填充为0, 一般没意义,有的平台用来做扩展标识。

类型 e_type成员用来表示ELF文件类型,系统通过这个值来判断文件类型,而不是扩展名。
常量含义
ET_REL1可重定位文件,一般是.o文件
ET_EXEC2可执行文件
ET_DYN3共享目标文件,一般为.so
机器类型 ELF文件格式被设计成在多平台下使用,和java不同,ELF文件不能一次编译处处使用,而是说不同平台下的ELF文件都遵循一套ELF标准。用e_machine成员表示平台属性。
常量含义
EM_M321AT&T WE32100
EM_SPARK2SPARC
EM_3863Intel x86
EM_68K4Motorola 68000
EM_88K5Motorala 88000
EM_8606Intel 80860
2016/

未完待续。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: