目录
1、制作静态库
之前对动静态库的认识:
libXXX.a-----静态库
静态链接:将库当中的代码拷贝到最终的可执行程序里,也就是,自己的源代码会变成.o ,将我用过的代码考到我的可执行程序里,从此我就不用再依赖这个静态库了。
libYYY.so------动态库
动态链接:是产生关系,调用这个库函数的地址填进去就好了
gcc默认是动态链接 库的名称是去掉前缀去掉后缀
把我们提供的方法给别人用:1、把源代码直接给他 2、把我们的源代码打包成库 就相当与头文件+库 给他
库里面的文件:a.c b.c c.c d.c
自己写的文件:main.c
让别人直接用我写的方法,就可以把源代码直接给他,就是将这些源代码全都编译成 .o 然后将这些 .o直接链接就形成可执行程序了。因此,我们只需要将a.c b.c c.c d.c 这些文件先全部都变成a.o b.o c.o d.o然后再打包,形成一个库 libXXX.a 而最后只需要将自己的.c文件变为.o文件和库结合就好了
Makefile
lib=libmymath //我们生成一个库 名字叫作libmymath
$(lib):mymath.o //形成这个库依赖的是mymath.o
ar -rc $@ $^ //将.o文件进行打包 ar:一个生成静态库的命令 就是将所有的.o打包形成一个.a
//将几个.o的文件放到目标文件里,
mymath.o:mymath.c
gcc -c $^ //将源文件直接形成对应的.o文件 将mymath.c形成mymath.o
.PHONY:clean // 这个清理是将我们生成的mymath.o和mymath.c 还有lib文件删除
clean: //
rm -rf *.o *.a lib //清理执行这样的命令
.PHNOY:output //发布我们自己写的静态库 可以让别人进行使用
output:
mkdir -p lib/include //-p是创建路径
mkdir -p lib/mymathlib
cp *.h lib/include //将头文件全都拷贝到lib目录下的include里
cp *.a lib/mymathlib //将.a文件全都拷贝到lib/mymathlib中
//静态库和头文件就分开了
mymath.h
#pragma once
#include <stdio.h>
extern int myerrno;// 声明一下这个变量
int add(int x, int y);//加
int sub(int x, int y);//减
int mul(int x, int y);//乘
int div(int x, int y);//除
mymath.c
#include "mymath.h"
int myerrno = 0;
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
if(y == 0)
{
myerrno = 1;
return -1;
}
return x/y;
}
make形成库 make output 发步库
上面的图片可以看出 静态库里面头文件和库文件就都有了
总结:将c语言里面的string stdio等等对应的.c源文件全变成.o 然后再用ar命令把他们打个包,形成的.a 文件就是静态库,用户在写的时候用来头文件的方法,在最后链接的时候在静态库里将对应的.o找出来, mymath用到这个库里的方法拷到main.c就完成了。
静态库默认没有x ,静态库只要编译时能找到就可以了,静态库只要将代码拷贝到源文件就可以了。静态库不需要被加载,因为静态库已经在你的可执行程序里了,只需要把你的可执行程序加载进来就好了。
gcc下将所有的.c文件形成.o,然后ar命令再将所有的.o文件进行打包。
三个阶段:
- 编译阶段:在编写完源代码后,首先需要将其编译成目标文件。
- 创建静态库:多个目标文件可以通过工具(如
ar
在 Unix/Linux 中)合并成一个静态库。 - 链接阶段:在编译最终程序时,静态库中的目标文件会被复制到最终的可执行文件中。这一过程通常是在链接器阶段完成的
使用的工具:
- 创建:在 Linux 中使用
ar
命令创建静态库 - 链接:通过编译器的选项(如
gcc
的-static
选项)来链接静态库。
缺点: a、每个使用该静态库的程序都会包含一份库的代码,可能导致生成的可执行文件较大。b、无法在多个程序间共享内存,减少了内存使用的效率。
2、站在使用者角度使用库
我想让使用者用库,直接把lib文件夹给他就好了
我们已将拿到了,让我们来使用一下吧
如上图这样,当我们gcc main.c 的时候为什么编不过呢?
当我们编译代码时,我们要首先进行预处理,进行头文件展开,展开的第一件事情就是要将你所用的头文件找到,而在找的时候就是在你的当前路径下去找,又因为现在这个路径下就不存在这个头文件 mymath.h 就算将目录拷贝过来也没有用 " cp lib/include/mymath.h . " gcc不论是搜索头文件、源文件、库都会去 a、系统默认搜索路径去搜索 b、当前目录 但是这个mymath.h既不在系统里也不在当前路径。 必须和源代码在同一级目录下 因此我们可以使用 “ gcc main.c -I(大i) ./lib/include/ " 告诉gcc如果在系统和当前目录下找不到就去指定目录下找头文件 -I就是指明头文件的路径。 找不到方法的实现,也就是找不到库 "gcc main.c -I ./lib/include/ -L ./lib/mymathlib/ " 用-L 去指定的路径下去找库 因为代码里面已经知道我们要用哪个头文件了 但是代码里不知道要用哪个库 所以我们必须告诉我们要用哪个库 因为这个路径下可能有多个库”gcc main.c -I(大i) ./lib/include/ -L ./lib/mymathlib/ -l(小L)mymath(libmymath.a)"去掉前缀和后缀“gcc main.c -I ./lib/include/ -L ./lib/mymathlib/ -lmymath"(不加空格)
1、第三方库,在以后使用的时候,必定要用gcc -l(小L)
2、如果系统中只提供静态链接,gcc则只能对该库进行静态链接
3、如果系统中需要链接多个库,则gcc可以提供多个库
安装就是将头文件和库文件拷贝到系统路径里。
也可以建立软连接来找到库 和 头文件:
sudo ln -s /home/whb/108/lesson24/test/lib/include /usr/include/myinc给头文件所在的路径建立软连接
在代码里面包含 #include "myinc/mymath.h" 头文件就好啦
sudo ln -s /home/whb/108/lesson24/test/lib/mymathlib/libmymath.a /lib64/libmymath.a
3、制作动态库
myprintf.h
#pragma once
#include <stdio.h>
void Printf();
myprintf.c
#indlude "myprint.h"
void Printf()
{
printf("hello new world!\n);
printf("hello new world!\n);
printf("hello new world!\n);
printf("hello new world!\n);
}
mylog.h
#pragma once
#include <stdio.h>
void Log(const char*);
mylog.c
#include "mylog.h"
void Log(const char*info)
{
printf("Warning: %s\n",info);
}
形成.o文件
gcc链接的时候默认采用动态链接的方式,为什么?因为动态库的形成就是使用gcc命令的,而静态库使用ar命令
gcc -fPIC -c mylog.c -fPIC:产生与位置无关码 因为是直接用偏移量进行对库中函数进行编址
libmymethod.so 是 库 也是 可执行程序
gcc -shared(代表形成库)-o(形成可执行)libmymethod.so *.o(用所有.o文件形成一个动态库)
//静态库
ls test/lib/mymathlib/
libmymath.a
可以看到上面的静态库是没有可执行的,因为,静态库就是将来提供源代码,提供二进制的。只是在形成可执行的时候把静态库中的内容拷过去,拷贝完成之后编译就完成了。而且,静态库是不会加载到内存的。
动态库最重要的就是和可执行程序产生关联,当我们的可执行程序执行的时候,要访问动态库的内容,就要跳转到动态库,就代表动态库注定被加载,只有可执行程序这种文件才能在系统层面上快速帮我们加载。
如何理解可执行权限??
当前这个文件是否会以可执行程序的形式加载到内存里,库虽然没有main函数但是它有方法,库里面的方法也要被main函数调用,也是将来程序的一部分,所以它也算是可执行程序的一种。因此,库的形成默认就带了X.而静态库并不是不能执行,只是它不能单独执行,它需要别人来用它。
Makefile
dy-lib=libmymethod.so //形成一个动态库
static-lib=libmymath //我们生成一个静态库 名字叫作libmymath
.PHONY:all
all: $(dy-lib) $(static-lib)
$(static-lib):mymath.o //形成这个库依赖的是mymath.o
ar -rc $@ $^ //将.o文件进行打包 ar:一个生成静态库的命令 就是将所有的.o打包形成一个.a
//将几个.o的文件放到目标文件里,
mymath.o:mymath.c
gcc -c $^ //将源文件直接形成对应的.o文件 将mymath.c形成mymath.o
$(dy-lib):mylog.o myprint.o
gcc -shared -o $@ $^
mylog.o:mylog.c
gcc -fPIC -c $^
myprint.o:myprint.c
gcc -fPIC -c $^
.PHONY:clean // 这个清理是将我们生成的mymath.o和mymath.c 还有lib文件删除
clean: //
rm -rf *.o *.a *。so lib //清理执行这样的命令
.PHNOY:output //发布我们自己写的静态库 可以让别人进行使用
output:
mkdir -p mylib/include //-p是创建路径
mkdir -p mylib/lib
cp *.h mylib/include //将头文件全都拷贝到lib目录下的include里
cp *.a mylib/lib //将.a文件全都拷贝到lib/mymathlib中
//静态库和头文件就分开了
cp *.so mylib/lib
通过以上方法就形成了mylib
编译形成可执行程序main.c (a.out是gcc编译器在编译.c文件时,默认生成的可执行文件。编译成功后,当前目录下就会默认生成一个a.out可执行文件)
-I 找头文件,给出地址(不需要给出名字是因为代码中已经写了知道名字了) -L 找库 必须给出路径和名字 以为一个路径下可能有多个库
经过上面的步骤还是不能运行代码,因为,我们上面只是告诉编译器动态库在哪里。但是当编译完之后,加载器还是不知道动态库在哪里。
解决办法:
1、拷贝到系统默认的库路径 /lib64/usr/lib64/
2、在系统默认路径 /lib64/usr/lib64/下建立软链接
3、将自己的库所在的路径,添加到系统的环境变量LD_LIBRARY_PATH中。 这个环境变量是:搜索用户自定义的库路径 的环境变量 将我们库的路径添加到这个环境变量里,
4、在ect/ld.so.conf.d 目录下 建立自己的动态库路径的配置文件,然后重新ldconfig即可。让动态库和系统里的库一样, 在这个目录下存在很多的配置文件,系统在自己维护自己的动态库的时候 配置文件里放的都是路径 在这个目录下建立.conf文件再将动态库路径放进去系统就可以找到了。
总结:实际情况,我们用的库都是别人成熟的库,都采用直接安装到系统的方式。
动态库在进程运行的时候是要被加载的(静态库不需要)
常见的动态库被所有的可执行程序(动态链接的)都要使用 动态库 --共享库
所以,动态库在系统中加载之后,会被所有进程共享!!!
4、动态库是怎么被加载的
当我们的代码要使用库的时候,因此,我们就需要将这个库加载到内存中。但是加载到物理内存里进程还是看不到的,不能使用库里的方法。因此,需要将这个库通过页表映射到地址空间当中的共享区。正文想调printf直接跳转到共享区去执行,执行完之后从共享区返回到正文,就可以实现在地址空间上对库的访问。结论:建立映射,从此往后,我们执行的任何代码,都是在我们的进程地址空间中进行执行。就是在地址空间上进行远距离的跳转。 我们就可以实现我的代码运行和库的代码调用。事实:在系统中,一定会存在多个动态库---OS管理起来---先描述,再组织。在系统中,所有库的加载情况,OS非常清楚。
当第二个进程想执行2.exe并且也想用3.so库时,OS回去甄别这个库有没有被加载,因为记载情况OS很清楚,因为现在已经加载了,那么,OS也会将动态库经过页表映射到第二个进程的共享区,因此,只需要在进程中存在一份共享库就可以被所有的库共享使用了。所以,动态库也叫共享库,就是用地址空间+页表 将库映射到自己的地址空间里,就可以实现多个进程的共享。 也就是第二次就不用加载了,只需要映射就好了。
当磁盘中已经加载进一个库时,当第二个进程同样也需要使用这个库,不需要再去加载,只需要映射到自己的共享区就好了。
共享库libc.so -> errno全局变量 进程共享同一个库,都对errno作修改,岂不是会出现进程之间的干扰???答案是:不会,共享区在用户空间,对用户空间的数据作写入,这块空间时共享的,就会发生写时拷贝。
什么是虚拟地址??什么是物理地址??CPU读到的指令里面用的地址,是什么地址??
程序没有加载前的地址(程序)
程序编译好之后,内部有地址的概念吗? 答案是:有
当一个程序还没有被加载的时候,它的程序内部其实是有地址的。我们在磁盘上看到的可执行程序的顺序的排布和加载到地址空间的规则基本上是一致的。 可执行程序的内部已经有地址了。编译器在编译程序时,给每一个程序在附地址的时候在把每一个数据段在分段的时候,它已经在考虑加载的问题了。 因此,编译器也要考虑操作系统!!!
上面的这些地址其实就是虚拟地址。也就是说,当程序还没有加载到内存,已经把虚拟地址加载好了。 为了好区分 还没有被加载到内存的地址被称作逻辑地址。
程序加载后的地址(进程)
例如上图这些代码和数据加载到内存之后,它们都是要占空间的。 这个地址就是物理地址。
因此,每一条指令或语句都有两个地址,加载到内存之前的虚拟地址,和加载到内存之后的物理地址。
可执行程序的入口地址----entry(不是物理地址,是虚拟地址)
当程序已经被加载到内存,CPU要执行程序的时候,CPU是如何确定第一条指令在哪里的???
CPU中有一个寄存器eip/PC指针, 一个进程会记录自己的工作目录cwd,进程属性exe:能找到自己的可执行程序 当执行可执行程序时,直接先将entry入口地址 加载到 eip寄存器当中。
当程序还没有被加载到内存的时候,让CPU的eip寄存器拿到entry入口地址,然后去代码段去执行,但是,此时还没有在页表中形成虚拟和物理的映射,就会触发缺页中断。等到程序被加载到内存之后,就可以在页表中形成物理和虚拟的映射。当代码逐一向下执行的时候,执行到需要调用函数的时候, CPU内读到的指令,可能有数据,可能也有地址!! 这个指令用到的地址是虚拟地址。 我们要调这个虚拟地址的时候,在地址空间里,找到虚拟地址地址,然后再转成物理地址,找到之后再从找到的地址处开始执行。 整个过程,从读取程序当中的地址到内部分析处理到继续二次重新访问,整个过程读到的指令的地址全部都是虚拟地址。当可执行程序加载进来的时候,已经天然的把它编成了虚拟地址,所以,识别的时候CPU读到的全都是虚拟地址。 指令间进行定位是用虚拟地址,可是最后读取指令的时候就必须把虚拟转为物理。
动态库的地址:
当有一个动态库,动态库在磁盘上,例如是printf 我们知道,当程序写好时,系统已经为每一行程序分配了虚拟地址。那么当一个程序调用printf这个库的时候,程序就需要去地址空间的共享区去寻找,那么也就是,这个动态库必须映射到共享区。 我们拿着printf的虚拟地址去共享区里面去找。但是,关键问题是:共享库大了,具体映射到哪里呢? 动态库被加载到固定地址空间中的位置是不可能的!(加载到固定位置是因为在代码区要使用库就要跳转到共享区的库的地址位置处,因此,我们想让它是固定的)因此,我们就需要保证库可以在虚拟内存中,任意位置加载!! 那么就让自己内部的函数不要采用绝对编址,只表示每个函数在库中的偏移量即可!! 就例如,动态库在编的时候,printf的地址不是绝对地址,而是printf在这个库中相对于这个库的偏移量是多少,那么在正文部分用的printf不是地址 而是偏移量,那么我们就可以把库加载进来,然后在地址空间的共享区里面随便放,放好之后建立映射让我可以找到我对应的库就ok,OS只需要记住库在虚拟内存的起始地址,也就是只要记下每一个库的起始地址就可以。因为OS要管理库,也就说明OS是知道每一个库的位置的。我们在正文代码段要访问的时候,只需要用要使用的库的起始地址+库的偏移量就可以找到。
静态库为什么不谈加载??不谈与位置无关码??
因为静态库会直接将程序拷贝到可执行程序里。 静态库是直接拷贝到程序里的,就相当于库当中的方法就是我的方法,所以静态库当中的地址在编址的时候,因为我的程序都采用的是平坦模式(绝对编址),所以你将库的代码拷到我的程序里面,就不会用偏移量,直接按照据对编址就编好了。