Linux--软硬链接、动静态库

农民  金牌会员 | 2025-3-21 20:37:36 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 979|帖子 979|积分 2937

一、深刻理解软硬链接

在Linux中,链接是一种将文件大概目次毗连到其他位置的方法,分为硬链接软链接。
   
  硬链接:硬链接是通过在文件系统中创建一个新的文件该文件与原文件共享雷同的 inode(索引节点)。inode 是文件系统中用于标识文件的唯一编号,包罗了文件的元数据,如文件的权限、所有者、大小、创建时间等。硬链接和原文件在本质上是同一个文件,只是有不同的文件名指向同一个 inode。
  
特点:
  

  • 多个文件名对应同一个文件实体:对硬链接文件进行修改,会直接影响到原文件,因为它们共享雷同的数据块。
  • 不能超过文件系统:由于 inode 是文件系统内的概念,硬链接只能在同一个文件系统中创建。
  • 不可以对目次创建硬链接:Linux 规定不允许对目次创建硬链接,这是为了避免文件系统中出现循环引用,导致文件系统结构混乱。
  • 删除原文件不影响硬链接:只要还有一个硬链接存在,文件的数据就不会被删除。只有当所有的硬链接都被删除后,文件才会真正从文件系统中消散(引用计数)。
  创建硬链接的命令:
使用  命令:ln 来创建硬链接。ln命令默认情况下创建的是硬链接。比方,ln file1 file2会创建一个名为file2的硬链接,指向file1。
比方:在为当前目次的myfile1目次的myproc的可执行步伐在当前目次创建硬链接proc1


硬链接数:不同文件指向同一个inode的数量。
比方:ll命令显示的第二列就是硬链接数,比方:

  
实在我们正常创建的文件也是硬链接,即创建的文件名与inode号创建映射关系
  
新建的文件,硬链接数为1,就是有这个文件名和inode的一组映射;
新建的目次,硬链接数为2,这个inode和文件名

. (当前目次)有两组映射,假如在这个新建的目次里再新建一个目次,后创建的目次里还有.. 与 先创建的目次的inode也有一组映射。
Linux 新建的目次一定有 . 和文件名两个硬链接,假如在新建的目次下再创建一个目次,那么前一个新建的目次会有..硬链接指向它。
删除一个硬链接,只是文件的inode的引用计数-1,假如引用计数不为0,那么删除一个硬链接并不影响文件(所以文件备份可以不进行拷贝,只需要创建硬链接即可)

根目次的 . 指向自身,根目次的 .. 也指向自身。根目次的硬链接数至少为 3(/和.和..),其中一个硬链接来源于根目次自身,另一个来源于根目次下的 .和.. 。根目次的 .. 仅对根目次自身硬链接数有贡献,和其他目次硬链接计数无关。在系统中,/dev 目次挂载在根目次下,/dev 的 .. 指向根目次。

    软链接(符号链接):软链接是一个独立的文件(拥有自己独立的inode),它包罗了指向另一个文件或目次的路径信息。软链接有点雷同于 Windows 系统中的快捷方式,它只是提供了一个指向目标文件的指针,而不是直接指向文件的数据块
  
特点:
  

  • 可以超过文件系统:软链接存储的是目标文件的路径,所以可以指向不同文件系统中的文件或目次
  • 可以对目次创建软链接:常用于在不同目次之间创建快捷访问方式,方便用户操纵。
  • 原文件删除后软链接失效假如原文件被删除或移动,软链接将无法找到目标文件,从而成为一个无效的链接,通常会显示为 “broken symbolic link”。
  • 有自己独立的 inode:软链接本身是一个独立的文件,有自己的 inode 和文件属性,与目标文件的 inode 不同。
  创建软链接的命令:
使用  命令:ln -s 来创建软链接。比方,ln -s file1 file2会创建一个名为file2的软链接,指向file1。
比方:在为当前目次的myfile1目次的myproc的可执行步伐在当前目次创建软链接proc


软链接的文件类型显示为l。

删除软链接:
1、使用rm
  2、使用unlink
  
应用场景:
创建软链接,就不用将可执行步伐下载到/usr/bin目次下:

  
    环状路径问题
  
硬链接:
为什么不允许为目次创建硬链接?
硬链接是指多个文件名指向同一个 inode。对于文件来说,这是可行的,但对于目次来说,会引发以下问题:
  

  • Linux 文件系统是一个树状结构,每个目次都有一个明确的父目次(通过 .. 表示)。假如允许为目次创建硬链接,大概会导致目次树中出现循环(环路)。比方:

    • 目次 A 中包罗硬链接到目次 B。
    • 目次 B 中包罗硬链接到目次 A。
    • 这样,遍历目次树时会出现无限循环,破坏文件系统的完备性。


  
软链接:
为什么允许为目次创建软链接?
  软链接(符号链接)是一个特殊的文件,它包罗另一个文件或目次的路径。与硬链接不同,软链接不会直接指向 inode,因此不会引发上述问题。

软链接的本质?
  

  • 软链接是一个独立的文件,它的内容是目标路径。
  • 操纵系统会分析软链接的内容,跳转到目标路径。
  • 由于软链接不直接涉及 inode 的引用计数,因此不会破坏文件系统的树状结构。
  
  
二、动静态库

1.什么是库

   库是写好的现有的,成熟的,可以复用的代码。现实中每个步伐都要依赖很多底子的底层库,不大概每个代码都从零开始。
本质上来说库是一种可执行代码的二进制形式,可以被操纵系统载入内存执行。库有两种:
  

  • 静态库:.a[Linux]、.lib[windows]
  • 动态库:.so[Linux]、.dll[windows]
  

  2.用于打包的示例代码

实现简易的IO操纵:
  1. //my_stdio.h
  2. #define SIZE 1024
  3. #define FLUSH_NONE 0 //不缓存
  4. #define FLUSH_LINE 1 //行缓存
  5. #define FLUSH_FULL 2 //全缓存
  6. typedef struct IO_FILE
  7. {
  8.     int flag; //刷新方式
  9.     int fileno; //文件描述符
  10.     int cap; //容量
  11.     int size; //大小
  12.     char outbuffer[SIZE]; //缓冲区
  13. }mFILE;
  14. //操作
  15. mFILE *mfopen(const char *filename,const char *mode);
  16. int mfwrite(const void *ptr,int num,mFILE *stream);
  17. void mfflush(mFILE *stream);
  18. void mfclose(mFILE *stream);
  19. //
  20. int my_strlen(const char *str);
复制代码
  1. //my_stdio.c
  2. #include "my_stdio.h"
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <sys/stat.h>
  6. #include <sys/types.h>
  7. #include <unistd.h>
  8. #include <fcntl.h>
  9. mFILE *mfopen(const char *filename,const char *mode)
  10. {
  11.     int fd = -1;
  12.     if(strcmp(mode,"r") == 0)
  13.         fd = open(filename,O_RDONLY);
  14.     else if(strcmp(mode,"w") == 0)
  15.         fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
  16.     else if(strcmp(mode,"a") == 0)
  17.         fd = open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);
  18.     if(fd < 0)
  19.         return NULL;
  20.     mFILE *mf = (mFILE *)malloc(sizeof(mFILE));
  21.     if(mf == NULL)
  22.     {
  23.         close(fd);
  24.         return NULL;
  25.     }
  26.     mf->fileno = fd;
  27.     mf->cap = SIZE;
  28.     mf->size = 0;
  29.     mf->flag = FLUSH_LINE;
  30.     return mf;
  31. }
  32. int mfwrite(const void *ptr,int num,mFILE *stream)
  33. {
  34.     memcpy(stream->outbuffer,ptr,num);
  35.     stream->size += num;
  36.     //检测是否刷新
  37.     if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size - 1] == '\n')
  38.         mfflush(stream);
  39.     return num;
  40.    
  41. }
  42. void mfflush(mFILE *stream)
  43. {
  44.     if(stream->size > 0)
  45.     {
  46.         write(stream->fileno,stream->outbuffer,stream->size);//写入
  47.         //刷新到外设
  48.         fsync(stream->fileno);
  49.         stream->size = 0;
  50.     }
  51. }
  52. void mfclose(mFILE *stream)
  53. {
  54.     //关闭前刷新
  55.     if(stream->size > 0)
  56.         mfflush(stream);
  57.     close(stream->fileno);
  58. }
  59. int my_strlen(const char *str)
  60. {
  61.     const char *end = str;
  62.     while(*end != '\0')
  63.         end++;
  64.     return end - str;
  65. }
