ELF(Executable and Linking Format)文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,ELF是一种对象文件的格式,用于定义不同类型的对象文件(Object files)的内容是什么、以及都以怎样的格式去存放这些内容。它有三种不同的类型:

1、  可重定位的目标文件(Relocatable)

这是由汇编器汇编生成的 .o 文件。后面的链接器把一个或多个可重定位的目标文件作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o可重定位的目标文件归档(archive)成 .a 静态库文件。

2、  可执行文件(Executable)

我们编译出的程序都是可执行文件。在 Linux 系统里面,存在两种可执行的东西。除了这里说的可执行文件,另外一种就是可执行的脚本(如shell脚本)。注意这些脚本不是可执行文件,它们只是文本文件,但是执行这些脚本所用的解释器就是可执行文件,比如 bash shell 程序。

3、共享库(Shared Object)

这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。

下面我们分析上一篇中“求一组数的最大值的汇编程序”经过汇编之后生成的目标文件max.o和链接之后生成的可执行文件max的格式,从而理解汇编、链接和加载执行的过程。共享库以后再详细介绍。

ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而执行一个ELF文件时,在加载器(Loader)看来它是由Program Header Table描述的一系列Segment的集合。如下图所示。

左边是从汇编器和链接器的视角来看这个文件,开头的ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,SectionHeader Table中保存了所有Section的描述信息。右边是从加载器的视角来看这个文件,开头是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加载过程中没有用到,所以是可有可无的。注意Section Header Table和Program Header Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。

我们在汇编程序中用.section声明的Section会成为目标文件中的Section,此外汇编器还会自动添加一些Section(比如符号表)。Segment是指在程序运行时加载到内存的具有相同属性的区域,由一个或多个Section组成,比如有两个Section都要求加载到内存后可读可写,就属于同一个Segment。有些Section只对汇编器和链接器有意义,在运行时用不到,也不需要加载到内存,那么就不属于任何Segment。

目标文件需要链接器做进一步处理,所以一定有Section Header Table;可执行文件需要加载运行,所以一定有Program Header Table;而共享库既要加载运行,又要在加载时做动态链接,所以既有Section Header Table又有Program Header Table。

下面用readelf工具读出目标文件max.o的ELF Header和Section Header Table,然后我们逐段分析。

ELF Header中描述了操作系统是UNIX,体系结构是x86-64。SectionHeader Table中有8个节头(Section Header),在文件中的位置(或者叫文件地址)从216(0xD8)开始,每个节(Section)64字节。这个目标文件没有Program Header。

从节头(Section Header)中读出各Section的描述信息,其中.text.data是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。Addr是这些段加载到内存中的地址(我们讲过程序中的地址都是虚拟地址),加载地址要在链接时填写,现在空缺,所以是全0。OffSize两列指出了各Section的文件地址,比如.data从文件地址0x6d开始,一共0x38个字节,回去翻一下程序,.data中定义了14个4字节的整数,一共是56个字节,也就是0x38个。根据以上信息可以描绘出整个目标文件的布局。

max.o这个文件不大,我们直接用hexdump工具把目标文件的字节全部打印出来看。

左边一列是文件中的地址,中间是每个字节的16进制表示,右边是把这些字节解释成ASCII码所对应的字符。中间有一个*号表示省略的部分全是0。.data段对应的是这一块:

.shstrtab和.strtab这两个Section中存放的都是ASCII码:

可见.shstrtab中保存着各Section的名字,.strtab中保存着程序中用到的符号的名字。每个名字都是以'\0'结尾的字符串。

我们知道,C语言的全局变量如果在代码中没有初始化,就会在程序加载时用0初始化。这种数据属于.bss段,在加载时它和.data段一样都是可读可写的数据,但是在ELF文件中.data段需要占用一部分空间保存初始值,而.bss段则不需要。也就是说,.bss段在文件中只占一个Section Header而没有对应的Section,程序加载时.bss段占多大内存空间在Section Header中描述。

我们继续分析readelf输出的最后一部分,是从.rel.text和.symtab这两个Section中读出的信息。

.rel.text告诉链接器指令中的哪些地方需要重定位。

