读书笔记:使用objdump分析目标文件

[这篇读书笔记可能要分几天完成]

0x80 前言

最近几天扮演了一下很多人眼里的仇敌——面试官,说句实在话,自己也就从中国海洋育儿所毕业不到一年,工作两年多,而很多应聘者扣头上就是三年工作经验,最初的时候心想我有什么资格去面试行业道路上的老前辈。几场面试下来,这种想法就不存在了。的确,牛人云集的互联网行业谁也不敢说大话,但是很多应聘者都有一个通病,那就是对技术只知其一不知其二,他们更多满足于使用上而忽略了基础的重要性,更有甚者对于底层或者基础知识嗤之以鼻。

最近几场面试我都会循序渐进的问几个问题,问题最终都停在是否阅读过源码这件事情上,我承认,我没有看所有的源码,我没有足够的时间去阅读所有的代码,但我对我感兴趣或者说我认为有用的源代码都会尝试去学习,因为我觉得别人的代码总有值的我们学习的地方,我不可能记住代码,但我会尽可能的去领悟他们的设计思想,说不定哪天你也会需要开发这样一个东西,它可以帮助你快速的技术选型与定桩。

还有一点,基础知识的重要性,我深知基础的重要性,也承认我的基础不是很扎实,但是如果你连最基本的基础知识都掌握的不够好,你说在团队中是骨干是核心我是不信的,忽略了基础的开发按照我的理解只是在用不同的方式堆砌代码,毫无技术含量可言,代码也必然枯燥乏味。我也一直在反思自己,最近项目也算是比较繁忙,忙什么,忙需求,自己也写了不少日记吐槽自己,可是我转念一想理需求是作为开发又不得不做还必须做好的事情,我学习了很多,时间很紧(当然我还是能抽出时间打打游戏放松一下,我不是机器我也需要休息)却很充实,文章中断的多了但是我认为这是一个好的现象,盲目的坚持写些没用的东西(比如现在)其实也是浪费时间。最近又拿出时间来看《程序员的自我修养》,我决定认真的看完,并记录下自己的收获,也算是让自己的努力有一个可以追溯的证据。

0x81 目标文件

最近又看到了目标文件这一章(又是因为之前断断续续看的都忘得差不多了),目标文件就是经过编译但未经过链接的中间文件。Linux下目标文件和可执行文件的内容与结构很相似,所以它们通称为ELF文件,包括动态链接库和静态链接库也是ELF文件。

中间文件和可执行文件

目标文件的结构到底是怎样的?它们通常按类别存储,而每一个类别叫做一个Section,比如编译后的机器指令存放在Code Section,数据存放在Data Section当中。

0x82 生成目标文件

为了方便以后到回头来结合书看,我就用书上的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SimpleSection.c
int printf(const char* format, ...);

int global_init_var = 84;
int global_uninit_var;

void func1(int i) {
printf("%d\n", i);
}

int main(void) {
static int static_var = 85;
static int static_var2;

int a = 1;
int b;

func1(static_var + static_var2 + a + b);

return a;
}

我们使用gcc生成32位目标文件:

生成目标文件

目标文件大小为1344个字节,接下来我们使用objdump工具分析目标文件内容。

0x83 objdump分析基本信息

objdump来自于包binutils,隶属于GNU Development Tools,它的主要功能就是分析出目标文件的各种信息,比如objdump -h SimpleSection.o

objdump结果

从上述的结果我们可以看出SimpleSection.o一共有7个Section,分别是.text、.data、.bss、.rodata、.comment、.note.GNU-stack、.eh_frame对应代码段、数据段、符号块预留段、只读数据段、编译器信息段、栈标记段、异常处理段(书上没有,exception handling的意思,内容类似debug_frame段)。

其中的Size表示段长,File Off表示段起始偏移,Algn表示对齐,所以我们可以看出各段是连续存储的,最初是ELF Header,它记录了最基本的信息。

在每一行的下面都跟了一行信息,它们是对该段的描述,它们的含义如下:

  • CONTENTS 占据文件内容
  • ALLOC 运行时申请内存
  • LOAD 可作为装载数据
  • RELOC Load时可重定向
  • READONLY 内容只读
  • CODE 内容为机器代码
  • DATA 程序数据

我们可以发现.bss段是只申请内存而不占据文件内容的,因此.bss段可以视作未初始化的全局变量和局部静态变量的预留位置。

0x84 目标文件分析(2017-06-15更新)

从前面的基本信息中我们可以发现,目标文件是按段存储的,接下来我们来分别看每个段的内容。

  1. 代码段(.text)
    关于目标文件的内容我们可以先用objdump显示出来,使用它的objdump -s, --full-contents Display the full contents of all sections requested参数,控制台会打印16进制的文件内容。

    16进制内容

    通过这种方式我们可以看每个段的内容,代码段长度0x62与之前的信息一致,然后使用objdump -d, --disassemble Display assembler contents of executable sections参数,可以显示代码段的反汇编指令:

    代码段反汇编

    从图中能看到我们定义的两个函数名,这两个函数的地址就保存在符号表当中,如果SimpleSection.o被strip工具脱符,那就看不到函数地址,但是我们可以推断出来只是不知道名字。

    stripped反汇编

    从反汇编的结果来看,0x1b和0x61相对位置的0xc3正好是函数返回ret操作。

  2. 数据段和只读数据段(.data/.rodata)
    从16进制内容中可以看出,数据段正好8字节,每4字节对应84和85,也就是我们源代码中的global_init_var及static_var这两个初始化的全局和静态局部整形变量。global_uninit_var和static_var2则不在这一段,看符号表会发现static_var2是在BSS段,而global_uninit_var没有存放在任何段,只是用一个COMMON符号标记,按书中所说,这跟编译器的实现有关,全局未初始化变量也有可能记录在BSS段。

    至于只读数据段25640a00正是源代码中的”%d\n”字符串,其中0a是ASCII中的换行,最后的00是终止符。

static int x1 = 0; static int x2 = 1;这两种数据所在的段是不一定一致的,x2在数据段很容易理解,而x1赋值为0在某些编译器看来它相当于是未初始化的,所以经过编译器的优化它可能会存放在BSS段当中。

  1. 其他段
    其他的内容主要都是一些附加信息,比如comment段显示的是编译器的信息。

简单的使用objdump就说这么多,接下来就结合另一个elf文件分析利器readelf剖析它们的文件结构(以我的表达能力可能得写好几篇,然后烂尾= =不得不说写东西实在是个体力活)。