从程序员角度看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共享库很好的共存。
有件事情是另人感兴趣的。