.symtab是符号表。Ndx列是每个符号所在的Section编号,例如data_items在第3个Section里(也就是.data),各Section的编号见SectionHeader Table。Value列是每个符号所代表的地址,在目标文件中,符号地址都是相对于该符号所在Section的相对地址,比如data_items位于.data段的开头,所以地址是0,_start位于.text段的开头,所以地址也是0,但是start_loop和loop_exit相对于.text段的地址就不是0了。从Bind这一列可以看出_start这个符号是GLOBAL的,而其它符号是LOCAL的,GLOBAL符号是在汇编程序中用.globl指示声明过的符号。

现在剩下.text段没有分析,objdump工具可以把程序中的机器指令反汇编(Disassemble),那么反汇编的结果是否跟原来写的汇编代码一模一样呢?我们对比分析一下。

左边是机器指令的字节,右边是反汇编结果。显然,所有的符号都被替换成地址了,比如je 26,注意没有加$的数表示内存地址,而不表示立即数。这条指令后面的<loop_exit>并不是指令的一部分,而是反汇编器从.symtab和.strtab查到的符号名称,写在后面是为了有更好的可读性。目前所有的跳转指令和内存访问指令(mov 0x0(,%edi,4),%eax)中的地址都是符号的相对地址,下一步链接器要修改这些指令,把其中的地址都改成加载时的内存地址,这些指令才能正确执行。

现在我们按前面的步骤分析可执行文件max,看看链接器都做了什么改动。

在ELF Header中,Type改成了EXEC,由目标文件变成可执行文件了,Entry point address改成了0x8048074(这是_start符号的地址),还可以看出,多了两个Program Header,少了两个Section Header。

在Section Header Table中,.text和.data的加载地址分别改成了0x0804 8074和0x0804 90a0。.bss段没有用到,所以被删掉了。.rel.text段就是用于链接过程的,链接完了就没用了,所以也删掉了。

多出来的Program Header Table描述了两个Segment的信息。.text段和前面的ELF Header、Program Header Table一起组成一个Segment(FileSiz指出总长度是0x9e),.data段组成另一个Segment(总长度是0x38)。VirtAddr列指出第一个Segment加载到虚拟地址0x0804 8000(注意在x86平台上后面的PhysAddr列是没有意义的),第二个Segment加载到地址0x0804 90a0。Flg列指出第一个Segment的访问权限是可读可执行,第二个Segment的访问权限是可读可写。最后一列Align的值0x1000(4K)是x86平台的内存页面大小。在加载时要求文件中的一页对应内存中的一页。

这个可执行文件很小,总共也不超过一页大小,但是两个Segment必须加载到内存中两个不同的页面,因为MMU的权限保护机制是以页为单位的,一个页面只能设置一种权限。此外还规定每个Segment在文件页面内偏移多少加载到内存页面仍然偏移多少,这样规定是为了简化链接器和加载器的实现。

原来目标文件符号表中的Value都是相对地址,现在都改成绝对地址了。此外还多了三个符号__bss_start、_edata和_end,这些是在链接过程中添进去的,加载器可以利用这些信息把.bss段初始化为0。

再看一下反汇编的结果:

指令中的相对地址都改成绝对地址了。其实只是反汇编的结果不同了,指令根本没改。为什么不用改指令就能跳转到新的地址呢?因为跳转指令中指定的是相对于当前指令向前或向后跳多少字节,而不是指定一个完整的内存地址,内存地址有32位,这些跳转指令只有16位,显然也不可能指定一个完整的内存地址,这称为相对跳转。

