Linux 软硬连接&动静态库
文章目录
软硬链接
建立软链接:ln -s [被链接文件] [软链接文件]
sy@hcss-ecs-bd8b:~/linux/lesson21$ ls
file1.txt
sy@hcss-ecs-bd8b:~/linux/lesson21$ ln -s file1.txt link.soft
sy@hcss-ecs-bd8b:~/linux/lesson21$ ll -i
total 8
411488 drwxrwxr-x 2 sy sy 4096 Jan 8 15:38 ./
135817 drwxrwxr-x 18 sy sy 4096 Jan 8 15:37 ../
411493 -rw-rw-r-- 1 sy sy 0 Jan 8 15:37 file1.txt
411494 lrwxrwxrwx 1 sy sy 9 Jan 8 15:38 link.soft -> file1.txt
软链接的本质是一个独立的文件,文件内容保存被链接文件的路径。
软链接会创建一个新的文件与新的inode编号,软链接类似于Windows中的快捷方式。
建立硬链接:ln [被链接文件] [硬链接文件]
sy@hcss-ecs-bd8b:~/linux/lesson21$ ll
total 8
drwxrwxr-x 2 sy sy 4096 Jan 8 15:41 ./
drwxrwxr-x 18 sy sy 4096 Jan 8 15:41 ../
-rw-rw-r-- 1 sy sy 0 Jan 8 15:37 file1.txt
-rw-rw-r-- 1 sy sy 0 Jan 8 15:41 file2.txt
lrwxrwxrwx 1 sy sy 9 Jan 8 15:38 link.soft -> file1.txt
sy@hcss-ecs-bd8b:~/linux/lesson21$ ln file2.txt link.hard
sy@hcss-ecs-bd8b:~/linux/lesson21$ ll
total 8
drwxrwxr-x 2 sy sy 4096 Jan 8 15:41 ./
drwxrwxr-x 18 sy sy 4096 Jan 8 15:41 ../
-rw-rw-r-- 1 sy sy 0 Jan 8 15:37 file1.txt
-rw-rw-r-- 2 sy sy 0 Jan 8 15:41 file2.txt
-rw-rw-r-- 2 sy sy 0 Jan 8 15:41 link.hard
lrwxrwxrwx 1 sy sy 9 Jan 8 15:38 link.soft -> file1.txt
硬链接本质是在目录下添加新的文件名与文件inode编号的映射关系,并让被链接的引用计数+1,删除文件时先对文件的引用计数-1,如果引用计数减为0,说明没有文件与该inode存在映射关系,再通过「伪删除」删除文件。
创建一个空目录1时,目录1的硬链接数为2。因为每一个目录下都会有.目录和..目录,其中.目录就是目录1的一个硬链接,所以一个空目录的硬链接有该空目录下的.目录,所以硬链接数为2。
sy@hcss-ecs-bd8b:~/linux/lesson21$ ls -ldi dir/./
411499 drwxrwxr-x 2 sy sy 4096 Jan 8 16:19 dir/./
sy@hcss-ecs-bd8b:~/linux/lesson21$ ls -ldi dir/
411499 drwxrwxr-x 2 sy sy 4096 Jan 8 16:19 dir/
当目录1中有子目录时,目录的硬链接数会加上子目录数,因为每个子目录的..目录是目录1的硬链接。
sy@hcss-ecs-bd8b:~/linux/lesson21$ ls -ldi ./
411488 drwxrwxr-x 6 sy sy 4096 Jan 8 16:19 ./
sy@hcss-ecs-bd8b:~/linux/lesson21$ ls -ldi a/../
411488 drwxrwxr-x 6 sy sy 4096 Jan 8 16:19 a/../
sy@hcss-ecs-bd8b:~/linux/lesson21$ ls -ldi b/../
411488 drwxrwxr-x 6 sy sy 4096 Jan 8 16:19 b/../
sy@hcss-ecs-bd8b:~/linux/lesson21$ ls -ldi c/../
411488 drwxrwxr-x 6 sy sy 4096 Jan 8 16:19 c/../
sy@hcss-ecs-bd8b:~/linux/lesson21$ ls -ldi dir/../
411488 drwxrwxr-x 6 sy sy 4096 Jan 8 16:19 dir/../
得到一个结论: 目录的硬链接数 − 2 = 目录下的子目录个数 目录的硬链接数-2=目录下的子目录个数 目录的硬链接数−2=目录下的子目录个数
注意:
- Linux可以给目录创建软链接
- Linux不允许用户给目录创建硬链接,系统可以为目录创建硬链接,
.和..就是目录的硬链接。
环路问题
当用户为目录创建硬链接时,此时在目录下访问文件就可能出现环路问题。有如下目录结构:
/home/linux/lesson21/dir/link.hard,其中link.hard文件是lesson21目录的硬链接,那么会出现环路问题。在
link.hard中使用ls命令,从根目录开始查找,系统会尝试递归列出link.hard的内容。由于link.hard是对lesson21的硬链接,它实际上指向lesson21,而lesson21中又包含dir,dir中又包含link.hard。这种结构会导致递归查找过程如下:
lesson21→dir→link.hardlink.hard→lesson21→dir→link.hard- 循环往复,路径永远无法结束。
因为硬链接是目录的直接引用,系统会不断重复访问相同的目录结构,进入无限循环。
.和..是系统创建的特殊硬链接,系统允许他们指向目录。
动静态库
动静态链接
-
安装gcc工具时,系统也会自动安装对应的动态库,如果要安装静态库则需要手动安装。
-
gcc/g++编译时,默认使用的是动态链接,若要指定静态链接,在编译时加上
-static。 -
通过
ldd(load dynamic library )命令查看可执行文件链接了哪些动态库。
sy@hcss-ecs-bd8b:~/linux/lesson21$ cat main.c
#include <stdio.h>
int main()
{
return 0;
}
sy@hcss-ecs-bd8b:~/linux/lesson21$ gcc main.c -o a.out # 动态链接
sy@hcss-ecs-bd8b:~/linux/lesson21$ ldd a.out
linux-vdso.so.1 (0x00007ffd2a1fb000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fda0de70000) # c标准库
/lib64/ld-linux-x86-64.so.2 (0x00007fda0e0a9000)
sy@hcss-ecs-bd8b:~/linux/lesson21$ ll a.out
-rwxrwxr-x 1 sy sy 15776 Jan 8 20:19 a.out*
- 通过
file命令查看可执行文件链接库是动态链接还是静态链接。
sy@hcss-ecs-bd8b:~/linux/lesson21$ file a.out
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f593abe086161ce758678e314dd80a7e65f9bd7a, for GNU/Linux 3.2.0, not stripped
sy@hcss-ecs-bd8b:~/linux/lesson21$ gcc main.c -o a.out -static #指定静态链接 前提是已经安装了C静态库
sy@hcss-ecs-bd8b:~/linux/lesson21$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=2e1eda62d5f755377435c009e856cd7b9836734e, for GNU/Linux 3.2.0, not stripped
sy@hcss-ecs-bd8b:~/linux/lesson21$ ll a.out
-rwxrwxr-x 1 sy sy 900216 Jan 8 20:21 a.out*
- 相比于动态链接,静态链接的程序运行时不再依赖库,同时文件体积更大。
库文件名与库名
- Linux下,动态库后缀为
.so,静态库后缀为.a;windows下。动态库后缀为.dll,静态库后缀为.lib。 - 一个库的名字需要去掉前缀lib和后缀,如果一个库文件为
libstdc++.so,这个库名字为stdc++。链接特定库时,我们通过库的名字来指定链接哪个库。
库的本质
- 库本质是编译后的
.o目标文件和头文件
动静态库的制作和使用
库的作用:
- 隐藏源代码
- 提高开发效率
- 开发时,我们可以直接使用别人库中实现的函数,这样自己就不需要造轮子。
- 库中存放的是编译后的
.o文件,库的开发者可以在隐藏自己源文件的同时对外提供编译好的.o文件来使用。
静态库制作与使用的流程:
-
编译源文件,生成目标文件。
gcc -c *.c *.o。- 别人链接库实际上是链接
.o文件,因此制作库一定要先将源文件编译为.o文件。
- 别人链接库实际上是链接
-
制作静态库。
ar -rc *.o lib[库名字].a。-
如果库文件不存在,
-c会创建一个新的归档文件,而-r则将指定的文件插入到归档中。 -
如果库文件已经存在,
-r会直接插入文件或替换同名文件。
-
-
安装静态库到系统中。
cp lib[库名字].a /lib64。- 这一步是为了方便通过gcc命令编译时使用更少的参数。gcc编译时默认会在系统库路径下寻找需要链接的库,如果需要链接的库不在系统库路径下,gcc编译时需要添加
-L选项指定库路径。 - Linux下的系统库的路径通常为
/lib、/lib64、/usr/lib、/usr/lib64。
- 这一步是为了方便通过gcc命令编译时使用更少的参数。gcc编译时默认会在系统库路径下寻找需要链接的库,如果需要链接的库不在系统库路径下,gcc编译时需要添加
-
编译程序时指定链接的库。
gcc main.c -l[库名字] -static。- gcc静态编译C文件时默认链接C静态标准库,若要链接其他的库需要通过
-l显示指定。
- gcc静态编译C文件时默认链接C静态标准库,若要链接其他的库需要通过
下面我们来制作一个静态库并使用它
-
编写源文件mymath.c和mystdio.c。
-
编译源文件
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ gcc -c mymath.c sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ gcc -c mystdio.c sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ ll total 56 -rw-rw-r-- 1 sy sy 64 Jan 8 20:52 mymath.c -rw-rw-r-- 1 sy sy 35 Jan 8 22:37 mymath.h -rw-rw-r-- 1 sy sy 1232 Jan 10 16:21 mymath.o -rw-rw-r-- 1 sy sy 2625 Jan 10 16:21 mystdio.c -rw-rw-r-- 1 sy sy 611 Jan 8 22:39 mystdio.h -rw-rw-r-- 1 sy sy 4112 Jan 10 16:22 mystdio.o drwxrwxr-x 3 sy sy 4096 Jan 8 22:45 user/ -
制作静态库
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ ar -rc libmyc.a mymath.o mystdio.o sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ ll -rw-rw-r-- 1 sy sy 5622 Jan 10 16:23 libmyc.a -rw-rw-r-- 1 sy sy 64 Jan 8 20:52 mymath.c -rw-rw-r-- 1 sy sy 35 Jan 8 22:37 mymath.h -rw-rw-r-- 1 sy sy 1232 Jan 10 16:21 mymath.o -rw-rw-r-- 1 sy sy 2625 Jan 10 16:21 mystdio.c -rw-rw-r-- 1 sy sy 611 Jan 8 22:39 mystdio.h -rw-rw-r-- 1 sy sy 4112 Jan 10 16:22 mystdio.o drwxrwxr-x 3 sy sy 4096 Jan 8 22:45 user/ -
使用静态库
cp *.a user/ cp *.h user/ cd user/ gcc -o main.static main.c -static -L . -lmyc # 指定静态链接,指定链接库所在路径,指定链接的静态库名 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ./main.static c=3 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ cat main.c #include "mystdio.h" #include "mymath.h" #include <stdio.h> int main() { _MyFILE* file = MyFopen("log.txt", "w"); if (file == NULL) return 1; int c = Add(1, 2); printf("c=%d\n", c); return 0; } sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ file main.static # 静态链接的可执行文件 main.static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=b07635796326b4eb5e9eff2d4bc95e56814e4b71, for GNU/Linux 3.2.0, not stripped
动态库制作与使用的流程:
- 使用位置无关码编译源文件生成目标文件,
gcc -fPIC -c *.c *.o。- 动态库中的
.o文件需要通过-fPIC选项生成。
- 动态库中的
- 制作动态库。
gcc -shared -o lib[库名字].so *.o- 动态库的制作通过gcc工具即可。
- 打包动态库。
- 发布动态库往往会将库文件和头文件一起打包发送
- 可以通过
make构建整个动态库。
- 安装动态库到系统库中。
- 安装动态库分为两步,一安装库文件到系统库路径下,二安装头文件到系统搜索的头文件路径下。
- linux下系统库路径
/lib,/lib64,/usr/lib,/usr/lib4。 - linux下头文件的搜索路径
/usr/include。 - 如果没有安装动态库,编译时需要指定链接的动态库所在路径以及头文件所在路径。通过
-L选项指定库文件所在路径;-I选项指定头文件所在路径。
- 编译时指定链接的库。
- 如果已经安装动态库到系统中,那么编译时不需要指定动态库的路径,但需要指定链接的库是哪个,因为gcc默认链接的动态库是C动态标准库。
下面我们来制作一个动态库并使用它:
-
编写源文件mystdio.c与,mymath.c
-
通过
make构建整个动态库libmyc.so:mymath.o mystdio.o gcc -shared -o $@ $^ # 通过位置无关吗生成目标文件 %.o:%.c gcc -c -fPIC $< #打包动态库 .PHONY:output output: mkdir mylib/include -p mkdir mylib/lib -p cp *.h mylib/include cp *.so mylib/lib tar czf mylib.tar.gz mylib .PHONY:clean clean: rm -rf *.o rm -rf libmyc.so rm -rf mylib rm -rf mylib.tarsy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ make gcc -c -fPIC mymath.c gcc -c -fPIC mystdio.c gcc -shared -o libmyc.so mymath.o mystdio.o sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ sudo cp *.so /usr/lib64 [sudo] password for sy: sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ ls /usr/lib64 ld-linux-x86-64.so.2 libmyc.so -
发布库
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ make output mkdir mylib/include -p mkdir mylib/lib -p cp *.h mylib/include cp *.so mylib/lib tar czf mylib.tar.gz mylib sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ mv mylib.tar.gz user -
将库安装到系统:库文件、头文件
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib$ cd user/ sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ tar xzf mylib.tar.gz # 解压整个库 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ tree mylib mylib ├── include # 库用到的头文件 │ ├── mymath.h │ └── mystdio.h └── lib # 库文件 └── libmyc.so 2 directories, 3 files # 安装库文件到系统 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ sudo cp mylib/lib/libmyc.so /usr/lib64 # 安装头文件到系统 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ sudo cp mylib/include/* /usr/include/ # 动态链接编译源文件 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ gcc -o main.dynamic main.c -lmyc -
执行代码动态链接的可执行文件
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ldd main.dynamic linux-vdso.so.1 (0x00007ffd8d5f7000) libmyc.so => not found # 成功链接了我们的动态库libmyc.so,但是运行时找不到我们的库 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f93fe477000) /lib64/ld-linux-x86-64.so.2 (0x00007f93fe6b0000) sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ./main.dynamic ./main.dynamic: error while loading shared libraries: libmyc.so: cannot open shared object file: No such file or directory # 加载动态库失败
至此,我们能够以动态链接的方式编译文件了,这会生成一个动态链接的可执行文件,但要运行这个文件还需要指定运行时的库的查找路径。上面的操作仅仅告诉编译器gcc在哪里查找哪个库,运行文件时也需要告诉操作系统库的所在路径。
动态库的查找
头文件查找
对于动静态库,在编译时的预处理阶段编译器要对指定的头文件进行查找,编译器默认情况下需要在系统头文件路径/usr/include下查找对应的头文件,如果没有头文件不在系统头文件路径下,则应该在gcc编译时通过-I选项显式指定头文件所在路径。
编译时库的查找
对于动静态库,编译时需要告诉编译器我们要链接的库在哪个路径下以及链接库的名字。编译器默认情况下会去系统库路径下查找指定的库文件。如果库文件不在系统库文件路径下,则应该在gcc编译时通过-L选项显示指定库文件所在的路径。除此之外,不论库文件是否在系统库路径下,都应该通过-l选项指定链接的库的名字是什么。
运行时库的查找
对于静态库而言,链接后的可执行文件已经包含了所有需要用到的头文件、库文件,所有需要用到的文件都已经存在于可执行文件中了。当链接静态库后,程序一定可以运行,因为操作系统可以在可执行文件中找到需要用到的所有文件。
对于动态库而言,链接后的可执行文件不一定能运行,当系统执行到动态库中的函数时需要去动态库中动态查找相关的函数。所以对于动态链接,我们需要告诉操作系统运行时去哪里查找动态库,从而执行对应的函数。
指定运行时库的查找路径:
-
将库文件添加到系统库路径
/lib/lib是系统库的目录,编译时,gcc默认会去这个路径下查找库文件,运行时,操作系统也会去这个路径下查找库文件。sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ sudo cp mylib/lib/libmyc.so /lib/ # 拷贝到系统库路径下,操作系统会去这个路径下搜索我们的库文件 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ldd main.dynamic linux-vdso.so.1 (0x00007fff53397000) libmyc.so => /lib/libmyc.so (0x00007f9cf2fbe000) # 运行时成功找到我们的动态库 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9cf2d95000) /lib64/ld-linux-x86-64.so.2 (0x00007f9cf2fd3000) sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ./main.dynamic c=3 -
将库文件所在路径导入环境变量
LD_LIBRARY_PATHLD_LIBRARY_PATH是动态链接器搜索动态库路径的首要选项。当动态库存放在非默认路径时,可以使用LD_LIBRARY_PATH指定额外的搜索路径。将库所在目录添加到环境变量
LD_LIBRARY_PATH即可。临时设置环境变量
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ LD_LIBRARY_PATH=/home/sy/linux/lesson21/testlib/user/mylib/lib ./main.dynamic # 只在当前指令有效 c=3 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sy/linux/lesson21/testlib/user/mylib/lib #只在当前会话有效 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ./main.dynamic c=3永久设置环境变量
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ vim ~/.bashrc # 打开该用户的配置文件 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sy/linux/lesson21/testlib/user/mylib/lib # 配置全局环境变量 sy@hcss-ecs-bd8b:source ~/.bashrc # 使配置文件生效 -
为库文件添加软链接
添加比较成熟的库可以采用第一种方案,即直接将整个库文件添加到系统库目录下。自己实现的库通常不是那么成熟,如果添加到系统库下可能会产生污染问题,建议将自己的实现的库通过软链接的方式添加到系统库目录下而不是添加整个库文件。
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ sudo ln -s /home/sy/linux/lesson21/testlib/mylib/lib/libmyc.so /lib/libmyc.so sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ./main.dynamic c=3 -
添加库文件至系统动态库配置文件
/etc/ld.so.conf文件:这是动态链接器的主配置文件。它可以直接包含库路径,也可以引用其他文件。/etc/ld.so.conf.d目录:这是一个辅助目录。动态链接器会读取该目录下的所有.conf文件,并将它们的内容视为额外的库路径配置。sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ sudo touch /etc/ld.so.conf.d/libmyc.conf # 添加一个库路径的配置文件 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ sudo vim /etc/ld.so.conf.d/libmyc.conf # root权限打开配置文件,添加我们库所在的目录 sudo ldconfig # 重新加载配置文件 sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ldd main.dynamic linux-vdso.so.1 (0x00007ffd65898000) libmyc.so => /home/sy/linux/lesson21/testlib/user/mylib/lib/libmyc.so (0x00007f865f476000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f865f24d000) /lib64/ld-linux-x86-64.so.2 (0x00007f865f48b000) sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ./main.dynamic c=3在
libmyc.conf配置文件中添加链接时在哪个目录下查找我们的库!
动态库的加载
动静态库的连接问题
当都动静态库同时存在时,gcc默认使用动态库进行动态链接,添加-static选项使用静态库进行静态链接。
我们的库中有静态库和动态库,它们的都为myc。
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ ll
total 16
drwxrwxr-x 3 sy sy 4096 Jan 12 16:28 ./
drwxrwxr-x 3 sy sy 4096 Jan 12 14:55 ../
-rw-rw-r-- 1 sy sy 214 Jan 10 16:26 main.c
drwxrwxr-x 4 sy sy 4096 Jan 10 16:37 mylib/
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ tree mylib/
mylib/
├── include
│ ├── mymath.h
│ └── mystdio.h
└── lib
├── libmyc.a
└── libmyc.so
使用gcc默认编译生成可执行文件mytest,并查看mytest的链接类型为动态链接。
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ gcc -o mytest main.c -L mylib/lib -lmyc
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ file mytest
mytest: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=704390598b20b3cfcf8b44f2f77c44d12797d9c4, for GNU/Linux 3.2.0, not stripped
只存在静态库时,gcc默认可以使用静态链接,不需要添加-static选项。链接后的程序不一定整体是静态链接的
现删除动态库myc,只保留静态库,使用gcc默认编译,并查看可执行文件mytest链接类型。
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ mv mylib/lib/libmyc.so .
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ tree mylib/
mylib/
├── include
│ ├── mymath.h
│ └── mystdio.h
└── lib
└── libmyc.a
2 directories, 3 files
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ gcc -o mytest main.c -L mylib/lib -lmyc
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ file mytest
mytest: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6a0d8ec95485104a20c96f0b97f87610493f6fdb, for GNU/Linux 3.2.0, not stripped
只存在动态库时,gcc不能通过-static选项进行静态链接。
现删除静态库myc,只保留动态库,使用gcc静态编译,发现编译报错。
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ mv libmyc.so mylib/lib/
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ mv mylib/lib/libmyc.a .
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ tree mylib/
mylib/
├── include
│?? ├── mymath.h
│?? └── mystdio.h
└── lib
└── libmyc.so
2 directories, 3 files
sy@hcss-ecs-bd8b:~/linux/lesson21/testlib/user$ gcc -o mytest main.c -L mylib/lib/libmyc.a -lmyc -static
/usr/bin/ld: cannot find -lmyc: No such file or directory
建议:gcc编译时使用动态链接,不添加-static选项。
宏观理解动态库的加载
一个程序运行后系统会为其分配一个就进程地址空间,进程地址空间被划分很多段,每个段存储专门的数据。同时操作系统也会为这个进程分配一个页表存放进程地址空间的虚拟地址到程序被加载到物理内存上的地址的映射关系。库本质就是一堆编译后的目标文件的集合,当一个进程链接静态库时,只需要将静态库中的所有目标问价你拷贝到使用静态库的程序目标文件的代码区。。对于静态链接的程序来说,一旦编译链接完成后,库就已经在程序的代码区中了,调用库函数就和调用普通函数一样在代码去中查找。
但如果程序与库进行动态链接,那么库不会被拷贝到程序的代码区,而是在执行到库相关的操作时才加载到内存并通过页表建立与进程地址空间中共享区的映射关系。
访问动态库:

