ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
 
0 自我小结
 
==================虚拟存储器管理================================
linux就像是一个大的加工生产企业, 它接了很多客户的很多任务, 各任务的原材料等都放在仓库中, linux的内存就像是他的加工生产区,这个区被分为很多的块,一块就是一个加工生产车间,因任务众多,加工生产车间有限,需要合理利用生产车间; linux根据仓库中生产计划指令,在这个时刻去加工某个客户任务的某部分资料,如果这部分资料已在某个生产车间内,则继续加工,如果不在,则找一个生产车间,将待加工材料装进来,加工,该车间加工完这个任务当前的计划后,很可能被放入仓库中,生产车间被用来加工生产另一任务的计划,类似这样...
 
可执行文件的各个段 被映射到虚拟地址空间中, 同时虚拟地址空间中还包含有其他的各个代码部分(如库 堆 栈等)【因为虚拟空间32位,有4G,而实际物理存储器,内存很小,所以很有可能可执行文件的某个虚拟空间(页)中的内容是在CPU真正执行到了那里后,才被调到内存对应的某个页中,再被CPU执行的,所以很可能并不是可执行文件的所有段都一开始就全部加载到物理内存中】, cpu要访问某个虚拟地址单元,先检查该单元是否已经映射到了物理内存中,如果已在,就直接处理,否则就要先把这个地址对应的可执行文件的虚拟页且存放在磁盘上内容先加载到某个物理页中,再执行...
 
虚拟存储器(VM)被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。(概念)
VM系统将虚拟存储器分割为 虚拟页(VP)
物理存储器被分割为 物理页(PP); 虚拟页与物理页的大小一致;
虚拟页中的数据或代码要被执行时,需要将该虚拟页的内容缓存(映射)到某物理页中;
 
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
 

存储器映射

虚拟存储器区域 与 一个磁盘上的对象关联起来,用这个关联对象来初始化这个虚拟存储器区域的内容,这个过程叫做存储器映射【存储器强调的是对虚拟存储器区域的初始化】;

虚拟存储器区域与普通的文件映射,比如与可执行文件的映射,这时可执行文件中的程序头已经包含了可执行文件与虚拟空间地址的映射关系,这个程序头中指明了可执行文件的各个段映射到虚拟存储器空间的虚拟地址 以及 读写权限等信息。这就是ELF中的映射信息。虚拟存储器只不过根据块的大小将这些规划好的段(区域)再映射一下而已。

虚拟存储器区域还可以映射到一个全0的匿名文件(使用该虚拟区域时,用这个全0的匿名文件初始化);

其实不论如何映射(映射只是在使用到该分配且未缓存的虚拟区域时,用对应的映射区域进行初始化对应的物理页而已,一旦一个虚拟页被初始化了,他就在一个由内核维护的专门的交换文件(swap file)之间进行换来换去【这里强调的是虚拟页对应的物理页初始化后,换进换出的过程】。这个交换文件就是交换空间(交换区域)。因此交换空间限制了当前运行着的(所有)进程能够分配的虚拟页面的总是。

ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

共享对象

由于很多的进程包含有公共的只读文件区域等(如C库中printf等),如果每个进程都在屋里存储器重包含这样的常用代码,那将是一种浪费。

存储器映射提供了一种机制,用来控制多个进程如何共享对象;

我们可以将一个对象映射到虚拟存取器区域中去,且这个对象要么是共享对象要么是私有对象;

共享对象:多个进程将这个共享对象映射到各自的虚拟空间,且各进程共享同一个共享对象的物理空间,某个进程进程对该共享对象的写操作,其他的进程是可见的,且写操作会反映在磁盘原始文件中;每个对象有唯一的文件名,内核可迅速判定进程1已映射该对象,且进程2的页表条目指向相应的物理页面,关键是即使对象被映射到多个进程的共享区域,物理存储器中也只存放共享对象的一个拷贝
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

私有对象:私有对象使用写时拷贝的方法,物理存储器只保持私有对象的一份拷贝,这个各个进程共享这个对象的一个物理拷贝

Fork函数:

一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构和页表的原样拷贝。它将两个进程中的每个页面都为标记只读,并将两个进程中的每个区域结构都标记为私有的写时拷贝。

当fork在新进程中返回时,新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。当这两个进程中的任一个后来进行写操作时,写时拷贝机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

execve函数:

execve("a.out",NULL,NULL) ;

execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:

删除已存在的用户区域。删除当前进程虚拟地址用户部分中的已存在的区域结构。

映射私有区域。为新程序的文本、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时拷贝的。文本和数据区域被映射为a.out文件中的文本和数据区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的。

映射共享区域。如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。

下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
 

使用mmap函数的用户级存储器映射:

 

[cpp] view plaincopy

#include 

#include 

void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset) ; 

//返回:若成功时则为指向映射区域的指针,若出错则为MAP_FAILED(-1) 

 

mmap函数要求内核创建一个新的虚拟存储器区域是,最好是从地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的片(chunk)映射到这个新区域。连续的对象片大小为length字节,从距文件开始处偏移量为offset字节的地方开始。start地址仅仅是一个暗示,通常被定义为NULL。

ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

munmap函数删除虚拟存储器的区域:

#include 

#include 

int  munmap( void *start,   size_t length);

Munmap函数删除从虚拟地址start开始的,由接下来的length字节组成的区域。接下来对已删除区域的引用会导致段错误。

 

动态存储器分配

虽可通过低级的mmap、munmap函数来创建与删除虚拟存取器的区域,但是用动态存储器分配器更方便,更易于移植;

 

动态存储器分配器维护着一个进程的虚拟存储器区域—堆heap; 堆紧接着未初始化的bss区域后面,并向上生长,对每个进程,内核维护着一个变量brk,他指向堆的顶部;


ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

分配器的分类:

显式分配器:要求应用显式地释放任何已分配的块。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。

隐式分配器:要求适配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫垃圾收集器(java),而自动释放未使用的已分配的块的过程叫做垃收集。

C语音用malloc free等来从堆中分配虚拟空间;

ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
 

 

 

 
linux的虚拟存储器系统:linux是如何组成虚拟存储器的?
  linux将虚拟存储器组织成一些区域(段)的集合,一个区域就是已经存在的(已分配的)虚拟存储器的连续片(chunk),这些页是以某种方式相关联的。例如代码中的 代码段  数据段 堆 共享库段 用户栈 都是不同的区域。每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程引用。因为虚拟地址空间有间隙,即内核不用记录那些不存在的虚拟页(未分配的),而这样的页也不占用存储器、磁盘或内核本身的任何额外资源。
内核为系统中的每个进程维护一个单独的任务结构(task_struct),任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(如 PID、指向用户栈的指针、可执行目标文件的名字以及程序计数器)
task_struct 中的一个条目指向mm_struct,他描述了虚拟存储器的当前状态,mm_struct中包含有pgd 和 mmap,pgd指向第一级页表(页全局目录)的基址,mmap指向一个vm_area_structs的链接,该链表中的每一个vm_area_structs都描述了当前虚拟地址空间的一个区域(也就是一个段),当内核运行这个进程时,他就将pgd存放在CR3控制寄存器中;
vm_start:指向这个区域(段)的起始处 
vm_end:指向这个区域(段)的结束处 
vm_prot:指向这个区域(段)内包含的所有页的读写许可权限
vm_flags:描述该区域的页面是与其他进程共享的还是这个进程私有的
vm_next:指向下一个
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
 