C语言的本质(30)——C语言与汇编之ELF文件格式的更多相关文章

  1. C语言的本质(28)——C语言与汇编之用汇编写一个Helloword

    为了更加深入理解C语言的本质,我们需要学习一些汇编相关的知识.作为最基本的编程语言之一,汇编语言虽然应用的范围不算很广,但是非常重要.因为它能够完成许多其它语言所无法完成的功能.就拿 Linux 内核 ...

  2. C语言的本质(15)——C语言的函数接口入门

    C语言的本质(15)--C语言的函数接口 函数的调用者和其实现者之间存在一个协议,在调用函数之前,调用者要为实现者提供某些条件,在函数返回时,实现者完成调用者需要的功能. 函数接口通过函数名,参数和返 ...

  3. C语言的本质(10)——指针本质

    指针,大概是C语言中最难理解的概念之一了.指针这个东西是C语言中的一个基本概念,C99中对于指针的定义是: 1. 指针的类型是derived from其它类型,也就是说指针的类型是由它指向的类型决定的 ...

  4. C语言的本质(7)——C语言运算符大全

    C语言的本质(7)--C语言运算符大全 C语言的结合方向 C语言中各运算符的结合性分为两种,即左结合性(自左至右)和右结合性(自右至左).例如算术运算符的结合性是自左至右,即先左后右.如有表达式 x- ...

  5. C语言的本质(4)——浮点数的本质与运算

    C语言的本质(4)--浮点数的本质与运算 C语言规定了3种浮点数,float型.double型和long double型,其中float型占4个字节,double型占8个字节,longdouble型长 ...

  6. C语言的本质(3)——整数的本质与运算

    C语言的本质(3)--整数的本质与运算 计算机存储的最小单位是字节(Byte),一个字节通常是8个bit.C语言规定char型占一个字节的存储空间.如果这8个bit按无符号整数来解释,则取值范围是0~ ...

  7. C语言入门---第九章 C语言指针

    没学指针就是没学C语言! 指针是C语言的精华,也是C语言的难点. 所谓指针,也就是内存的地址,所谓指针变量,也就是保存了内存地址的变量.不过人们往往不会区分两者的概念,而是混淆在一起使用. ===== ...

  8. 12天学好C语言——记录我的C语言学习之路(Day 12)

    12天学好C语言--记录我的C语言学习之路 Day 12: 进入最后一天的学习,用这样一个程序来综合考量指针和字符串的关系,写完这个程序,你对字符串和指针的理解应该就不错了. //输入一个字符串,内有 ...

  9. 12天学好C语言——记录我的C语言学习之路(Day 10)

    12天学好C语言--记录我的C语言学习之路 Day 10: 接着昨天的指针部分学习,有这么一个题目: //还是四个学生,四门成绩,只要有学生一门功课没及格就输出这个学生的所有成绩 /*//progra ...

随机推荐

  1. SIM卡尺寸及剪卡教程

    手机SIM卡有全尺寸SIM卡.Mini-SIM卡.Micro-SIM卡.Nano-SIM卡.Embedded-SIM卡等类型,目前主流手机基本都是趋向使用Micro-SIM卡和Nano-SM卡. 一. ...

  2. OVERLAY代码重入

    OVERLAY代码重入问题:自己遇到的问题 编写的测试代码如下: #include <stdio.h> #define BYTE unsigned char #define BYTE un ...

  3. 用JSON 和 Google 实现全文翻译

    unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms ...

  4. Json之语法,格式

    JSON 文本格式在语法上与创建 JavaScript 对象的代码相同.由于这种相似性,无需解析器,JavaScript 程序能够使用内建的 eval() 函数,用 JSON 数据来生成原生的 Jav ...

  5. STL源码剖析读书笔记之vector

    STL源码剖析读书笔记之vector 1.vector概述 vector是一种序列式容器,我的理解是vector就像数组.但是数组有一个很大的问题就是当我们分配 一个一定大小的数组的时候,起初也许我们 ...

  6. 关于 require的缓存

    有两个文件 a.js内容如下: var add = require("./t.js").add; var add2 = require("./t.js").ad ...

  7. poj 2377 Bad Cowtractors(最大生成树!)

    Description Bessie has been hired to build a cheap internet network among Farmer John's N (2 <= N ...

  8. cmd用到的基本操作

    dir #显示当前目录中的文件和子目录 dir /a #显示当前目录中的文件和子目录,包括隐藏文件和系统文件 a = all dir c: /a:d #显示 C 盘当前目录中的目录 d = direc ...

  9. SQL SERVER 2005 错误:18456

    安装好SQL SERVER 2005之后,Windows身份验证无法登陆,出现18456错误.而sql server 身份验证可以用sa用户登陆. 解决办法: 用sa用户登陆,执行SQL 语句: CR ...

  10. CATransform3D参数的意义

    经常忘记CATransform3D各参数的意思,记下来好好理解下   struct CATransform3D { CGFloat m11(x缩放),m12(y切变),m13(旋转),m14(); C ...