动态库和静态库

 动静态库概念

 静态库的后缀是a,动态库的后缀是so。

简单设计一个静态库

首先,定义一个.h和一个.c作为头文件和实现。

头文件

#pragma once                                                                                                                                                                             
  2 #include<stdio.h>
  3 
  4 extern int myerror;
  5 
  6 int add(int a,int b);
  7 int sub(int a,int b);
  8 int mul(int a,int b);
  9 int div(int a,int b);

方法实现

#include "mymath.h"
  2 
  3 int myerrno = 0;
  4 
  5 int add(int x, int y)
  6 {
  7     return x + y;
  8 }
  9 int sub(int x, int y)
 10 {
 11     return x - y;
 12 }
 13 int mul(int x, int y)
 14 {
 15     return x * y;
 16 }
 17 int div(int x, int y)
 18 {
 19     if(y == 0){
 20         myerrno = 1;
 21         return -1;
 22     }
 23     return x / y;
 24 }      

测试

#include"mymath.h"
    2 
E>  3 extern myerrno;
    4 
    5 int main()                                                                                                                                                                             
    6 {
    7   int n=div(10,0);
    8   printf("n=%d,myerrno=%d\n",n,myerrno);
    9   return 0;
   10 }

 这里要注意:C语言的传参是从右到左的,如果我们直接在printf里面按照这个顺序调用了div函数,那么打印出来的myerrno值不会符合预期。

 不过我们要知道,我们把方法提供给别人用,有两种办法。

1:是直接把源文件给它,让它一起编译。

2:假如我们不想让别人知道方法的具体实现,我们可以打包成库=库+.h。

这里我们当然用的是第二种,怎样让别人不知道呢?那么其实就是我们先把我们的方法编译,最后在链接阶段,再链接在一起就可以了。

所以静态库就是一堆.o文件的组合,最后再与我们的main.o进行链接。

 其中我们要用到一个生成静态库的命令 ar。  其中 -rc表示替换和创建,也就是如果目标文件存在,就把.o文件对里面替换,如果不存在就创建再放进去。

makefile

lib=libmymath.a
  2 
  3 $(lib):mymath.o
  4   ar -rc $@ $^
  5 mymath.o:mymath.c
  6   gcc -c $^
  7 
  8 .PHONY:clean
  9 clean:
 10   rm -rf *.o *.a lib                                                                                                                                                                     
 11 .PHONY:output
 12 output:
 13   mkdir -p lib/include
 14   mkdir -p lib/mymathlib
 15   cp *.h lib/include
 16   cp *.a lib/mymathlib

其中 这个output就是发布,在里面创建了一个lib文件夹 ,里面有又有两个文件夹分别放头文件和库文件。

执行效果

于是我们就有了lib这个文件夹。

当我们使用的时候

当时当我们编译后发现报错了

 很明显,系统找不到我们的头文件。因为系统会默认的去usr目录里面找

显然我们的头文件不在里面,所以系统找不到,报错了。

对于我们的gcc,可以加 -I(大写)来告诉系统默认目录找不到,就到指定目录下找

但是又报错了

但是这一次报错不是编译报错,而是链接时报错。

很明显,系统也没有找到我们写的静态库。跟刚刚那个问题一样。

于是我们又得给gcc加上两个选项。

1.-L,告诉gcc我们的库在哪个目录里

2.-l(小写的L) 告诉gcc在这个目录里我们具体要哪个库,而且需要注意:库的真实名字是去掉前缀:lib,去掉后缀:比如这里的.a,剩下的 mymath才是真实的库名字。而且库名字最好紧跟在 -l后面,不要加空格。

站在库的使用角度

  我们可以看到,我们定义了一个全局变量 myerrno,它是用来返回错误码的,当我们的函数调用出错后,我们可以根据返回的错误码来推断错误的原因,C标准库里也是这样设计的。 

从此以后,对于系统和语言级别的库,我们可以不加 这些,对于第三方库,我们也可以不加 -I(大写I),不加 -L,但是-l(小写L)必须要加。也就是说第三方库肯定是要使用 gcc -l的。

这样系统找头文件和库的问题就解决了。

但是这些操作确实很麻烦,如果想要简化操作,也有两种方法:

1.直接将头文件和库放到系统默认搜索的目录当中。

2.通过软链接,将生成的对应的软链接放到系统默认的搜索路径当中。

简单设计一个动态库

  我们可以用ldd指令来查看一个可执行程序的动态库链接情况

其中第二个是C标准库。

我们没有看到我们的mymath的库,因为它是静态链接的,如果系统中只提供静态链接,gcc则只能对该库进行静态链接。

如果我们在gcc时没有带static选项,则有动态库就用动态库,没有再用静态库。

 我们也可以直接把我们的头文件和库直接拷贝到系统默认路径下,这样就只需要gcc -l了。这其实也是我们今后使用别人的库的最常见的使用方式。

补充:

用这种方式来建立软链接也可以。 

动态库直接用gcc打包即可。

因为这个库里面没有main函数,所以要告诉编译器这不是一个可执行程序,所以要加 -shared

测试makefile