存储在磁盘上的虚拟页(VP)   DRAM中的物理页
                    ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载

Linux进程的虚拟存储器

 

虚拟页(VP)可以分为 未分配的虚拟页  缓存的虚拟页 未缓存的虚拟页
未分配的虚拟页:VM系统还没有分配(创建)(或使用)的虚拟页,未分配的虚拟页的块没有任何的数据和他们               相关联。
缓存的虚拟页:当前缓存在物理存储器中的已分配页
未缓存的虚拟页:还没有缓存到物理存储器中的已分配页
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
虚拟存储器系统必须有某种方法来判定一个虚拟页是否存放在DRAM中的某个地方,如果是,还必须知道这个虚拟页存放在哪个物理页中,如果否(没有命中),系统必须知道这个虚拟页存放在磁盘的哪个位置,在物理存储器中选择一个牺牲页,并将虚拟页从磁盘拷贝到DRAM中,替换这个牺牲页;
 
页表:存放在物理存储器中页表(page table)的数据结构,页表将虚拟页映射到物理页;每次地址翻译硬件将一个虚拟地址转换为物理地址时都会读取页表,页表由操作系统来维护;如下图中的左图就是一个页表,页表就是一个页表条目(PTE page table entry)的数组,虚拟地址空间中的每个页在页表中一个固定偏移量处都对应有一个PTE。下图中左部分通过有效位 1简化表示 表右部的物理页号或磁盘地址有效,即该虚拟页已缓存映射到了这个地址对应的物理页中, 0&null 表示 未分配的虚拟页
页命中:如果代码中 去读 VP1 VP2 VP7 VP4的字时, 由于这些虚拟页都已经缓存到DRAM中,故直接可以命中,虚拟地址直接转换为物理地址,进行操作;
缺页:如果代码中 去读 VP3的字时 , 由于VP3 没有缓存到DRAM中,所以会产生一个缺页异常,这时内核选择一个牺牲页如PP3,再从磁盘中拷贝VP3到存储PP3,更新PTE3。
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
 
尽管在整个运行过程中,程序引用的不同页面的总数可能超出物理存储器的总的大小,但是局部性原则保证了再任意时刻,程序将往往在一个较小的活动页面(active page)集合上工作,这个集合叫做工作集(working set) 或者驻集(resident set),在初始开销,也就是将工作集页面调度到存储器中之后,接下来对这个工作集的引用将导致命中,而不会产生额外的磁盘流量。如果程序没有良好的局部性,页面不停的换进换出,程序将出现一种不幸的现象,叫做颠簸(thrashing),程序运行很卡的时候就一般这样了,可以用getrusage函数来监测缺页的数量
 
Linux缺页异常处理
MMU在翻译某个虚拟地址A时,触发一个缺页,这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
1:虚拟地址A是合法的吗?A在某个区域(段)结构定义的区域内吗?缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start vm_end比较,如果非法,那么缺页处理程序就触发一个段错误,从而终止进程;其实进程可以通过mmap函数创建任意数量的新虚拟存储器区域,所以搜索区域结构链表花销很大,一般linux在链表中构建了一棵树,在这棵树上搜索。
2:试图进行存储器的访问是否合法? 进程是否有读写执行这个区域内页面的权限,访问不合法,缺页处理触发一个保护异常,进程终止
3:内核知道了这个缺页是由合法地址引起的,内核会先选择一个牺牲页,如果牺牲页被修改过,那么先将他交换出去,换入新的页面并更新页表,当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发生A到MMU,这次MMU就能正常翻译A地址了
Linux存储器映射(就是将虚拟存储空间 与 磁盘上的对象【这个对象可是普通文件(虚拟存储空间直接与可执行文件进行映射) 或 匿名文件】 进行映射)
通过将虚拟存存储器区域(段) 与 一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域(段)的内容,叫存储器的映射。虚拟存储器区域可以映射到两种类型的对象中的一种:
(1)Unix文件上的普通文件:
一个区域可以映射到一个普通磁盘文件的连续部分,例如一个可执行目标文件。文件区(section)被分成页大小的片,每一片包含一个虚拟页面的初始化内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理存储器,直到CPU第一次引用到页面(即发射一个虚拟地址,落在地址空间这个页面的范围之内)。如果区域比文件区要大,那么就用零来填充这个区域的余下部分。
(2)匿名文件:
一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理存储器中找到一个合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表,将这个页面标记为是驻留在存储器中的。注意在磁盘和存储器之间没有实际的数据传送。因为这个原因,映射到匿名文件的区域中的页面有时也叫做请求二进制零的页(demand-zero page)。
无论在哪种情况下,一旦一个虚拟页面被初始化了, 它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫做交换空间(swap space)或者交换区域(swap area)。需要意识到的很重要的一点,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。
===================================================
 
【参加 深入理解计算机系统 第7章】
gcc -v查看编译的过程
文件编译过程: 语言预处理器-->xxx.i-->编译器-->xxx.s-->汇编器-->xxx.o-->链接器-->可执行文件
 
目标文件分为三类:可重定位目标文件、共享目标文件、可执行目标文件
编译器、汇编器 可生产可重定位的目标文件(包括共享目标文件);链接器生成可执行目标文件
 
什么是符号解析?在代码中,变量 函数等符号 有 定义 和引用之分,因此符号解析的目的就是将每个符号引用与一个符号定义联系起来; 链接器来做符号解析,链接器将每个符号的引用 与 此时输入的可重定位目标文件中的符号表中的一个符号定义 联系起来。
 
什么是重定位?编译器和汇编器  生成从0地址开始的代码和数据节,链接器通过吧每个符号定义与一个存储器位置联系起来(简单理解为符号定义的位置),然后修改所有对这些符号的引用,使得他们指向这个存储器位置,从而重定位这些节。链接器在完成符号解析后(即把一个符号的引用与一个符号的定义联系起来)。接下来,链接器准备对各目标文件中的确定长度的相关节进行重定位了,合并输入模块,并为每个符号分配运行时的地址
 
重定位分为两步:
1:重定位节和符号定义: 链接器将多个目标文件中所有相同类型的节合并为同一类型的新的聚合节。如,各输入模块的.data节在最终可执行目标文件中被合并为一个.data节 ,然后,链接器将运行时存储器地址赋给新的聚合节(里面输入各模块的各个节 以及 各个符号),这步完成后,程序中的每个指令和全局变量都有唯一的运行时存储地址了。
2:重定位节中的符号引用
在这一步,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。
 
==============================================================
可以将各个.o目标文件打包成一个库来使用
 
ar:创建静态库(各个xx.o),插入 删除 列出 和 提前成员
strings:列出一个目标文件中所有可打印的字符串
strip:从目标文件中删除符号表信息
nm:列出一个目标文件的符号表中定义的符号
size:列出目标文件中节的名字和大小
readelf:显示一个目标文件的完整结构,包括ELF头中编码的所有信息(包含size和nm的功能)
objdump:所有二进制工具之母,能够显示一个目标文件中的所有的信息,他最大的作用是反汇编.text节中的二进制指令
ldd:列出一个可执行文件在运行时所需要的共享库
===================可重定位目标文件============================
可重定位目标文件 与 可执行目标文件 的组织结构大致是相同的;有些小差别;
ELF头:
.text:
.rodata:
.bss:
.symbol: 符号表【符号表由汇编器构造的】,存放的是程序中定义和引用的函数 和全局变量 或 静态变量(静态变量在有些书中统称为全局变量)的信息;因为局部变量是存放在堆栈中,所以符号表中是不包含局部变量条目的符号
.rel.text:.text中需要重定位的位置,比如引用另一个目标文件的函数等;可执行文件中 并不需要重定位信息,因此通常省略,除非用户显示地指示链接器包含这些信息;
.rel.data:
.debug:一个调试符号表,其条目是程序中定义局部变量 和类型定义(-g选项)
.line:原始C源程序中的行号与.text节中机器指令之间的对应关系
.strtab:一个字符串表,其内容包括.symbol和.debug节中的符号表 以及节头部中的节名字,字符串表 就是以null结尾的字符串序列         
 
