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

从程序员角度看ELF

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


[alert7@redhat62 dl] #cat Makefile
CC=gcc
LDFLAGS=-rdynamic
SHLDFLAGS=
RM=rm

all:dltest

libfoo.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 -ldl

clean:
    $(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.


★5 支持ELF的LINUX上的编译器GNU GCC

感谢Eric Youngdale (eric@aib.com),lan Lance Taylor (ian@cygnus.com)
还有许多为gcc支持ELF功能的默默做贡献的人。我们能用gcc和GNU的二进制
工具很容易的创建ELF可执行文件和共享库。

★5.1 共享C库 Shared C Library

在ELF下构造一个共享库比其他的容易的多。但是需要编译器,汇编器,
连接器的支持。首先,需要产生位置无关(position-independent)代码。
要做到这一点,gcc需要加上编译选项-fPIC
[alert7@redhat62 dl]# gcc -fPIC -O -c libbar.c

这时候就适合构造共享库了,加上-shared编译选项
[alert7@redhat62 dl]# gcc -shared -o libbar.so libbar.o

现在我们构造的libbar.so就可以被连接器(link editor)和动态连接器
(dynamic linker)。只要编译时带上-fPIC编译选项,可以把许多重定位
文件加到共享库中。为了把baz.o和共享库连接在一起,可以如下操作:
# gcc -O -c baz.c
# gcc -o baz baz.o -L. -lbar

在把libbar.so安装到动态连接器能找到的正确位置上之后,运行baz将
使libbar.so映象到baz的进程地址空间。内存中libbar.so的一份拷贝将
被所有的可执行文件(这些可执行程序连接时和它一块儿连接的或者
在运行时动态装载的)共享。

★5.2 共享C++库 Shared C++ Library

在共享c++库中主要的困难是如何对待构造函数和析构函数。
在SunOS下,构造和使用一个共享的ELF C库是容易的,但是在SunOS下不能
构造共享的C++库,因为构造函数和析构函数有特别的需求。为止,在ELF
中的.init和.init section提供了完美的解决方法。

当构造共享C++库时,我们使用crtbegin.o和crtend.o这两个特殊的版本,
(它们已经是经过-fPIC的)。对于连接器(link editor)来说,构造共享
的C++库几乎是和一般的可执行文件一样的。全局的构造函数和析构函数
被.init和.fini section处理(在上面3.1节中已经讨论过)。

但一个共享库被映射到进程的地址空间时,动态连接器将在传控制权给程序
之前执行_init函数,并且将为_fini函数安排在共享库不再需要的时候被
执行。

连接选项-shared是告诉gcc以正确的顺序放置必要的辅助文件并且告诉它将
产生一个共享库。-v选项将显示什么文件什么选项被传到了连接器
(link editor).

[alert7@redhat62 dl]# gcc -v -shared -o libbar.so libbar.o
Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs
gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)
/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/collect2 -m elf_i386
-shared -o libbar.so /usr/lib/crti.o /usr/lib/gcc-lib/i386-redhat
    -linux/egcs-2.91.66/crtbeginS.o
-L/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66
-L/usr/i386-redhat-linux/lib libbar.o -lgcc -lc --version-script
/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/libgcc.map
-lgcc /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtendS.o
/usr/lib/crtn.o

crtbeginS.o和crtendS.o用-fPIC编译的两个特殊的版本。带上-shared
创建共享库是重要的,因为那些辅助的文件也提供其他服务。我们将在
5.3节中讨论。

★5.3 扩展的GCC特性

GCC有许多扩展的特性。有些对ELF特别的有用。其中一个就是__attribute__。
使用__attribute__可以使一个函数放到__CTOR_LIST__或者__DTOR_LIST__里。
例如:

[alert7@redhat62 dl]# cat miss.c

#include <stdio.h>
#include <stdlib.h>

static void foo(void) __attribute__ ((constructor));
static void bar(void) __attribute__ ((destructor));


int main(int argc, char *argv[])
{
        printf("foo == %p\n", foo);
        printf("bar == %p\n", bar);

        exit(EXIT_SUCCESS);
}