dy-lib=libmymethod.so
  2 static-lib=libmymath.a
  3 
  4 .PHONY:all
  5 all:$(dy-lib) $(static-lib)
  6 
  7 $(static-lib):mymath.o
  8     ar -rc $@ $^
  9 mymath.o:mymath.c
 10     gcc -c $^
 11 
 12 $(dy-lib):log.o print.o                                                                                                                                                                  
 13     gcc -shared -o $@ $^
 14 log.o:log.c
 15     gcc -fPIC -c $^
 16 print.o:print.c
 17     gcc -fPIC -c $^
 18 
 19 
 20 .PHONY:clean
 21 clean:
 22   rm -rf *.o *.a *.so mylib 
 23 
 24 .PHONY:output
 25 output:
 26   mkdir -p mylib/include
 27   mkdir -p mylib/lib
 28   cp *.h mylib/include
 29   cp *.a mylib/lib 
 30   cp *.so mylib/lib

 

然后我们生成了.so的动态库,我们发现它的权限居然带x,它没有main函数,肯定是运行不了的,为什么会带x权限呢?首先静态库它是直接与我们的程序链接在一起的,它们当程序启动后,它是不加载到内存的,(当然,链接进去的是它的拷贝)。但是当程序执行后,如果调用到了动态库的方法或者数据,是要把动态库加载到内存中,这也是一种间接的使用,它不能独立的执行,所以它带x权限。

执行结果

 结果正确。

 拓展一个图形界面的库

动态库的加载 

  动态库在进程被运行的时候,也是要被加载的。(静态库没有)  

  常见的动态库被所有的可执行程序都要使用,所以也叫共享库。

那么它怎么被进程们所找到的呢?

当动态库被加载到内存后,对应的进程在调用到库的内容时,会跳转到共享区,然后通过页表找到物理内存中的共享库。 

结论:建立映射,我们执行的任何代码,都是在我们的进程地址空间中执行的。

所以一个动态库可能会被多个进程所共享,并且系统在运行中,一定会有多个动态库,系统要想对这些库进行管理,也要先描述,再组织。

库为什么能被共享,是由进程们的进程地址空间和它们各自的页表,页表进行映射到一个共享的库来完成的。

另外我们知道库中是有全局变量的,当这个库被多个进程引用时,它的页表也会有引用计数,当发生修改时,也会进行写时拷贝。

关于地址

  当我们的程序编译好后,其实也已经有了地址的概念了。现代计算机用的是平坦模式。

也就是0~4GB,编译器也要考虑操作系统。

 所以我们的程序在编译好后,它已经有虚拟地址了,不过此时程序还没有运行,所以叫做逻辑地址。

补充

我们有没有想过,CPU是如何执行我们的指令的呢?

 其实我们的指令会被拆分成很多简单的指令, CPU只能执行这些比较简单的指令,然后这些指令组合起来,就实现了我们的指令,当代CPU必须先被内置指令集,指令集被分为两大类,一类为精简指令集,一类为复杂指令集。也就是说,芯片在被制作时,它已经被硬件工程师提前写好了一些CPU能识别的基础指令。一开始是二进制编程,但是很不方便,所以这些push mov就是一些注记符,其实就是对一些二进制指令之间的一些映射。

程序加载后的地址 

 

 我们的程序在编译好后,会把自己的入口地址给确定好,也就是entry。它也是逻辑地址。

 在CPU内有一个EIP/PC寄存器。

程序加载时,这个寄存器读到的一个地址就是入口地址,也就是虚拟地址了。当然,是PCB这些先加载好。

因为我们读到的是虚拟地址,于是我们会到页表中去找,但此时页表还没有建立映射关系,所以此时会发生缺页中断,又因为磁盘读取到内存是以页为单位的,也就是4kb,所以也就会加载一大片代码和数据到内存中,此时这一大片也会进行缺页中断,在页表中一一建立映射关系。 因为程序在编译的时候,逻辑地址就已经都有了,以页为单位加载到内存后,就与页表进行了映射。

此后,CUP就会逐条进行读取语句。

当我们的CPU读到函数调用的指令时,它就会继续到正文代码区,到页表中去查找映射关系,没有就会发生缺页中断。

总结:CPU内读取到的指令,内部可能有数据,也可能有地址,但是全都是虚拟地址! 

动态库的地址

首先我们要知道什么是绝对地址

比如说内存的编号从0000...到FFFFF.....,中间随便拿一块地址,它的编号是0x11223344,这个地址就是绝对地址。 

我们知道,在程序用调用了printf这个函数,在编译阶段会被转化成地址,指向我们的C标准库。 

但是当程序运行时,它就会顺着这个指针到共享区里去找,但是共享区那么大,而且可能会有多个库,我们的这个库怎么可能被加载到固定地址呢?显然是不可能的。

当CPU执行的,会到共享区里去找,找到后在页表中建立映射关系,但是共享区可能会很大, 具体要映射到哪呢?因为我们在调用这个函数的时候,它是首先我们知道,动态库被加载到固定地址空间中的位置是不可能的。

库可以在虚拟内存的任意位置加载,它可以让自己的内部函数不要采用绝对编址,它只表示每个函数在库中的偏移量即可!

所以我们的调用printf转化成的地址比如0x11223344,它就是这个库中的函数的偏移量。

所以,我们只需要找个这个库的起始地址,然后加上这个 偏移量就可以找到我们的函数了。

所以我们的库加载到任意位置都可以。

这就能解释了在我们编译时链接动态库时的一个选项,fPIC:产生位置无关码 了。 

也就是使我们的库在任意位置加载,函数使用偏移量的方式编址。

所以为什么静态库不加载,不谈位置无关码。就是因为 库的函数已经拷贝到可执行程序里了,相当于库的方法已经是我们的方法了,所以它在虚拟地址上的位置就是确定的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值