C语言的ELF文件格式学习

最近的lab里面有ELF文件相关的,所以成这个几乎,学点ELF的东西。

ELF,是一种文件格式。暂时,只看可执行文件的ELF文件格式。

首先,给出文件的格式的布局图:

光看这个很难理解,所以写一个小的程序,用readelf来结合的看。

程序比较简单:

#include <stdio.h>
#include <stdlib.h>

int data[100] ={0};
int bss[100];

int main()
{
	int i=0;
	for(i=0; i<100; i++)
		bss[i] = i;
	printf("the bss[3]= %d\n", bss[3]);
	return 1;

}

首先,通过readelf -h 命令,来看elf头:

首先,第一个magic,魔数,这个主要是程序用来确认读入的是否是elf文件头,其中,第一个7f是默认的,后面的45,4c,46就是ascii码里面的elf相对于的码值,后面的01,没有实际意义。每次程序在读取elf头文件的时候,都会确认魔数是否正确,以防读入的不是elf文件。

接下来的class,Data,Version,OS/ABI, ABI Version type, machine version 都是一些关于机器,系统还有文件版本的一些信息,不是这次的主要内容,看看就好。

接下来的Entry point address 0x8048330 表示程序的入口地址,即程序载入完成后,第一条指令从这个地方开始,从指令上来说,就是在整个程序建立了进程,将相应的虚拟地址映射载入内存后,做完了所有的准备工作之后,将要开始执行程序了,此时,将eip 置为0x8048330这个值。刚开始学C的时候,很多人都认为,之所以要有main函数,是因为他是程序的入口,程序执行的第一条指令就是main函数。如果是这样,那在0x8048330这个位置的函数就应该是main函数了。

通过程序来看,用objdump将程序反汇编:

可以看到,程序的08048330的部分是一个叫-start的一个函数,并不是我们想当然的main。为什么?来看看<_start>函数的主要内容,扫一眼,就发现,这个函数的主要内容是在要存相关的寄存器,后面跳转到一个叫<[email protected]>函数下面去了,也就是说,在程序真正的执行main操作之前,还进行了其他的函数操作,也就是main并不是真正的第一个执行的函数。那在main之前,到底那些函数都干了什么呢?

其实很简单。main函数在开始的时候,里面的变量,直接开始的时候就在栈里,然后一开始就可以直接使用malloc和new等来申请堆空间,那栈和堆的刚开始的设置地址是什么?在main里好像没有设置吧?还有比如stdin,stdout等都没有打开,所以,main前面的_start等函数做的就是这种工作,初始化堆栈信息,并且打开标准输入输出等文件。

简单来说,就是一下内容:

//***********************************************

__start:

     init stack;

     init heap;

     open stdin;

     open stdout;

     open stderr;

     :

     push argv;

     push argc;

     call _main; (调用 main)

     :

     destory heap;

     close stdin;

     close stdout;

     close stderr;

     :

     call __exit;

**********************************************/

所以,main只是整个程序的中间函数,并不是程序最开始执行的函数!!!

接下来,Start of program headers:          52 (bytes into file)

Start of section headers:          5120 (bytes into file)

这就是程序表头和Section 表头的地址。这个地址,表示程序头和Section 表头的首地址距离ELF文件头地址的偏移量。

举个例子,程序总是从磁盘读入内存的。假设程序在磁盘的位置是0x1000,那程序头和Section 表头在磁盘的位置就是0x1052和0x6210,,注意,这个是磁盘的地址。

什么是程序表头和Section 表头。通俗的来讲,程序表是一张表,里面有程序需要从磁盘载入内存的所有内容的相关信息。而系统把这些需要载入内存的内容分成了一个一个的块,这些块需要一张表来管理并且记录他们的信息。而这个表就是程序表。相应的,Section 表就是记录每个Section 的相应信息的一张表。程序表和Section 表,在程序中的表示方法,就是两个结构体的数组,所以程序表头和Section 表头就是两个结构体数组的首地址。

下面的flag,应该是标志位什么的,暂时没搞明白。

在下面:  Size of this header:               52 (bytes)

这个就表示,这张elf头文件大小事52字节,参考前面的Start of program headers:          52 (bytes into file),程序头的内容在磁盘中也是从elf首字节下面的低52个字节里面,这就表示在磁盘中,elf头表之后,马上就是程序头的内容,和上面的elf的布局图相符合。