复制代码
  1. #include "my_stdio.h"
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. int main()
  5. {
  6.     mFILE *mf = mfopen("test.txt","w");
  7.     if(mf == NULL)
  8.     {
  9.         printf("open file failed\n");
  10.         return 1;
  11.     }
  12.     char *str = "hello world\n";
  13.     int len = my_strlen(str);
  14.     mfwrite(str,len,mf);
  15.     mfclose(mf);
  16.     return 0;
  17. }
复制代码
3.静态库

   静态库(.a):步伐在编译链接的时候把库的代码链接到可执行文件中,步伐运行的时候将不再需要静态库。
一个可执行步伐大概用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默以为动态链接库只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc的-static 强转设置链接静态库。
    命令:ar -rc 库名 .o文件聚集,ar用于打包静态库,r选项表示假如该静态库存在就replace,c选项表示假如该静态库不存在就create
留意:库名必须是lib为前缀,静态库以.a为后缀,动态库以.so为后缀,在.a可以跟上.0.1等表示版本。
makefile例子:
  1. libmystdio.a:my_stdio.o
  2.         @ar -rc $@ $^
  3.         @echo "build $^ to $@ ... done"
  4. %.o:%.c
  5.         @gcc -c $<
  6.         @echo "compling $< to $@... done"
  7. .PHONY:clean
  8. clean:
  9.         @rm -rf *.a *.o stdc*
  10.         @echo "clean done"
  11. .PHONY:output
  12. output:
  13.         @mkdir -p stdc/include
  14.         @mkdir -p stdc/lib
  15.         @cp -f *.h stdc/include
  16.         @cp -f *.a stdc/lib
  17.         @tar -czf stdc.tar.gz stdc
  18.         @echo "output done"
复制代码
  使用:只要有.h头文件和静态库文件就可以使用了
 
  使用1: 安装静态库和.h到系统
将.h文件拷贝到/usr/include(安装的本质就是拷贝,编译器查找头文件就在这个路径);再将库文件拷贝到/lib64。(因为是从系统查找头文件,所以使用<>) /lib64是/usr/lib64的软链接,在这个目次下,有非常多的库,用gcc/g++进行编译时,对于gcc/g++编译器来说,我们自己写的库属于第三方库,用gcc/g++时需要指定第三方库,使用选项:-l库名(库名需要去掉前缀lib和后缀.a), 比方:gcc main.c -lmystdio,原库名为libmystdio.a

  
使用2:只有静态库和.h文件且都在当前路径,但不安装到系统
头文件不在系统了,gcc查找不在系统路径/usr/include的头文件,需要 "" 来引用头文件;gcc查找头文件先去系统路径下找,再去当前路径下找。 gcc查静态库时,不会在当前路径查找,只在系统路径/usr/ib64下查找, gcc选项 -L路径 表示gcc去当前指定路径下查找库,同时选项 -l库名(库名去掉前缀和后缀) 表示使用指定的库。 比方:gcc main.c -o main -L. -lmystdio

  
使用3:打包静态库,将库和头文件压缩并发布(使用带路径的库)

  
gcc默认查找头文件先去系统路径/usr/include查找,再去当前路径下查找,假如需要这两个以外的头文件,就需要用选项 -I头文件(大写i)路径 指明查找头文件的路径。 库文件不但需要指明库文件所在路径(在系统路径/usr/lib64就不需要指明),还要指明哪个库 比方:gcc main.c -o main -Istdc/include -Lstdc/lib -lmystdio

静态库使用小总结:
  

  • 静态库默认查找路径为/usr/lib64,默认不会主动到其他路径查找
  • 使用其他路径的静态库,需要使用 -L路径 指定,比方:-L. (在当前路径查找)
  • 无论是默认路径还是指定路径,使用哪一个静态库都需要具体指定,使用 -l静态库(去掉lib前缀和.a后缀),比方:-lmystdio,原库名:libmystdio.a。
  • -I(大写的i):指定头文件搜索路径
  4.动态库 

   

  • 动态库(.so):步伐在运行的时候才去链接动态库的代码,多个步伐共享使用库的代码
  • 一个与动态库链接的可执行文件仅仅包罗它用到的函数入口地址的一个表而不是外部函数所在目标文件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操纵系统从磁盘上的该动态库中复制到内存中这个过程称为动态链接(dynamic linking)
  • 动态库可以在多个步伐间共享,所以动态链接使得可执行文件更小,节流了磁盘空间。操纵系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节流了内存和磁盘空间。
    动态库名称:前缀lib开头,后缀.so结尾,在.so可以跟上.0.1版本号
 
  打包动态库:使用gcc/g++(可以生成可执行文件,还能生成动态库),使用选项 -shared 表明生成动态库而不是可执行文件
比方:
.c -> .o(需要特殊处理,形成与位置无关码,使用选项-fPIC):
gcc -fPIC -c my_stdio.c
gcc -fPIC -c my_string.c
.o -> 打包成库(需要用选项-shared表明生成动态库而不是可执行文件):
gcc -o libmystdio.so my_stdio.o my_string.o -shared

makefile的例子:
  1. libmystdio.so:my_stdio.o
  2.         @gcc -o $@ $^ -shared
  3.         @echo "build $^ to $@ ... done"
  4. %.o:%.c
  5.         @gcc -fPIC -c $<
  6.         @echo "compling $< to $@... done"
  7. .PHONY:clean
  8. clean:
  9.         @rm -rf *.so *.o stdc*
  10.         @echo "clean done"
  11. .PHONY:output
  12. output:
  13.         @mkdir -p stdc/include
  14.         @mkdir -p stdc/lib
  15.         @cp -f *.h stdc/include
  16.         @cp -f *.so stdc/lib
  17.         @tar -czf stdc.tar.gz stdc
  18.         @echo "output done"
复制代码
    动态库使用:
  使用1:安装动态库和头文件到系统;
需要指明库名称(选项:-l库名称(去前缀和后缀)):gcc main.c -lmystdio
  命令:ldd 可执行步伐,查看可执行步伐依赖哪些库
  所有命令依赖c标准库动态库,删掉就运行不起来了;
  动态库不可以任意删除,会影响可执行步伐执行;
  静态库可以删除,不影响可执行步伐执行;
  
  使用2:只有静态库和.h文件且都在当前路径,不安装到系统
  头文件查找:默认先查找系统路径,再查找当前路径,其他路径需要指明
库文件查找:默认查找系统路径,不会查找当前路径,除系统路径外都得指明,且无论使用哪个路径下(包括系统路径)具体的库也需要指明(指明方式与静态库一样)
例子:gcc main.c -o main -L. -lmystdio,但这个命令对于动态库是存在问题的,
  
  使用3:打包动态库,将库和头文件压缩并发布(使用带路径的库)

  
与静态库一样,指明头文件路径,指明动态库路径,指明具体的动态库,但假如直接使用gcc main.c -o main -Istdc/include -Lstdc/lib -lmystdio,会发现可执行步伐已生成,但缺失动态库

缘故原由:上面指明动态库路径和具体动态库只是告诉了编译器,所以编译能通过(可执行步伐可以或许形成的缘故原由),但步伐运行时OS要加载你的步伐,动态库的具体信息只告诉了编译器并没有告诉OS,OS使用动态库时也会去系统路径/usr/lib64下查找,此时的库不在系统路径下,所以OS找不到,导致动态库缺失。

解决方法:给系统指定动态库路径,
法1、拷贝到系统默认路径,好比/usr/lib64,/usr/lib;
  
  法2、在系统默认路径创建软链接;
  
  法3、Linux系统中,OS查找库默认去系统路径是因为存在环境变量:
LD_LIBRARY_PATH,所以可以修改环境变量;
假如没有该环境变量,就需要导入,export LD_LIBRARY_PATH = 路径(多个路径用:隔开),假如需要永久存在该变量,就需要添加到配置文件中(以前讲过)。
  
  法4、为了让OS自主找到这个动态库,使用全局的配置路径/etc/ld.so.conf.d
