# rm -rf /tmp/foo在libfoo.a中的.o文件必须用-fPIC编译或者至少和PIC(位置无关)是 兼容的。当使用static void * __libc_subinit_bar__ __attribute__ ((section ("_libc_subinit")))=&(bar);来把一个标号放到一个没有被连接器定义的section中(在这里是 _libc_subinit).连接器将所有在_libc_subinit section中的标号共同 创建两个标号,一个是__start__libc_subinit和__stop__libc_subinit, 它们作为C的标志符被使用。警告: 下面是完全可能的:连接器可能不能搜索到包含_libc_subinit section 的文件(该section中没有程序执行需要的标号)。这就使程序要确定使 _libc_subinit section能被连接器搜索得到。一种解决的办法是:把一个dummy标号放到_libc_subinit section中, 在文件中定义它,使它参考引用_libc_subinit section. ★5.5 Linux下的ELF在Linux下ELF的执行有独特的特性,这些特性对Linux的使用者来说是很有用 的。一些Linux自己的扩展跟Solaris ELF的执行很类似。★5.5.1 ELF的宏(macros)<gnu-stabs.h>中,定义了能维护标号的一些宏。elf_alias(name1,name2) 为标号name1定义一个化名name2.当文件中标号名已经被定义的时候 应该是有很用的。weak_alias(name1,name2) 为标号name1定义一个弱化名name2。仅当name2没有在任何地方定义 时,连接器就会用name1解析name2相关的符号。在文件中定义的 标号name1也会同样处理。elf_set_element(set,symbol) 强迫标号成为set集合的元素。为每个set集合创建一个section.symbol_set_declare(set) 在该模块中宣告一个集合set.事实上宣告了两个标号: 1 一个set的开始标号 extern void * const __start_set 2 一个set的结尾标号 extern void * const __stop_setsymbol_set_first_element(set) 返回一个指针(void * const *),指向set集合第一个元素。symbol_set_end_p(set,ptr) 假如ptr(void * const *)逐渐增加指向set的最后一个元素, 就返回为true.使用这些宏,程序员能任意从不同的源文件中创建列表。 ★5.5.2 library(库)的定位和搜索路径在Linux下,大部分系统的library库被安装在/usr/lib目录下。只有一些 基本的共享库被安装在/lib目录下。例如:libc.so,libcurses.so,libm.so ,libtermcap.so(各个版本对应的文件会有些不同),在其他部分被mount上 之前,那些文件是启动Linux系统所必须的。连接器默认的搜索路径是 /lib,/usr/lib,/usr/local/lib,/usr/i486-linux/lib。环境变量LD_LIBRARY_PATH也可保存目录列表,用(:)分开,该变量被动态 连接器检查并用该变量指出的目录查找共享库。 例如:/usr/X11R6/lib:/usr/local/lib:告诉动态连接器查找共享库除了 现在在默认的目录查找外,然后在/usr/X11R6/lib目录,然后再是 /usr/local/lib目录,然后再是当前目录。新的环境变量ELF_LD_LIBRARY_PATH扮演着和LD_LIBRARY_PATH类似的角色。 因为LD_LIBRARY_PATH也被老的a.out DLL linux的共享库使用。为了避免 来自DLL连接器的不必要的警告,对于在Linux下ELF的动态连接器来说, 最好使用LD_LIBRARY_PATH环境变量。另外一个特性是/etc/ld.so.conf文件,该文件包含了一些目录列表。 例如:/usr/X11R6/lib /usr/lib /usr/kerberos/lib /usr/i486-linux-libc5/lib /usr/lib/gconv /usr/lib/qt-2.1.0/lib /usr/lib/qt-1.45/lib程序ldconfig将把/etc/ld.so.conf文件中列出的搜索目录中的所有的 共享库存储到/etc/ld.so.cache中。假如共享库已经被从默认的目录中 移走,Linux ELF动态连接库将在/etc/ld.so.cache文件中找该共享库。★5.5.3 共享库的版本在ELF系统上,假如两个共享库有同样的应用程序二进制接口(ABI)的子集 的话,对那些仅仅使用那些ABI子集的程序来说,这两个共享库是可以互相 通用的(当然那两个共享库有同样的函数功能)。当一个库被改变,只要新的ABI和前面一个版本的共享库有100%的兼容的话, 所有和前面版本连接的程序在新的共享库下也能很好的运行。为了支持这, foo库必须小心的维护:1.这个共享库应该如下构造:[alert7@redhat62 dl]# gcc -shared -Wl,-soname,libfoo.so.major \ -o libfoo.so.major.minor.patch-level libfoo.o动态连接器运行时将试着定位和映象libfoo.so.major而不管事实上用的共享 文件名libfoo.so.major.patch-level。2.一个符号连接应该指向正确的共享库[alert7@redhat62 dl]# ln -s libfoo.so.major.minor.patch-level \ libfoo.so.major3.当ABI改变和原来版本不兼容的时,主(major)版本号应该升级。当搜索共享库的时候,Linux连接器将使用最新的共享库(它们有最高的 major,minor和patch level的版本号)。 ★5.5.4 共享(shared)库和静态(static)库的混合连接默认情况下,假如共享库可用,连接器会使用共享库。但是-Bdynamic和 -Bstatic提供了很好控制库的方法。它们可以决定用共享库还是用静态库。传-Bdynamic和-Bstatic选项给连接器,如下操作: # gcc -o main main.o -Wl,-Bstatic \ -lfoo -Wl,-Bdynamic -lbar# gcc -o main main.o -Wl,-Bstatic 告诉连接器所有的库(象libc等等)都使用静态的版本。 ★5.5.5 装载附加的共享库在ELF系统上,为了执行一个ELF文件,内核要把控制权交给动态连接器 ld-linux.so.1(在linux上动态连接器是ld-linux.so.1,版本不同也会不同的, 在默认的redhat6.2上是/lib/ld-linux.so.2)。在绝对路径/lib/ld-linux.so.1 以二进制存放着。假如动态连接器不存在,没有哪个ELF可执行文件能运行。动态连接器执行以下一个步骤完成从程序到进程映象: 1.分析可执行文件中的动态信息section,决定需要哪些库。 2.定位和映象(map)那些共享库,并且分析它们动态信息section 决定是否需要附加的共享库。 3.为可执行程序和那些需要的共享库执行重定位。 4.调用共享库中提供的任何初始化函数并且安排共享库提供的 清除(cleanup)函数在共享库卸栽出进程空间的时候运行。 5.传控制给程序 6.为应用程序提供函数的迟延装定服务 7.为应用程序提供动态转载服务。环境变量LD_PRELOAD设置共享库名或者用":"把文件名隔开。动态连接器在 任何那些请求的共享库之前把环境变量LD_PRELOAD的共享库装载到进程地址 空间去。例如:# LD_PRELOAD=./mylibc.so myprog这里./mylibc.so将第一时间map到程序myprog的空间。因为动态连接器在找 寻标号的时候总是使用第一次碰到的标号,所以我们可以使用LD_PRELOAD来 覆盖标准共享库中的函数。这个特性对程序员来说是很有用的,可用来在还 没有建好整个共享库的时候对单个函数功能先做调试实验。我们可以这样: #gcc -c -fPIC -O3 print.c #gcc -shared print.o -o print.so.1.0 创建自己的共享连接库★5.5.6 Linux下动态装载(Dynamic loading)_dlinfo是动态连接接口库的一个函数。它列出所有映射到执行程序和通过 dlopen打开的每个共享库。它的输出类试以下:List of loaded modules 00000000 50006163 50006200 Exe 1 50007000 5000620c 50006200 Lib 1 /lib/elf/libd1.so.1 5000a000 500062c8 50006200 Lib 2 /lib/elf/libc.so.4 50000000 50006000 00000000 Int 1 /lib/elf/ld-linux.so.1 500aa000 08006f00 08005ff0 Mod 1 ./libfoo.soModules for application (50006200): 50006163 5000620c /lib/elf/libdl.so.1 500062c8 /lib/elf/libc.so.4 50006000 /lib/ld-linux.so.1 Modules for handle 8005ff0 08006f00 ./libfoo.so 500062c8 /lib/elf/lib.so.4 50006163 5000620c /lib/elf/libd1.so.1 500062c8 /lib/elf/libc.so.4 50006000 /lib/elf/ld-linux.so.1以上可被用来解释动态的连接和动态的装载。在linux支持ELF上配置的GCC假如使用了-rdynamic选项,它将把 -export-dynamic传给连接器。强烈建议使用动态装载。这就是为什么在 我们的Makefile例子中使用了LDFLAGS=-rdynamic。暂时,这个选项只能在 linux下使用。但是-Wl,-export-dynamic能在其他的平台上把-export-dynamic 传给GNU的连接器。你能在GNU link editor的[3]和[4]部分找到它详细的描述。 ★6 位置无关代码(PIC)的汇编语言编程当用gcc指定-fPIC的时候,gcc将从C源代码中产生PIC的汇编语言代码。但有 时候,我们需要用汇编语言来产生PIC代码。在ELF下,PIC的实现是使用基寄存器(base register)。在PIC下,所有的 标号引用都是通过基寄存器实现的,为此,要用汇编写PIC的话,必须保存 基寄存器(base register)。由于位置无关代码,控制传送指令的目的地址 必须被替换或者是在PIC情况下计算的。对X86机器来说,该基寄存器 (base register)就是ebx.这里我们将介绍在X86上安全的PIC汇编代码的 两种方法。这些技术在Linux C库中也被使用到。 ★6.1 在C中内嵌汇编gcc支持内嵌汇编的声明,可让程序员在C语言中使用汇编语言。当写LINUX系 统调用接口的时候这是很有用的,而无须使用机器相关指令。在linux 下系统调用是通过int $0x80的。一般的,系统调用会有三个参数:#include <sys/syscall.h>extern int errno;int read( int fd,void *buf ,size count) { long ret;__asm__ __volatile__ ("int $0x80" :"=a"(ret) :"O"(SYS_read),"b"((long)fd), "c"((long)buf),"d"((long)count):"bx"); if (ret>=0) { return (int) ret: } errno=-ret; retrun -1; }以上汇编代码把系统调用号SYS_read放到了eax中,fd到ebx中,buf到 ecx中,count到edx中,从int $0x80中返回值ret放在eax中。在不用 -fPIC的情况下,这样定义运行良好。带-fPIC的gcc应该要检查ebx是否被 被改变,并且应该在汇编代码里保存和恢复ebx。但是不幸的是,事实上不是 这样的。我们为了支持PIC必须自己写汇编代码。#include <sys/syscall.h>extern int errno;int read( int fd,void *buf ,size count) { long ret;__asm__ __volatile__ ("pushl %%ebx\n\t" "movl %%esi,%%ebx\n\t" "int $0x80\n\t" "popl %%ebx" :"=a"(ret) :"O"(SYS_read),"S"((long)fd), "c"((long)buf),"d"((long)count):"bx"); if (ret>=0) { return (int) ret: } errno=-ret; return -1; }这里首先把fd放到esi中,然后保存ebx,把esi移到ebx,在int $0x80后恢复 ebx。这样保证ebx不被改变(除了在int $0x80中断调用中)。同样的原则也 适用于其他内嵌的汇编。在任何时候,当ebx可能要被改变时,千万要记得保存和恢复ebx. ★6.2 用汇编语言编程假如我们在系统调用时需要传5个参数时候,内嵌的汇编代码即使是PIC 的,也不能工作,因为x86没有足够的寄存器。我们需要直接用汇编语言 编写。syscall(int syscall_number,...)的一般汇编代码如下: .file "syscall.S" .text .global syscall .global errno .align 16syscall: pushl 5ebp movl %esp,%ebp pushl %edi pushl %esi pushl %ebx movl 8(%ebp),%eax movl 12(%ebp),%ebx movl 16(%ebp),%ecx movl 20(%ebp),%edx movl 24(%ebp),%esi movl 28(%ebp),%edi int $0x80 test %eax,%eax jpe .LLexit negl %eax movl %eax,errno movl $-1, %eax.LLexit: popl %ebx popl %esi popl %edi movl %ebp,%esp popl %ebp ret .type syscall,@function .L_syscall_end: .size syscall,.L_syscall_end -syscall在PIC下,我们必须通过GOT(global offset table)来访问任何全局变量 (除了保存在基寄存器ebx中的)。修改的代码如下:.file "syscall.S" .text .global syscall .global errno .align 16 syscall: pushl %ebp movl %esp,%ebp pushl %edi pushl %esi pushl %ebx call .LL4 .LL4: popl %ebx addl $_GLOBAL_OFFSET_TABLE_+[.- .LL4],%ebx pushl %ebx movl 8(%ebp),%eax movl 12(%ebp),%ebx movl 16(%ebp),%ecx movl 20(%ebp),%edx movl 24(%ebp),%esi movl 28(%ebp),%edi int $0x80 popl %ebx movl %eax,%edx test %edx,%edx jge .LLexit negl %edx movl errno@GOT(%ebx),%eax movl %edx,(%eax) movl $-1,%eax .LLexit: popl %ebx popl %esi popl %edi movl %ebp,%esp popl %ebp ret .type syscall,@function .L_syscall_end: .size syscall,.L_syscall_end-syscall假如要得到PIC的汇编代码,但是又不知道如何写,你可以写一个C的,然后如下 编译:#gcc -O -fPIC -S foo.c它将告诉gcc产生汇编代码foo.s输出,根据需要,可以修改它。 ★7 结束语根据以上讨论的,我们可以得出这样的结论:ELF是非常灵活的二进制格式。 它提供了非常有用的功能。这种规范没有给程序和程序员太多限制。它使 创建共享库容易,使动态装载和共享库的结合更加容易。在ELF下,在C++ 中,全局的构造函数和析构函数在共享库和静态库中用同样方法处理。 [译注: 到此,文章是翻译好了,但里面的一些东西看起来可能 有点问题,比如说_libc_subinit section没有他说的 那个功能,-dynamic-linker选项在默认的redhat 6.2系统 上不能用,_dlinfo动态连接接口库函数好象在linux没有实现
转载:http://hi.baidu.com/estellejiang/blog/item/6a56840105155405728da505.html
|