Size of program headers:           32 (bytes)

这个表示每个程序头表中,每一项的大小。前面说过,程序头表就是一个结构体的数组,那这个32byte就代表这个结构体的大小。

Number of program headers:         9

这个代表程序头表中,程序头的个数。和上面的程序头表的每一项大小相乘,就是整个程序头表的大小。

Size of section headers:           40 (bytes)

Number of section headers:         36

这两个数据,就是Section 表的数据,和程序头表的数据时一样的。

在磁盘中,这张elf头文件的形式是一个struct, 整个的内容和下面的代码相似:

struct Elf {
	uint32_t e_magic;	// must equal ELF_MAGIC
	uint16_t e_type;
	uint16_t e_machine;
	uint32_t e_version;
	uint32_t e_entry;
	uint32_t e_phoff;
	uint32_t e_shoff;
	uint32_t e_flags;
	uint16_t e_ehsize;
	uint16_t e_phentsize;
	uint16_t e_phnum;
	uint16_t e_shentsize;
	uint16_t e_shnum;
	uint16_t e_shstrndx;
};

这个和上面的readelf得到的结果是一一对应的。

接下来,看program header

这个就是程序头表里面的内容,这些程序头记录着程序需要拷贝到内存上的所有内容,需要把这些内容拷贝到内存上,才能实现程序的运行。这个程序的程序头总共有9项。每一项都有相应的属性,一项一项来看。

可以看到,第一项的type是PHDR,他表示我们要保存这项内容是程序头表。其中,offset表示这项内容保存的起始地址与程序头地址的偏移。即系统要通过这个偏移地址,从磁盘中读取相应的程序头的内容到内存中。有了从磁盘读入的地方,那肯定要有放到内存中的地方。VirtAddr就表示这部分内容需要要放到虚拟内存中的起始地址,后面的PhysAddr就是为了兼容采用实地址模式的系统。后面的FileSize和memSize分别表示程序头在文件中和在内存中的大小。(两个大小可以不一样,但一定是filesize<=memsize).后面的flg就是表示这个程序头的标志位,其中R表示可读,W表示可写,E表示可执行。最后的Align就表示这段程序头的对齐方式。其中0x4就表示4字节对齐,0x1000就表示4K对齐。

下面来验证一下:第一段程序头的type指出了这段程序头里的内容是表示程序头表的。他的起始文件位置是偏离程序起始文件位置0x34个字节的地方。通过计算可以知道,0x34=52,和前面的elf头文件中程序头表的起始位置吻合。后面的filesize是0x120=288byte, 通过elf知道,程序头表里总共有9项,每一项程序头占了32个字节,这样整个程序头的大小就是32*9=288.这个也吻合。接下来通过gdb查看程序在0x8048034的内容。

由于每一项程序头是的大小事32byte=0x20byte,所以上面两行代表一个程序头,可以看到和readelf给出的内容是完全吻合的,也就是在这块地址空间中,存的是相应的程序头表。

接下去的8项程序头,都是一样的,只是type不一样,每种type所代表的含义如下:

PHDR保存程序头表。

INTERP指定在程序已经从可执行映射到内存之后,必须调用解释器。在这里解释器并不意味着二进制文件的内存必须由另一个程序解释。它指的是这样的一个程序:通过链接其他库,来满足未解决的引用。

LOAD表示一个从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等等。

DYNAMIC段保存了其他动态链接器(即,INTERP中指定的解释器)使用的信息。

NOTE保存了专有信息。

仔细观察会发现,在两个属性为load的程序头中,包含了其他7个程序头中的所有段内容。这点,从载入的虚拟内存上的地址范围也可以发现这个问题。所以,在程序载入时,应该只需要载入type为load的两个程序头就可以了,而其他的程序头只是为了方便查找相应的内容的。

在上面的这章程序表的下面,还有每项程序头所包含的段的信息:

可以再里面看到比较熟悉的.text, .data 和.bss段。这就表示,所有程序头其实是程序里面section的一部分。整个程序按照一定的方法,被划分了若干个section,而程序头就是在所有section中,需要被载入到内存的那部分section。从真个程序文件的架构也可以看到,在那里面根本就没有程序头的部分。