在/etc/ld.so.conf.d路径下创建以.conf为后缀的配置文件,把动态库的路径写入该配置文件中(需要root权限),再执行命令ldconfig(需要root权限)更新配置路径就可以让系统找到这个动态库(关掉shell也没事)
   

  •  假如同时提供.so和.a,gcc/g++默认使用动态库,假如需要使用静态库,就需要使用选项-static

  • 假如要强制静态链接(-static),必须提供静态库
  • 假如只提供静态库,但链接方式是动态链接的,gcc/g++只能针对你的.a局部性采用静态链接 (静态库并不是直接将整个库拷贝进去,而是用到什么方法使用什么方法)
  

三、目标文件

通过编译生成目标文件,实在就是通过编译翻译成CPU可以或许直接运行的机器代码。在讲用到的目标文件和库通过链接器链接形成可执行步伐。

比方:存在一个main.c和一个func.c,main.c调用了func.c的函数,通过gcc -c来分别编译这两个源文件,可以得到main.o和func.o这两个目标文件,末了通过链接main.c可以找打func.c的函数。

 当一个工程有很多个源文件,假如我们其中一个文件进行了修改,需要重新编译的就只有它这一个文件生成目标文件,不需要浪费时间重新编译整个工程。
目标文件是一个二进制文件,文件的格式ELF,是对二进制代码的一种封装。



四、ELF文件

   1.定义与用途

  ELF(Executable and Linkable Format),即可执行与可链接格式 ,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式的标准。它在操纵系统中充当着关键脚色,是步伐从源代码到可执行步伐过程中告急的中间表示形式。
    2.文件类型

  

  • 可重定位文件(.o)
    这类文件包罗着可以与其他目标文件进行链接的代码和数据。在软件开辟过程中,当我们对多个源文件分别进行编译时,每个源文件通常会生成对应的可重定位文件。比方,有main.c和func.c两个源文件,使用gcc -c main.c和gcc -c func.c命令编译后,会分别得到main.o和func.o,它们就是可重定位文件。这些文件中的代码和数据还未进行地址的终极确定在链接阶段会根据需要进行重定位操纵,以便组合成可执行文件大概共享目标文件。
  • 可执行文件
    是经过链接器将多个可重定位文件以及大概用到的库文件等链接在一起后,生成的可以或许直接在操纵系统上运行的步伐。它包罗了完备的步伐执行所需的指令和数据,并且地址已经确定,操纵系统可以按照其规定的格式和结构加载到内存中执行。好比我们一样平常使用的各种应用步伐,像文本编辑器、浏览器等,本质上都是可执行文件。
  • 共享目标文件(.so)
    也称为共享库,是一种特殊的目标文件。它可以在运行时被多个步伐共享使用。比方,许多步伐都会用到的 C 标准库libc.so,它包罗了诸如printf、malloc等常用函数的实现。当步伐需要使用这些函数时,不需要在自身内部重复包罗这些函数的代码,而是在运行时动态链接到libc.so库,调用其中的函数。这样做的利益是节流了内存空间,进步了代码的复用性。
  • 内核转储文件(core dumps)
    当步伐在运行过程中发生异常(如段错误、非法指令等)导致崩溃时,操纵系统可以将此时进程的内存映像、寄存器状态等执行上下文信息保存到一个文件中,这个文件就是内核转储文件。开辟人员可以借助调试工具(如gdb)加载内核转储文件,分析步伐崩溃时的状态,查找导致步伐异常的缘故原由。
    3.文件结构

  

  • ELF 头(ELF header)
    位于 ELF 文件的起始位置,它就像是文件的 “目次索引”,包罗了文件的基本信息,如文件类型(是可重定位文件、可执行文件还是共享目标文件等)、机器类型(如 x86、ARM 等)、字节序(大端序还是小端序) ,以及步伐头表和节头表的位置和大小等告急信息。通过读取 ELF 头,操纵系统或工具可以快速相识文件的团体结构和基本属性,为后续的加载、链接等操纵提供底子信息。
  • 步伐头表(Program header table)
    对于可执行文件和共享目标文件而言,步伐头表至关告急。它描述了文件中各个段(segment)的信息是 ELF 文件在内存中的映射单元步伐头表记录了每个段的类型(如代码段、数据段等)、在文件中的偏移量、大小、在内存中的起始地址、权限(可读、可写、可执行等)等(定位一个段,偏移量+起始地址)。操纵系统在加载可执行文件时,会依据步伐头表的信息将文件的各个段准确地映射到内存中,为步伐的执行做好准备。
  • 节头表(Section header table)
    节(section)是 ELF 文件中更细粒度的构成单元,节头表则提供了对各个节的具体描述。它记录了每个节的名称、类型、在文件中的偏移量、大小、链接信息(假如节与其他节存在关联关系)等。不同的节用于存储不同类型的数据,比方代码节(.text)存放机器指令,数据节(.data)存放已初始化的全局变量和局部静态变量,符号表节(.symtab)存放符号信息(函数名、变量名等及其相关属性)等。在链接过程中,链接器会根据节头表的信息来处理各个节之间的关系,如重定位操纵就需要依据节头表找到相关的节进行处理(比方函数调用需要用到符号表)。
  • 节(Section)

    • 代码节(.text):也叫文本段,是步伐执行的核心部分,其中存储着经过编译生成的机器指令。当步伐被加载到内存并开始执行时,CPU 会从代码节中读取指令并执行。代码节通常具有只读和可执行的权限,以防止步伐在运行过程中意外修改自身的指令代码。
    • 数据节(.data):用于保存已初始化的全局变量和局部静态变量。这些变量在步伐运行前已经被赋予了初始值,它们在步伐的生命周期内不停存在于内存中,供步伐中的函数访问和修改。
    • .bss 节:存放未初始化的全局变量和局部静态变量。与.data 节不同的是,.bss 节在 ELF 文件中并不占用实际的磁盘空间,只是在步伐加载到内存时,操纵系统会为其分配相应大小的内存空间并初始化为 0。这样做可以节流磁盘空间,因为未初始化的变量在步伐开始执行前并不需要实际的初始值存储。
    • .rodata 节:即只读数据节,主要存储常量字符串和其他只读数据。比方在 C 语言中,const char *str = "hello world"; 中的字符串常量 "hello world" 就大概存储在.rodata 节中。该节具有只读属性防止步伐意外修改这些常量数据。
    • 符号表节(.symtab):记录了步伐中定义和引用的符号信息,包括函数名、变量名、全局符号、局部符号等。每个符号都有对应的名称、类型(函数、变量等)、所在节的索引、在节内的偏移量等信息。符号表在链接过程中起着关键作用,链接器通过符号表来分析不同目标文件之间的符号引用关系,确保步伐中各个部分可以或许准确地相互调用和访问数据。
    • 重定位节(.rel.text、.rel.data 等)重定位是将目标文件中未确定的地址信息进行修正的过程重定位节记录了在链接过程中需要进行重定位的位置和相关信息。比方,当一个可重定位文件中的代码引用了另一个目标文件中的函数或变量时,在链接之前这个引用的地址是不确定的,重定位节就会记录下这些需要修正地址的位置,链接器根据这些信息对目标文件进行调解,使终极生成的可执行文件或共享目标文件中的地址准确无误。

  

  4.ELF形成可执行的过程

 

.o生成可执行的大概流程:

  • 将多份 C/C++ 源代码,翻译成为目标 .o 文件
  • 将多份 .o 文件section进行合并(合并是在链接时进行)
  • 符号分析:链接器会扫描所有输入文件的符号表,将不同目标文件中雷同符号进行合并和分析。对于未定义的外部符号引用(即引用了其他目标文件或库中的符号),链接器会在符号表中查找该符号的定义
  • 重定位:根据符号分析的结果,对目标文件中未确定的地址进行修正。好比,将目标文件中对函数或变量的引用地址修改为它们在终极生成的可执行文件中的准确地址。