void foo(void)
{
        printf("hi dear njlily!\n");
}

void bar(void)
{
        printf("missing u! goodbye!\n");
}

[alert7@redhat62 dl]# gcc -o miss miss.c
[alert7@redhat62 dl]# ./miss
hi dear njlily!
foo == 0x8048434
bar == 0x8048448
missing u! goodbye!

我们来看看是否加到了.ctors和.dtors中。
[alert7@redhat62 dl]# objdump -s -j .ctors miss

miss:     file format elf32-i386

Contents of section .ctors:
8049504 ffffffff 34840408 00000000           ....4.......

[alert7@redhat62 dl]# objdump -s -j .dtors miss

miss:     file format elf32-i386

Contents of section .dtors:
8049510 ffffffff 48840408 00000000           ....H.......

已经把foo和bar地址分别放到了.ctors和.dors,显示34840408只是因为
x86上是LSB编码的,小端序。

__attribute__ ((constructor))促使函数foo在进入main之前会被自动调用。
__attribute__ ((destructor))促使函数bar在main返回或者exit调用之后
会被自动调用。foo和bar必须是不能带参数的而且必须是static void类型的
函数。在ELF下,这个特性在一般的可执行文件和共享库中都能很好的工作。


我们也可以创建自己的section,在这里我创建了一个alert7 section.
[alert7@redhat62 dl]# cat test.c
#include <stdio.h>
#include <stdlib.h>

static void foo(void) __attribute__ ((section ("alert7")));
static void bar(void) __attribute__ ((section ("alert7")));


int main(int argc, char *argv[])
{
        foo();

        printf("foo == %p\n", foo);
        printf("bar == %p\n", bar);
        bar();
        exit(EXIT_SUCCESS);
}

void foo(void)
{
        printf("hi dear njlily!\n");
}
void bar(void)
{
        printf("missing u! goodbye!\n");
}
[alert7@redhat62 dl]# gcc -o test test.c
[alert7@redhat62 dl]# ./test
hi dear njlily!
foo == 0x804847c
bar == 0x8048490
missing u! goodbye!

[alert7@redhat62 dl]# objdump -x test
....
Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .interp       00000013  080480f4  080480f4  000000f4  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
...
12 alert7        00000026  0804847c  0804847c  0000047c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
...

[alert7@redhat62 dl]# objdump -D test
Disassembly of section alert7:

0804847c <foo>:
804847c:       55                      push   %ebp
804847d:       89 e5                   mov    %esp,%ebp
804847f:       68 de 84 04 08          push   $0x80484de
8048484:       e8 a3 fe ff ff          call   804832c <_init+0x70>
8048489:       83 c4 04                add    $0x4,%esp
804848c:       c9                      leave
804848d:       c3                      ret
804848e:       89 f6                   mov    %esi,%esi

08048490 <bar>:
8048490:       55                      push   %ebp
8048491:       89 e5                   mov    %esp,%ebp
8048493:       68 ef 84 04 08          push   $0x80484ef
8048498:       e8 8f fe ff ff          call   804832c <_init+0x70>
804849d:       83 c4 04                add    $0x4,%esp
80484a0:       c9                      leave
80484a1:       c3                      ret

在这里,我创建了一个自己的alert7 section,并把foo,bar两个函数放到了这个
section中。一般定义的函数都会放在.text section中。



★5.3.1 在C库中的初始化函数

另外一个GCC的特性是__attribute__( section ("sectionname") ).使用这个,
能把一个函数或者是数据结构放到任何的section中。

static void
foo (int argc,char **argc,char **envp)
    __attribute__ ((section ("_libc_foo")));

static void
foo (int argc,char **argv,char **envp)
{
}

static void
bar (int argc,char **argv,char **envp)
{
}

static void * __libc_subinit_bar__
    __attribute__ (( section ("_libc_subinit")))=&(bar);

这里,我们把foo放到了_libc_foo section,把__libc_subinit_bar__放
到了_libc_subinit section中。在Linux C库中,_libc_subinit 是一个特别
的section,它包含了一个函数指针(有如下原型)的数组。