接下来:section header

由于可执行文件的段比较多,所以就不全部截出来了。只给出部分的段:

这里主要看text,data,bss三个段。

其中,text表示的是程序的代码段。可以看到,他在内存中的地址是0x8048330,就是整个程序的入口地址。

后面的data和bss段。首先看.bss段,可以看到,bss段下面的comment段在文件中的偏移地址是一样的。这也就是大家常说的,.bss段在文件中是不占大小的。这是因为,bss段代表的是未初始化的全局变量。在C里面,未初始化的全局变量会被初始化为0,因此就不用在文件中给bss分配空间,因为只要变量属于bss段,那他就是0.而bss段中的变量,在内存中的总大小,就可以通过section段表来记录。也就是上面的bss段的size部分。

可以通过上面的程序头部分进行验证。通过上面的程序头中,.data和bss段都是出于程序头的03项,

单独把第三项拉出来:

可以看到,在这一项的程序头中,filesize金额memsize是不一样的,两者相间,0x45c-0x100=0x35c。比.bss段的大小多了c字节大小,这个多出来的c字节的大小不知道怎么回事。我猜想应该和字节对齐有关系。因为这段程序头在内存中的起始位置是0x0849f14, 加上内存中的大小,就是0x084a370。如果没有这个多出来的c字节,那可能下面的内容要实现字节对齐就比较麻烦,所以编译器认为的加了一个c字节上去。

那bss段的变量是怎么被初始化为0的?

在程序被载入内存的时候,只需要从文件拷贝filesize大小的内容进入内存,然后剩下的部分,将其全部以0填充就可以了。

从上面的程序头中包含的段映射可以看到,bss段是被放在他所在的程序头的最下面的,甚至是整个程序在内存空间中最下面的位置。所以通过上面的方法,就可以将bss段所属的变量全部清0,就完成了初始化为0的操作。而变量可以通过符号表来解决,其所在的位置。

符号表(只列出data和bss的信息):

在程序中,两个全局变量:

<span style="white-space:pre">	</span>int data[100] ={0};
<span style="white-space:pre">	</span>int bss[100];

在符号表里面,value应该就是变量的首地址,后面的size就是变量所占的内存大小。由于两个变量都是int型的大小为100的数组,所以大小为400。后面的NDX指出了符号在哪个段,从段表中找段25,可以看到,段25就是bss段。可能是由于我data[0]的值也是0,这样就导致了data和bss两个值都是0,所以编译器优化,就把两个变量都放到了bss段。

重新编一个简单的程序:

#include <stdio.h>
#include <stdlib.h>

int d=10;
int b;

int main()
{
	int i=0;
	printf("the out is  %d\n", b+d+i);
	return 1;

}

符号表和段表为:

可以看到,b,d所对应的的段序号正是data和bss段,而d的地址为0x0804a014, data的起始地址为0x0804a00c,size为c,所以d就是存储在data段的后4个字中的大小。而b变量也正好存储在bss段的后4个字节中。

从后来改过的程序可以看到,程序是通过符号表来进行初始化全局变量的的过程和上面的分析是符合的。

总结:

整个程序,在文件中,是通过elf头文件来管理的,elf中记录了程序中所有内容的信息。整个程序,首先分成了n个段,这些段的信息都存储在程序的段表里面。而程序头就是在所有的段中,需要载入内存的段。程序头由程序头表来记录其信息,包括其在文件中的位置和载入的内存的位置,还有其在文件中的大小和在内存中的大小等关键信息。所以在程序运行的时候,首先就是要找到程序头表的位置,把程序头表中所表示的段,根据程序头中的信息,载入到内存中,然后再运行程序。

通过这个elf的学习,搞明白了几个以前一直比较模糊的问题:

bss段为什么在文件中是0字节的

在程序中的全局变量是如何初始化的。

还有以前一直没弄明白bss,data段和内存管理中的分段的那个段到底是不是一个东西,现在对这些都比较明白了,学学这个还是有好处的,可以让一些问题更加清晰。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-11-10 13:51:40

C语言的ELF文件格式学习的相关文章

ELF文件格式解析