5.ELF可执行文件加载



  • 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
  • 合并原则:雷同属性,好比:可读,可写,可执行,需要加载时申请空间等
  • 这样,即便是不同的Section(节),在加载到内存中,大概会以segment(段)的形式,加载到一起
  • 很显然,这个合并工作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在ELF的 步伐头表(Program header table)中
   使用命令查看可执行步伐的Section(显示节头表信息):
  1. readelf -S 可执行程序 //查看可执行程序的Section
复制代码
  1. ayanami@EVA:~/myhome/myfile1$ readelf -S myproc
  2. There are 37 section headers, starting at offset 0x81d8:
  3. Section Headers:
  4.   [Nr] Name              Type             Address           Offset
  5.        Size              EntSize          Flags  Link  Info  Align
  6.   [ 0]                   NULL             0000000000000000  00000000
  7.        0000000000000000  0000000000000000           0     0     0
  8.   [ 1] .interp           PROGBITS         0000000000000318  00000318
  9.        000000000000001c  0000000000000000   A       0     0     1
  10.   [ 2] .note.gnu.pr[...] NOTE             0000000000000338  00000338
  11.        0000000000000030  0000000000000000   A       0     0     8
  12.   [ 3] .note.gnu.bu[...] NOTE             0000000000000368  00000368
  13.        0000000000000024  0000000000000000   A       0     0     4
  14.   [ 4] .note.ABI-tag     NOTE             000000000000038c  0000038c
  15.        0000000000000020  0000000000000000   A       0     0     4
  16.   [ 5] .gnu.hash         GNU_HASH         00000000000003b0  000003b0
  17.        0000000000000028  0000000000000000   A       6     0     8
  18.   [ 6] .dynsym           DYNSYM           00000000000003d8  000003d8
  19.        0000000000000138  0000000000000018   A       7     1     8
  20.   [ 7] .dynstr           STRTAB           0000000000000510  00000510
  21.        000000000000016e  0000000000000000   A       0     0     1
  22.   [ 8] .gnu.version      VERSYM           000000000000067e  0000067e
  23.        000000000000001a  0000000000000002   A       6     0     2
  24.   [ 9] .gnu.version_r    VERNEED          0000000000000698  00000698
  25.        0000000000000050  0000000000000000   A       7     2     8
  26.   [10] .rela.dyn         RELA             00000000000006e8  000006e8
  27.        0000000000000120  0000000000000018   A       6     0     8
  28.   [11] .rela.plt         RELA             0000000000000808  00000808
  29.        0000000000000060  0000000000000018  AI       6    24     8
  30.   [12] .init             PROGBITS         0000000000001000  00001000
  31.        000000000000001b  0000000000000000  AX       0     0     4
  32.   [13] .plt              PROGBITS         0000000000001020  00001020
  33.        0000000000000050  0000000000000010  AX       0     0     16
  34.   [14] .plt.got          PROGBITS         0000000000001070  00001070
  35.        0000000000000010  0000000000000010  AX       0     0     16
  36.   [15] .plt.sec          PROGBITS         0000000000001080  00001080
  37.        0000000000000040  0000000000000010  AX       0     0     16
  38.   [16] .text             PROGBITS         00000000000010c0  000010c0
  39.        00000000000001b1  0000000000000000  AX       0     0     16
  40.   [17] .fini             PROGBITS         0000000000001274  00001274
  41.        000000000000000d  0000000000000000  AX       0     0     4
  42.   [18] .rodata           PROGBITS         0000000000002000  00002000
  43.        0000000000000014  0000000000000000   A       0     0     4
  44.   [19] .eh_frame_hdr     PROGBITS         0000000000002014  00002014
  45.        0000000000000044  0000000000000000   A       0     0     4
  46.   [20] .eh_frame         PROGBITS         0000000000002058  00002058
  47.        00000000000000ec  0000000000000000   A       0     0     8
  48.   [21] .init_array       INIT_ARRAY       0000000000003d78  00002d78
  49.        0000000000000010  0000000000000008  WA       0     0     8
  50.   [22] .fini_array       FINI_ARRAY       0000000000003d88  00002d88
  51.        0000000000000008  0000000000000008  WA       0     0     8
  52.   [23] .dynamic          DYNAMIC          0000000000003d90  00002d90
  53.        0000000000000200  0000000000000010  WA       7     0     8
  54.   [24] .got              PROGBITS         0000000000003f90  00002f90
  55.        0000000000000070  0000000000000008  WA       0     0     8
  56.   [25] .data             PROGBITS         0000000000004000  00003000
  57.        0000000000000010  0000000000000000  WA       0     0     8
  58.   [26] .bss              NOBITS           0000000000004040  00003010
  59.        0000000000000118  0000000000000000  WA       0     0     64
  60.   [27] .comment          PROGBITS         0000000000000000  00003010
  61.        000000000000002b  0000000000000001  MS       0     0     1
  62.   [28] .debug_aranges    PROGBITS         0000000000000000  0000303b
  63.        0000000000000030  0000000000000000           0     0     1
  64.   [29] .debug_info       PROGBITS         0000000000000000  0000306b
  65.        000000000000263c  0000000000000000           0     0     1
  66.   [30] .debug_abbrev     PROGBITS         0000000000000000  000056a7
  67.        0000000000000629  0000000000000000           0     0     1
  68.   [31] .debug_line       PROGBITS         0000000000000000  00005cd0
  69.        000000000000016a  0000000000000000           0     0     1
  70.   [32] .debug_str        PROGBITS         0000000000000000  00005e3a
  71.        0000000000001710  0000000000000001  MS       0     0     1
  72.   [33] .debug_line_str   PROGBITS         0000000000000000  0000754a
  73.        00000000000002cc  0000000000000001  MS       0     0     1
  74.   [34] .symtab           SYMTAB           0000000000000000  00007818
  75.        0000000000000498  0000000000000018          35    25     8
  76.   [35] .strtab           STRTAB           0000000000000000  00007cb0
  77.        00000000000003ba  0000000000000000           0     0     1
  78.   [36] .shstrtab         STRTAB           0000000000000000  0000806a
  79.        000000000000016a  0000000000000000           0     0     1
  80. Key to Flags:
  81.   W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  82.   L (link order), O (extra OS processing required), G (group), T (TLS),
  83.   C (compressed), x (unknown), o (OS specific), E (exclude),
  84.   D (mbind), l (large), p (processor specific)
复制代码
团体结构
  

  • 输出首先表明该 ELF 文件有 37 个节头,起始偏移量为 0x81d8。
  节头信息字段阐明
  

  • [Nr]:节的编号,从 0 开始。
  • Name:节的名称,如.interp、.text、.data等,每个名称代表不同的寄义和用途。
  • Type:节的类型,常见的类型有PROGBITS(步伐数据)、DYNSYM(动态符号表)、STRTAB(字符串表)等。
  • Address:节在内存中的虚拟地址。假如该节在文件中没有对应的内容(如.bss节),则地址大概为 0。
  • Offset:节在文件中的偏移量,即从文件开头到该节的字节数。
  • Size:节的大小,以字节为单元。
  • EntSize:节中每个条目标大小。比方,在符号表节中,每个符号条目都有固定的大小。
  • Flags:节的标志位,用于描述节的属性,如W(可写)、A(可分配)、X(可执行)等。
  • Link:与该节相关的其他节的索引。比方,动态符号表节的Link字段大概指向字符串表节。
  • Info:依赖于节类型的附加信息。比方,在重定位节中,Info字段大概包罗重定位的目标符号索引。
  • Align:节的对齐要求,以字节为单元。比方,值为 8 表示节需要在 8 字节边界上对齐。
    使用命令查看Section合并的segment(显示步伐头表信息): 
  1. readelf -l 可执行程序 //查看Section合并的segment