void (*) (int argc,char **argv,char **envp);

这里的argc,argv,envp跟在main中的有同样的意义。该section中的函数在进入
main函数之前就会被调用。这是很有用的,可用来在Linux C库中初始化一些
全局变量。

    [译注:_libc_subinit section真有这个特别的功能吗?我是没有试
    成功,如果有人试成功或者认为我理解有误的地方,千万记得mail给
    我:)
    测试程序如下:
    #include <stdio.h>
    #include <stdlib.h>
    static void foo(int argc,char **argv,char **envp)
    {
        printf("hi dear njlily!\n");
    }
    
    int main(int argc, char *argv[])
    {
        printf("foo == %p\n", foo);
        exit(EXIT_SUCCESS);
    }
    
    static void * __libc_subinit_bar__
            __attribute__ (( section ("_libc_subinit")))=&(foo);
    
    [alert7@redhat62 dl]# gcc -o test1 test1.c
    [alert7@redhat62 dl]# ./test1
    foo == 0x8048400
    :( 用objdump,显示已经创建了一个_libc_subinit section,并且
    该section前四个字节就是foo地址0x8048400
    
    ]



★5.4 利用GCC和GNU ld

这一些命令行的选项对GCC和GNU ld创建ELF时特别有用。-shared告诉gcc
产生一个共享库,该共享库能在连接时和其他的共享文件一起形成可执行
文件,该共享库也能在运行时装载进可执行文件的地址空间。使用-shared
是创建一个共享ELF库的首选方法。

另外一个有用的命令行选项是-Wl,ldoption,传递参数ldoption作为连接器
的选项。假如ldoption包含多个逗号,将分离成多个选项。

-static选项将产生一个和static库一道连接的可执行文件。当没有开启
-static选项时,连接器首先试着用共享库,假如共享版本不可用,然后
再试着用静态(static)库。

这里还有些特别的命令行选项对ELF来说特别的或者说是有用的。

-dynamic-linker file
    设置动态连接器(dynamic linker)的名字。默认的动态连接器
    或者是/usr/lib/libc.so.1或者是/usr/lib/libd1.so.1
    

-export-dynamic
    告诉连接器使在可执行文件中的所有标号对动态连接器可用。当一个
    动态装载进的共享库参考可执行文件中的标号,该标号一般在动态连
    接时是不可用时,这时候就特别有用。

-lfile
    加文件到需要连接的列表中。该选项可用在许多时候。ld将搜索它的
    path-list查找文件libfile.so(也就是说假如库为libbar.so,那么
    使用的时候就这样使用,-lbar),或者libfile.a(static版本的)。
    一些情况下,共享库名libfile.so会被存储在resulting executable
    或者是共享库中。当resulting executable或者是共享库被装载进内
    存,动态连接器也将把使用记录过的共享库装载到进程的地址空间去。
    在以后的事情情况下,把必要的函数和数据被拷贝到可执行文件,减
    少代码长度。

-m emulation
    仿效emulation连接器r.-V参数可列出所有可用的选项.

-M | -Map mapfile
    把连接map输出到标准输出或者一个mapfile文件里,该连接map含有
    关于标号被ld映象到了哪里的一些诊断信息,还有全局共同的存储
    分配信息。

-rpath directory
    加一个目录到运行时library的搜索路径。所有的-rpath参数被连接
    在一起然后传给动态连接器。它们被用来在运行时定位共享库。

-soname name
    当创建一个共享库时,指定的名字被放在共享库中。当和这个共享
    库连接的可执行文件被运行,动态连接器将试着map记录着的指定
    名字的共享库而不是传给连接器的文件名。

-static
    告诉连接器不要和任何共享库连接。

-verbose
    告诉连接器打印它每个要打开的文件名。

linux下gcc beta版本使用-dynamic-linker file选项设置动态连接器为
/lib/ld-linker.so.1。该选项可以使ELF和a.out共享库很好的共存。

有件事情是另人感兴趣的。

[1] [2] [3]

编辑 webmaster

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