1. ELF文件简介 首先,你需要知道的是所谓对象文件(Object files)有三个种类: 可重定位的对象文件(Relocatable file) 这是由汇编器汇编生成的 .o 文件.后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file).我们可以使用 ar 工具将众多的 .o Relocata

elf文件格式

android是建立在linux的基础上,其底层代码是安装linux可执行文件——elf的格式来组装的.本文结合android中的so文件来了解elf格式,资料大多收集于网上:elf格式位于android源码:elf.h. elf大致可分为三部分:elf头.程序头表.节区头表:当然还有上图没标出的动态符号表, elf头: #define EI_NIDENT 16 typedef struct { unsigned char e_ident[EI_NIDENT]; //magic Elf32_Ha

Android逆向之旅---SO(ELF)文件格式详解

第一.前言 从今天开始我们正式开始Android的逆向之旅,关于逆向的相关知识,想必大家都不陌生了,逆向领域是一个充满挑战和神秘的领域.作为一名Android开发者,每个人都想去探索这个领域,因为一旦你破解了别人的内容,成就感肯定爆棚,不过相反的是,我们不仅要研究破解之道,也要研究加密之道,因为加密和破解是相生相克的.但是我们在破解的过程中可能最头疼的是native层,也就是so文件的破解.所以我们先来详细了解一下so文件的内容下面就来看看我们今天所要介绍的内容.今天我们先来介绍一下elf文件的

[Debug]linux elf文件格式

linux elf文件格式 浅谈Linux的可执行文件格式ELF Linux中ELF格式文件介绍

[易语言]连接ACCESS数据库学习

一.支持库配置 工具-支持库配置-数据库操作支持库 二.添加控件 启动窗口添加控件(数据库连接/记录集/超级列表框) 三.数据库连接 数据库连接1.连接Access ("数据库文件路径", "数据库密码") 例:数据库连接1.连接Access (取运行目录 () + "H:\m.mdb", "") 四.定义变量 .局部变量 索引, 整数型 .局部变量 用户ID, 整数型 .局部变量 日期时间, 文本型 五.例子 .支持库 eD

c语言到汇编的学习

[内存结构] C程序通过编译-汇编-连接,最后到可执行文件.载入内存有这几个部分: text:正文段,存放的是可执行的机器码段 data:存放初始化之后的全局变量和静态变量 bbs:存放未初始化的静态变量和全局变量 heap:堆,由程序员自己分配和释放,程序结束时,操作系统也会释放. stack: 栈,编译器自动分配,存放函数的参数,局部变量 下图是典型的内存布局图 #include <stdio.h> #include <stdlib.h> void foo(int x){pri

Linux及安全实践四——ELF文件格式分析

Linux及安全实践四——ELF文件格式分析 一.ELF文件格式概述 1. ELF:是一种对象文件的格式,用于定义不同类型的对象文件中都放了什么东西.以及都以什么样的格式去放这些东西. 二.分析一个ELF文件 以一个最简单的helloworld程序为例 1. ELF文件头 使用工具查看ELF文件头:readelf -h obj 在/usr/include/elf.h中可以找到文件头结构定义: 大小总共为64字节,换算成十六进制为0x40.在十六进制代码中找到前0x40字节,即为文件头信息部分(阅

R语言——绘图函数深入学习

利用R自带数据集 通过data()函数可以查看R自带数据集. > data() 返回以下结果,每一条记录都是一个数据,键入相应的数据名称可以查看具体信息. Data sets in package ¡®datasets¡¯: AirPassengers Monthly Airline Passenger Numbers 1949-1960 BJsales Sales Data with Leading Indicator BJsales.lead (BJsales) Sales Data wit

一个资深C语言工程师说如何学习C语言

谈及C语言,我想凡是学过它的朋友都有这样一种感觉,那就是"让我欢喜让我忧."欢喜的是,C语言功能非常强大.应用广泛,一旦掌握了后,你就可以理直气壮地对他人说"我是电脑高手!",而且以后若是再自学其他语言就显得轻而易举了.忧虑的是,C语言犹如"少林武功"一般博大精深,太难学了.其实就笔者认为C语言并非是"difficult(困难)"的,只要你能理清思路,掌握它的精髓,那么自学C语言是一件非常容易且又其乐无穷的事.今天本人就与大家