从程序员的角度看ELF.docx
《从程序员的角度看ELF.docx》由会员分享,可在线阅读,更多相关《从程序员的角度看ELF.docx(43页珍藏版)》请在冰点文库上搜索。
从程序员的角度看ELF
★概要:
这篇文档从程序员的角度讨论了linux的ELF二进制格式。
介绍了一些ELF执行
文件在运行控制的技术。
展示了如何使用动态连接器和如何动态装载ELF。
我们也演示了如何在LINUX使用GNUC/C++编译器和一些其他工具来创建共享的
C/C++库。
★1前言
最初,UNIX系统实验室(USL)开发和发布了ExecutableandlinkingFormat
(ELF)这样的二进制格式。
在SVR4和Solaris2.x上,都做为可执行文件默认的
二进制格式。
ELF比a.out和COFF更强大更灵活。
结合一些适当的工具,程序员
使用ELF就可以在运行时控制程序的流程。
★2ELF类型
三种主要的ELF文件类型:
.可执行文件:
包含了代码和数据。
具有可执行的程序。
例如这样一个程序
#filedltest
dltest:
ELF32-bitLSBexecutable,Intel80386,version1,
dynamicallylinked(usessharedlibs),notstripped
.可重定位文件:
包含了代码和数据(这些数据是和其他重定位文件和共享的
object文件一起连接时使用的)
例如这样文件
#filelibfoo.o
libfoo.o:
ELF32-bitLSBrelocatable,Intel80386,version1,
notstripped
.共享object文件(又可叫做共享库):
包含了代码和数据(这些数据是在连接
时候被连接器ld和运行时动态连接器使用的)。
动态连接器可能称为
ld.so.1,libc.so.1或者ld-linux.so.1。
例如这样文件
#filelibfoo.so
libfoo.so:
ELF32-bitLSBsharedobject,Intel80386,version
1,notstripped
ELFsection部分是非常有用的。
使用一些正确的工具和技术,程序员就能
熟练的操作可执行文件的执行。
★3.init和.finisections
在ELF系统上,一个程序是由可执行文件或者还加上一些共享object文件组成。
为了执行这样的程序,系统使用那些文件创建进程的内存映象。
进程映象
有一些段(segment),包含了可执行指令,数据,等等。
为了使一个ELF文件
装载到内存,必须有一个programheader(该programheader是一个描述段
信息的结构数组和一些为程序运行准备的信息)。
一个段可能有多个section组成.这些section在程序员角度来看更显的重要。
每个可执行文件或者是共享object文件一般包含一个sectiontable,该表
是描述ELF文件里sections的结构数组。
这里有几个在ELF文档中定义的比较
特别的sections.以下这些是对程序特别有用的:
.fini
该section保存着进程终止代码指令。
因此,当一个程序正常退出时,
系统安排执行这个section的中的代码。
.init
该section保存着可执行指令,它构成了进程的初始化代码。
因此,当一个程序开始运行时,在main函数被调用之前(c语言称为
main),系统安排执行这个section的中的代码。
.init和.finisections的存在有着特别的目的。
假如一个函数放到
.initsection,在main函数执行前系统就会执行它。
同理,假如一
个函数放到.finisection,在main函数返回后该函数就会执行。
该特性被C++编译器使用,完成全局的构造和析构函数功能。
当ELF可执行文件被执行,系统将在把控制权交给可执行文件前装载所以相关
的共享object文件。
构造正确的.init和.finisections,构造函数和析构函数
将以正确的次序被调用。
★3.1在c++中全局的构造函数和析构函数
在c++中全局的构造函数和析构函数必须非常小心的处理碰到的语言规范问题。
构造函数必须在main函数之前被调用。
析构函数必须在main函数返回之后
被调用。
例如,除了一般的两个辅助启动文件crti.o和crtn.o外,GNUC/C++
编译器--gcc还提供两个辅助启动文件一个称为crtbegin.o,还有一个被称为
crtend.o。
结合.ctors和.dtors两个section,c++全局的构造函数和析构函数
能以运行时最小的负载,正确的顺序执行。
.ctors
该section保存着程序的全局的构造函数的指针数组。
.dtors
该section保存着程序的全局的析构函数的指针数组。
ctrbegin.o
有四个section:
1.ctorssection
local标号__CTOR_LIST__指向全局构造函数的指针数组头。
在
ctrbegin.o中的该数组只有一个dummy元素。
[译注:
#objdump-s-j.ctors
/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtbegin.o
/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtbegin.o:
fileformatelf32-i386
Contentsofsection.ctors:
0000ffffffff ....
这里说的dummy元素应该就是指的是ffffffff
]
2.dtorssection
local标号__DTOR_LIST__指向全局析构函数的指针数组头。
在
ctrbegin.o中的该数组仅有也只有一个dummy元素。
3.textsection
只包含了__do_global_dtors_aux函数,该函数遍历__DTOR_LIST__
列表,调用列表中的每个析构函数。
函数如下:
Disassemblyofsection.text:
00000000<__do_global_dtors_aux>:
0:
55 push %ebp
1:
89e5 mov %esp,%ebp
3:
833d0400000000 cmpl x0,0x4
a:
7538 jne 44<__do_global_dtors_aux+0x44>
c:
eb0f jmp 1d<__do_global_dtors_aux+0x1d>
e:
89f6 mov %esi,%esi
10:
8d5004 lea 0x4(%eax),%edx
13:
891500000000 mov %edx,0x0
19:
8b00 mov (%eax),%eax
1b:
ffd0 call *%eax
1d:
a100000000 mov 0x0,%eax
22:
833800 cmpl x0,(%eax)
25:
75e9 jne 10<__do_global_dtors_aux+0x10>
27:
b800000000 mov x0,%eax
2c:
85c0 test %eax,%eax
2e:
740a je 3a<__do_global_dtors_aux+0x3a>
30:
6800000000 push x0
35:
e8fcffffff call 36<__do_global_dtors_aux+0x36>
3a:
c7050400000001 movl x1,0x4
41:
000000
44:
c9 leave
45:
c3 ret
46:
89f6 mov %esi,%esi
4.finisection
它只包含一个__do_global_dtors_aux的函数调用。
请记住,它仅是
一个函数调用而不返回的,因为crtbegin.o的.finisection是这个
函数体的一部分。
函数如下:
Disassemblyofsection.fini:
00000000<.fini>:
0:
e8fcffffff call 1<.fini+0x1>
crtend.o
也有四个section:
1.ctorssection
local标号__CTOR_END__指向全局构造函数的指针数组尾部。
2.dtorssection
local标号__DTOR_END__指向全局析构函数的指针数组尾部。
3.textsection
只包含了__do_global_ctors_aux函数,该函数遍历__CTOR_LIST__
列表,调用列表中的每个构造函数。
函数如下:
00000000<__do_global_ctors_aux>:
0:
55 push %ebp
1:
89e5 mov %esp,%ebp
3:
53 push %ebx
4:
bbfcffffff mov xfffffffc,%ebx
9:
833dfcffffffff cmpl xffffffff,0xfffffffc
10:
740c je 1e<__do_global_ctors_aux+0x1e>
12:
8b03 mov (%ebx),%eax
14:
ffd0 call *%eax
16:
83c3fc add xfffffffc,%ebx
19:
833bff cmpl xffffffff,(%ebx)
1c:
75f4 jne 12<__do_global_ctors_aux+0x12>
1e:
8b5dfc mov 0xfffffffc(%ebp),%ebx
21:
c9 leave
22:
c3 ret
23:
90 nop
4.initsection
它只包含一个__do_global_ctors_aux的函数调用。
请记住,它仅是
一个函数调用而不返回的,因为crtend.o的.initsection是这个函
数体的一部分。
函数如下:
Disassemblyofsection.init:
00000000<.init>:
0:
e8fcffffff call 1<.init+0x1>
crti.o
在.initsection中仅是个_init的函数标号。
在.finisection中的_fini函数标号。
crtn.o
在.init和.finisection中仅是返回指令。
Disassemblyofsection.init:
00000000<.init>:
0:
8b5dfc mov 0xfffffffc(%ebp),%ebx
3:
c9 leave
4:
c3 ret
Disassemblyofsection.fini:
00000000<.fini>:
0:
8b5dfc mov 0xfffffffc(%ebp),%ebx
3:
c9 leave
4:
c3 ret
编译产生可重定位文件时,gcc把每个全局构造函数挂在__CTOR_LIST上
(通过把指向构造函数的指针放到.ctorssection中)。
它也把每个全局析构函挂在__DTOR_LIST上(通过把指向析构函的指针
放到.dtorssection中)。
连接时,gcc在所有重定位文件前处理crtbegin.o,在所有重定位文件后处理
crtend.o。
另外,crti.o在crtbegin.o之前被处理,crtn.o在crtend.o之后
被处理。
当产生可执行文件时,连接器ld分别的连接所有可重定位文件的ctors和
.dtorssection到__CTOR_LIST__和__DTOR_LIST__列表中。
.initsection
由所有的可重定位文件中_init函数组成。
.fini由_fini函数组成。
运行时,系统将在main函数之前执行_init函数,在main函数返回后执行
_fini函数。
★4ELF的动态连接与装载
★4.1动态连接
当在UNIX系统下,用C编译器把C源代码编译成可执行文件时,c编译驱动器一般
将调用C的预处理,编译器,汇编器和连接器。
. c编译驱动器首先把C源代码传到C的预处理器,它以处理过的宏和
指示器形式输出纯C语言代码。
. c编译器把处理过的C语言代码翻译为机器相关的汇编代码。
. 汇编器把结果的汇编语言代码翻译成目标的机器指令。
结果这些
机器指令就被存储成指定的二进制文件格式,在这里,我们使用的
ELF格式。
. 最后的阶段,连接器连接所有的object文件,加入所有的启动代码和
在程序中引用的库函数。
下面有两种方法使用lib库
--staticlibrary
一个集合,包含了那些object文件中包含的library例程和数据。
用
该方法,连接时连接器将产生一个独立的object文件(这些
object文件保存着程序所要引用的函数和数据)的copy。
--sharedlibrary
是共享文件,它包含了函数和数据。
用这样连接出来的程序仅在可执行
程序中存储着共享库的名字和一些程序引用到的标号。
在运行时,动态
连接器(在ELF中也叫做程序解释器)将把共享库映象到进程的虚拟
地址空间里去,通过名字解析在共享库中的标号。
该处理过程也称为
动态连接(dynamiclinking)
程序员不需要知道动态连接时用到的共享库做什么,每件事情对程序员都是
透明的。
★4.2动态装载(DynamicLoading)
动态装载是这样一个过程:
把共享库放到执行时进程的地址空间,在库中查找
函数的地址,然后调用那个函数,当不再需要的时候,卸载共享库。
它的执行
过程作为动态连接的服务接口。
在ELF下,程序接口通常在中被定义。
如下:
void*dlopen(constchar*filename,intflag);
constchar*dlerror(void);
constvoid*dlsym(voidhandle*,constchar*symbol);
intdlclose(void*handle);
这些函数包含在libdl.so中。
下面是个例子,展示动态装载是如何工作的。
主程序在运行时动态的装载共享库。
一方面可指出哪个共享库被使用,哪个
函数被调用。
一方面也能在访问共享库中的数据。
[alert7@redhat62dl]#catdltest.c
#include
#include
#include
#include
#include
typedefvoid(*func_t)(constchar*);
voiddltest(constchar*s)
{
printf("Fromdltest:
");
for(;*s;s++)
{
putchar(toupper(*s));
}
putchar('\n');
}
main(intargc,char**argv)
{
void*handle;
func_tfptr;
char*libname="./libfoo.so";
char**name=NULL;
char*funcname="foo";
char*param="DynamicLoadingTest";
intch;
intmode=RTLD_LAZY;
while((ch=getopt(argc,argv,"a:
b:
f:
l:
"))!
=EOF)
{
switch(ch)
{
case'a':
/*argument*/
param=optarg;
break;
case'b':
/*howtobind*/
switch(*optarg)
{
case'l':
/*lazy*/
mode=RTLD_LAZY;
break;
case'n':
/*now*/
mode=RTLD_NOW;
break;
}
break;
case'l':
/*whichsharedlibrary*/
libname=optarg;
break;
case'f':
/*whichfunction*/
funcname=optarg;
}
}
handle=dlopen(libname,mode);
if(handle==NULL)
{
fprintf(stderr,"%s:
dlopen:
'%s'\n",libname,dlerror());
exit
(1);
}
fptr=(func_t)dlsym(handle,funcname);
if(fptr==NULL)
{
fprintf(stderr,"%s:
dlsym:
'%s'\n",funcname,dlerror());
exit
(1);
}
name=(char**)dlsym(handle,"libname");
if(name==NULL)
{
fprintf(stderr,"%s:
dlsym:
'libname'\n",dlerror());
exit
(1);
}
printf("Call'%s'in'%s':
\n",funcname,*name);
/*callthatfunctionwith'param'*/
(*fptr)(param);
dlclose(handle);
return0;
}
这里有两个共享库,一个是libfoo.so一个是libbar.so。
每个都用同样的全局
字符串变量libname,分别各自有foo和bar函数。
通过dlsym,对程序来说,他们
都是可用的。
[alert7@redhat62dl]#catlibbar.c
#include
externvoiddltest(constchar*);
constchar*constlibname="libbar.so";
voidbar(constchar*s)
{
dltest("Calledfromlibbar.");
printf("libbar:
%s\n",s);
}
[alert7@redhat62dl]#catlibfoo.c
#include
externvoiddltest(constchar*s);