当其他的进程也需要访问这个库时,如果有多个程序使用同一个库,只需要将这个库加载到内存中一次,再像页表中添加库和共享区的映射关系,多个程序就可以在自己的共享区中访问这个库了。当一个程序使用一个库时,操作系统首先要判断这个库有没有被加载到内存,如果加载了就可以直接建立映射并使用,否则操作系统首先应该将库加载到内存,这保证了一个库最多只会被加载到内存一次,提高了内存的使用率。
一个动态库一旦被加载到内存后就可以被多个程序使用,所以将这些动态库叫做共享库。
系统中会同时存在多个已经加载的库,操作系统需要将这些库管理起来,因此操作系统会定义已经加载到内存中的库的结构体,其中包含这些库的名字、被加载到内存的起始地址等属性。
编址模式
编译生成的可执行程序文件已经按照一定的编制方式
绝对编址和相对编址
绝对编址是从0地址处开始编址,每一条语句都对应一个地址,下一条语句的地址一定是比上一条语句的地址递增/递减的。相对编址是以某一个地址为初始地址,下一条语句的地址表示偏移初始地址多少。

可执行文件中的指令已经有地址了并且可执行文件会被分区。
程序不一定被加载到内存中才有地址,当一个程序被编译生成为可执行文件时,它就已经有了地址。现代计算机中的可执行程序大部分的编制方式是绝对编址,并且可执行程序在被加载到内存前就已经按照一定的类别(属性、权限等)划分为不同的区域了。
可执行程序按照绝对编址的方式组织:

