代码改变世界

C语言动态链接库和静态链接库原理探究

2022-05-13 09:46  幻霞  阅读(458)  评论(1编辑  收藏  举报

前言:用惯了IDE(集成开发环境),或者说一开始就用的IDE,出了什么问题大脑一片空白,只能去网上查找资料,尤其是库的链接方面的知识,IDE只是简简单单的描述:包含对应库的目录,导入包,但这一句话背后IDE究竟做了什么,越早理解这个底层的东西以后就能省越多的时间,同时心里也能更踏实些。

devc++是一款经典的c/c++IDE,它采用的是MINGW的GCC编译器,注意这个编译器是linux上移植到windows中的编译器,这一点对于理清整个过程很重要.

后缀名称区别:

linux中静态库后缀为.a,动态库后缀为.so,

windows中静态库后缀为.lib,动态库后缀为.dll

 

一.静态库

这里先去介绍在windows环境下用gcc来编译生成.a 和 .so文件

 

我们先在Dev-Cpp\MinGW64\bin文件夹中找到gcc,把它的路径拷贝下来

然后到F:盘(示例盘) 创建一个空文件夹a_so中(实例名称),然后再在这个文件夹中创建一个.c文件,全名a.c。(F:/a_so/a.c)

文件内容我们这样写,

int fun(int in){
    return in; 
}

接着打开cmd,先写对应盘符,然后跳到gcc的所在目录,命令行如下:

D:
cd D:.../省略一些中间目录/bin/ 

打上如下命令(即将c文件生成链接a.o)

gcc -c F:\a_so\a.c -o F:\a_so\a.o

这样你就得到了一个.o文件,接下来再输入一条命令

ar -v -q F:\a_so\libstatic_lib.a F:\a_so\a.o

-v是显示打包的细节:给你一个被打包的列表,-q是强制覆盖之前打过的包.

这样你就把一个.o文件打包为静态库文件libstatic_lib.a了(静态库文件要遵守前面有lib的规则),接着我们试着调用,在a.c同级目录下创建test.c,里面这样写

#include<stdio.h>
#include"a.h"
int main(){
   int a=fun(6);
   printf("%d",a); 
  getchar();
return 0; }

这里有一个a.h,其实是因为我们的程序找不到fun函数,所以我们写个头文件让它找到,头文件里面的内容是:

int fun(int in);

这时候如果我们在命令行中执行这一个指令

gcc F:\a_so\test.c -o F:\a_so\test.exe -L"F:\a_so" -lstatic_lib -static-libgcc
-l是指定库,按照规则,静态库都是libname.a的格式,可以简写为-lname,当然你也可以把-L"" -lname直接换成F:\a_so\libstatic_lib.a,-L是指定库目录,-static-libgcc是强制导入静态库,

然后在a_so的目录你就会看到test.exe文件,执行后你就会得到这个结果

 二.动态库

 接下来是动态库

动态库创建:

在a_so文件下创建so.c文件,里面这样写:

int add(int a,int b){
    return a+b;    
}  
int multiplication(int a,int b){
    return a*b;
} 

然后再命令行中:

gcc F:\a_so\so.c -o F:\a_so\libDynamic_lib.so --shared

这样就得到一个.so文件,接着我们先动态调用它

创建d_d_use.c,文件中这样写:

#include<stdio.h>
#include<windows.h>
int main() {
    typedef int (*pfunc)(int x,int y);//定义函数指针 
    HINSTANCE dllHandle =LoadLibrary(TEXT("libDynamic_lib.so"));//句柄实例,载入库,如果是dll就改so为dll 
    pfunc pf1 = (pfunc)GetProcAddress(dllHandle,TEXT("add"));//声明函数指针并赋值 
    pfunc pf2 = (pfunc)GetProcAddress(dllHandle,TEXT("multiplication"));
    if(pf1!=NULL) printf("%d\n",pf1(5,6));//调用 
    if(pf2!=NULL) printf("%d\n",pf2(5,6));
    FreeLibrary(dllHandle);//释放 
    return 0; 
} 

 

然后直接在dev中编译运行,我们会得到如下结果:

 

 正好符合预期(低版本windows库可能不支持so文件的加载),其实把动态链接库输出换成.dll也是同样的结果,这里就不重复演示了

 

接下来是静态链接动态库,之所以之后才说这个是因为为了简化,动态库创建的时候我没有去导出函数

接下来创建d_s_use.c,文件中这样写:

#include<stdio.h>
__declspec(dllimport) int add(int a,int b);
__declspec(dllimport) int multiplication(int a,int b);
int main() {
    printf("%d\n",add(5,6));
    printf("%d\n",multiplication(5,6));
  getchar();
return 0; }
__declspec(dllimport)是导入函数的意思如果去掉就变成我们在c文件中声明函数了

之后,先不慌,静态导入还需要.lib/.a文件,我们用gcc命令行把创建.so文件的so.c利用一下,创建为.a文件

gcc F:\a_so\so.c -o F:\a_so\libDynamic_lib.a --shared

然后再把我们测试静态调用.so文件的c文件用命令行编译链接成.exe

gcc F:\a_so\d_s_use.c -o F:\a_so\d_s_use.exe F:\a_so\libDynamic_lib.a F:\a_so\libDynamic_lib.so

把需要的两个库文件链接起来,这两者缺一不可,最后我们得到.exe文件,执行一下得到:

看起来符合预期,但真的是这样吗?

其实我们调用的.a文件是so.c文件生成的,而这个文件已经实现了这两个函数,所以实际上我们不能确定到底调用的是,a里面的内容还是.so里面的内容

虽然如果你把.so文件去掉会显示缺失so文件,不过为了严谨起见,我们重新再来一遍

这个时候我们的a_so文件夹里面挺乱的,我们再在这下面创建一个newtmp文件夹来存放重写的文件

所以我们重写一下so.c作为n_so.c内容如下:

__declspec(dllexport) int add(int a,int b){
    return a+b;    
}  
__declspec(dllexport) int multiplication(int a,int b){
    return a*b;
} 
__declspec(dllexport)和__declspec(dllimport)类似,是用来导出的,import是导入

然后我们执行gcc命令:

gcc -shared -o F:\a_so\newtmp\n_so.so F:\a_so\newtmp\n_so.c -Wl,--out-implib,F:\a_so\newtmp\n_so_a.a

--out-implib是创建它的导入库lib文件,不可缺少,Wl其实是穿多个参数的指令

然后我们就得到了.so和导入库.a文件然后我们写个测试文件tst_so_s.c:
#include<stdio.h>
__declspec(dllimport) int add(int a,int b);
__declspec(dllimport) int multiplication(int a,int b);
int main() {
    printf("%d\n",add(5,6));
    printf("%d\n",multiplication(5,6));
    getchar(); 
    return 0; 
}
接着执行命令行
gcc F:\a_so\newtmp\tst_so_s.c -o F:\a_so\newtmp\tst_so_s.exe F:\a_so\newtmp\n_so_a.a F:\a_so\newtmp\n_so.so

上述命令行是把c文件处理输出为.exe,链接.a导入库和.so动态库,其实光链接.a或者.so都能通过,但不能都没有,说明.a文件内部链接了.so文件的内容

然后我们得到了一个exe文件,执行结果如下:

 

为了验证.a文件里面没有实现函数,我们把.so文件给去掉,再执行exe文件,你会得到这个结果:

 

 

 可以发现最终.a文件是调用了so文件中函数的实现,

做了一个镜像实验,把.a文件移除,发现还能运行,说明.a文件是给开发者使用的,用来得到函数集.

细心的朋友可能发现这里我没有用.h文件,这是为了简化最初的逻辑,其实原理是这样的,如果你写了.h和.c文件,编译器的第一步就是预编译,把两者结合作为.i文件输出,我们通常的一些诸如#define的预编译也是写在.c文件中的不是?