来源:http://mypyg.iteye.com/blog/845915
so其实就是shared object的意思。今天看了上面的博客,感觉好吃力。赶紧做个笔记记录一下。下面的内容大多都是连接中的,穿插我自己的笔记
牵扯到ELF格式,gcc编译选项待补,简单实用的说明一下,对Linux下的so文件有个实际性的认识。
1.so文件是什么?
2.怎么生成以及使用一个so动态库文件?
3.地址空间,以及线程安全.
4.库的初始化,解析:
5.使用我们自己库里的函数替换系统函数:
1.so文件是什么?
也是ELF格式文件,共享库(动态库),类似于DLL。节约资源,加快速度,代码升级简化。
知道这么多就够了,实用主义。等有了印象再研究原理。
2.怎么生成以及使用一个so动态库文件?
先写一个C文件:s.c
#includeint count; void out_msg(const char *m) {//2秒钟输出1次信息,并计数 for(;;) {printf("%s %d\n", m, ++count); sleep(2);} }
编译:得到输出文件libs.o
gcc -fPIC -g -c s.c -o libs.o
----------------------------------------------------------------------
-fPIC: -fPIC作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意 位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
-g: 令 gcc 生成调试信息,该选项可以利用操作系统的“原生格式(native format)”生成调试信息。GDB 可以直接利用这个信息,其它调试器也可以使用这个调试信息
-c: 仅执行编译操作,不进行连接操作。
-o: 指定生成的输出文件名称
注意!-c,-o不是指.c文件和.o文件!!
----------------------------------------------------------------------
链接:得到输出文件libs.so
gcc -g -shared -Wl,-soname,libs.so -o libs.so libs.o -lc
-----------------------------------------------------------------------
上述语句中 libs.o是输入文件
-shared:
Produce a shared object which can then be linked with other objects to form an executable. Not all systems support this option. For predictable results, you must also specify the same set of options used for compilation (-fpic, -fPIC, or model suboptions) when you specify this linker option.
-Wl: 注意第二个字母是小写的L,不是I
Pass option as an option to the linker. If option contains commas, it is split into multiple options at the commas. You can use this syntax to pass an argument to the option. For example, -Wl,-Map,output.map passes -Map output.map to the linker. When using the GNU linker, you can also get the same effect with -Wl,-Map=output.map.
-soname:
soname的关键功能是它提供了兼容性的标准:
当要升级系统中的一个库时,并且新库的soname和老库的soname一样,用旧库链接生成的程序使用新库依然能正常运行。这个特性使得在Linux下,升级使得共享库的程序和定位错误变得十分容易。
在Linux中,应用程序通过使用soname,来指定所希望库的版本,库作者可以通过保留或改变soname来声明,哪些版本是兼容的,这使得程序员摆脱了共享库版本冲突问题的困扰。
-lc:
-l 是直接加上某库的名称,如-lc是libc库 -L 是库的路径,搜索的时候优先在-L目录下搜索
------------------------------------------------------------------------
一个头文件:s.h
#ifndef _MY_SO_HEADER_ #define _MY_SO_HEADER_ void out_msg(const char *m); #endif
再来一个C文件来引用这个库中的函数:ts.c
#include#include "s.h" int main(int argc, char** argv) { printf("TS Main\n"); out_msg("TS "); sleep(5); //这句话可以注释掉,在第4节的时候打开就可以。 printf("TS Quit\n"); }
编译链接这个文件:得到输出文件ts
gcc -g ts.c -o ts -L. -ls
执行./ts,嗯:成功了。。。还差点
得到了ts:error while loading shared libraries: libs.so: cannot open shared object file: No such file or directory
系统不能找到我们自己定义的libs.so,那么告诉他,修改变量LD_LIBRARY_PATH,为了方便,写个脚本:e(文件名就叫e,懒得弄长了)
export LD_LIBRARY_PATH=${pwd}:${LD_LIBRARY_PATH} ./ts
执行:./e &
屏幕上就开始不停有信息输出了,当然TS Quit你是看不到的,前面是个死循环,后面会用到这句
----------------------
& 放在启动参数后面表示设置此进程为后台进程。默认情况下,进程是前台进程,这时就把Shell给占据了,我们无法进行其他操作,对于那些没有交互的进程,很多时候,我们希望将其在后台启动,可以在启动参数的时候加一个'&'实现这个目的。
----------------------
3.地址空间,以及线程安全:
如果这样:
./e &开始执行后,稍微等待一下然后再 ./e&,
这个时候屏幕信息会怎么样呢?全局变量count会怎么变化?
会是两个进程交叉输出信息,并且各自的count互不干扰,虽然他们引用了同一个so文件。
也就是说只有代码是否线程安全一说,没有代码是否是进程安全这一说法。
下面的还没细看,汗
4.库的初始化,解析:
windows下的动态库加载,卸载都会有初始化函数以及卸载函数来完成库的初始化以及资源回收,linux当然也可以实现。
ELF文件本身执行时就会执行一个_init()函数以及_fini()函数来完成这个,我们只要把自己的函数能让系统在这个时候执行
就可以了。
修改我们前面的s.c文件:
重新制作 libs.so,ts本是不用重新编译了,代码维护升级方便很多。
然后执行: ./e &
可以看到屏幕输出:(不完整信息,只是顺序一样)
Init
Main
OK
Quit
Fini
可以看到我们自己定义的初始化函数以及解析函数都被执行了,而且是在最前面以及最后面。
如果s.c中的sleep(5)没有注释掉,那么有机会:
./e&
./e&连续执行两次,那么初始化函数和解析函数也会执行两次,虽然系统只加载了一次libs.so。
如果sleep时候kill 掉后台进程,那么解析函数不会被执行。
5.使用我们自己库里的函数替换系统函数:
创建一个新的文件b.c:我们要替换系统函数malloc以及free(可以自己写个内存泄露检测工具了)
#includevoid* malloc(int size) { printf("My malloc\n"); return NULL; } void free(void* ad) { printf("My free\n"); }
老规矩,编译链接成一个so文件:得到libb.so
gcc -fPIC -g -c b.c -o libb.o
gcc -g -shared -Wl,-soname,libb.so -o libb.so -lc
修改s.c:重新生成libs.so
void out_msg() { int *p; p = (int*)malloc(100); free(p); printf("Stop Ok!\n"); }
修改脚本文件e:
export LD_PRELOAD=${pwd}libb.so:${LD_PRELOAD} export LD_LIBRARY_PATH=${pwd}:${LD_LIBRARY_PATH} ./ts
关键就在LD_PRELOAD上了,这个路径指定的so将在所有的so之前加载,并且符号会覆盖后面加载的so文件中的符号。如果可执行文件的权限不合适(SID),这个变量会被忽略。
执行:./e &
嗯,可以看到我们的malloc,free工作了。
2017年07月10日 18:38:43 eydwyz 阅读数:3064
1:创建test.h, test.c文件
[cpp] view plain copy
[cpp] view plain copy
2:将其编译成动态库[cpp] view plain copy
3:创建主文件main.c
[cpp] view plain copy
4:编译,运行
[cpp] view plain copy
完成了,呵呵
/
dlopen()是一个强大的库函数。该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。比如 Apache Web 服务器利用这个函数在运行过程中加载模块,这为它提供了额外的能力。一个配置文件控制了加载模块的过程。这种机制使得在系统中添加或者删除一个模块时,都不需要重新编译 可以在自己的程序中使用 dlopen()。dlopen() 在 dlfcn.h 中定义,并在 dl 库中实现。它需要两个参数:一个文件名和一个标志。文件名可以是我们学习过的库中的 soname。标志指明是否立刻计算库的依赖性。如果设置为 RTLD_NOW 的话,则立刻计算;如果设置的是 RTLD_LAZY,则在需要的时候才计算。另外,可以指定 RTLD_GLOBAL,它使得那些在以后才加载的库可以获得其中的符号。
当库被装入后,可以把 dlopen() 返回的句柄作为给 dlsym() 的第一个参数,以获得符号在库中的地址。使用这个地址,就可以获得库中特定函数的指针,并且调用装载库中的相应函数。
dlsym()的函数原型是void* dlsym(void* handle,const char* symbol)该函数在
dlclose(void *handle))用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
//
dlopen()
dlopen函数打开一个函数库然后为后面的使用做准备。C语言原形是:void * dlopen(const char *filename, int flag);
如果文件名filename是以“/”开头,也就是使用绝对路径,那么dlopne就直接使用它,而不去查找某些环境变量或者系统设置的
函数库所在的目录了。否则dlopen()
就会按照下面的次序查找函数库文件:
1. 环境变量LD_LIBRARY指明的路径。 2. /etc/ld.so.cache中的函数库列表。 3. /lib目录,然后/usr/lib。不过一些很老的
a.out的loader则是采用相反的次序,也就是先查/usr/lib,然后是/lib。dlopen()函数中,参数flag的值必须是RTLD_LAZY或者RTLD_NOW,RTLD_LAZY的意思是resolve undefined symbols as code from the dynamic library is executed,而RTLD_NOW的含义是resolve all undefined symbols before dlopen() returns and fail if this cannot be done'。如果有好几个函数库,它们之间有一些依赖关系的话,例如X依赖Y,那么你就要先加载那些被依赖的函数。例如先加载Y,然后加载X。dlopen()函数的返回值是一个句柄,然后后面的函数就通过使用这个句柄来做进一步的操作。如果打开失败dlopen()就返回一个NULL。如果一个函数库被多次打开,它会返回同样的句柄。如果一个函数库里面有一个输出的函数名字为_init,那么_init就会在dlopen()这个函数返回前被执行。我们可以利用这个函数在我的函数库里面做一些初始化的工作。我们后面会继续讨论这个问题的。
dlerror() :通过调用dlerror()函数,我们可以获得最后一次调用dlopen(),dlsym(),或者dlclose()的错误信息。
/*************************************************************************************************************************************************************************************/
Linux提供了一套API来动态装载库。下面列出了这些API:
- dlopen,打开一个库,并为使用该库做些准备。
- dlsym,在打开的库中查找符号的值。
- dlclose,关闭库。
- dlerror,返回一个描述最后一次调用dlopen、dlsym,或dlclose的错误信息的字符串。
C语言用户需要包含头文件dlfcn.h才能使用上述API。glibc还增加了两个POSIX标准中没有的API:
- dladdr,从函数指针解析符号名称和所在的文件。
- dlvsym,与dlsym类似,只是多了一个版本字符串参数。
在Linux上,使用动态链接的应用程序需要和库libdl.so一起链接,也就是使用选项-ldl。但是,编译时不需要和动态装载的库一起链接。程序3-1是一个在Linux上使用dl*例程的简单示例。
延迟重定位(Lazy Relocation)
延迟重定位/装载是一个允许符号只在需要时才重定位的特性。这常在各UNIX系统上解析函数调用时用到。当一个和共享库一起链接的应用程序几乎不会用到该共享库中的函数时,该特性被证明是非常有用的。这种情况下,只有库中的函数被应用程序调用时,共享库才会被装载,否则不会装载,因此会节约一些系统资源。但是如果把环境变量LD_BIND_NOW设置成一个非空值,所有的重定位操作都会在程序启动时进行。也可以在链接器命令行通过使用-z now链接器选项使延迟绑定对某个特定的共享库失效。需要注意的是,除非重新链接该共享库,否则对该共享库的这种设置会一直有效。
初始化(initializing)和终止化(finalizing)函数
有时候,以前的代码可能用到了两个特殊的函数:_init和_fini。_init和_fini函数用在装载和卸载某个模块(注释14)时分别控制该模块的构造器和析构器(或构造函数和析构函数)。他们的C语言原型如下:
void _init(void);
void _fini(void);
当一个库通过dlopen()动态打开或以共享库的形式打开时,如果_init在该库中存在且被输出出来,则_init函数会被调用。如果一个库通过dlclose()动态关闭或因为没有应用程序引用其符号而被卸载时,_fini函数会在库卸载前被调用。当使用你自己的_init和_fini函数时,需要注意不要与系统启动文件一起链接。可以使用GCC选项 -nostartfiles 做到这一点。
但是,使用上面的函数或GCC的-nostartfiles选项并不是很好的习惯,因为这可能会产生一些意外的结果。相反,库应该使用__attribute__((constructor))和__attribute__((destructor))函数属性来输出它的构造函数和析构函数。如下所示:
void __attribute__((constructor)) x_init(void)
void __attribute__((destructor)) x_fini(void)
构造函数会在dlopen()返回前或库被装载时调用。析构函数会在这样几种情况下被调用:dlclose()返回前,或main()返回后,或装载库过程中exit()被调用时。
我们通过一个例子来讲解dlopen系列函数的使用和操作:
主程序:
动态库:
主程序编译: gcc test.c -ldl -rdynamic
动态库编译: gcc -shared -fPIC -nostartfiles -o mylib.so mylib.c
主程序通过dlopen()加载一个.so的动态库文件, 然后动态库会自动运行 _init() 初始化函数, 初始化函数打印一个提示信息, 然后调用主程序的注册函数给结构体重新赋值, 然后调用结构体的函数指针, 打印该结构体的值. 这样就充分的达到了主程序和动态库的函数相互调用和指针的相互传递.
gcc参数 -rdynamic 用来通知链接器将所有符号添加到动态符号表中(目的是能够通过使用 dlopen 来实现向后跟踪).
gcc参数 -fPIC 作用: 当使用.so等类的库时,当遇到多个可执行文件共用这一个库时, 在内存中,这个库就不会被复制多份,让每个可执行文件一对一的使用,而是让多个可执行文件指向一个库文件,达到共用. 宗旨:节省了内存空间,提高了空间利用率.
《嵌入式Linux内存使用与性能优化》主要讲述嵌入式系统开发中的两个难点:系统的内存使用与系统性能优化。第6章讲述进程启动速度。本节说的是动态库的初始化。
作者:史子旺 叶超群 蔡建宇来源:机械工业出版社|2009-07-10 11:09
收藏
分享
6.5 动态库的初始化
在loader完成了对动态库的内存映射之后,需要运行动态库的一些初始化函数,来完成设置动态库的一些基本环境。这些初始化函数主要包含两个部分:
(1)动态库的构造和析构函数机制。
(2)动态库的全局变量初始化工作。
1.动态库的构造和析构函数机制
在Linux中,提供了一个机制:在加载和卸载动态库时,可以编写一些函数,处理一些相应的事物,我们称这些函数为动态库的构造和析构函数,其代码格式如下:
在编译共享库时,不能使用"-nonstartfiles"或"-nostdlib"选项,否则,构建与析构函数将不能正常执行(除非你采取一定措施)。
注意,构造函数的参数必须为空,返回值也必须为空。
举个例子,动态库文件a、c的代码如下:
编译成动态库:
主程序hello.c如下:
编译:
也就是说,在运行hello时,加载完liba.so后,自动运行liba.so的初始化函数。
===========================================================================================================================================================================================================================================================================
《嵌入式Linux内存使用与性能优化》主要讲述嵌入式系统开发中的两个难点:系统的内存使用与系统性能优化。第6章讲述进程启动速度。本节说的是动态库的初始化。
作者:史子旺 叶超群 蔡建宇来源:机械工业出版社|2009-07-10 11:09
收藏
分享
2.全局变量初始化
在介绍全局变量初始化之前,先举个例子:
g1是个全局变量。
使用GCC对其进行编译:
使用G++对其进行编译:
编译成功!
可见GCC和G++对于这种全局变量初始化的方法,支持力度是不一样的。
运行hello:
这说明,进程在加载liba.so后,为了初始化全局变量g1,其会运行reti来初始化g1。
再来看一个C++的例子:
编译动态库:
在动态库liba.so中,声明了一个类型为Myclass的全局变量g1。
主程序
编译执行文件:
这说明,进程在加载liba.so后,为了初始化全局变量g1,将会运行Myclass的构造函数。这些构造函数的运行,必然会导致加载动态库时间缓慢。如果构造函数修改了成员变量的话,其还会产生dirty page,有时候你会发现程序刚进入main函数,就已经产生了大量的dirty page。
为什么非内置类型的全局变量,需要在main函数之前就构造出来呢?
============================================================================================================================================================================================================================================================================
《嵌入式Linux内存使用与性能优化》主要讲述嵌入式系统开发中的两个难点:系统的内存使用与系统性能优化。第6章讲述进程启动速度。本节说的是动态库的初始化。
作者:史子旺 叶超群 蔡建宇来源:机械工业出版社|2009-07-10 11:09
收藏
分享
在C语言中,其全局变量保存在.data段。在启动过程中,loader只是简单地使用mmap将数据段映射到内存中,这些全局变量只有在第一次使用到的时候才会为其分配物理内存,其在启动过程中是不需要运行什么构造函数的。
而在C++语言中,对于非内置类型的全局变量,一方面需要在main函数之前就准备好,需要的时候马上就可以使用;另一方面,全局对象内部的成员变量,不能像C语言那样一个简单的内存映射就可以了,故系统在bss节为全局对象分配了内存,运行构造函数,来初始化其值。这也就决定了,对于非内置类型的全局对象,系统要在main函数之前,运行其构造函数,完成全局变量的初始化。
总的来讲,对于非内置类型的全局变量,无论在进程启动时是否会用到该全局变量,都需要运行其构造函数创建该全局变量,其对进程启动有如下影响:
(1)由于运行了一些不必要的构造函数,减缓了进程的启动速度。
(2)构造函数修改了类的成员变量,这时一方面会产生page fault,从而减缓进程的启动速度;另一方面,也会产生一些不必要的dirty page,造成内存上的浪费。
在系统优化过程中,笔者曾经在一个进程的main函数中第一条指令前增加了一个pause语句,很惊奇地发现,在进程还没有做任何事情的时候,仅仅是加载动态库,进程和动态库的数据段就使用了800KB的物理内存,就是这个原因。
从优化的角度来讲,要尽量减少全局对象的使用。
对于一个给定的动态库(或进程),首要的问题是如何来查看其包含了哪些全局对象,以便程序员对其进行优化。可以通过查看动态库(或进程)的符号表,来查找全局对象。请看以下代码:
在上面的例子中,全局对象obj才会导致在main函数之前,运行其构造函数。
pobj只是一个对象的指针,并不需要运行构造函数。
在函数func内部的sobj,在第一次进入func时,才会运行构造函数。
main函数中的mobj,将在程序运行到此时,才会运行构造函数。
通过符号表来定位全局对象:
mobj不是全局变量,也不是静态变量,故其不在符号表中。
sobj在函数func中,故G++在编译过程中,会将其改名,在对象的名称前面加上函数名。并且可以看到有两个符号ZGVZ4funcvE4sobj、ZZ4funcvE4sobj,都包含sobj,其中第一个符号是用来标识sobj静态对象是否被创建;ZZ4funcvE4sobj则指向静态对象sobj。详情可以参见8.4.5节。
obj 是全局对象,并且其大小为8。
pobj是指向全局的对象指针,其大小为4。
故要想查找全局对象,可以遵从下面的原则:
在bss节,类型为OBJECT,不包含函数名,对象大小>4,基本可以认为是全局对象。
在bss节,类型为OBJECT,不包含函数名,对象大小=4,有可能是全局对象,需要你到代码中去搜索、确认。