gcc 动态链接库某些细节
一、生成
生成网上文章很多,这里大体写下。
有三个文件mytool.h、mytool.c、test.c,其中test.c:
mytool.c里面是mycpy的实现,就不贴了。
动态链接:
1.gcc -c mytool.c -fPIC -o mytool.o
2.gcc -shared -o libmytool.so mytool.o //两步也可以合起来 gcc mytool.c -fPIC -shared -o libmytool.so
3.gcc -o bin test.c -lmytool -L./ -Wl,-rpath=./ //-l后的mytool表示需要去-L后面的目录 ./当前目录去找libmytool.so,-Wl+-rpath表示要在执行bin时去当前目录./下找libmytool.so动态链接
执行:
./bin //输出haha
//修改mytool.c使得mycpy在返回前printf("in mycpy "),重新执行动态链接1\2步
./bin //输出in mycpy haha
可见动态链接的方式可以在不重新编译main函数文件的前提下更新mycpy函数。
二、-fPIC
在上面的步骤1中出现了-fPIC是位置无关代码(Position-Independent Code),这个选项是gcc编译so所必须的(man如是说),直观地看一下效果:
用mycase.c为例子,用到了被诟病的goto好展示点:
int equal(int a,int b){
if(a == b)
goto yes;
return 0;
yes:
return 1;
}
编译步骤:
gcc -c mycase.c -fPIC -S //生成mycase.s汇编代码
gcc -c mycase.c -fPIC //生成mycase.o编译后机器码
先看相关的汇编代码,vim mycase.s:
movl 8(%ebp), %eax //因为栈是向下生长的,栈底ebp+8实际上是上一个函数栈的栈顶-8,比如a函数调用b(int x,int y),会把xy的值压入栈顶,再备份程序计数器和当前栈底,之后调用b,b要取参数就得从自己的栈底还往上
cmpl 12(%ebp), %eax //同上,就是比较a和b的大小
jne .L2 //期望在这儿看到位置无关的信息
nop
.L3:
movl $1, %eax
jmp .L4
.L2:
movl $0, %eax
再看下机器码,objdump -d mycase.o
3: 8b 45 08 mov 0x8(%ebp),%eax
6: 3b 45 0c cmp 0xc(%ebp),%eax //比较a和b
9: 75 08 jne 13 <equal+0x13> //不同跳到equal+13,就是下面的mov $0x0,%eax
b: 90 nop
c: b8 01 00 00 00 mov $0x1,%eax
11: eb 05 jmp 18 <equal+0x18> //相同在$0x1,%eax后跳到equal+18,就是更新栈底要返回的代码
13: b8 00 00 00 00 mov $0x0,%eax
18: 5d pop %ebp
19: c3 ret
注意这儿的两个jmp和jne都有equal+,就是当前函数的首地址加上一个位移。所谓的位置无关,是和函数首地址无关。
静态链接对比下:
gcc main.c mycase.c
objdump -d a.out
相关的行:
0804840c <equal>:
........
8048412: 3b 45 0c cmp 0xc(%ebp),%eax
8048415: 75 08 jne 804841f <equal+0x13> //注意jne地址写死804841f
8048417: 90 nop
8048418: b8 01 00 00 00 mov $0x1,%eax
804841d: eb 05 jmp 8048424 <equal+0x18> //jmp地址写死8048424
804841f: b8 00 00 00 00 mov $0x0,%eax
8048424: 5d pop %ebp
因为equal的地址在静态链接时已经固定,所以jmp可以在链接时完成计算,运行时也就无需再对equal地址做加法偏移。
三、动态链接直观体现
同样的二中的例子,main.c我们写成:
#include "mycase.h"
#include <unistd.h>
int main(){
equal(1,2);
sleep(100); //留点时间好观察
}
按照gcc mycase.c -fPIC -shared -o libmycase.so;gcc -o bin main.c -lmycase -L./ -Wl,-rpath=./编译链接完成后。
./bin
ps aux|grep './bin'
pmap 进程号
如此两次得到的结果:
b7720000 4K r-x-- libmycase.so
b7721000 4K r---- libmycase.so
b7722000 4K rw--- libmycase.so
和
b76e5000 4K r-x-- libmycase.so
b76e6000 4K r---- libmycase.so
b76e7000 4K rw--- libmycase.so
b76e8000 8K rw--- [ anon ]
b76ea000 4K r-x-- [ anon ]
b76eb000 128K r-x-- ld-2.19.so
b770b000 4K r---- ld-2.19.so
b770c000 4K rw--- ld-2.19.so
bfcc6000 132K rw--- [ stack ]
两次的线性地址位置都不同,所以equal的地址是不可能确定的,equal+0x13计算jne位置必须在运行时计算。
还有一点,用lsof -p 进程号可以得到
bin 9069 liu3 mem REG 8,7 6788 1585856 /home/liu3/blogs/interview/mytool/mycase/libmycase.so
mem就是传说中的共享内存。看分布共享内存是和栈一样从上往下开辟空间的,这点和堆不一样。
四、插曲:-fPIC是否必要?
在我的gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)上加不加都一样。编译出来的libmycase.so的md5sum值一样。
看了http://www.linuxidc.com/Linux/2011-06/37268.htm,中心内容大概是动态链接有可重定位代码和位置无关代码两种,可重定向和静态链接没有多大区别。都要写死代码位置,只是一个链接一次,一个每次执行前都要链接从而保证对so的实时更新感知,可重定向是没法真正共享的。看来我的机器没有选择,都是位置无关的用法(因为用的是共享内存)。
五、“动态加载”?dlopen
还有一种dlopen的玩法,如下:
编译过程:
gcc main.c -ldl
./a.out
ps aux|grep out //得到进程号
kill -SIGUSR1 进程号
输出:
in
1 and 2 are not equal
out
修改libmycase源码,在a和b不同时输出1,重新编译gcc mycase.c -fPIC -shared -o libmycase.so
再次kill -SIGUSR1 进程号,输出
in
1 and 2 are equal //可见新的so已生效
out
这是一个简单的“热加载”例子。
六、五的一些细节
dlopen的原理我没弄多明白,下面举几个现象,可以有点直观的认识:`
把五中 dlclose(dp);代码去掉,得到。
./a.out,这时用pmap、lsof去看都没有libmycase.so的信息;
kill -SIGUSR1 进程号,上述两个命令可见加载到了共享内存区,单从这点看和动态链接比较类似。
一、生成
生成网上文章很多,这里大体写下。
有三个文件mytool.h、mytool.c、test.c,其中test.c:
#include <stdio.h>
#include <stdlib.h>
#include "mytool.h"
int main(){
const char *src = "hahaha nihao";
char *dst = (char *)malloc(1000);
mycpy(src,dst,5);
printf("%s\n",dst);
}
mytool.c里面是mycpy的实现,就不贴了。
动态链接:
1.gcc -c mytool.c -fPIC -o mytool.o
2.gcc -shared -o libmytool.so mytool.o //两步也可以合起来 gcc mytool.c -fPIC -shared -o libmytool.so
3.gcc -o bin test.c -lmytool -L./ -Wl,-rpath=./ //-l后的mytool表示需要去-L后面的目录 ./当前目录去找libmytool.so,-Wl+-rpath表示要在执行bin时去当前目录./下找libmytool.so动态链接
执行:
./bin //输出haha
//修改mytool.c使得mycpy在返回前printf("in mycpy "),重新执行动态链接1\2步
./bin //输出in mycpy haha
可见动态链接的方式可以在不重新编译main函数文件的前提下更新mycpy函数。
二、-fPIC
在上面的步骤1中出现了-fPIC是位置无关代码(Position-Independent Code),这个选项是gcc编译so所必须的(man如是说),直观地看一下效果:
用mycase.c为例子,用到了被诟病的goto好展示点:
int equal(int a,int b){
if(a == b)
goto yes;
return 0;
yes:
return 1;
}
编译步骤:
gcc -c mycase.c -fPIC -S //生成mycase.s汇编代码
gcc -c mycase.c -fPIC //生成mycase.o编译后机器码
先看相关的汇编代码,vim mycase.s:
movl 8(%ebp), %eax //因为栈是向下生长的,栈底ebp+8实际上是上一个函数栈的栈顶-8,比如a函数调用b(int x,int y),会把xy的值压入栈顶,再备份程序计数器和当前栈底,之后调用b,b要取参数就得从自己的栈底还往上
cmpl 12(%ebp), %eax //同上,就是比较a和b的大小
jne .L2 //期望在这儿看到位置无关的信息
nop
.L3:
movl $1, %eax
jmp .L4
.L2:
movl $0, %eax
再看下机器码,objdump -d mycase.o
3: 8b 45 08 mov 0x8(%ebp),%eax
6: 3b 45 0c cmp 0xc(%ebp),%eax //比较a和b
9: 75 08 jne 13 <equal+0x13> //不同跳到equal+13,就是下面的mov $0x0,%eax
b: 90 nop
c: b8 01 00 00 00 mov $0x1,%eax
11: eb 05 jmp 18 <equal+0x18> //相同在$0x1,%eax后跳到equal+18,就是更新栈底要返回的代码
13: b8 00 00 00 00 mov $0x0,%eax
18: 5d pop %ebp
19: c3 ret
注意这儿的两个jmp和jne都有equal+,就是当前函数的首地址加上一个位移。所谓的位置无关,是和函数首地址无关。
静态链接对比下:
gcc main.c mycase.c
objdump -d a.out
相关的行:
0804840c <equal>:
........
8048412: 3b 45 0c cmp 0xc(%ebp),%eax
8048415: 75 08 jne 804841f <equal+0x13> //注意jne地址写死804841f
8048417: 90 nop
8048418: b8 01 00 00 00 mov $0x1,%eax
804841d: eb 05 jmp 8048424 <equal+0x18> //jmp地址写死8048424
804841f: b8 00 00 00 00 mov $0x0,%eax
8048424: 5d pop %ebp
因为equal的地址在静态链接时已经固定,所以jmp可以在链接时完成计算,运行时也就无需再对equal地址做加法偏移。
三、动态链接直观体现
同样的二中的例子,main.c我们写成:
#include "mycase.h"
#include <unistd.h>
int main(){
equal(1,2);
sleep(100); //留点时间好观察
}
按照gcc mycase.c -fPIC -shared -o libmycase.so;gcc -o bin main.c -lmycase -L./ -Wl,-rpath=./编译链接完成后。
./bin
ps aux|grep './bin'
pmap 进程号
如此两次得到的结果:
b7720000 4K r-x-- libmycase.so
b7721000 4K r---- libmycase.so
b7722000 4K rw--- libmycase.so
和
b76e5000 4K r-x-- libmycase.so
b76e6000 4K r---- libmycase.so
b76e7000 4K rw--- libmycase.so
b76e8000 8K rw--- [ anon ]
b76ea000 4K r-x-- [ anon ]
b76eb000 128K r-x-- ld-2.19.so
b770b000 4K r---- ld-2.19.so
b770c000 4K rw--- ld-2.19.so
bfcc6000 132K rw--- [ stack ]
两次的线性地址位置都不同,所以equal的地址是不可能确定的,equal+0x13计算jne位置必须在运行时计算。
还有一点,用lsof -p 进程号可以得到
bin 9069 liu3 mem REG 8,7 6788 1585856 /home/liu3/blogs/interview/mytool/mycase/libmycase.so
mem就是传说中的共享内存。看分布共享内存是和栈一样从上往下开辟空间的,这点和堆不一样。
四、插曲:-fPIC是否必要?
在我的gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)上加不加都一样。编译出来的libmycase.so的md5sum值一样。
看了http://www.linuxidc.com/Linux/2011-06/37268.htm,中心内容大概是动态链接有可重定位代码和位置无关代码两种,可重定向和静态链接没有多大区别。都要写死代码位置,只是一个链接一次,一个每次执行前都要链接从而保证对so的实时更新感知,可重定向是没法真正共享的。看来我的机器没有选择,都是位置无关的用法(因为用的是共享内存)。
五、“动态加载”?dlopen
还有一种dlopen的玩法,如下:
//函数作为演示,异常没写全,signal也最好别用
#include <stdio.h>
#include <dlfcn.h>
#include <signal.h>
//头文件已经不包含任何的equal函数了
int (* efun)(int a,int b); //efun=equal fun,简单的比较int函数
void test(int a,int b){ //相等输出equal,否则输出not equal
if(efun == NULL)
return;
if(efun(a,b))
printf("%d and %d are equal\n",a,b);
else
printf("%d and %d are not equal\n",a,b);
}
void user1Handler(int num){ //注册一个SIGUSR1信号来体现“动态效果”
printf("in\n");
void *dp = dlopen("./libmycase.so",RTLD_LAZY); //打开
efun = dlsym(dp,"equal"); //加载equal函数,也就是去找equal符号
test(1,2);
dlclose(dp); //关闭,否则下一次进入不生效,原因不深究
printf("out\n");
}
int main(){
signal(SIGUSR1,user1Handler); //注册信号
while(1){
sleep(100); //一辈子sleep等信号
}
}
编译过程:
gcc main.c -ldl
./a.out
ps aux|grep out //得到进程号
kill -SIGUSR1 进程号
输出:
in
1 and 2 are not equal
out
修改libmycase源码,在a和b不同时输出1,重新编译gcc mycase.c -fPIC -shared -o libmycase.so
再次kill -SIGUSR1 进程号,输出
in
1 and 2 are equal //可见新的so已生效
out
这是一个简单的“热加载”例子。
六、五的一些细节
dlopen的原理我没弄多明白,下面举几个现象,可以有点直观的认识:`
把五中 dlclose(dp);代码去掉,得到。
./a.out,这时用pmap、lsof去看都没有libmycase.so的信息;
kill -SIGUSR1 进程号,上述两个命令可见加载到了共享内存区,单从这点看和动态链接比较类似。