China Open source community
站内导航:

 
 
 
当前位置: 首页 >> 程序设计 >> 从程序员角度看ELF
 

从程序员角度看ELF

作者:      来源:     发表时间:2006-04-30     浏览次数:      字号:    


[alert7@redhat62 dl]# gcc -shared -o libbar.so libbar.o -lfoo

假如libfoo.so被用来创建共享库时,有趣的时候就会发生了。当libbar.so
被映象到进程的地址空间的时候,动态连接器也把libfoo.so映象到内存。
假如libbar.so需要libfoo.so的时候,这个特性非常有用。实际上使用
libbars.o库的程序编译时是不需要-lfoo的。假如archive版本的libfoo.a
被使用,当在libbar.a中的标号被libbar.o引用时,它将会被搜索到。假使在
libbar.so中包含libfoo.a甚至它们根本不被libbar.o使用,在这样的情况下
必须逐步把.o文件加到libbar.o中:

# rm -rf /tmp/foo
# mkdir /tmp/foo
# (cd /tmp/foo/;ar -x ....../libfoo.a)
# gcc -shared -o libbar.so libbar.o /tmp/foo/*.o
# 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_set

symbol_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.major

3.当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.so

Modules 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 16

syscall:
    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没有实现
    等等一系列问题,欢迎讨论指正
    mailto: alert7@21cn.com
                 alert7@xfocus.org
]

参考:

1. Operating System API Reference:UNIX SVR4.2,UNIX Press,1992

2. SunOs 5.3 Linker and Libraries Manual,SunSoft ,1993.

3. Richard M.Stallman,Using and porting GNU CC for version 2.6,
   Free Software Foundation,September 1994.

4. Steve Chamberlain and Roland Pesch,Using ld:The GNU linker,ld
   version 2,Cygnus Support,January 1994.

[1] [2] [3]

编辑 webmaster

 
 
 
评论
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •  
    中国源码网 - WWW.YUANMA.ORG - 中国开放源代码社区