===================可执行目标文件加载============================
先看下面的可执行目标文件的分析,再看这里的加载吧,排版问题
 
     Program Headers\\这个就是ELF Header中描述的程序头描述表的起始位置,他告诉系统如何创建进程映                \\像用来构造进程映像的目标文件必须具有程序头部表
                  \\他描述了进程映像的目标文件的“段”包含对应的一个或者多个“节区”,也就                      \\是“段内容(Segment Contents)”
                 \\该程序头描述表(数组)来描述了内存中的可执行的进程映射是如何构成的信息
                \\Type  Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
                \\如 LOAD 0x000000 0x08048000 0x08048000 0x0076c 0x0076c R E 0x1000
               \\上面描述的是一个加载段,加载目标文件中的对应的内容到内存中对应的位置
 
proc/进程PID/maps  查看某进程的虚拟地址空间是如何分配利用的。
 
cat /proc/1/statm
487 185 133 31 0 67 0
很简单地返回7组数字,每一个的单位都是一页 (常见的是4KB)
分别是
size:任务虚拟地址空间大小
Resident:正在使用的物理内存大小
Shared:共享页数
Trs:程序所拥有的可执行虚拟内存大小
Lrs:被映像倒任务的虚拟内存空间的库的大小
Drs:程序数据段和用户态的栈的大小
dt:脏页数量
 
===================可执行目标文件============================
ELF可执行文件(a.out)或类似编译器编译出的文件(.bin), 都是由各个节(section:如.text .bss .data等)组成;运行时,各个节被加载到内存中,此时的内存中以段(Segment:如代码段 数据段)的形式组织相关的节,以便程序运行;
 
ELF可执行文件(a.out): 在linux下,可使用readelf  nm objdump 以及gdb 来查看与调试可运行的文件;
ELF可执行文件的组成形式如下:
 
 readelf -a a.out: 读取显示执行文件中的所有的头表信息
 objdump -s a.out: 显示可节的具体内容
 nm a.out:列出目标文件中的所有符号信息
 gdb :调试
 
一般C语言的编译后执行语句都编译成机器代码,保存在.text段;已初始化的全局变量和局部静态变量都保存在. data段;未初始化的全局变量和局部静态变量一般放在一个叫."bss"的段里。我们知道未初始化的全局变量和局部静态变量默认值都为0,本来它们也可以被放在.data段的,但是因为它们都是0,所以为它们在.data段分配空间并且存放数据0是没有必要的。程序运行的时候它们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为.bss段。所以.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。
【函数中的局部变量将放在栈中,既不在.data 也不在.bss中】
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
一个进程的内存映像,从低地址开始分为五部分
正文段
初始化数据段
未初始化数据段
堆区
栈区
【栈由该区域的最高地址向低地址增长,而堆由该区域的低地址向高地址增长】
【当一程序启动运行的初期,并没有把该程序所需要的所有的物理空间分配给它,而是只分配了满足当时可以使之运行的几个页面。程序继续运行(虚拟地址)需读取新的页面时,发现该页面不在内存中,就要用一定的算法为该进程对应的虚拟地址空间分配一内存页面】
 

简单来讲,程序的装入到运行的主要包含以下几个步骤:

1:读入可执行文件的头部信息以确定其文件格式及地址空间的大小;

2:以段的形式划分地址空间;

3:将可执行程序读入地址空间中的各个段,建立虚实地址间的映射关系;

4:将bbs段清零;

5:创建堆栈段;

6:建立程序参数、环境变量等程序运行过程中所需的信息;