可执行程序被划分为多个区域:

在运行前,可执行程序就已经划分为代码段和数据段了,其中:
text
- 表示代码段的大小(单位:字节)。
- 代码段包含程序的机器指令(即可执行代码)。
- 在此输出中,代码段大小为
1641字节。
data
- 表示初始化数据段的大小(单位:字节)。
- 初始化数据段存储程序中显式初始化的全局变量和静态变量。
- 在此输出中,初始化数据段大小为
632字节。
bss
- 表示未初始化数据段的大小(单位:字节)。
- 未初始化数据段用于存储程序中未初始化的全局变量和静态变量。这些变量在程序运行时被初始化为零。
- 在此输出中,
bss段大小为8字节。
dec
- 表示
text、data和bss三段的总大小(单位:字节)。 - 在此输出中,总大小为
2281字节。
hex
- 是
dec值的十六进制表示。 - 在此输出中,十六进制值为
8e9。
filename
- 表示目标文件的名称。
- 在此输出中,文件名是
mytest。
逻辑地址、虚拟地址、物理地址
逻辑地址是可执行程序运行前每一条指令对应的地址。也就是可执行程序在磁盘上的地址,是我们上面看到的绝对地址。运行时,操作系统会为可执行程序分配进程地址空间,虚拟地址是指代进程地址空间上的地址。物理地址是可执行程序被加载到内存中的实际地址,CPU最终执行的指令一定是在物理地址上的。
当运行程序后,操作系统首先会为这个可执行程序分配task_struct、进程地址空间、页表等数据结构,再将可执行程序加载到内存。可执行程序加载前就有了响应的逻辑地址,加载到内存后可执行程序有了内存上的物理地址,操作系统加载程序到内存后就对该进程的页表内容构建逻辑地址到物理地址的映射关系。
可执行程序的ELF表头
可执行程序除了代码段和数据段还存在一个用于保存该程序是如何分区、组织、使用了哪些动态库的表头、程序的入口地址,这个表头在Linux下称为ELF表头。操作系统创建进程地址空间时,初始化的字段会从可执行程序的ELF表头中获取。ELF表头记录了代码段和数据段的起始地址(这个地址当然是逻辑地址),操作系统会根据ELF表头记录的逻辑初始化填充进程地址空间的代码段和数据段(堆区和栈区是运行时才申请的),所以进程地址空间是可以在程序被加载到内存前就初始化一部分的,并且初始的值由可执行程序来决定,所以逻辑地址和虚拟地址是一样的,只不过一个是针对磁盘上的可执行程序,另一个则是指进程地址空间。
虚拟地址空间是操作系统和编译器共同实现的。
编译器将源程序编译为可执行程序,这个过程中涉及将程序绝对编址,同时形成ELF表头,操作系统负责创建进程地址空间并用编译生成的ELF表头初始化进程地址空间的部分区域。所以说虚拟地址空间是操作系统和编译器共同实现的。
理解动态库的链接和加载问题
一般程序的加载
当运行一个程序时
-
操作系统会为其分配
task_struct、进程地址空间、页表 -
用可执行程序的ELF表头初始化进程地址空间
-
将可执行程序加载到内存,并用物理地址与逻辑地址填充页表
-
将ELF表头的程序入口地址填入PC寄存器中
-
CPU从PC寄存器里面的地址读取指令(PC寄存器中保存指令的地址为虚拟地址,因为PC的值是通过 ELF表头函数入口地址初始化的,函数入口地址是针对磁盘上的可执行文件,也就是逻辑地址),在通过IR寄存器分析指令。
-
CPU通过MMU硬件单元和页表找到指令所在的物理地址,并执行对应的指令,执行时会根据指令的操作让pc指针指向其他指令的地址或者是下一条指令的地址(如执行的指令是
jmpcall则会让pc指针指向操作数) -
当进行函数调用时,在可执行文件上函数调用的格式形如
call [调用函数的逻辑地址],操作系统早在加载函数时就建立了函数的逻辑地址和物理地址的映射关系,所以当CPU取到函数的逻辑地址时能找到其对应的物理地址,从而执行函数调用过程。
磁盘上的可执行程序或者汇编程序都是有地址的,并且在这些程序里面使用的一定是逻辑地址。只有当程序被加载到内存中才有物理地址。

