ELF文档整理,大部分是转别人的东西,也加上了一些自己的理解和实例.
elf文件格式 是unix上用的文件格式.可以和windows的PE文件格式相对照.unix的可执行文件没有明确的后缀,姑且用.exe来表示。
扩展名对应关系:
可重定位文件:.o
可共享文件: .so (即动态连接库)
可执行文件: .exe
变量类型:
内存变量: 2个基本属性:变量的地址 和 变量的内容
寄存器变量: 主要属性是其存放的内容。
变量在ELF文件里的位置:
全局/静态变量: 位于文件的.data或.bss段。随文件加载进内存里,有一个固定的内存地址,是一个内存变量。
临时变量: 位于文件的.text段。作为堆栈变量,最终化为.text段里的esp/ebp+n汇编语句。理论上也算是个内存变量。但是在堆栈区内存中临时生、灭。
映像:文件的2种存在形式。放在硬盘上叫文件映像,加载进内存叫内存映像。
符号:即变量/函数的名字。elf文件里符号表和重定位表 比较重要.符号表记录函数和变量的地址,重定位表记录还有哪些符号的地址没有确定,以及重定位计算的方法.
由c源程序到最终产生所有地址和跳转偏移量都明确的2进制汇编指令文件 有几道加工工序:编译器+汇编器(生成*.o),连接器(生成*.so或*.exe),动态加载解析器(加载.exe或.so进入内存)。编译器+汇编器 每次只处理1个源程序。连接器负责把多个*.o文件装配起来,解析多个源文件相互之间的符号引用。
编译器+汇编器 加工产出的文件里包含1系列汇编语句代码, 一般分成 .text + .data + .bss 几个段,每个段里的汇编代码都会包含不能确定的地址,主要有2部分地址需要确定:
1。符号本身的地址是多少(即符号在文件里的偏移量和在内存里的真实地址)
2。符号里的内容如果是个地址,则需要确定出该地址是多少。
编译器首先尽可能的计算每个符号的准确地址,如果确定不出来(比如引用了其他源文件里的符号),则会在文件里生成一些辅助的节(section),把关于地址计算的一些中间结果记录在里面,供连接器参考继续进行重定位,如果连接器还是计算不出具体地址,就把中间处理结果继续记录在文件的辅助节里,让动态加载器继续重定位。
转: ELF文件装入内存
ELF文件格式
每个操作系统对于在其内核上运行的可执行程序二进制文件映像都有特定的要求和规定,包括例如文件映像的格式,文件映像在进程用户空间的布局(程序段、数据段、堆栈段的划分等等),文件映像装入用户空间的地址是否可以浮动、以及如何浮动,是否支持动态连接、以及如何连接,如何进行系统调用,等等。这些要求和规定合在一起就构成了具体操作系统的“应用(软件)二进制界面(Application Binary Interface)”,缩写成ABI。显然,ABI是二进制文件映像的生产者(即编译器/连接器)和使用者(即文件映像加载器/启动手段)之间的一组约定。而我们一般所说的二进制文件映像格式,实际上并不仅仅是指字面意义上的、类似于数据结构定义那样的“格式”,还包括了跟文件映像装入内存过程有关的其它约定。所以,二进制文件映像格式是ABI的主体。
目前的Linux ABI是在Unix系统5的时期(大约在1980年代)发展起来的,其主体就是ELF,这是“可执行映像和连接格式(Executable and Lnking Format)”的缩写。
ELF文件映像加载
ELF的可执行文件与共享库在结构上非常类似,它们都具有一个程序段表,用来描述这些段如何映射到进程空间.
对于EXEC可执行文件来说,段的加载位置是固定的,可执行文件的段表中如实反映了段在内存中的加载地址.对于DYN共享库来说,段的加载位置是浮动的,位置无关的,共享库文件的段表反映的是以0作为基准地址的相对内存加载地址.尽管共享库的连接工作是不充分的,为了便于测试动态链接器,Linux允许应用程序直接加载共享库运行(dlopen -> dlsym -> dlclose 的方式 可参考《Intel平台下linux中ELF文件动态链接的加载、解析及实例分析》).
1.首先是内核态操作过程。即通过execve()系统调用启动一个ELF格式的应用程序文件时发生于Linux内核中的活动。首先加载应用程序的文件头,检查应用程序的段表,查看应用程序是否具有描述动态链接器的段(PT_INTERP类型的segment),然后加载动态链接器的文件头。先把应用程序的段(PT_LOAD类型的segments)\后把动态链接器的段都映射(elf_map()->do_mmap())到当前进程的用户空间,同时并存。然后CPU在返回用户空间时进入新的程序入口(固定为_start)。如果有解释器映像存在,那么这就是解释器映像的程序入口(解释器里定义的全局符号 _start),否则就是用户程序入口(start.s里定义的全局符号 _start, 两个_start符号定义在不同的文件中,对于内核而言,只要找到_start就可以了,哪个_start可见就调用哪一个。)。那么什么情况下有解释器映像存在,什么情况下没有呢?如果用户程序与各种库的连接是静态连接,因而无需依靠共享库(即动态连接库),那就不需要解释器映像,启动用户程序运行的条件已经具备;否则就一定要有解释器映像存在。现代的应用程序一般都使用共享库,所以一般都需要有解释器映像。详细解释可参考《漫谈兼容内核之八 ELF映像的装入(一)》
用户空间堆栈建立和argc、argv[]、envc、envp[]等参数传递由do_execve()->copy_strings()和create_elf_tables()、setup_arg_pages()完成.
系统调用路径:sys_execve() -> do_execve() -> search_binary_handler() -> load_elf_binary()
2.然后是用户态操作过程。第1步操作使得动态链接器运行的启动条件已经具备了。但启动应用程序的运行条件还不具备。因为还需要装入(映射)应用程序所需的共享库文件,并使应用程序与这些共享库内存映像之间建立起动态连接,而这需要由动态链接器在用户空间完成.详细解释可参考《漫谈兼容内核之九ELF映像的装入(二)》和《Before main() 分析》.
动态连接器的作用大体上就是:
● 检查应用程序中类型为DYNAMIC的Segment,其中每个类型为NEEDED的表项都指定了一个需要用到的共享库。
● 对于所需的每个共享库,(映射)装入该共享库文件到用户空间(加载过程与第1步相似),并根据库文件的内存装入地址对其实行重定位操作,包括修正动态库的GOT表中的原始内容。
● 实施动态连接,主要是找到函数的真正地址,填写进(应用程序中)相应的GOTn表项。
两种动态连接的库函数调用过程,简明的流程如下:
1. 第1次懒连接:
调用点 —〉PLTn —〉GOTn —〉PLT0 —〉GOT0 —〉_dl_runtime_resolve() —〉fixup()—〉被调用函数入口 —〉返回调用点
2. 完成了动态连接之后:
调用点 —〉PLTn —〉GOTn —〉被调用函数入口 —〉返回调用点
● 检查应用程序直接使用的每个(一级)共享库,如果又要用到别的(二级)共享库,就对其递归实施上述操作。余类推。
● 最后转入应用程序的程序入口。
调用路径:_dl_start() —〉_start() —〉__libc_start_main() —〉main()
同一共享库映射到多个进程用户空间
最后还要说明,同一个共享库的映像可以同时被映射到多个进程的用户空间。比方说,要是映像中的某个页面此刻存在于某个物理页面,那么这个物理页面就被映射到所有装入了这个共享库的进程中,只是在各个进程中的虚拟地址可能不同(但都在用户空间)。也就是说,一个拷贝为多个进程所共享,所以才叫“共享”库。不过这只是大体上而言,实际的情况还要复杂一些。读者在前面看到,wine映像有两个类型为LOAD的Segment。前者的访问权限为可读可执行、但是不可写,这当然可以为多个进程所共享。而后者的访问权限却是可读可写,这就不能由多个进程共享了。所以,凡属这个Segment中的页面,每个有关的进程就各有其自己的物理页面。再看这两个Segment中的内容。前者有(例如) .text和.plt等Section。这是可以理解的,因为.text是程序代码,这对于所有共享这个程序库的进程都一样;而.plt就是PLT,里面的内容也是对于所有共享这个程序库的进程都一样。再说,这些Section的内容也不会随着程序的运行而改变(不可写)。后者的内容则有(例如).data、.bss、.got等Section。不言而喻,.data和.bss中都是数据,当然不能让不同的进程互相干扰,必须得各有各的物理空间。至于.got的内容,那也得因进程而异。因为这是用来建立动态连接的,但是同一共享库在不同进程中的映射地址却可能不同,从而引起.got的内容也不相同。
在与别的进程共享程序段等等信息之余,每个进程都需要有些私有的、“本地的”信息,不能与别的进程共享,这是很自然、也比较容易实现的,因为毕竟不同的进程“生活”在不同的空间中。而同在一个空间的若干线程,则一般是不分彼此、“肝胆相照”的。但是,有时候也会需要有些“私房”,特别是在对一些全局量的使用上需要有只属于本线程的拷贝。为了解决这样的问题,就发展起来一种技术称为“线程本地存储(Thread Local Storage)”、即TLS。当然,动态连接器对于支持TLS的共享库有特殊的处理,这里就不深入进去了。
转: Linux动态链接技术
在动态链接的应用程序或共享库中,ELF的程序头表里会有一个PT_DYNAMIC类型的描述符,它指出了.dynamic段的位置,dynamic段用来描述动态链接过程。当应用程序调用共享库(*.so)里的函数时,要通过.plt段进行跳转。plt段又称为过程连接表,每16个字节为一项,plt的第1项为专用,其余plt项目内容为:连接器(ld)为每个全局函数所生成的一小段不可更改的汇编代码。plt段是只读的可执行的段,包含在.text段中一起映射到内存。
plt依赖于全局偏移量表GOT,GOT段是一个可写的数据段,包含在.data段中一起映射到内存。内存中的GOT随时要被动态链接器修改,用来存放重定位得到的全局变量/函数的绝对地址。GOT的前3项为专用,GOT表的第1个指针指向.dynamic段,GOT表的第2个指针指向本模块的link_map结构,GOT表的第3个指针指向动态解析函数<_dl_linux_resolver>。
为了少做无用功,Linux采用了懒惰连接动态解析技术,就是说在加载共享库时,并不马上进行函数地址的解析,只有当函数调用发生时才进行解析。在连接器(ld)生成的可执行程序里面,每个全局函数在GOT表里的指针不是其真实地址,而是指向该函数对应的plt项里的第2条汇编指令(1条pushl指令: 将该函数对应的重定位项的索引压入堆栈,作为参数)。应用程序调用共享库里的函数其实是跳转到该函数对应的plt项里的汇编指令,每个plt项里的汇编指令几乎都是一样一样的:
1.先jmp到该函数对应的GOT表项处,
2.然后 push 该函数对应的重定位表项的索引,
3.然后jmp到plt表第1项
plt第1项里的汇编代码跳转到GOT表的第3个指针,执行动态解析函数<_dl_linux_resolver>。找到该函数所在的共享库(*.so)的内存映像,并找到目标函数的真正内存地址。将该函数的地址填写在该函数对应的GOT表项里。以后程序再次调用该函数,仍然是跳转到该函数对应的PLT项,接下来跳到该函数对应的GOT表项所含地址执行,此时GOT表项里已经是该函数的真实地址,不需要再动态解析了。
转: /lib/ld-linux.so.2的源代码 glibc目录 rtld.c
>> 当执行一个动态链接的函数时,会先进入解释器,由它来完成相应的动作。
偶昨天忽然蒙了, 想不通共享库是如何加载的。 ULK2说, 共享库在不同进程间可以通过Copy on Write共享页框, 也就是同一个物理页影射到不同进程的地址空间中。 可是动态连接器(/lib/ld-linux.so.2)是用户空间的程序, 它负责为进程mmap所需要的共享库, 我的疑惑是:既然动态连接器在用户态执行, 它凭什么知道已加载的共享库的物理页地址, 从而把它映射到自己的进程地址空间中?
>>还有就是, 既然一个so在系统中只有一份内存拷贝, 那么就得有某个东西维护着 “目前所有已加载共享库” 的信息。 我觉得这样的功能不大可能在用户态实现, 只能是内核来做──可内核在哪里维护着这样的信息呢?
答:
inode->i_mapping(struct address_space) contains all the pages cached for this file.
So when you mmap a *.so file to the address space, kernel will know wether this file has been cached
原来dynamic linker使用MAP_PRIVATE调用mmap来映射共享库的, 这样就使得各个进程之间以Copy on Write的方式共享了该库。
转: 10.1 链接器
关键词 编译器 链接器 可执行文件
编译器的输出是一个可重定位的目标文件,而链接器的输出是一个可执行文件。
链接器扮演的角色是双重的。首先,它将在链表中指定的文件在物理上合成为
一个程序文件;第二,它解析外部引用和内存地址。每当一个文件中的代码涉
及到另一个文件中的代码时,外部引用就会产生。这可以通过一个函数的调用
或者一个全局变量的引用来实现。例如,当在下面所示的两个文件被链接在一
起的时候,文件2对于变量count的引用必须被解析。这正是链接器告诉文件2中
的代码,在内存的什么地方可以找到count.
文件1:
int count;
void display(void);
int main(void)
{
count = 10;
display();
return 0;
}
文件2:
#include
extern int count;
void display(void)
{
printf("%d", count);
}
与此类似,链接器也“告诉”文件1,函数display()被定位在何处,这样这个函
数就可以被调用。
当编译器为文件2产生目标代码的时候,因为它没有办法知道count将被定位于内
存中的什么地方,所以它用一个占位符代替count的地址。当文件1被编译的时候,
同样的事情也会发生,display()函数的地址是未知的,所以要使用占位符。这个
过程形成了可重定位代码(relocatable code)的依据。当两个文件被链接器连接
在一起的时候,占位符被相关的地址替换。
为了更好地理解可重定位代码,必须首先理解绝对代码(absolute code)。尽管今
天它已极少被使用,但在计算机发展的早期,一个程序被编译来运行于一个特定
的内存区间的,这种情况很常见。使用这种方法编译时,所有的地址在编译时被
固定。因为地址是固定的,所以程序只能被加载进内存的一个确定的区域并在其
中运行:这个区域用于编译好的程序。从另一个方面说,可重定位代码以一种地
址信息不固定的方式进行编译。当生成一个可重定位的目标文件的时候,链接器
赋值给每个调用、跳转或全局变量一个位移。当文件被载入内存运行的时候,加
载程序自动将这些位移解析成地址,这些地址就是内存中的工作区域,程序将被
载入其中。这就是说,一个可重定位的程序可以被载入并运行在许多不同的内存
区域中。
10.2 库文件和目标文件
尽管库文件和目标文件相似,当时它们仍有一个极其重要的差异:并不是库中所
有的代码都被加到程序中。当链接由几个目标文件构成的程序时,每个目标文件
中的所有代码都成为最终可执行文件程序的一部分。无论代码是否被实际使用,
这种情况都会发生。换句话说,所有的在链接时被指定的目标文件都是被“加在
一起”来形成程序。然而,对库文件来说却不是这样。
库是函数的集合。与目标文件不同,库文件存储了每个函数的名字、函数的目标
代码和用于链接过程中的重定位信息。当程序引用一个包含于库中的函数时,链
接器会查找那个函数并把那个函数的代码加到程序中去。这样,只有你实际上在
程序中用到的函数才被加到可执行文件中。
摘自原创(译本): << Borland C++ Builder: The Complete Reference >>
(美) Herbert Schildt、Greg Guntle
Web源代码下载:
转: 操作系统原理
(一)、用户程序的主要处理阶段
1、 编辑阶段(形成符号空间)
2、 编译阶段(生成目标代码)
3、 连接阶段 (确定相对地址)
将编译后得到的一组目标模块以及它们所需的库函数装配成一个完整的装入模块。
4、 装入阶段 (可以确定物理地址)
将装入模块放入分到的内存区中。这时需要进行重定位,即将装入模块的逻辑地址转变为内存的实际物理地址。
5、 运行阶段 (可以确定物理地址)
运行可执行的程序file1.exe。
vi cc ld 装入程序
用户 à file1.c à file1.o à file1.exe à 内存 à 进程运行
编辑阶段 编译阶段 连接阶段 装入阶段 运行阶段
装配模块虽然具有统一的地址空间,但是仍是以“0”作为参考地址,
即是浮动的。要把它装入内存执行,就要确定装入内存的实际物理
地址,并修改程序中与地址有关的代码,这一过程称为地址重定位。
逻辑地址: à 物理地址:
LOAD 1, 500 LOAD 1, 1500
1、逻辑地址--用户程序经编译后,每个目标模块以0为基地址进行
的顺序编址。逻辑地址又称相对地址,相对基地址而言。
2、物理地址--内存中各物理存储单元的地址从统一的基地址进行的
顺序编址。物理地址又称绝对地址,它是数据在内存中的实际存储
地址。
3、重定位--把逻辑地址转变为内存的物理地址的过程。根据重定位
时机的不同,又分为静态重定位(装入内存时重定位)和动态重定
位(程序执行时重定位)。
3.1 静态重定位
在程序执行之前(装入内存时)进行重定位。它根据装配模块将要
装入的内存起始地址,直接修改装配模块中的有关使用地址的指令。
(为了程序的正确运行)内存空间是一维的线性空间
装入时机:在目标程序装入内存时
优点:无需硬件地址变换机构(装入程序负责)
缺点:连续装入、难共享
注意:程序重定位以后就不能在内存中移动;二是要求程序
的存储空间是连续的,不能把程序存储到若干个不连续
的区域中。
3.2 动态重定位
程序执行过程中进行,即在每次访问内存单元前才进行地址变换。
动态重定位可使装配模块不加任何修改就装入内存,但是它需要硬
件——重定位寄存器的支持。
装入时机:在程序运行时(每次访问内存之前)
优点:无需连续存放、便于共享
缺点:需专门硬件机构、软件算法实现复杂
阅读(765) | 评论(0) | 转发(0) |