★概要: 这片文档从程序员的角度讨论了linux的ELF二进制格式。介绍了一些ELF执行 文件在运行控制的技术。展示了如何使用动态连接器和如何动态装载ELF。 我们也演示了如何在LINUX使用GNU C/C++编译器和一些其他工具来创建共享的 C/C++库。★1前言最初,UNIX系统实验室(USL)开发和发布了Executable and linking Format (ELF)这样的二进制格式。在SVR4和Solaris 2.x上,都做为可执行文件默认的 二进制格式。ELF比a.out和COFF更强大更灵活。结合一些适当的工具,程序员 使用ELF就可以在运行时控制程序的流程。 ★2 ELF类型三种主要的ELF文件类型:.可执行文件:包含了代码和数据。具有可执行的程序。 例如这样一个程序 # file dltest dltest: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked (uses shared libs), not stripped.可重定位文件:包含了代码和数据(这些数据是和其他重定位文件和共享的 object文件一起连接时使用的) 例如这样文件 # file libfoo.o libfoo.o: ELF 32-bit LSB relocatable, Intel 80386, version 1, not stripped.共享object文件(又可叫做共享库):包含了代码和数据(这些数据是在连接 时候被连接器ld和运行时动态连接器使用的)。动态连接器可能称为 ld.so.1,libc.so.1 或者 ld-linux.so.1。 例如这样文件 # file libfoo.so libfoo.so: ELF 32-bit LSB shared object, Intel 80386, version 1, not strippedELF section部分是非常有用的。使用一些正确的工具和技术,程序员就能 熟练的操作可执行文件的执行。★3 .init和.fini sections在ELF系统上,一个程序是由可执行文件或者还加上一些共享object文件组成。 为了执行这样的程序,系统使用那些文件创建进程的内存映象。进程映象 有一些段(segment),包含了可执行指令,数据,等等。为了使一个ELF文件 装载到内存,必须有一个program header(该program header是一个描述段 信息的结构数组和一些为程序运行准备的信息)。一个段可能有多个section组成.这些section在程序员角度来看更显的重要。每个可执行文件或者是共享object文件一般包含一个section table,该表 是描述ELF文件里sections的结构数组。这里有几个在ELF文档中定义的比较 特别的sections.以下这些是对程序特别有用的:.fini 该section保存着进程终止代码指令。因此,当一个程序正常退出时, 系统安排执行这个section的中的代码。 .init 该section保存着可执行指令,它构成了进程的初始化代码。 因此,当一个程序开始运行时,在main函数被调用之前(c语言称为 main),系统安排执行这个section的中的代码。.init和.fini sections的存在有着特别的目的。假如一个函数放到 .init section,在main函数执行前系统就会执行它。同理,假如一 个函数放到.fini section,在main函数返回后该函数就会执行。 该特性被C++编译器使用,完成全局的构造和析构函数功能。当ELF可执行文件被执行,系统将在把控制权交给可执行文件前装载所以相关 的共享object文件。构造正确的.init和.fini sections,构造函数和析构函数 将以正确的次序被调用。★3.1 在c++中全局的构造函数和析构函数在c++中全局的构造函数和析构函数必须非常小心的处理碰到的语言规范问题。 构造函数必须在main函数之前被调用。析构函数必须在main函数返回之后 被调用。例如,除了一般的两个辅助启动文件crti.o和crtn.o外,GNU C/C++ 编译器--gcc还提供两个辅助启动文件一个称为crtbegin.o,还有一个被称为 crtend.o。结合.ctors和.dtors两个section,c++全局的构造函数和析构函数 能以运行时最小的负载,正确的顺序执行。 .ctors 该section保存着程序的全局的构造函数的指针数组。.dtors 该section保存着程序的全局的析构函数的指针数组。 ctrbegin.o 有四个section: 1 .ctors section 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: file format elf32-i386 Contents of section .ctors: 0000 ffffffff .... 这里说的dummy元素应该就是指的是ffffffff ] 2 .dtors section local标号__DTOR_LIST__指向全局析构函数的指针数组头。在 ctrbegin.o中的该数组仅有也只有一个dummy元素。 3 .text section 只包含了__do_global_dtors_aux函数,该函数遍历__DTOR_LIST__ 列表,调用列表中的每个析构函数。 函数如下:Disassembly of section .text:00000000 <__do_global_dtors_aux>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 3d 04 00 00 00 00 cmpl $0x0,0x4 a: 75 38 jne 44 <__do_global_dtors_aux+0x44> c: eb 0f jmp 1d <__do_global_dtors_aux+0x1d> e: 89 f6 mov %esi,%esi 10: 8d 50 04 lea 0x4(%eax),%edx 13: 89 15 00 00 00 00 mov %edx,0x0 19: 8b 00 mov (%eax),%eax 1b: ff d0 call *%eax 1d: a1 00 00 00 00 mov 0x0,%eax 22: 83 38 00 cmpl $0x0,(%eax) 25: 75 e9 jne 10 <__do_global_dtors_aux+0x10> 27: b8 00 00 00 00 mov $0x0,%eax 2c: 85 c0 test %eax,%eax 2e: 74 0a je 3a <__do_global_dtors_aux+0x3a> 30: 68 00 00 00 00 push $0x0 35: e8 fc ff ff ff call 36 <__do_global_dtors_aux+0x36> 3a: c7 05 04 00 00 00 01 movl $0x1,0x4 41: 00 00 00 44: c9 leave 45: c3 ret 46: 89 f6 mov %esi,%esi 4 .fini section 它只包含一个__do_global_dtors_aux的函数调用。请记住,它仅是 一个函数调用而不返回的,因为crtbegin.o的.fini section是这个 函数体的一部分。 函数如下: Disassembly of section .fini:00000000 <.fini>: 0: e8 fc ff ff ff call 1 <.fini+0x1> crtend.o 也有四个section: 1 .ctors section local标号__CTOR_END__指向全局构造函数的指针数组尾部。 2 .dtors section local标号__DTOR_END__指向全局析构函数的指针数组尾部。 3 .text section 只包含了__do_global_ctors_aux函数,该函数遍历__CTOR_LIST__ 列表,调用列表中的每个构造函数。 函数如下: 00000000 <__do_global_ctors_aux>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 53 push %ebx 4: bb fc ff ff ff mov $0xfffffffc,%ebx 9: 83 3d fc ff ff ff ff cmpl $0xffffffff,0xfffffffc 10: 74 0c je 1e <__do_global_ctors_aux+0x1e> 12: 8b 03 mov (%ebx),%eax 14: ff d0 call *%eax 16: 83 c3 fc add $0xfffffffc,%ebx 19: 83 3b ff cmpl $0xffffffff,(%ebx) 1c: 75 f4 jne 12 <__do_global_ctors_aux+0x12> 1e: 8b 5d fc mov 0xfffffffc(%ebp),%ebx 21: c9 leave 22: c3 ret 23: 90 nop 4 .init section 它只包含一个__do_global_ctors_aux的函数调用。请记住,它仅是 一个函数调用而不返回的,因为crtend.o的.init section是这个函 数体的一部分。 函数如下: Disassembly of section .init:00000000 <.init>: 0: e8 fc ff ff ff call 1 <.init+0x1> crti.o 在.init section中仅是个_init的函数标号。 在.fini section中的_fini函数标号。crtn.o 在.init和.fini section中仅是返回指令。Disassembly of section .init:00000000 <.init>: 0: 8b 5d fc mov 0xfffffffc(%ebp),%ebx 3: c9 leave 4: c3 ret Disassembly of section .fini:00000000 <.fini>: 0: 8b 5d fc mov 0xfffffffc(%ebp),%ebx 3: c9 leave 4: c3 ret编译产生可重定位文件时,gcc把每个全局构造函数挂在__CTOR_LIST上 (通过把指向构造函数的指针放到.ctors section中)。 它也把每个全局析构函挂在__DTOR_LIST上(通过把指向析构函的指针 放到.dtors section中)。连接时,gcc在所有重定位文件前处理crtbegin.o,在所有重定位文件后处理 crtend.o。另外,crti.o在crtbegin.o之前被处理,crtn.o在crtend.o之后 被处理。当产生可执行文件时,连接器ld分别的连接所有可重定位文件的ctors 和 .dtors section到__CTOR_LIST__和__DTOR_LIST__列表中。.init section 由所有的可重定位文件中_init函数组成。.fini由_fini函数组成。运行时,系统将在main函数之前执行_init函数,在main函数返回后执行 _fini函数。 ★4 ELF的动态连接与装载★4.1 动态连接当在UNIX系统下,用C编译器把C源代码编译成可执行文件时,c编译驱动器一般 将调用C的预处理,编译器,汇编器和连接器。. c编译驱动器首先把C源代码传到C的预处理器,它以处理过的宏和 指示器形式输出纯C语言代码。. c编译器把处理过的C语言代码翻译为机器相关的汇编代码。. 汇编器把结果的汇编语言代码翻译成目标的机器指令。结果这些 机器指令就被存储成指定的二进制文件格式,在这里,我们使用的 ELF格式。. 最后的阶段,连接器连接所有的object文件,加入所有的启动代码和 在程序中引用的库函数。 下面有两种方法使用lib库 --static library 一个集合,包含了那些object文件中包含的library例程和数据。用 该方法,连接时连接器将产生一个独立的object文件(这些 object文件保存着程序所要引用的函数和数据)的copy。 --shared library 是共享文件,它包含了函数和数据。用这样连接出来的程序仅在可执行 程序中存储着共享库的名字和一些程序引用到的标号。在运行时,动态 连接器(在ELF中也叫做程序解释器)将把共享库映象到进程的虚拟 地址空间里去,通过名字解析在共享库中的标号。该处理过程也称为 动态连接(dynamic linking)程序员不需要知道动态连接时用到的共享库做什么,每件事情对程序员都是 透明的。 ★4.2 动态装载(Dynamic Loading)动态装载是这样一个过程:把共享库放到执行时进程的地址空间,在库中查找 函数的地址,然后调用那个函数,当不再需要的时候,卸载共享库。它的执行 过程作为动态连接的服务接口。在ELF下,程序接口通常在<dlfcn.h>中被定义。如下:void *dlopen(const char * filename,int flag); const char * dlerror(void); const void * dlsym (void handle*,const char * symbol); int dlclose(void * handle);这些函数包含在libdl.so中。下面是个例子,展示动态装载是如何工作的。 主程序在运行时动态的装载共享库。一方面可指出哪个共享库被使用,哪个 函数被调用。一方面也能在访问共享库中的数据。[alert7@redhat62 dl]# cat dltest.c #include <stdio.h> #include <stdlib.h> #include <getopt.h> #include <dlfcn.h> #include <ctype.h>typedef void (*func_t) (const char *);void dltest(const char *s) { printf("From dltest:"); for (;*s;s++) { putchar(toupper(*s)); } putchar('\n'); }main(int argc,char **argv) { void *handle; func_t fptr; char * libname = "./libfoo.so"; char **name=NULL; char *funcname = "foo"; char *param= "Dynamic Loading Test"; int ch; int mode=RTLD_LAZY; while ((ch = getopt(argc,argv,"a:b:f:l:"))!=EOF) { switch(ch) { case 'a':/*argument*/ param=optarg; break; case 'b':/*how to bind*/ switch(*optarg) { case 'l':/*lazy*/ mode = RTLD_LAZY; break; case 'n':/*now*/ mode = RTLD_NOW; break; } break; case 'l':/*which shared library*/ libname= optarg; break; case 'f':/*which function*/ 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);/*call that function with 'param'*/ (*fptr)(param);dlclose(handle); return 0;}这里有两个共享库,一个是libfoo.so一个是libbar.so。每个都用同样的全局 字符串变量libname,分别各自有foo和bar函数。通过dlsym,对程序来说,他们 都是可用的。[alert7@redhat62 dl]# cat libbar.c #include <stdio.h>extern void dltest(const char *); const char * const libname = "libbar.so";void bar (const char *s) { dltest("Called from libbar."); printf("libbar:%s\n",s); } [alert7@redhat62 dl]# cat libfoo.c #include <stdio.h>extern void dltest (const char *s); const char *const libname="libfoo.so";void foo(const char *s) { const char *saved=s; dltest("Called from libfoo"); printf("libfoo:"); for (;*s;s++); for (s--;s>=saved;s--) { putchar (*s); } putchar ('\n'); }使用Makefile文件来编译共享库和主程序是很有用的。因为libbar.so和 libfoo.so也调用了主程序里的dltest函数。[alert7@redhat62 dl] #cat Makefile CC=gcc LDFLAGS=-rdynamic SHLDFLAGS= RM=rmall:dltestlibfoo.o:libfoo.c $(CC) -c -fPIC $<libfoo.so:libfoo.o $(CC) $(SHLDFLAGS) -shared -o $@ $^libbar: libbar.c $(CC) -c -fPIC $<libbar.so:libbar.o $(CC) $(SHLDFLAGS) -shared -o $@ $^dltest: dltest.o libbar.so libfoo.so $(CC) $(LDFLAGS) -o $@ dltest.o -ldlclean: $(RM) *.o *.so dltest处理流程:[alert7@redhat62 dl]# export ELF_LD_LIBRARY_PATH=. [alert7@redhat62 dl]# ./dltest Call 'foo' in 'libfoo.so': From dltest:CALLED FROM LIBFOO libfoo:tseT gnidaoL cimanyD [alert7@redhat62 dl]# ./dltest -f bar bar:dlsym:'./libfoo.so: undefined symbol: bar' [alert7@redhat62 dl]# ./dltest -f bar -l ./libbar.so Call 'bar' in 'libbar.so': From dltest:CALLED FROM LIBBAR. libbar:Dynamic Loading Test 在动态装载进程中调用的第一个函数就是dlopen,它使得共享可库对 运行着的进程可用。dlopen返回一个handle,该handle被后面的dlsym 和dlclose函数使用。dlopen的参数为NULL有特殊的意思---它使得在 程序导出的标号和当前已经装载进内存的共享库导出的标号通过dlsym 就可利用。在一个共享库已经装载进运行着的进程的地址空间后,dlsym可用来 获得在共享库中导出的标号地址。然后就可以通过dlsym返回的地址 来访问里面的函数和数据。当一个共享库不再需要使用的时候,就可以调用dlclose卸载该函数库。 假如共享库在启动时刻或者是通过其他的dlopen调用被装载的话,该 共享库不会从调用的进程的地址空间被移走。假如dlclose操作成功,返回为0。dlopen和dlsym如果有错误,将返回 为NULL。为了获取诊断信息,可调用dlerror.
转载%D7%BF%B1%A3%CC%D8/blog/item/ec449e2ba15d43325343c145.html
|