复制代码
  1. ayanami@EVA:~/myhome/myfile1$ readelf -l myproc
  2. Elf file type is DYN (Position-Independent Executable file)
  3. Entry point 0x10c0
  4. There are 13 program headers, starting at offset 64
  5. Program Headers:
  6.   Type           Offset             VirtAddr           PhysAddr
  7.                  FileSiz            MemSiz              Flags  Align
  8.   PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
  9.                  0x00000000000002d8 0x00000000000002d8  R      0x8
  10.   INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
  11.                  0x000000000000001c 0x000000000000001c  R      0x1
  12.       [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  13.   LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
  14.                  0x0000000000000868 0x0000000000000868  R      0x1000
  15.   LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
  16.                  0x0000000000000281 0x0000000000000281  R E    0x1000
  17.   LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
  18.                  0x0000000000000144 0x0000000000000144  R      0x1000
  19.   LOAD           0x0000000000002d78 0x0000000000003d78 0x0000000000003d78
  20.                  0x0000000000000298 0x00000000000003e0  RW     0x1000
  21.   DYNAMIC        0x0000000000002d90 0x0000000000003d90 0x0000000000003d90
  22.                  0x0000000000000200 0x0000000000000200  RW     0x8
  23.   NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
  24.                  0x0000000000000030 0x0000000000000030  R      0x8
  25.   NOTE           0x0000000000000368 0x0000000000000368 0x0000000000000368
  26.                  0x0000000000000044 0x0000000000000044  R      0x4
  27.   GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
  28.                  0x0000000000000030 0x0000000000000030  R      0x8
  29.   GNU_EH_FRAME   0x0000000000002014 0x0000000000002014 0x0000000000002014
  30.                  0x0000000000000044 0x0000000000000044  R      0x4
  31.   GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
  32.                  0x0000000000000000 0x0000000000000000  RW     0x10
  33.   GNU_RELRO      0x0000000000002d78 0x0000000000003d78 0x0000000000003d78
  34.                  0x0000000000000288 0x0000000000000288  R      0x1
  35. Section to Segment mapping:
  36.   Segment Sections...
  37.    00     
  38.    01     .interp
  39.    02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
  40.    03     .init .plt .plt.got .plt.sec .text .fini
  41.    04     .rodata .eh_frame_hdr .eh_frame
  42.    05     .init_array .fini_array .dynamic .got .data .bss
  43.    06     .dynamic
  44.    07     .note.gnu.property
  45.    08     .note.gnu.build-id .note.ABI-tag
  46.    09     .note.gnu.property
  47.    10     .eh_frame_hdr
  48.    11     
  49.    12     .init_array .fini_array .dynamic .got
复制代码
团体信息
  

  • Elf file type is DYN (Position - Independent Executable file):表示该 ELF 文件类型是动态链接的可执行文件,即位置无关的可执行文件。
  • Entry point 0x10c0:指定了步伐的入口点地址,即步伐开始执行的地址。
  • There are 13 program headers, starting at offset 64:阐明文件中有 13 个步伐头,步伐头表的起始偏移量为 64 字节。
  步伐头信息
  

  • Type:表示步伐头的类型,常见类型有PHDR(步伐头表自身)、INTERP(步伐表明器路径)、LOAD(加载段)等。
  • Offset:是该段在文件中的偏移量。
  • VirtAddr:是该段在虚拟内存中的地址。
  • PhysAddr:是该段在物理内存中的地址(对于大多数现代操纵系统,物理地址通常与虚拟地址雷同或相关)。
  • FileSiz:是该段在文件中的大小。
  • MemSiz:是该段在内存中的大小,大概大于文件中的大小,比方包罗未初始化的数据段(.bss)。
  • Flags:表示该段的权限和属性,如R(可读)、W(可写)、E(可执行)。
  • Align:表示该段在内存中的对齐方式。
  各步伐头具体分析
  

  • PHDR:

    • 描述了步伐头表本身的信息,其在文件中的偏移量为 0x40,虚拟地址和物理地址也是 0x40,大小为 0x2d8 字节,权限为只读,对齐方式为 8 字节。

  • INTERP:

    • 表示步伐表明器的路径,这里是/lib64/ld - linux - x86 - 64.so.2,偏移量为 0x318,大小为 0x1c 字节。

  • LOAD:

    • 有多个LOAD段,每个LOAD段表示一个可加载到内存的段。
    • 比方,第一个LOAD段从文件偏移 0x0 开始,加载到虚拟地址 0x0,大小为 0x868 字节,权限为只读,对齐方式为 0x1000 字节。它包罗了一些只读的初始化数据和代码,如.interp、.note等节。
    • 第二个LOAD段从文件偏移 0x1000 开始,加载到虚拟地址 0x1000,大小为 0x281 字节,具有可读和可执行权限,包罗了步伐的代码段,如.init、.plt、.text等节。

  • DYNAMIC:

    • 用于动态链接的信息,偏移量为 0x2d90,虚拟地址为 0x3d90,大小为 0x200 字节,权限为读写,对齐方式为 8 字节。

  • NOTE:

    • 有两个NOTE段,包罗了一些辅助信息,如编译器版本、目标系统等。

  • GNU_PROPERTY:

    • 与 GNU 特定的属性相关。

  • GNU_EH_FRAME:

    • 用于异常处理和睁开帧信息。

  • GNU_STACK:

    • 表示栈段,这里大小为 0,阐明栈的大小是根据运行时的需要动态分配的,权限为读写。

  • GNU_RELRO:

    • 用于标记只读数据段在加载时是否需要进行重定位。

  段到节的映射
  

  • Section to Segment mapping展示了每个步伐头所包罗的节。比方,Segment 02包罗了.interp、.note.gnu.property等多个节,这些节在内存中是被一起加载和管理的。
  为什么要将section合并成为segment
  

  • Section合并的主要缘故原由是为了减少页面碎片进步内存使用效率。假如不进行合并假设页面大小为4096字节(内存块基本大小,加载,管理的基本单元),假如.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
  • 此外,操纵系统在加载步伐时,会将具有雷同属性的section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
    使用命令查看ELF头的信息:
  1. readelf -h ELF格式文件 //显示ELF文件的文件头信息
复制代码
  1. ayanami@EVA:~/myhome/myfile1$ readelf -h myproc
  2. ELF Header:
  3.   Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  4.   Class:                             ELF64
  5.   Data:                              2's complement, little endian
  6.   Version:                           1 (current)
  7.   OS/ABI:                            UNIX - System V
  8.   ABI Version:                       0
  9.   Type:                              DYN (Position-Independent Executable file)
  10.   Machine:                           Advanced Micro Devices X86-64
  11.   Version:                           0x1
  12.   Entry point address:               0x10c0
  13.   Start of program headers:          64 (bytes into file)
  14.   Start of section headers:          33240 (bytes into file)
  15.   Flags:                             0x0
  16.   Size of this header:               64 (bytes)
  17.   Size of program headers:           56 (bytes)
  18.   Number of program headers:         13
  19.   Size of section headers:           64 (bytes)
  20.   Number of section headers:         37
  21.   Section header string table index: 36
复制代码

  • ELF 文件头魔数(Magic):魔数是 ELF 文件的标识符,开头四个字节是 0x7F、0x45、0x4C、0x46(即 ASCII 码的 DEL、E、L、F),表明这是一个 ELF 文件。背面的字节代表文件其他属性,如 02 表示 64 位 ELF 文件,01 表示当前 ELF 版本等。
  • ELF 文件的类别(Class):显示为 ELF64,阐明该 ELF 文件是 64 位的,与之相对的 ELF32 表示 32 位的 ELF 文件。
  • 数据编码方式(Data):采用二进制补码表示有符号整数,且是小端字节序。小端字节序指数据低位字节存于内存低地址,高位字节存于内存高地址。
  • ELF 版本(Version):这里是 1(current),代表 ELF 文件依照当前版本(版本号为 1)的 ELF 标准。
  • 操纵系统 / ABI(OS/ABI):显示为 UNIX - System V,阐明该 ELF 文件是为依照 UNIX System V ABI(应用二进制接口)的操纵系统计划的,很多类 UNIX 系统如 Linux 都依照这个 ABI。
  • ABI 版本(ABI Version):ABI 版本号为 0。
  • ELF 文件类型(Type):是 DYN(Position-Independent Executable file),即 DYN 表示该文件是一个动态链接的可执行文件,也就是位置无关的可执行文件(PIE),能在内存任意位置加载并执行,无需在链接或加载时重定位。
  • 目标机器架构(Machine):是 Advanced Micro Devices X86-64,表明该 ELF 文件是为 AMD 的 x86 - 64 架构处理器计划的,这种架构广泛用于现代个人盘算机和服务器。
  • ELF 文件版本(Version):再次显示为 0x1,夸大 ELF 文件版本是 1。
  • 入口点地址(Entry point address):为 0x10c0,这是步伐开始执行的内存地址,操纵系统加载并运行该 ELF 文件时,控制权会转移到这个地址开始执行步伐代码。
  • 步伐头表的起始位置(Start of program headers):是 64(bytes into file),步伐头表描述 ELF 文件中各个段的信息,这里表示步伐头表从文件第 64 个字节开始。
  • 节头表的起始位置(Start of section headers):是 33240(bytes into file),节头表描述 ELF 文件中各个节的信息,即节头表从文件第 33240 个字节开始。
  • 文件头标志(Flags):值为 0x0,标志位用于存储与目标机器相关的额外信息,这里表示没有设置特殊标志。
  • 文件头大小(Size of this header):为 64(bytes),即 ELF 文件头大小是 64 字节。
  • 步伐头表项的大小(Size of program headers):是 56(bytes),每个步伐头表项大小为 56 字节。
  • 步伐头表项的数量(Number of program headers):有 13 个,意味着该 ELF 文件有 13 个段。
  • 节头表项的大小(Size of section headers):是 64(bytes),每个节头表项大小为 64 字节。
  • 节头表项的数量(Number of section headers):有 37 个,即该 ELF 文件有 37 个节。
  • 节头字符串表的索引(Section header string table index):为 36,节头字符串表存储各个节的名称,这里表示节头字符串表对应的节头表项索引是 36。
  
五、理解链接与加载

1.静态链接

无论是自己的.o文件还是静态库中的.o文件,本质上都是把.o文件进行毗连的过程;所以研究静态链接本质就是研究.o是怎样链接的。
   有如下文件:

  
查看编译后的.o目标文件:
使用命令:objdump -d .o目标文件,objdump -d命令将代码段(.text)进行反汇编查看
  

  通过这里两个call指令,它们分别是printf和run函数,但它们的跳转地址都被设置为0,这是为什么? --  实在就是在编译main.c和func.c的时候,编译器完全不知道run函数和printf函数的存在(不知道位于内存的哪个区块,代码是什么),此时编译器只能将这两个函数的跳转地址先临时设为0。
  这个被设为0的地址,在链接的时候会进行修正,为了让毗连器未来在链接时可以或许准确定位到这些被修正的地址,在代码块(.data)中还存在一个重定位表,这张表未来在链接时,就会根据表里记录的地址将其修正。
  
验证过程(验证上图的run函数):
1、通过上图可知:run函数在main.o中的地址为0,即多个.o相互不知道对方。
2、读取func.o和main.c的符号表:

puts:就是printf的实现,run是我们自己定义的方法在main.o中未定义(定义在func.o中)
UND:就是undefine,表示未定义,说白了就是在本.o文件找不到
  3、读取main(可执行文件)的符号表:

4、读取main(可执行文件)终极的所有的section清单:

  5、通过反汇编证实main.o和func.o背面的00 00 00 00有没有被修改成为具体的终极函数地址。

终极这两个.o的代码合并到了一起,并进行了统一的编址
链接的时候,会修改.o中没有确定的函数地址,在合并完成之后,进行相关call地址,完成代码调用。(静态链接就是把库中.o进行合并,和上述过程一样)
  所以链接实在就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在一起之后,链接器会根据我们的.o文件大概静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这实在就是静态链接的过程。 链接过程中会涉及到对.o中外部符号进行地址重定位。


2.ELF加载与进程地址空间

   虚拟地址/逻辑地址 :

Q1:一个ELF步伐,在没有被加载到内存的时候,有没有地址呢?
一个ELF步伐,在没有被加载到内存的时候,本来就有地址,今世盘算机工作的时候,都采用"平展模式"进行工作(平展模式是一种内存结构方式,在平展模式下,ELF 文件中的各个段(如代码段、数据段等)在内存中是一连排列的,没有特殊的分段或分页机制将它们分隔开。这种模式使得步伐在内存中的结构相对简单和直接,便于操纵系统对步伐进行加载和执行。。所以也要求ELF对自己的代码和数据进行统一编址。
下面是objdump -s 反汇编之后的代码:

赤色框最左侧就是ELF的虚拟地址,实在,严格意义上应该叫做逻辑地址(起始地址 + 偏移量),但我们暂且以为起始地址是0,也就是说,实在虚拟地址在我们的步伐还没有加载到内存的时候,就已经把可执行步伐进行统一编址了。
  
Q2:进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从那里来到?
从ELF各个segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start,end]等范围数据,另外在用具体地址,填充页表。
  
这里可以得出一个结论:虚拟地址机制,不但仅OS要支持,编译器也要支持。
    重新理解进程虚拟地址空间:

ELF编译好之后,会把自己未来步伐的入口地址记录在ELF header的Entry字段中:

  
ELF文件怎样映射到虚拟地址空间?
操纵系统在加载 ELF 文件时,会根据步伐头表中的信息将 ELF 文件的各个段映射到进程虚拟地址空间的相应地区。
  

  • 代码段映射 -- ELF 文件中的代码段会被映射到进程虚拟地址空间的代码段地区,这样步伐的可执行指令就可以被 CPU 执行。
  • 数据段和 BSS 段映射 -- 数据段会被映射到进程虚拟地址空间的数据段地区,而 BSS 段会被映射到 BSS 段地区,并且在加载时会被初始化为 0。
  • 共享库映射 -- 假如 ELF 文件依赖于共享库,操纵系统会将共享库加载到进程虚拟地址空间的共享库地区,并进行符号分析和重定位,使得步伐可以调用共享库中的函数和使用其中的变量。
虚拟内存与物理内存通过页表的映射关系联系。  
  
CPU处理:  

  • 地址转换:CPU 在访问内存时,会将虚拟内存地址发送给内存管理单元(MMU)。MMU 会根据页表进行地址转换,将虚拟地址转换为物理地址,然后再从物理内存中读取数据。这个过程对于 CPU 来说是透明的,CPU 只需要使用虚拟内存地址进行操纵,而无需关心实际的物理内存地址。
  • 掩护机制:CPU 还使用页表实现内存掩护。通过页表中的标志位,CPU 可以判断当前访问的内存是否具有相应的权限,如是否可读、可写或可执行。假如进程试图访问没有权限的内存地区,CPU 会引发异常,由操纵系统进行处理,从而防止进程非法访问其他进程的内存空间或系统关键地区,包管系统的稳定性和安全性。
  
  虚拟地址空间是操纵系统,CPU,编译器共同协作下的产物。
为什么要有虚拟地址和虚拟地址空间?-- 除了之前的说法,还有使编译器不需要思量物理内存的情况,编译器只需要使用平展模式从全0到全F进项编址,编址之后,加载到内存使用虚拟地址以线性的方式对待整个代码和数据,此时就把操纵系统和编译器进行解耦了。
 
  并不是所有的section都加载到内存,每加载一个section,就在虚存创建映射关系(创建一个vm_area_struct指明start和end,并毗连到mm_struct),所以可执行步伐加载就可以按section数据节为单元来加载;一个section并非一下子全部加载进去,而是分批加载,通过创建和开释vm_area_struct来进行分批加载。
 
  最开始加载可执行步伐时,先加载了入口地址,接下来CPU进行虚拟到物理转化寻址时,假如触发缺页中断,再去懒加载section。
    3. 动态链接与动态库加载

   进程怎样看到动态库的?
  

内存映射:共享库XXX.so从磁盘加载到物理内存。通过虚拟地址,进程 A 能看到共享库在其虚拟地址空间的共享区。页表负责虚拟地址到物理地址的转换,使得进程 A 可以通过虚拟地址访问物理内存中加载的共享库代码和数据 。这样,多个进程可以共享同一份物理内存中的共享库实例,节流内存空间。具体过程:
  1. 进程启动与动态链接器加载
  当你执行一个可执行文件时,内核首先会读取该可执行文件的头部信息,辨认出它是否是动态链接的。假如是,内核会将控制权转交给动态链接器(通常是 /lib64/ld-linux-x86-64.so.2 这类文件)。具体而言,内核会把动态链接器加载到进程的地址空间,并将步伐的执行控制权传递给它。动态链接器负责后续共享库的查找、加载和链接工作。
  2. 查找共享库
  动态链接器会按照特定的搜索路径来查找可执行文件所依赖的共享库,搜索路径的优先级如下:
  

  • LD_LIBRARY_PATH 环境变量:这是用户自定义的共享库搜索路径。用户可以通过设置该环境变量,让动态链接器优先在指定的目次中查找共享库。比方,你可以在终端中执行 export LD_LIBRARY_PATH=/path/to/your/libsLD_LIBRARY_PATH 来添加自定义路径。
  • 可执行文件的 RUNPATH 或 RPATH:这两个字段记录在可执行文件的头部信息中。RPATH 是在链接时指定的固定搜索路径,而 RUNPATH 是在运行时使用的搜索路径,并且 RUNPATH 的优先级高于 RPATH。可以使用 patchelf 工具修改这些字段。
  • 系统默认路径:包括 /lib 和 /usr/lib 等,这些是系统预定义的共享库存放位置。
  • /etc/ld.so.cache:这是一个由 ldconfig 工具生成的缓存文件,它记录了系统中可用共享库的位置信息。动态链接器会先在这个缓存中查找所需的共享库,以进步查找效率。
  3. 加载共享库到物理内存
  一旦动态链接器找到了所需的共享库,就会使用 mmap 系统调用将共享库文件的内容映射到物理内存中。mmap 会在物理内存中分配一块一连的地区,并将共享库文件的内容从磁盘复制到该地区。这里采用的是按需分页(Demand Paging)的计谋,即并不是一次性将整个共享库加载到内存,而是在进程初次访问某个页面时才将其加载进来。
  4. 映射到进程的虚拟地址空间
  动态链接器会在进程的虚拟地址空间中为共享库分配一块一连的虚拟地址范围,通常是在进程的共享区(Shared Region)。然后,通过修改进程的页表,创建虚拟地址与物理内存中共享库页面的映射关系。这样,进程就可以通过虚拟地址来访问共享库的代码和数据。
  5. 符号分析与重定位
  

  • 符号分析:共享库和可执行文件中都包罗符号表,符号表记录了函数、变量等符号的名称和地址信息。动态链接器会分析可执行文件中对共享库符号的引用,在共享库的符号表中查找这些符号的实际地址。
  • 重定位:由于共享库在不同进程中的加载地址大概不同,因此需要对共享库和可执行文件中的代码和数据进行重定位。重定位的过程就是修改代码和数据中的地址引用,使其指向共享库中符号的实际地址。
  6. 初始化共享库
  一些共享库大概包罗初始化代码,这些代码会在共享库加载到进程地址空间后主动执行。比方,C++ 中的全局对象构造函数、__attribute__((constructor)) 修饰的函数等。动态链接器会负责调用这些初始化代码,确保共享库在使用前完成须要的初始化工作。
  7. 执行进程
  当所有共享库都加载完成、符号分析和重定位工作完成,并且共享库初始化也完成后,动态链接器会将控制权交还给可执行文件的入口点,进程开始正式执行。
    进程间怎样共享库的?
  

  

  • 虚拟内存与页表机制:每个进程都有独立的虚拟地址空间,通过页表将虚拟地址映射到物理地址。共享库被加载到物理内存后,动态链接器在各进程的虚拟地址空间中为其分配虚拟地址范围,并在进程页表中创建虚拟地址与共享库物理页面的映射。不同进程的虚拟地址可对应同一物理内存中的共享库页面,实现代码共享,节流内存空间。好比进程 A 和进程 B 都使用了 C 标准库,在物理内存中 C 标准库只需一份副本,通过页表映射,两个进程都能访问。
  • 动态链接与加载

    • 动态链接器作用:启动可执行文件时,内核加载动态链接器(如ld.so),由它负责共享库的查找、加载和链接。好比步伐依赖libpng.so库,动态链接器会按规则找到该库。
    • 查找共享库:动态链接器按LD_LIBRARY_PATH环境变量、可执行文件的RUNPATH或RPATH字段、系统默认路径(如/lib 、/usr/lib )、/etc/ld.so.cache缓存文件的顺序查找共享库。
    • 加载与映射:找到共享库后,使用mmap系统调用加载到物理内存,并映射到进程虚拟地址空间,还会进行符号分析与重定位,让进程能准确访问库中的函数和数据。

  • 共享库的特性 :

    • 位置无关代码:共享库编译时使用-fpic(位置无关代码)选项,生成的代码可在运行时被加载到任意虚拟地址处,多个进程能共享同一份库代码,而不用担心地址辩论问题。
    • 数据共享与隔离:固然共享库代码可被多个进程共享,但库中的全局和静态变量,每个进程通常拥有自己的副本,相互隔离。若要在进程间共享库中的数据,可借助 POSIX 共享内存等进程间通信机制,将需共享的数据映射到共享内存地区。

    什么是位置无关、位置无关代码(PIC)、位置无关可执行文件(PIE)?
  
位置无关(Position Independent)是指代码和数据在加载和运行时不依赖于特定的内存地址。
  
  位置无关代码(Position Independent Code,PIC)
  概念:
  位置无关代码是一种特殊的代码生成方式,使用这种方式生成的代码可以在内存的任意地址加载和执行,而不需要对代码中的地址引用进行修改。在共享库的场景中,由于多个进程大概会同时加载同一个共享库,并且每个进程的地址空间是独立的,共享库在不同进程中的加载地址大概不同,因此共享库通常需要使用 PIC 技术来实现。
  实现原理:
  

  • 全局偏移表(Global Offset Table,GOT):对于共享库中对全局变量和函数的引用,会通过 GOT 来间接访问GOT 是一个在数据段中的表每个条目存储着一个全局变量或函数的实际地址。今世码需要访问全局变量或调用函数时,会先通过相对地址访问 GOT 中的相应条目,再从条目中获取实际地址。这样,无论共享库加载到哪个地址,只需要更新 GOT 中的地址,而代码本身不需要修改。
  • 过程链接表(Procedure Linkage Table,PLT):用于处理对外部函数的调用。当第一次调用外部函数时,会通过 PLT 跳转到动态链接器,由动态链接器分析函数的实际地址,并将其填充到 GOT 中。后续的调用就可以直接通过 GOT 中的地址进行,避免了每次调用都进行符号分析的开销。
  编译选项:
  在 GCC 编译器中,可以使用-fpic或-fPIC选项来生成位置无关代码。其中,-fpic适用于生成较小的代码,而-fPIC则更加通用,适用于所有情况。比方:
  1. gcc -fpic -c mylib.c -o mylib.o
  2. gcc -shared mylib.o -o libmylib.so
复制代码
GOT的具体运作过程:
  1、GOT 的创建与初始化
  

  • 在编译阶段,编译器会为每个需要动态链接的全局变量和函数在 GOT 中分配一个条目。这些条目通常是按照一定的顺序排列的,以便在运行时可以或许快速地查找和访问。
  • 当共享库被加载到内存时,动态链接器会根据可执行文件的重定位信息,对 GOT 中的条目进行初始化。对于全局变量,动态链接器会将其初始化为变量在内存中的实际地址;对于函数,动态链接器会将其初始化为函数的入口地址。
  2、对全局变量的访问
  

  • 当步伐需要访问一个全局变量时,它会通过 GOT 来间接访问。具体来说,步伐会先根据变量的名称或索引,在 GOT 中找到对应的条目。
  • 然后,步伐会从 GOT 条目中获取全局变量的实际地址,并通过该地址来访问变量的值。由于 GOT 中的地址是在运行时动态确定的,因此无论共享库被加载到内存的什么位置,步伐都可以或许准确地访问全局变量。
  3、对函数的调用
  

  • 当步伐需要调用一个动态链接的函数时,它也会通过 GOT 来实现。首先,步伐会根据函数的名称或索引,在 GOT 中找到对应的条目。
  • 与全局变量不同的是,函数的 GOT 条目在第一次调用时通常是一个指向过程链接表(PLT)中相应条目标地址。当步伐第一次调用函数时,会通过 GOT 条目跳转到 PLT 中的相应条目。
  • 在 PLT 中,会有一段代码用于调用动态链接器,由动态链接器分析函数的实际地址,并将其填充到 GOT 中对应的条目中。这样,在后续的函数调用中,步伐就可以直接通过 GOT 中的实际地址来调用函数,而不需要再次通过 PLT 和动态链接器进行分析,进步了函数调用的效率。
  
  位置无关可执行文件(Position Independent Executable,PIE)
  概念:
  位置无关可执行文件是可执行文件的一种,它的代码和数据也可以在内存的任意地址加载和执行。与传统的可执行文件不同,PIE 文件在加载时会被随机地放置在内存的不同地址,从而增加了步伐的安全性,防止一些基于固定地址的攻击,如缓冲区溢出攻击。
  实现原理:
  PIE 的实现原理与 PIC 雷同,同样使用了 GOT 和 PLT 来处理地址引用。不同的是,PIE 是针对整个可执行文件而言的,而 PIC 主要用于共享库。
  编译选项:
  在 GCC 编译器中,可以使用-fPIE和-pie选项来生成位置无关可执行文件。比方:
  1. gcc -fPIE -c myprogram.c -o myprogram.o
  2. gcc -pie myprogram.o -o myprogram
复制代码
   动态链接
  

  这里的 libc.so 是 C 语言的运行时库,里面提供了常用的标准输入输出文件字符串处理等等这些功能。
  那为什么编译器默认不使用静态链接呢?静态链接会将编译产生的所有目标文件,连同用到的各种库,合并形成一个独立的可执行文件,它不需要额外的依赖就可以运行。照理来说应该更加方便才对是吧?
  静态链接最大的问题在于生成的文件体积大,并且相当泯灭内存资源。随着软件复杂度的提拔,我们的操纵系统也越来越臃肿,不同的软件就有大概都包罗了雷同的功能和代码,显然会浪费大量的硬盘空间。
  这个时候,动态链接的上风就体现出来了,我们可以将需要共享的代码单独提取出来,保存成一个独立的动态链接库,比及步伐运行的时候再将它们加载到内存,这样不但可以节流空间,因为同一个模块在内存中只需要保留一份副本,可以被不同的进程所共享。
  动态链接到底是怎样工作的??
  首先要交代一个结论,动态链接实际上将链接的整个过程推迟到了步伐加载的时候。好比我们去运行一个步伐,操纵系统会首先将步伐的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操纵系统会根据当前地址空间的使用情况为它们动态分配一段内存。
  当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以去修动态库中的那些函数跳转地址了。
  
  在 C/C++ 步伐中,当步伐开始执行时,它首先并不会直接跳转到 main 函数。实际上,步伐的入口点是_start,这是一个由 C 运行时库(通常是 glibc)或链接器(如 ld)提供的特殊函数。
   在_start 函数中,会执行一系列初始化操纵,这些操纵包括:
  

  • 设置堆栈:为步伐创建一个初始的堆栈环境。
  • 初始化数据段:将步伐的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
  • 动态链接:这是关键的一步,_start 函数会调用动态链接器的代码来分析和加载步伐所依赖的动态库(shared libraries)。动态链接器会处理所有的符号分析和重定位,确保步伐中的函数调用和变量访问可以或许准确地映射到动态库中的实际地址。

    • 动态链接器:

      • 动态链接器(如 ld - linux.so)负责在步伐运行时加载动态库。
      • 当步伐启动时,动态链接器会分析步伐中的动态库依赖,并加载这些库到内存中。

    • 环境变量和配置文件:

      • Linux 系统通过环境变量(如 LD_LIBRARY_PATH)和配置文件(如 /etc/ld.so.conf 及其子配置文件)来指定动态库的搜索路径。
      • 这些路径会被动态链接器在加载动态库时搜索。

    • 缓存文件:

      • 为了进步动态库的加载效率,Linux 系统会维护一个名为 /etc/ld.so.cache 的缓存文件。
      • 该文件包罗了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。


  • 调用__libc_start_main:一旦动态链接完成,_start 函数会调用__libc_start_main(这是 glibc 提供的一个函数)。__libc_start_main 函数负责执行一些额外的初始化工作,好比设置信号处理函数、初始化线程库(假如使用了线程)等。
  • 调用 main 函数:末了,__libc_start_main 函数会调用步伐的 main 函数,此时步伐的执行控制权才正式交给用户编写的代码。
  • 处理 main 函数的返回值:当 main 函数返回时,__libc_start_main 会负责处理这个返回值,并终极调用_exit 函数来停止步伐。
  上述过程描述了 C/C++ 步伐在 main 函数之前执行的一系列操纵,但这些操尴尬刁难于大多数步伐员来说是透明的。步伐员通常只需要关注 main 函数中的代码,而不需要关心底层的初始化过程。然而,相识这些底层细节有助于更好地理解步伐的执行流程和调试问题。
  
  动态库中的相对地址
  动态库为了随时进行加载,为了支持并映射到任意进程的任意位置,对动态库中的方法统一编址,采用相对编址的方案进行编址的(实在可执行步伐也一样,都要服从平展模式,只不外exe是直接加载的)。
  
  进程与库的映射:
  动态库也是一个文件,要访问也是要被先加载,要加载也是要先被打开;所以让进程找到动态库的本质就是文件操纵,不外我们访问库函数时,需要通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中。

  
  步伐进行库函数调用:
  

  • 库已经被我们映射到了当前进程的地址空间中
  • 库的虚拟起始地址我们也已经知道了
  • 库中每一个方法的偏移量地址我们也知道
  • 所以,访问库中任意方法,只需要知道库的起始虚拟地址+方法偏移量即可定位库中的方法
  • 而且,整个调用过程,是从代码区跳转到共享区调用完毕在返回到代码区整个过程完全在进程地址空间中进行的
  

  
  全局偏移量表GOT(global offset table):
  

  • 也就是说,我们的步伐运行之前,先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道
  • 然后对我们加载到内存中的步伐的库函数调用进行地址修改,在内存中二次完成地址设置这个叫做加载地址重定位)
  • 等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?
  所以,动态链接采用的做法是在.data(可执行步伐大概库自己)中专门预留一片地区用来存放函数的跳转地址,它也被叫做全局偏移表GOT表中每一项都是本运行模块要引用的一个全局变量或函数的地址。
  因为.data地区是可读写的,所以支持动态修改
  

   .got在加载的时候,会和.data合并成一个segment,然后加载在一起:

  

  

  • 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。
  • 在单个.so下,由于GOT表与.text 的相对位置是固定的,我们完全可以使用CPU的相对寻址来找到GOT表。
  • 在调用函数的时候会首先查表,然后根据表中 的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。
  • 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都可以或许正常运行,并且可以或许被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的缘故原由,PIC=相对编址+GOT。
  4.库间依赖



  • 不但仅有可执行步伐调用库
  • 库也会调用其他库!!库之间是有依赖的,怎样做到库和库之间相互调用也是与地址无关的呢??
  • 库中也有.GOT, 和可执行一样!这也就是为什么各人为什么都是 ELF 的格式!

由于 GOT 表中的映射地址会在运行时去修改,我们可以通过 gdb 调试去观察 GOT 表的地址变革。在这里我们只用知道原理即可,有兴趣可以参考:使用 gdb 调试 GOT


  • 由于动态链接在步伐加载的时候需要对大量函数进行重定位,这一步显然黑白常耗时的。为了进一步降低开销,我们的操纵系统还做了一些其他的优化,好比延迟绑定,大概也叫 PLT(过程毗连表(Procedure Linkage Table))。与其在步伐一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数大概在步伐运行期间一次都不会被使用到。
    思路是:GOT 中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码 /stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新 GOT 表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。


六、总结 



  • 静态链接的出现,进步了步伐的模块化程度。对于一个大的项目,不同的人可以独立地测试和开辟自己的模块。通过静态链接,生成终极的可执行文件。
  • 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。
  • 而动态链接实际上将链接的整个过程推迟到了步伐加载的时候。好比我们去运行一个步伐,操纵系统会首先将步伐的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

农民

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表