gcc 动态链接库某些细节

gcc 动态链接库某些细节

一、生成
生成网上文章很多,这里大体写下。
有三个文件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 进程号,上述两个命令可见加载到了共享内存区,单从这点看和动态链接比较类似。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值