动静态库概念
静态库的后缀是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:产生位置无关码 了。
也就是使我们的库在任意位置加载,函数使用偏移量的方式编址。
所以为什么静态库不加载,不谈位置无关码。就是因为 库的函数已经拷贝到可执行程序里了,相当于库的方法已经是我们的方法了,所以它在虚拟地址上的位置就是确定的。