动态库的加载
库也是磁盘级文件,也是有地址的,只不过动态库是相对编址。磁盘上的动态库是以库名为基地址的。
当CPU执行到动态库的操作时,操作系统会:
- 将动态库加载到内存
- 在进程地址空间的共享区分配一块区域给该库使用,库的起始地址就是在共享区的起始地址
- 添加库的虚拟地址(库的起始地址+偏移量)和物理地址的映射关系至页表
执行程序至访问共享库中的函数时,函数名会转化为库函数的虚拟地址,然后CPU通过MMU和页表找到库函数对应的物理地址,这样就可以访问内存中加载的库。 动态库在磁盘上时, 库的起始地址是未知的,只有当操作系统为库分配共享区的空间后,才能够知道库的起始地址,我们的代码段访问库都是在进程地址空间中通过库的起始地址+偏移量访问的。

ELF文件
- Linux下特有ELF格式 ,.o文件都是ELF格式,多个可重定位文件的相同段可以合并,链接成一个可执行文件,也是ELF格式。
- 常见的ELF文件:可重定位文件,可执行文件,共享目标文件,内核转储。
- 一个ELF文件由四部分组成:
- ELF头:描述整个ELF文件结构,可执行程序入口地址、版本信息、ELF文件各部分的起始地址,ELF头部大小,程序头大小,段表大小,段的个数
- Section Header Table:描述ELF文件各个节的起始位置以及位移、大小。程序执行时申请的进程地址空间通过Section Header Table初始化。
- Section Header Table描述了代码节、数据节、字符串表、符号表、以及其他编译期间生成的表。注意:进程地址空间中的栈区和堆区并不由Section Header Table描述,栈区和堆区是运行时申请的,而Section Header Table描述的是编译期间确定的段相关信息。
- 保存的段的起始地址、段表中的偏移量、段的读写执行属性
- readelf指令
- Program Header Table:
- 节vs段:
- ELF文件中不存在段,存在一个一个节。
- 不同的节,大小不一样,权限可能相同。加载ELF可执行文件时,加载器会将相同权限的节合并为一个段,映射到地址空间对应段(数据段、代码段),合并节可以统一管理具有相同权限的节。
- 主要考虑加载时将节合并为段的问题,Program Header Table保存了什么?
- 代码节的起始地址就是可执行文件的入口地址,保存到ELF头中
- 节vs段:
可执行程序加载
- 创建一个进程时,先创建内核进程数据结构,再加载ELF格式的二进制文件。Linux创建进程时,先执行
fork创建内核数据结构,再执行execve解析ELF文件,填写自己的虚拟地址空间,不一定要将ELF文件的代码段和数据段全加载到内存,在虚拟存储器中,懒加载。 - 编译文件时,编译器用平坦模式对可执行程序中的每一行代码进行编址,原则上从0号地址开始。这个地址叫做虚拟地址。这个**虚拟地址是由编译器生成的!**但由于可执行文件在磁盘上,我们更习惯将这个”虚拟地址“称为“逻辑地址”。
- 创建进程时,会为进程申请一个虚拟地址空间,整个进程的内核数据结构都保存在内存中,因此虚拟地址空间也保存在内存中,我们更习惯将虚拟空间中的地址叫做“虚拟地址”,虚拟地址空间的初始化是通过磁盘上的ELF可执行文件中的Program Header Table完成的,操作系统将ELF文件的Program Header Table加载到内存并读取各个段对应的逻辑地址,用这个逻辑地址填充虚拟地址空间中的相关段的虚拟地址。
- 虚拟地址空间中的每一条指令都有虚拟地址,这是由Program Header Table提供的,指令的物理地址在加载可执行程序至内存中才产生并保存到页表中,形成虚拟地址和物理地址的映射(页表保存的映射关系是页或段为单位)。
注意:页表并不保存每一条指令的虚拟地址和物理地址的映射关系,现代虚拟存储器通 常将虚拟内存和物理内存进行分页,一个虚拟地址包括页号和页内偏移两部分,页表保存的是虚拟页号和物理页号的映射以及虚拟页对应的外存地址。整个转换过程由MMU完成,速度快。
库的加载和链接
静态链接
本质是将库中相关代码拷贝到你的程序中。研究静态链接,本质是研究.o文件如何链接为可执行文件。编译时,调用某函数的文件没有实现该函数,但出现了声明,则编译生成的目标文件中该函数的地址是全零,表示暂时没有找到,未定义符号。链接后将用所有目标文件实现的函数去重定位为定义的函数。
静态链接将main函数中用到的所有函数定义拷贝到main函数中,使用的函数有自己的绝对地址,再将生成的目标文件的中调用函数的零地址用绝对地址替换。
最终:
- 多个.o文件合并方法的实现,进行统一编址。
- 地址重定位:链接时修改目标文件中未定义的函数调用地址。完成call调用。
动态链接
多个目标文件链接动态库时,链接生成的可执行文件中使用动态库中的函数的地址库函数在库中的偏移地址,同时可执行文件记录了程序需要使用的动态库路径,运行时按照指定路径可以找到动态库,再将动态库加载到物理内存中,建立页表项映射到虚拟地址空间中的共享区,最后,用共享库起始的虚拟地址+库函数的偏移地址替换库函数虚拟地址,调用函数时经过MMU转化即可在内存读取库函数定义。
事实上:因为代码区是只读的,因此,当程序加载到内存中时无法修改代码区,也就无法更改call指令后面的地址,那么动态链接又是如何做到的呢?
got表
.got:位于data区
.got的表项:
编译后,call后面跟着的是.got地址+函数在表中偏移地址,动态库加载后,修改.got的表项,引入.got,call后面的地址不和库地址直接相关,而是和got表强相关,而got表位于数据段,可以修改。
位置无关码
**动态链接时可执行程序中的库函数地址是在加载时确定的。**Linux下,程序的入口是_start函数,这是一个由C运行时库或链接器提供的特殊函数。在该函数中,负责执行一系列初始化工作:
- 为程序创建初始堆栈
- 设置初始化数据段:将程序的数据段(全局变量和静太变量)从初始化数据段复制到内存,并清零未初始化的数据段。
- 动态链接:调用链接器的代码解析和加载程序所以来的动态库。动态链接器处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确映射到动态库中的实际地址。2b
动态库也叫做共享库,多个程序链接同一个动态库时,当加载程序时,只需要加载一次动态库,对多个进程的页表建立和这个动态库映射关系即可。动态库的保证多个进程需要用的资源只需要在内存中保留一份。
操作系统如何知道哪些动态库已经加载?OS内部有保存已经加载的动态库相关数据结构。
因此,静态链接实际由编译器完成,动态链接实际由操作系统和加载器完成。动态链接是程序边运行时边进行重定位。
加载用户程序时,操作系统按如下顺序执行:
- 为用户程序创建内核数据结构。
- 将程序链接得动态库加载到物理内存中,在ELF文件的符号表中记录了链接动态库的路径。
- 在虚拟存储器下,按需加载程序的代码和数据。
更多推荐



所有评论(0)