从程序员角度看ELF(2)
时间:2007-04-09 来源:Echo CHEN
★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=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 ([email protected]),lan Lance Taylor ([email protected])
还有许多为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节中讨论。
★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=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 ([email protected]),lan Lance Taylor ([email protected])
还有许多为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节中讨论。
相关阅读 更多 +