7:启动运行。

 
//程序头表中 描述可执行文件 到 虚拟内存空间的映射关系
Program Headers:
  Type          Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR          0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP        0x000154 0x08048154 0x08048154 0x00013 0x00013 R  0x1
     [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD          0x000000 0x08048000 0x08048000 0x0076c 0x0076c R E 0x1000
  LOAD          0x000f08 0x08049f08 0x08049f08 0x00240 0x00324 RW  0x1000
  DYNAMIC       0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
  NOTE          0x000168 0x08048168 0x08048168 0x00044 0x00044 R  0x4
  GNU_EH_FRAME   0x000690 0x08048690 0x08048690 0x0002c 0x0002c R  0x4
  GNU_STACK     0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO     0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R   0x1
//程序头表中 描述段 到 节 的映射关系
 Section to Segment mapping:
  Segment Sections...
   00    
   01    .interp 
   02    .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03    .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04    .dynamic 
   05    .note.ABI-tag .note.gnu.build-id 
   06    .eh_frame_hdr 
   07    
   08    .init_array .fini_array .jcr .dynamic .got 
 
    ELF Header\\用来描述整个文件的组织,文件类型,机器类型,程序入口地址,
              \\Start of program headers程序头(描述表)起始位置,这是一个数组起始地址
              \\Number of program headers告诉我们这个数组包含了多少个程序段描述子成员
              \\Start of section headers节区头(描述表)起始位置,这是一个数组起始地址
              \\Number of section headers告诉我们这个数组包含了多少个节区描述子成员           
     Program Headers\\这个就是ELF Header中描述的程序头描述表的起始位置,他告诉系统如何创建进程映                \\像用来构造进程映像的目标文件必须具有程序头部表
                  \\他描述了进程映像的目标文件的“段”包含对应的一个或者多个“节区”,也就                       \\是“段内容(Segment Contents)”
                  \\该程序头描述表(数组)来描述了内存中的可执行的进程映射是如何构成的信息
               \\Type  Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
               \\如 LOAD 0x000000 0x08048000 0x08048000 0x0076c 0x0076c R E 0x1000
              \\上面描述的是一个加载段,加载目标文件中的对应的内容到内存中对应的位置
     【接下来就是各个节的数据,各个节的偏移地址与长度 在Section Headers中做描述】 
  【各个节区有各自对应的读写属性标志,MMU通过这个标志来控制各节区的可读写特性,.rodata等】
    【gcc 编译时 加入-g 选项,目标文件中将包含调试信息】 
     .interp \\ 此节区包含程序解释器的路径名      
     .note.ABI-tag \\   
     .note.gnu.build-i \\ 
     .gnu.hash \\   
     .dynsym  \\  
     .dynstr \\    
     .gnu.version \\   
     .gnu.version_r \\   
     .rel.dyn \\     
     .rel.plt \\    
     .init  \\ 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调            \\用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。
     .plt  \\    
     .text \\  包含程序的可执行指令   
     .fini \\ 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这           \\里的代码。  
     .rodata \\  包含只读数据
     .eh_frame_hdr \\ 
     .eh_frame  \\   
     .init_array\\   
     .fini_array  \\ 
     .jcr  \\      
     .dynamic  \\ 
     .got    \\  
     .got.plt  \\  
     .data    \\ 初始化了的数据  
     .bss     \\ 未初始化了的数据
     .comment  \\ 
     .shstrtab  \\ 
     .symtab  \\此节区包含一个符号表
     .strtab \\ 
     Section Headers\\这个就是ELF Header中描述的节区头描述表的起始位置,文件包含了多少个节,这个                  \\数组就有多少个成员,每个成员描述了对应一个节区的名字,偏移地址,虚拟地址,                   \\可读写信息等
 
readelf  -a  a.out :可查看ELF文件的各个头信息(这些信息描述了整个可执行文件的组织信息)
     如:
 
1 简介
    可执行链接格式(Executable and Linking Format)最初是由UNIX系统实验室(UNIX System Laboratories,USL)开发并发布的,作为应用程序二进制接口(Application Binary Interface,ABI)的一部分。工具接口标准(Tool Interface Standards,TIS)委员会将还在发展的ELF标准选作为一种可移植的目标文件格式,可以在32位Intel体系结构上的很多操作系统中使用[1, 2]。
     ELF标准的目的是为软件开发人员提供一组二进制接口定义,这些接口可以延伸到多种操作环境,从而减少重新编码、重新编译程序的需要。接口的内容包括目标模块格式、可执行文件格式以及调试记录信息与格式等。
    TIS给出的Portable Formats Specification 1.1版本中主要针对三种不同类型的目标文件作了规定,并规定了程序加载与动态链接相关过程细节,给出了标准ANSI C和libc例程必须提供的符号[1]。在该组织随后发布的Executable and Linking Format(ELF) Specification 1.2版本中则分如下三个部分,主要的不同点是对与操作系统相关的部分进行了重新组织:
???? Book I: Executable and Linking Format,描述ELF目标文件格式;
???? Book II: Processor Specific(Intel Architecture),描述ELF中与硬件相关的信息;
???? Book III: Operating System Specific,描述ELF中与操作系统相关的部分,例如System V Release 4信息等。
 
2 相关标准
2.0 System V

      Unix System V,是Unix操作系统众多版本中的一支。它最初由AT&T开发,在1983年第一次发布,因此也被称为AT&T System V。一共发行了4个System V的主要版本:版本1、2、3和4。System V Release 4,或者称为SVR4,是最成功的版本,成为一些UNIX共同特性的源头,例如SysV 初始化脚本(/etc/init.d),用来控制系统启动和关闭System V Interface Definition (SVID)是一个System V如何工作的标准定义。

     AT&T出售运行System V的专有硬件,但许多(或许是大多数)客户在其上运行一个转售的版本,这个版本基于AT&T的实现说明。流行的SysV派生版本包括Dell SVR4和Bull SVR4。当今广泛使用的System V版本是SCO OpenServer,基于System V Release 3,以及SUN Solaris和SCOUnixWare,都基于System V Release 4。

     System V是AT&T的第一个商业UNIX版本(UNIX System III)的加强。传统上,System V被看作是两种UNIX"风味"之一(另一个是BSD然而,随着一些并不基于这两者代码的UNIX实现的出现,例如LinuxQNX, 这一归纳不再准确,但不论如何,像POSIX这样的标准化努力一直在试图减少各种实现之间的不同。

2.1 System V ABI
    System VApplication Binary Interface(ABI)为已编译的应用程序定义了系统接口,同时也为安装脚本支持提供了一个最小环境。其目的是为应用程序提供一个标准的二进制接口,使得这些程序能够运行在符合X/Open Commen Application Environment Specification, Issue 4.2和System V Interface Definition, Fourth Edition的操作系统上二进制要包含与不同处理器体系结构相关的信息,所以ABI并不是只有一个规范,而是一个规范体系。System V ABI由两个部分组成:一个通用的部分,描述System V在所有硬件平台上都一致的接口[3];一个处理器相关的部分,描述特定于某个处理器体系结构的具体实现[4]。
System V ABI的主要参考标准包括:
???? 目标系统处理器的体系结构手册
???? System V Interface Definition(接口定义), 第4版
???? IEEE POSIX 1003.1-1990标准操作系统规范
???? X/Open Common Application Environment Specification(CAE), Issue 4.2
???? International Standard, ISO/IEC 9899:1990(E), Programming Languages – C, 12/15/90.
???? X Window System™, X Version 11, Release 5, 图形用户界面规范
 
2.2 LSB
  由于我们所关心的主要是Linux平台上目标文件的格式,所以Linux标准LSB (Linux Standard Base)也是重要的参考资料。LSB的目标是增强不同的Linux发布版本之间的兼容性,与ABI类似,也由两个部分组成:
???? gLSB(Generic LSB) 适用于所有体系结构
???? archLSB(Architecture Specific LSB) 特定某种体系结构的LSB
  目前,LSB由SourceForge开放源码项目社区提供支持。
 
3 ELF文件格式
     一个程序要想在内存中运行,除了编译之外还要经过链接装入这两个步骤。

    链接器和装入器的基本工作原理

     一个程序要想在内存中运行,除了编译之外还要经过链接和装入这两个步骤。从程序员的角度来看,引入这两个步骤带来的好处就是可以直接在程序中使用printf和errno这种有意义的函数名和变量名,而不用明确指明printf和errno在标准C库中的地址。当然,为了将程序员从早期直接使用地址编程的梦魇中解救出来,编译器和汇编器在这当中做出了革命性的贡献。编译器和汇编器的出现使得程序员可以在程序中使用更具意义的符号来为函数和变量命名,这样使得程序在正确性和可读性等方面都得到了极大的提高。但是随着C语言这种支持分别编译的程序设计语言的流行,一个完整的程序往往被分割为若干个独立的部分并行开发,而各个模块间通过函数接口或全局变量进行通讯。这就带来了一个问题,编译器只能在一个模块内部完成符号名到地址的转换工作,不同模块间的符号解析由谁来做呢?比如前面所举的例子,调用printf的用户程序和实现了printf的标准C库显然就是两个不同的模块。实际上,这个工作是由链接器来完成的。

为了解决不同模块间的链接问题,链接器主要有两个工作要做――符号解析和重定位:

符号解析:当一个模块使用了在该模块中没有定义过的函数或全局变量时,编译器生成的符号表会标记出所有这样的函数或全局变量,而链接器的责任就是要到别的模块中去查找它们的定义,如果没有找到合适的定义或者找到的合适的定义不唯一,符号解析都无法正常完成。

重定位:编译器在编译生成目标文件时,通常都使用从零开始的相对地址。然而,在链接过程中,链接器将从一个指定的地址开始,根据输入的目标文件的顺序以段为单位将它们一个接一个的拼装起来。除了目标文件的拼装之外,在重定位的过程中还完成了两个任务:一是生成最终的符号表;二是对代码段中的某些位置进行修改,所有需要修改的位置都由编译器生成的重定位表指出。

举个简单的例子,上面的概念对读者来说就一目了然了。假如我们有一个程序由两部分构成,m.c中的main函数调用f.c中实现的函数sum:

 
int i = 1;
int j = 2;
extern int sum();
void main()
{
       int s;
       s = sum(i, j);
}
int sum(int i, int j)
{
       return i + j;
}
在Linux用gcc分别将两段源程序编译成目标文件:
$ gcc -c m.c
$ gcc -c f.c
我们通过objdump来看看在编译过程中生成的符号表和重定位表:
$ objdump -x m.o
……
SYMBOL TABLE:
……
00000000 g O .data  00000004 i
00000004 g O .data  00000004 j
00000000 g F .text  00000021 main
00000000        *UND*  00000000 sum
RELOCATION RECORDS FOR [.text]:
OFFSET  TYPE            VALUE
00000007 R_386_32         j
0000000d R_386_32         i
00000013 R_386_PC32       sum
 
首先,我们注意到符号表里面的sum被标记为UND(undefined),也就是在m.o中没有定义,所以将来要通过ld(Linux下的链接器)的符号解析功能到别的模块中去查找是否存在函数sum的定义。另外,在重定位表中有三条记录,指出了在重定位过程中代码段中三处需要修改的位置,分别位于7、d和13。下面以一种更加直观的方式来看一下这三个位置:
$ objdump -dx m.o
Disassembly of section .text:
00000000 :
  0:   55 push  �p
  1:   89 e5 mov    %esp,�p
  3:   83 ec 04 sub    $0x4,%esp
  6:   a1 00 00 00 00 mov    0x0,�x
7: R_386_32     j
  b:   50 push  �x
  c:   a1 00 00 00 00 mov    0x0,�x
d: R_386_32     i
  11:   50 push  �x
  12:   e8 fc ff ff ff call  13
13: R_386_PC32  sum
  17:   83 c4 08 add   $0x8,%esp
  1a:   89 c0 mov   �x,�x
  1c:   89 45 fc mov   �x,0xfffffffc(�p)
  1f:   c9 leave
  20:   c3 ret
以sum为例,对函数sum的调用是通过call指令实现的,使用IP相对寻址方式。可以看到,在目标文件m.o中,call指令位于从零开始的相对地址12的位置,这里存放的e8是call的操作码,而从13开始的4个字节存放着sum相对call的下一条指令add的偏移。显然,在链接之前这个偏移量是不知道的,所以将来要来修改13这里的代码。那现在这里为什么存放着0xfffffffc(注意Intel的CPU使用little endian的编址方式)呢?这大概是出于安全的考虑,因为0xfffffffc正是-4的补码表示(读者可以在gdb中使用p /x -4查看),而call指令本身占用了5个字节,因此无论如何call指令中的偏移量不可能是-4。我们再看看重定位之后call指令中的这个偏移量被修改成了什么:
$ gcc m.o f.o
$ objdump -dj .text a.out | less
Disassembly of section .text:
……
080482c4 :
……
80482d6:      e8 0d 00 00 00 call   80482e8
80482db:      83 c4 08 add    $0x8,%esp
……
080482e8 :
……
 

可以看到经过重定位之后,call指令中的偏移量修改成0x0000000d了,简单的计算告诉我们:0x080482e8-0x80482db=0xd。这样,经过重定位之后最终的可执行程序就生成了。

可执行程序生成后,下一步就是将其装入内存运行。Linux下的编译器(C语言)是cc1,汇编器是as,链接器是ld,但是并没有一个实际的程序对应装入器这个概念。实际上,将可执行程序装入内存运行的功能是由execve(2)这一系统调用实现的。

简单来讲,程序的装入主要包含以下几个步骤:

1:读入可执行文件的头部信息以确定其文件格式及地址空间的大小;

2:以段的形式划分地址空间;

3:将可执行程序读入地址空间中的各个段,建立虚实地址间的映射关系;

4:将bbs段清零;

5:创建堆栈段;

6:建立程序参数、环境变量等程序运行过程中所需的信息;

7:启动运行。

 
3.1 简介
目标文件有三种类型
???? 可重定位文件(Relocatable File) 包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
???? 可执行文件(Executable File) 包含适合于执行的一个程序,此文件规定了 exec() 如何创建一个程序的进程映像。
???? 共享目标文件(Shared Object File) 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。
  目标文件全部是程序的二进制表示,目的是直接在某种处理器上直接执行。
 
3.1.1 目标文件中的数据表示
   目标文件格式支持 8 位字节/32位体系结构。不过这种格式是可以扩展的,目标文件因此以某些机器独立的格式表达某些控制数据,使得能够以一种公共的方式来识别和解释其内容。目标文件中的其它数据使用目标处理器的编码结构,而不管文件在何种机器上创建。
  ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
   目标文件中的所有数据结构都遵从“自然”大小和对齐规则。如果必要,数据结构可以包含显式的补齐,例如为了确保4字节对象按4字节边界对齐。数据对齐同样适用于文件内部。
  
3.2 目标文件格式
    目标文件既要参与程序链接又要参与程序执行。出于方便性和效率考虑,目标文件格式提供了两种并行视图,分别反映了这些活动的不同需求。下面 列出目标文件 在链接时  与 执行时 的格式视图。 两个过程的目标文件 在形式上 经过调整是有较大差别的。  
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
    文件开始处是一个ELF 头部(ELF Header),用来描述整个文件的组织。节区部分包含链接视图的大量信息:指令、数据、符号表、重定位信息等等。
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 //7f 45[E] 4c[L] 46[F]
  Class:                        ELF32
  Data:                         2's complement, little endian
  Version:                       1 (current)
  OS/ABI:                       UNIX - System V
  ABI Version:                   0
  Type:                         EXEC (Executable file)
  Machine:                       Intel 80386
  Version:                       0x1
  Entry point address:            0x8048320
  Start of program headers:         52 (bytes into file)
  Start of section headers:         4772 (bytes into file)
  Flags:                        0x0
  Size of this header:             52 (bytes)
  Size of program headers:         32 (bytes)
  Number of program headers:        9
  Size of section headers:         40 (bytes)
  Number of section headers:        30
  Section header string table index: 27
 
    程序头部表(Program Header Table),如果存在的话,告诉系统如何创建进程映像用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
Elf file type is EXEC (Executable file)
Entry point 0x8048320
There are 9 program headers, starting at offset 52
Program Headers:
  Type          Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR          0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP        0x000154 0x08048154 0x08048154 0x00013 0x00013 R  0x1
     [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD          0x000000 0x08048000 0x08048000 0x0076c 0x0076c R E 0x1000
  LOAD          0x000f08 0x08049f08 0x08049f08 0x00240 0x00324 RW  0x1000
  DYNAMIC       0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
  NOTE          0x000168 0x08048168 0x08048168 0x00044 0x00044 R  0x4
  GNU_EH_FRAME   0x000690 0x08048690 0x08048690 0x0002c 0x0002c R  0x4
  GNU_STACK     0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO     0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R   0x1
 Section to Segment mapping: 
//目标文件的“段”包含一个或者多个“节区”,也就是“段内容(Segment Contents)”
  Segment Sections...
   00    
   01    .interp 
   02    .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03    .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04    .dynamic 
   05    .note.ABI-tag .note.gnu.build-id 
   06    .eh_frame_hdr 
   07    
   08    .init_array .fini_array .jcr .dynamic .got
 
    节区头部表(Section Heade Table)包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。
Section Headers:
  [Nr] Name            Type           Addr     Off   Size   ES Flg Lk Inf Al
  [ 0]                NULL          00000000 000000 000000 00     0   0  0
  [ 1] .interp         PROGBITS       08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag    NOTE          08048168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE           08048188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash        GNU_HASH       080481ac 0001ac 000020 04   A  5   0  4
  [ 5] .dynsym         DYNSYM         080481cc 0001cc 000050 10   A  6   1  4
  [ 6] .dynstr         STRTAB         0804821c 00021c 00004c 00   A  0   0  1
  [ 7] .gnu.version     VERSYM        08048268 000268 00000a 02   A  5   0  2
  [ 8] .gnu.version_r   VERNEED        08048274 000274 000020 00   A  6   1  4
  [ 9] .rel.dyn        REL           08048294 000294 000008 08   A  5   0  4
  [10] .rel.plt        REL           0804829c 00029c 000018 08   A  5  12  4
  [11] .init           PROGBITS       080482b4 0002b4 000023 00  AX  0   0  4
  [12] .plt            PROGBITS       080482e0 0002e0 000040 04  AX  0   0 16
  [13] .text           PROGBITS       08048320 000320 000262 00  AX  0   0 16
  [14] .fini           PROGBITS       08048584 000584 000014 00  AX  0   0  4
  [15] .rodata         PROGBITS       08048598 000598 0000f8 00   A  0   0  4
  [16] .eh_frame_hdr    PROGBITS       08048690 000690 00002c 00   A  0   0  4
  [17] .eh_frame        PROGBITS       080486bc 0006bc 0000b0 00   A  0   0  4
  [18] .init_array      INIT_ARRAY     08049f08 000f08 000004 00  WA  0   0  4
  [19] .fini_array      FINI_ARRAY     08049f0c 000f0c 000004 00  WA  0   0  4
  [20] .jcr            PROGBITS       08049f10 000f10 000004 00  WA  0   0  4
  [21] .dynamic        DYNAMIC        08049f14 000f14 0000e8 08  WA  6   0  4
  [22] .got            PROGBITS       08049ffc 000ffc 000004 04  WA  0   0  4
  [23] .got.plt        PROGBITS       0804a000 001000 000018 04  WA  0   0  4
  [24] .data           PROGBITS       0804a020 001020 000128 00  WA  0   0 32
  [25] .bss            NOBITS         0804a160 001148 0000cc 00  WA  0   0 32
  [26] .comment        PROGBITS       00000000 001148 000055 01  MS  0   0  1
  [27] .shstrtab        STRTAB         00000000 00119d 000106 00      0   0  1
  [28] .symtab         SYMTAB         00000000 001754 0004c0 10     29  47  4
  [29] .strtab         STRTAB         00000000 001c14 0002e5 00     0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
There are no section groups in this file.
    注意:尽管图中显示的各个组成部分是有顺序的,实际上除了 ELF 头部表以外,其他节区和段都没有规定的顺 序
 
3.3 ELF Header部分
  文件的最开始几个字节给出如何解释文件的提示信息。这些信息独立于处理器,也独立于文件中的其余内容。ELF Header部分可以用下图中的数据结构表示:
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
其中,e_ident数组给出了ELF的一些标识信息,这个数组中不同下标的含义如表 2所示:
           ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
 
注意:在 32 位 Intel 体系结构上要求:
1、标志
    位置               取值
    e_ident[EI_CLASS]     ELFCLASS32
    e_ident[EI_DATA]     ELFDATA2LSB
2、处理器标识(e_machine)成员必须是 EM_386。
 
ELF Header中各个字段的说明如表 4:
 
e_ident   目标文件标识
 
e_type    目标文件类型:
         ET_NONE   0  未知目标文件格式 
         ET_REL   1  可重定位文件
        ET_EXEC 2 可执行文件
        ET_DYN  3 共享目标文件
        ET_CORE 4 Core 文件(转储格式)
        ET_LOPROC  0xff00  特定处理器文件
        ET_HIPROC  0xffff  特定处理器文件
        ET_LOPROC 和 ET_HIPROC 之间的取值用来标识与处理器相关的文件格式。
e_machine 给出文件的目标体系结构类型
         EM_NONE  0  未指定
         EM_M32   1  AT&T WE 32100
        EM_SPARC  2  SPARC
         EM_386  3  Intel 80386
         EM_68K  4  Motorola 68000
         EM_88K  5  Motorola 88000
         EM_860  7  Intel 80860
         EM_MIPS  8  MIPS RS3000
        其它值都是保留的。特定处理器的 ELF 名称会使用机器名来进行区分。
 
e_version 目标文件版本  EV_NONE  0  非法版本; EV_CURRENT  1  当前版本
e_entry   程序入口的虚拟地址。如果目标文件没有程序入口,可以为 0。
e_phoff   程序头部表格(Program Header Table)的偏移量(按字节计算)。如果文件没有程序头部表格,可          以为 0。
e_shoff   节区头部表格(Section Header Table)的偏移量(按字节计算)。如果文件没有节区头部表格,可          以为 0
e_flags  保存与文件相关的,特定于处理器的标志。标志名称采用EF_machine_flag 的格式
e_ehsize  ELF 头部的大小(以字节计算)。
e_phentsize   程序头部表格的表项大小(按字节计算)。
e_phnum     程序头部表格的表项数目。可以为 0 [该目标文件中一共有多少个段Segment]
===========Program Header Table===================
  Segment Sections...
  00    
  01    .interp 
  02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
  03    .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
  04    .dynamic 
  05    .note.ABI-tag .note.gnu.build-id 
  06    .eh_frame_hdr 
  07    
  08    .init_array .fini_array .jcr .dynamic .got
==================================================
e_shentsize   节区头部表格的表项大小(按字节计算)。
e_shnum    节区头部表格的表项数目。可以为 0。[该目标文件中一共有多少个节区Section]
================Section Header Table=====================================
Section Headers:
  [Nr] Name            Type          Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                 NULL           00000000 000000 000000 00     0   0  0
  [ 1] .interp          PROGBITS       08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE           08048168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE          08048188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash        GNU_HASH       080481ac 0001ac 000020 04   A  5   0  4
  [ 5] .dynsym          DYNSYM        080481cc 0001cc 000050 10   A  6   1  4
  [ 6] .dynstr          STRTAB        0804821c 00021c 00004c 00   A  0   0  1
  [ 7] .gnu.version     VERSYM         08048268 000268 00000a 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED        08048274 000274 000020 00   A  6   1  4
  [ 9] .rel.dyn         REL           08048294 000294 000008 08   A  5   0  4
  [10] .rel.plt         REL           0804829c 00029c 000018 08   A  5  12  4
  [11] .init            PROGBITS       080482b4 0002b4 000023 00  AX  0   0  4
  [12] .plt            PROGBITS       080482e0 0002e0 000040 04  AX  0   0 16
  [13] .text            PROGBITS       08048320 000320 000262 00  AX  0   0 16
  [14] .fini            PROGBITS       08048584 000584 000014 00  AX  0   0  4
  [15] .rodata          PROGBITS       08048598 000598 0000f8 00   A  0   0  4
  [16] .eh_frame_hdr     PROGBITS       08048690 000690 00002c 00   A  0   0  4
  [17] .eh_frame        PROGBITS       080486bc 0006bc 0000b0 00   A  0   0  4
  [18] .init_array       INIT_ARRAY      08049f08 000f08 000004 00  WA  0   0  4
  [19] .fini_array       FINI_ARRAY      08049f0c 000f0c 000004 00  WA  0   0  4
  [20] .jcr            PROGBITS       08049f10 000f10 000004 00  WA  0   0  4
  [21] .dynamic         DYNAMIC        08049f14 000f14 0000e8 08  WA  6   0  4
  [22] .got            PROGBITS       08049ffc 000ffc 000004 04  WA  0   0  4
  [23] .got.plt         PROGBITS       0804a000 001000 000018 04  WA  0   0  4
  [24] .data            PROGBITS       0804a020 001020 000128 00  WA  0   0 32
  [25] .bss            NOBITS         0804a160 001148 0000cc 00  WA  0   0 32
  [26] .comment         PROGBITS       00000000 001148 000055 01  MS  0   0  1
  [27] .shstrtab        STRTAB         00000000 00119d 000106 00     0   0  1
  [28] .symtab          SYMTAB        00000000 001754 0004c0 10    29  47  4
  [29] .strtab          STRTAB        00000000 001c14 0002e5 00     0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
There are no section groups in this file.
======================================================
e_shstrndx   节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参              数可以为SHN_UNDEF。
 
3.4 节区(Sections)
节区中包含目标文件中的所有信息,除了:ELF 头部、程序头部表格、节区头部表格。节区满足以下条件:
(1). 目标文件中的每个(每类)节区都有对应的节区头部描述它(在Section Header Table 节区头部表格中对包含的每个节区都有描述,且每个节区还有各自的节目头部【不是节区头部表格,节区头表格只有一个,对各个节目集中描述】),反过来,有节区头部不意味着有节区。
(2). 每个节区占用文件中一个连续字节区域(这个区域可能长度为 0)。
(3). 文件中的节区不能重叠,不允许一个字节存在于两个节区中的情况发生。
(4). 目标文件中可能包含非活动空间(INACTIVE SPACE)。这些区域不属于任何头部和节区,其内容未指定。
 
如下面的描述
Relocation section '.rel.dyn' at offset 0x294 contains 1 entries:
 Offset    Info    Type          Sym.Value  Sym. Name
08049ffc  00000206 R_386_GLOB_DAT    00000000  __gmon_start__
Relocation section '.rel.plt' at offset 0x29c contains 3 entries:
 Offset    Info    Type          Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   printf
0804a010  00000207 R_386_JUMP_SLOT   00000000   __gmon_start__
0804a014  00000307 R_386_JUMP_SLOT   00000000  __libc_start_main
 
3.4.1 节区头部表格【目标文件中只有一个节区头部表格, 这个表格集中描述各个节区的位置等信息,每个节目又有自己的节目头部,来描述本节目的信息】
   ELF 头部中,e_shoff 成员给出从文件头到节区头部表格的偏移字节数;e_shnum给出表格中条目数目;e_shentsize 给出每个项目的字节数。从这些信息中可以确切地定位节区的具体位置、长度。
节区头部表格中比较特殊的几个下标如下:
 
节区头部表格中的特殊下标
SHN_UNDEF  0  标记未定义的、缺失的、不相关的,或者没有含义的节区引用
SHN_LORESERVE  OXFF00  保留索引的下界
SHN_LOPROC  0XFF00  保留给处理器特殊的语义
SHN_HIPROC  0XFF1F  保留给处理器特殊的语义
SHN_ABS     OXFFF1   包含对应引用量的绝对取值。这些值不会被重定位所影响
SHN_COMMON OXFFF2 相对于此节区定义的符号是公共符号。如 FORTRAN 中 COMMON 或者未分配的 C 外部变量。
SHN_HIRESERVE  0XFFFF保留索引的上界
介于 SHN_LORESERVE 和 SHN_HIRESERVE 之间的表项不会出现在节区头部表中。
 
3.4.2 节区头部
每个节区头部可以用如下数据结构描述:
typedef struct{
    Elf32_Word sh_name;
   Elf32_Word sh_type;
   Elf32_Word sh_flags;
   Elf32_Addr sh_addr;
   Elf32_Off sh_offset;
   Elf32_Word sh_size;
   Elf32_Word sh_link;
   Elf32_Word sh_info;
   Elf32_Word sh_addralign;
   Elf32_Word sh_entsize;
}Elf32_Shdr;
对其中各个字段的解释如下:
sh_name   给出节区名称。是节区头部字符串表节区(Section Header String Table Section)的索引。名字          是一个 NULL  结尾的字符串。
sh_type  为节区的内容和语义进行分类。参见节区类型。
sh_flags  节区支持 1 位形式的标志,这些标志描述了多种属性。
sh_addr  如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则此字段为 0。
sh_offset  此成员的取值给出节区的第一个字节与文件头之间的偏移。不过,SHT_NOBITS 类型的节区不占用文           件的空间,因此其 sh_offset 成员给出的是其概念性的偏移。
sh_size   此成员给出节区的长度(字节数)。除非节区的类型是SHT_NOBITS,否则节区占用文件中的                sh_size 字节。类型为SHT_NOBITS 的节区长度可能非零,不过却不占用文件中的空间
sh_link   此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
sh_info   此成员给出附加信息,其解释依赖于节区类型。
sh_addralign  某些节区带有地址对齐约束。例如,如果一个节区保存一个doubleword,那么系统必须保证整             个节区能够按双字对齐。sh_addr 对 sh_addralign 取模,结果必须为 0。目前仅允许取值为               0 和 2 的幂次数。数值 0 和 1 表示节区没有对齐约束。
sh_entsize    某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节             数。如果节区中并不包含固定长度表项的表格,此成员取值为 0。
 
索引为零(SHN_UNDEF)的节区头部也是存在的,尽管此索引标记的是未定义的节区引用。这个节区的内容固定如下:
表 7 SHN_UNDEF(0)节区的内容
    字段名称  取值  说明
    sh_name  0  无名称
   sh_type  SHT_NULL  非活动
   sh_flags  0  无标志
    sh_addr  0  无地址
   sh_offset  0  无文件偏移
    sh_size  0  无尺寸大小
    sh_link  SHN_UNDEF  无链接信息
    sh_info  0  无辅助信息
   sh_addralign  0  无对齐要求
   sh_entsize  0  无表项
3.4.2.1 节区类型—sh_type字段
SHT_NULL     0  此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义。
SHT_PROGBITS  1  此节区包含程序定义的信息,其格式和含义都由程序来解释。
SHT_SYMTAB    2   此节区包含一个符号表。目前目标文件对该种类型的节区都只能包含一个,不过这个             限制将来可能发生变化。一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的             符号,尽管也可用来实现动态链接。
SHT_STRTAB   3  此节区包含字符串表。目标文件可能包含多个字符串表节区。
SHT_RELA     4   此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的                   Elf32_Rela 类型。目标文件可能拥有多个重定位节区。
SHT_HASH     5   此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一              个目标文件只能包含一个哈希表,不过此限制将来可能会解除。
SHT_DYNAMIC  6  此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这               一限制。
SHT_NOTE     7   此节区包含以某种方式来标记文件的信息。
SHT_NOBITS   8  这种类型的节区不占用文件中的空间,其他方面和SHT_PROGBITS 相似。尽管此节区不包             含任何字节,成员sh_offset 中还是会包含概念性的文件偏移
SHT_REL     9  此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel                 类型。目标文件中可以拥有多个重定位节区。
SHT_SHLIB    10   此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。
SHT_DYNSYM   11  作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也                 可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。
SHT_LOPROC   0X70000000  这一段(包括两个边界),是保留给处理器专用语义的。
SHT_HIPROC   OX7FFFFFFF  这一段(包括两个边界),是保留给处理器专用语义的。
SHT_LOUSER   0X80000000  此值给出保留给应用程序的索引下界。
SHT_HIUSER   0X8FFFFFFF  此值给出保留给应用程序的索引上界
其它的节区类型是保留的
 
3.4.2.2 sh_flags 字段
   sh_flags字段定义了一个节区中包含的内容是否可以修改、是否可以执行等信息。如果一个标志位被设置,则该位取值为1。未定义的各位都设置为0。
 
表 9 节区头部的 sh_flags字段取值
名称          取值
SHF_WRITE     0x1
SHF_ALLOC     0x2
SHF_EXECINSTR  0x4
SHF_MASKPROC   0xF0000000
 
其中已经定义了的各位含义如下:
SHF_WRITE: 节区包含进程执行过程中将可写的数据。
SHF_ALLOC: 此节区在进程执行过程中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0。
SHF_EXECINSTR: 节区包含可执行的机器指令。
SHF_MASKPROC: 所有包含于此掩码中的四位都用于处理器专用的语义。
 
3.4.2.3 sh_link和sh_info字段
 
根据节区类型的不同,sh_link 和 sh_info 的具体含义也有所不同:
sh_link和sh_info字段解释
sh_type             sh_link            sh_info
SHT_DYNAMIC   此节区中条目所用到的字符串表格的节区头部索引    0
SHT_HASH     此哈希表所适用的符号表的节区头部索引          0
SHT_REL      相关符号表的节区头部索引      重定位所适用的节区的节区头部索引
SHT_RELA     相关符号表的节区头部索引     重定位所适用的节区的节区头部索引     
SHT_SYMTAB   相关联的字符串表的节区头部索引  相关联的字符串表的节区头部索引最后一个局部符号(绑定                                       STB_LOCAL)的符号表索引值加一
SHT_DYNSYM   相关联的字符串表的节区头部索引  相关联的字符串表的节区头部索引最后一个局部符号(绑定                                       STB_LOCAL)的符号表索引值加一
其它   SHN_UNDEF   0
 
3.4.3 特殊节区【讲述各个节区的内容等情况】
 
很多节区中包含了程序和控制信息。下面的表中给出了系统使用的节区,以及它们的类型和属性
 
 常见特殊节区
名称    类型       属性        含义
.bss   SHT_NOBITS   SHF_ALLOC + SHF_WRITE   包含将出现在程序的内存映像中的未初始化数据。根据定                                         义,当程序开始执行,系统将把这些数据初始化为 0此                                   节区不占用文件空间
.comment  SHT_PROGBITS  (无)                    包含版本控制信息。
.data     SHT_PROGBITS    SHF_ALLOC + SHF_WRITE    这些节区包含初始化了的数据,将出现在程序的内存                                       映像中。
.data1    SHT_PROGBITS    SHF_ALLOC + SHF_WRITE     同上
.debug    SHT_PROGBITS  (无)               此节区包含用于符号调试的信息
.dynamic  SHT_DYNAMIC                     此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC                                         位。是否 SHF_WRITE 位被设置取决于处理器。
.dynstr   SHT_STRTAB   SHF_ALLOC           此节区包含用于动态链接的字符串,大多数情况下这些字                                       符串代表了与符号表项相关的名称。
.dynsym   SHT_DYNSYM  SHF_ALLOC            此节区包含了动态链接符号表
 
.fini    SHT_PROGBITS  SHF_ALLOC + SHF_EXECINSTR   此节区包含了可执行的指令,是进程终止代码的                                 一部分。程序正常退出时,系统将安排执行这里的代码
.got     SHT_PROGBITS                  此节区包含全局偏移表
.hash     SHT_HASH    SHF_ALLOC           此节区包含了一个符号哈希表。
 
.init    SHT_PROGBITS   SHF_ALLOC + SHF_EXECINSTR   此节区包含了可执行指令,是进程初始化代码的                             一部分当程序开始执行时,系统要在开始调用主程序入口之前                              (通常指 C 语言的 main 函数)执行这些代码。
.interp   SHT_PROGBITS   此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节                       区,那么节区的属性将包含SHF_ALLOC 位,否则该位为 0。
.line     SHT_PROGBITS    (无)   此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对                              应关系。其内容是未定义的。
.note     SHT_NOTE       (无)   此节区中包含注释信息,有独立的格式。
.plt     SHT_PROGBITS          此节区包含过程链接表(procedure linkage table)
 
.relname   SHT_REL    这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节                    区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节                     区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
 
.relaname   SHT_RELA  同上
.rodata    SHT_PROGBITS  SHF_ALLOC  这些节区包含只读数据,这些数据通常参与进程映像的不可写段
.rodata1   SHT_PROGBITS  SHF_ALLOC  同上
.shstrtab   SHT_STRTAB  此节区包含节区名称
.strtab     SHT_STRTAB   此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载                      的段,段中包含符号串表,节区的属性将包含SHF_ALLOC 位,否则该位为 0。
.symtab    SHT_SYMTAB    此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号                        表,那么节区的属性中包含SHF_ALLOC 位,否则该位置为 0。
.text    SHT_PROGBITS    SHF_ALLOC + SHF_EXECINSTR    此节区包含程序的可执行指令
在分析这些节区的时候,需要注意如下事项:
1:以“.”开头的节区名称是系统保留的。应用程序可以使用没有前缀的节区名称,以避免与系统节区冲突。
2:目标文件格式允许人们定义不在上述列表中的节区。
3:目标文件中也可以包含多个名字相同的节区。
4:保留给处理器体系结构的节区名称一般构成为:处理器体系结构名称简写 + 节区名称。
5:处理器名称应该与 e_machine 中使用的名称相同。例如 .FOO.psect 街区是由FOO 体系结构定义的 psect 节区。
另外,有些编译器对如上节区进行了扩展,这些已存在的扩展都使用约定俗成的名称,如:
???? .sdata
???? .tdesc
???? .sbss
???? .lit4
???? .lit8
???? .reginfo
???? .gptab
???? .liblist
???? .conflict
???? …
3.5 字符串表(String Table)
字符串表节区包含以NULL(ASCII码0)结尾的字符序列,通常称为字符串。ELF目标文件通常使用字符串来表示符号和节区名称。对字符串的引用通常以字符串在字符串表中的下标给出。
一般,第一个字节(索引为 0)定义为一个空字符串。类似的,字符串表的最后一个字节也定义为 NULL,以确保所有的字符串都以NULL结尾。索引为0的字符串在不同的上下文中可以表示无名或者名字为 NULL的字符串。
允许存在空的字符串表节区,其节区头部的sh_size成员应该为0。对空的字符串表而言,非0的索引值是非法的。
例如:对于各个节区而言,节区头部的sh_name成员包含其对应的节区头部字符串表节区的索引,此节区由ELF 头的e_shstrndx 成员给出。下图给出了包含 25 个字节的一个字符串表,以及与不同索引相关的字符串。
ELF 可执行链接文件分析(a.out)、虚拟存储、虚拟内存管理、程序加载
在使用、分析字符串表时,要注意以下几点:
???? 字符串表索引可以引用节区中任意字节。
???? 字符串可以出现多次
???? 可以存在对子字符串的引用
???? 同一个字符串可以被引用多次。
???? 字符串表中也可以存在未引用的字符串。
 
3.6 符号表(Symbol Table)
目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引。索引0表示表中的第一表项,同时也作为未定义符号的索引。
符号表项的格式如下:
typedef struct {
    Elf32_Word st_name;
   Elf32_Addr st_value;
   Elf32_Word st_size;
   unsigned char st_info;
   unsigned char st_other;
   Elf32_Half st_shndx;
} Elf32_sym;
其中各个字段的含义说明如表 13:
st_name   包含目标文件符号字符串表的索引,其中包含符号名的字符串表示。如果该值非 0,则它表示了给出          符号名的字符串表索引,否则符号表项没有名称。
         注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
分享: