深入理解程序构造(一)

真正了不起的程序员对自己的程序的每一个字节都了如指掌。

编码

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

目标文件的格式##

目前流行的可执行文件格式(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指令,最后一个0xc3main()的最后一个指令ret

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

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

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

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

BSS段(.bss)###

.bss段存放的是未初始化的全局变量和局部静态变量。上面代码中的global_var2a2就被存放在.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文件也包含其它段。下表列举了一些常见的段。

常用的段名 说明
.rodata1 Read Only Data,这种段里存放的是只读数据,比如字符串常量,全局const变量,和".rodata"一样
.comment 存放的是编译器版本信息,比如字符串:"GCC:(GUN)4.2.0"
.debug 调试信息
.dynamic 动态链接信息
.hash 符号哈希表
.line 调试时的行号表,即源代码行号和编译后指令的对应表
.note 额外的编译器信息。比如程序的公司名,发布版本号
.strtab String Table字符串表,用于存储ELF文件中用到的各种字符串
.symtab Symbol Table符号表
.shstrtab Section 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_REL 1 可重定位文件,一般是.o文件
ET_EXEC 2 可执行文件
ET_DYN 3 共享目标文件,一般为.so

机器类型 ELF文件格式被设计成在多平台下使用,和java不同,ELF文件不能一次编译处处使用,而是说不同平台下的ELF文件都遵循一套ELF标准。用e_machine成员表示平台属性。

常量 含义
EM_M32 1 AT&T WE32100
EM_SPARK 2 SPARC
EM_386 3 Intel x86
EM_68K 4 Motorola 68000
EM_88K 5 Motorala 88000
EM_860 6 Intel 80860

2016/
未完待续。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容