当前位置: 首页 > article >正文

【Linux】动/静态库

目录

1. 整体学习思维导图

2. 库是什么

2.1 库的概念

2.2 库的后缀区分

3. 库的制作

3.1 静态库

3.1.1 使用.o文件连接生成.exe文件

3.1.2 生成一个静态库 

3.1.3 使用一个静态库 

3.2 动态库

3.2.1 生成一个动态库

3.2.2 使用一个动态库

总结:

4. ELF文件

4.1 什么是ELF文件

4.1.1 以下文件格式为ELF:

4.1.1 什么是ELF文件:

4.2 静态链接,研究.o文件如何链接

4.3 ELF文件的加载,ELF -> 进程

4.3.1 我们知道只有当一个可执行程序执行时才会加载到内存,如果加载到内存他是否存在地址?

4.3.2 进程与ELF的联系 

4.4 动态库如何与我们进程关联

4.5 动态库如何加载

4.5.1 全局偏移量表GOT(globaloffsettable)

4.5.2 库与库之间的调用

4.5.3 PLT


1. 整体学习思维导图

2. 库是什么

2.1 库的概念

库是一些函数实现的二进制代码,他们是可以直接执行,库的内容是一些我们经常需要使用的一些方法如:printf/scanf这类函数,我们要加快开发速率不可能再去实现一遍,我们一般都是使用c库封装好的,包上头文件直接使用!

2.2 库的后缀区分

  • 对于Linux系统

    • 静态库 .a

    • 动态库 .so

  • 对于Windows系统

    • 静态库 .lib

    • 动态库 .dll

3. 库的制作

总体概念(无论动静态库)

  • 动/静态库不要实现main函数

  • 头文件.h是对源文件实现方法的说明书

  • 所有的库(动/静)都是源文件的,以.o后缀结尾

3.1 静态库

  1. 静态库的本质就是对.o文件进行一个打包,静态库.a->归档文件->使用时不需要进行解包->gcc/g++直接使用即可!

3.1.1 使用.o文件连接生成.exe文件

场景一:ouyang同学实现了MyFile/MyStrlen,niuma同学不想实现想要直接使用,ouyang同学给他传来.o文件,他连接自己实现的main函数调用即可!

  • ouyang同学生成.o文件拷贝给niuma同学,使用说明书.h文件也需要拷贝过去

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ouyang]$ cd ../niuma/
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 20
-rw-rw-r-- 1 ouyang ouyang  517 Mar  3 16:11 main.c
-rw-rw-r-- 1 ouyang ouyang  602 Mar  3 16:14 MyFile.h
-rw-rw-r-- 1 ouyang ouyang 3400 Mar  3 16:14 MyFile.o
-rw-rw-r-- 1 ouyang ouyang   49 Mar  3 16:14 MyStrlen.h
-rw-rw-r-- 1 ouyang ouyang 1272 Mar  3 16:14 MyStrlen.o
  • niuma同学生成自己的main.o文件进行连接生成可执行程序.exe

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc *.o
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 44
-rwxrwxr-x 1 ouyang ouyang 13352 Mar  3 16:30 a.out
-rw-rw-r-- 1 ouyang ouyang   514 Mar  3 16:22 main.c
-rw-rw-r-- 1 ouyang ouyang   514 Mar  3 16:22 main_cp.c
-rw-rw-r-- 1 ouyang ouyang  2248 Mar  3 16:30 main.o
-rw-rw-r-- 1 ouyang ouyang   602 Mar  3 16:14 MyFile.h
-rw-rw-r-- 1 ouyang ouyang  3400 Mar  3 16:14 MyFile.o
-rw-rw-r-- 1 ouyang ouyang    49 Mar  3 16:14 MyStrlen.h
-rw-rw-r-- 1 ouyang ouyang  1272 Mar  3 16:14 MyStrlen.o
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ./a.out 
len = 14
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ cat log.txt 
Hello MyFile!
Hello MyFile!
Hello MyFile!
Hello MyFile!
Hello MyFile!

3.1.2 生成一个静态库 

ar -rc mylibc.a *.o
# rc --> replace and create 归档所有.o文件,如果存在就覆盖,不存在就创建
# mylibc.a 静态库的名称 使用时需要去掉lib和.a

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 12
-rw-rw-r-- 1 ouyang ouyang  514 Mar  3 16:22 main.c
-rw-rw-r-- 1 ouyang ouyang 2009 Mar  3 16:45 stdc.tgz

3.1.3 使用一个静态库 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/ -L stdc/lib/ -lmylibc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ./main 
len = 14
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls
log.txt  main  main.c  makefile  stdc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ cat log.txt 
Hello MyFile!
Hello MyFile!
Hello MyFile!
Hello MyFile!
Hello MyFile!

我们会发现我们在使用gcc编译时,带上了两个选项和路径,这是为什么呢?

  • -I,-L -lmylibc

我们试一试不带这两个选项会发生什么?

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c 
main.c:4:20: fatal error: MyFile.h: No such file or directory
 #include "MyFile.h"
                    ^
compilation terminated.

我们发现main.c文件找不到对应的头文件,这是因为头文件和main.c不在同一路径中!而我们系统默认去自己的头文件库中找不存在就会报错,因此-I是告诉gcc(默认在系统和当前目录下找寻)去哪个路径下找寻头文件。 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/
/tmp/ccCjcIhR.o: In function `main':
main.c:(.text+0x13): undefined reference to `Myfopen'
main.c:(.text+0x69): undefined reference to `Myfwrite'
main.c:(.text+0x75): undefined reference to `Myflush'
main.c:(.text+0x8e): undefined reference to `Myfclose'
main.c:(.text+0x9a): undefined reference to `MyStrlen'
collect2: error: ld returned 1 exit status

我们现在的问题是找不到头文件对应实现的方法了,我们同样的需要告诉gcc我们要使用哪个库进行连接。 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/ -lmylibc
/usr/bin/ld: cannot find -lmylibc
collect2: error: ld returned 1 exit status

我们告诉了gcc找寻mylibc库进行连接,但是系统默认是在/user/bin/ld寻找,同样的我们的库并没实现在其中我们需要告诉我们库对应的位置,使用-L+路径。 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/ -L stdc/lib/ -lmylibc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ldd main
        linux-vdso.so.1 =>  (0x00007fff5d559000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fa970340000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa97070e000)
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls
main  main.c  makefile  stdc

我们通过以上过程可以发现,库需要安装到系统中才方便使用,而库安装-->本质就是拷贝对应的系统之中。

我们也发现ldd我们的可执行程序并没有我们的静态库的链接,这是因为我们的静态库已经拷贝至可执行程序中了,这也是为什么静态链接的可执行程序空间大的原因,包括即使我们删除了之前的静态库,程序依旧可以执行的原因!

3.2 动态库

3.2.1 生成一个动态库

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ouyang]$ make
gcc -fPIC -c MyFile.c
gcc -fPIC -c MyStrlen.c
gcc -o libmylibc.so MyFile.o MyStrlen.o -shared
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ouyang]$ ll
total 48
-rwxrwxr-x 1 ouyang ouyang 12864 Mar  3 19:33 libmylibc.so
-rw-rw-r-- 1 ouyang ouyang   259 Mar  3 19:33 makefile
-rw-rw-r-- 1 ouyang ouyang   245 Mar  3 17:22 makefile_static
-rw-rw-r-- 1 ouyang ouyang  2084 Mar  3 15:56 MyFile.c
-rw-rw-r-- 1 ouyang ouyang   602 Mar  3 15:56 MyFile.h
-rw-rw-r-- 1 ouyang ouyang  3456 Mar  3 19:33 MyFile.o
-rw-rw-r-- 1 ouyang ouyang   129 Mar  3 16:00 MyStrlen.c
-rw-rw-r-- 1 ouyang ouyang    49 Mar  3 16:00 MyStrlen.h
-rw-rw-r-- 1 ouyang ouyang  1272 Mar  3 19:33 MyStrlen.o

3.2.2 使用一个动态库

我们按照使用静态库的方式生成一个可执行程序

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ gcc -o main main.c -I stdc/include/ -L stdc/lib/ -lmylibc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
total 24
-rwxrwxr-x 1 ouyang ouyang 8800 Mar  3 19:40 main
-rw-rw-r-- 1 ouyang ouyang  514 Mar  3 16:22 main.c
-rw-rw-r-- 1 ouyang ouyang   94 Mar  3 19:39 makefile
drwxrwxr-x 4 ouyang ouyang 4096 Mar  3 19:34 stdc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ./main 
./main: error while loading shared libraries: libmylibc.so: cannot open shared object file: No such file or directory

我们按照我们之前的方式成功生成可执行程序,但是却不可以执行,查看main发现没有找到我们自己封装的动态库,这是为什么呢?因为我们前面的指令都是告诉gcc我们的库在什么位置,我们现在的命令是执行可执行程序,我们并没有告诉我们的可执行程序我们的动态库位置,因此导致了动态库找不到的问题!

  • 解决方案:

  1. 拷贝至系统:我们发现我们并没有告诉main可执行程序我们c标准库的位置,它依然找到,说明默认会去系统查找,我们只需要拷贝至系统就可以链接了

    1. 拷贝 .so 文件到系统共享库路径下,⼀般指 /usr/lib、/usr/local/lib、/lib64

  2. 建立软连接:向系统共享库路径下建立同名软连接,连接指向我们实现的动态库

  3. 更改环境变量: LD_LIBRARY_PATH (没有可以自己创建一个)

    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ echo $LD_LIBRARY_PATH 
    :/home/ouyang/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ lib]$ pwd
    /home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ lib]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ lib]$ echo $LD_LIBRARY_PATH 
    :/home/ouyang/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ lib]$ cd ..
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ stdc]$ cd ..
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ll
    total 24
    -rwxrwxr-x 1 ouyang ouyang 8800 Mar  3 19:40 main
    -rw-rw-r-- 1 ouyang ouyang  514 Mar  3 16:22 main.c
    -rw-rw-r-- 1 ouyang ouyang   94 Mar  3 19:39 makefile
    drwxrwxr-x 4 ouyang ouyang 4096 Mar  3 19:34 stdc
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ldd main
            linux-vdso.so.1 =>  (0x00007fff30141000)
            libmylibc.so => /home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib/libmylibc.so (0x00007fa667a86000)
            libc.so.6 => /lib64/libc.so.6 (0x00007fa6676b8000)
            /lib64/ld-linux-x86-64.so.2 (0x00007fa667c89000)
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ./main 
    len = 14
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls
    log.txt  main  main.c  makefile  stdc

    当然我们重启之后这个临时配置的环境变量就会消失,要永久保存需要系统配置。

    4. ldconfig方案:配置/etc/ld.so.conf.d/ , ldconfig更新生效。

    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls /etc/ld.so.conf.d/
    kernel-3.10.0-957.21.3.el7.x86_64.conf  kernel-3.10.0-957.el7.x86_64.conf  mysql-x86_64.conf
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ sudo touch /etc/ld.so.conf.d/mylibso.conf
    [sudo] password for ouyang: 
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ls /etc/ld.so.conf.d/
    kernel-3.10.0-957.21.3.el7.x86_64.conf  kernel-3.10.0-957.el7.x86_64.conf  mylibso.conf  mysql-x86_64.conf

     这个操作需要管路员才能操作!

    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ cat /etc/ld.so.conf.d/mylibso.conf 
    /home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ldd main
            linux-vdso.so.1 =>  (0x00007fff6c2e8000)
            libmylibc.so => /home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_3_3_lib/niuma/stdc/lib/libmylibc.so (0x00007f3bb5efe000)
            libc.so.6 => /lib64/libc.so.6 (0x00007f3bb5b30000)
            /lib64/ld-linux-x86-64.so.2 (0x00007f3bb6101000)

    总结:

    1. gcc/g++默认会去使用动态库(动静态库同时存在时)

    2. 如果动静态库同时存在非要静态链接需要带上 -static选项

    3. 如只存在静态库,无论加不加-static,都使用静态链接

    4. ELF文件

    4.1 什么是ELF文件

    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_3_5]$ file main.exe 
    main.exe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=f509ef97646a4c0b361e15ac85eda3db9048a110, not stripped
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_3_5]$ file code.o 
    code.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_3_5]$ file main.o
    main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ ouyang]$ file libmylibc.so 
    libmylibc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=6c459975831b4db7227b252daf3b14bdb33d70e2, not stripped

    通过以上代码我们会发现一个问题,可执行文件.exe和重定位目标文件.o文件类型都是ELF类型的文件,那么哪些文件是ELF文件,什么是ELF文件?

    4.1.1 以下文件格式为ELF

    • 可重定向目标文件:以.o为结尾的文件

    • 可执行程序文件:可以执行的文件

    • 共享目标文件:以.so结尾的文件,即动态库文件

    4.1.1 什么是ELF文件:

    我们先了解ELF文件的组成部分:

    我们之前所认知的可执行文件.exe我们简单的说是由代码+数据组成!其实可执行文件作为ELF格式文件,是由多个.o文件链接形成的,由于文件类型的相同其中少不了把多个文件合并成一个文件的过程!

    1. 多个section(节)会合并成segment(段)

    2. 那么将节合并成段需要一个合并规则,规范合并哪些,怎么合并需要有一个准则标准,这个合并的原则存在于Program Header Table之中

    我们拿两段简单代码来观察合并之前的准备,其中我们需要用到以下命令和代码:

    • 代码:

      /* main.c */
      #include <stdio.h>
      
      extern void Run();
      
      int main()
      {
          Run();
          printf("I am main.c\n");
          return 0;
      }
      /* code.c */
      #include <stdio.h>
      
      void Run()
      {
          printf("Runing......!\n");
      }
    • readelf -S _ELF文件_ -- 读取section Header Table(这是一个数组,其中包含对section的描述)

    • readelf -l _ELF文件_ -- 读取 Program Headers

     section Header Table中描述了各个节的信息。

     红框中的描述是权限R/W/E。

    从图中我们可以得知那些数据节(section)需要合并在一起成为一个段(segment)。

    3. 为什么要将section合并成一个segment

    这个问题我们需要先引进文件的加载,我们知道在Ext文件系统之中我们为了效率会一次去访问八个扇区也就是一个块(4KB大小), 而内存的每一次申请空间为了能够方便交互也是4KB大小,也就是说我们存储一个数据会出现碎片化,存储不足4KB的情况,这个空间我们叫做页面,合并可以减少页面的碎片化,如.text部分4097字节这就需要2个页面存储,此时.init部分的大小为256字节需要一个页面,一旦合并只需要2个页面即可,提高了工具利用率。

    4. section Header Table(这是一个数组,其中包含对section的描述)有什么用?

    这个节表头看起来像节的管理者,他可以清楚的告诉我们节的信息。

    • 链接视角:我们在链接.o文件需要合并section为一个segment,那么那些需要合并在一起,那些不合并在一起,section Header Table就是告诉静态链接节的信息位置,让链接去区分合并!

    • 加载视角:我们观察第二张图可以发现有描述权限的内容,这一点很关键,我们知道我们平时操作文件时需要对应的权限,这个权限是加载到内存的,但是系统怎么知道那些内容可读还是可写?这就section Header Table的作用了,他会告诉操作系统哪些该加载到什么地方,完成文件的初始化。

    5. Program Header Table的作用?

    在了解作用之前我们需要知道使用什么命令去查看这个表头:

    readelf -h _ELF文件_  # 查看 Program Header Table

    • Entry point address:描述程序的入口地址,这个有什么用呢?

    • Start of program headersprogram headers的起始位置

    • Start of section headerssection headers的起始位置

    我们发现program headers Table保存的是整个ELF文件各个部分的分布信息位置,它的主要目的是定位文件的其他部分。

    4.2 静态链接,研究.o文件如何链接

    研究链接之前我们需要得到得到链接前文件中的内容有什么:

    # 将目标文件进行反汇编
    objdump -d XXX.o > XXX.s

     

    以上我们发现.o文件中反汇编后callq去调用的函数都没有地址?地址为什么都是0?这是因为在编译时,编译器并不知道对应的函数地址在哪,只能先填充0来表示,等到后面链接时在进行地址重定位!这就是为什么我们称.o文件是可重定位目标文件。 

    readelf -s XXX.o # 简略读取

    ouyang@iZ2ze0j6dd76e0o9qypo2rZ:~/linux_-git_-warehouse/dir_2025_3_6$ readelf -s main.exe 
    
    Symbol table '.dynsym' contains 7 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
         2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
         3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
         4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
         5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
         6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
    
    Symbol table '.symtab' contains 67 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         1: 0000000000000318     0 SECTION LOCAL  DEFAULT    1 
         2: 0000000000000338     0 SECTION LOCAL  DEFAULT    2 
         3: 0000000000000358     0 SECTION LOCAL  DEFAULT    3 
         4: 000000000000037c     0 SECTION LOCAL  DEFAULT    4 
         5: 00000000000003a0     0 SECTION LOCAL  DEFAULT    5 
         6: 00000000000003c8     0 SECTION LOCAL  DEFAULT    6 
         7: 0000000000000470     0 SECTION LOCAL  DEFAULT    7 
         8: 00000000000004f2     0 SECTION LOCAL  DEFAULT    8 
         9: 0000000000000500     0 SECTION LOCAL  DEFAULT    9 
        10: 0000000000000520     0 SECTION LOCAL  DEFAULT   10 
        11: 00000000000005e0     0 SECTION LOCAL  DEFAULT   11 
        12: 0000000000001000     0 SECTION LOCAL  DEFAULT   12 
        13: 0000000000001020     0 SECTION LOCAL  DEFAULT   13 
        14: 0000000000001040     0 SECTION LOCAL  DEFAULT   14 
        15: 0000000000001050     0 SECTION LOCAL  DEFAULT   15 
        16: 0000000000001060     0 SECTION LOCAL  DEFAULT   16 
        17: 0000000000001208     0 SECTION LOCAL  DEFAULT   17 
        18: 0000000000002000     0 SECTION LOCAL  DEFAULT   18 
        19: 0000000000002020     0 SECTION LOCAL  DEFAULT   19 
        20: 0000000000002070     0 SECTION LOCAL  DEFAULT   20 
        21: 0000000000003db8     0 SECTION LOCAL  DEFAULT   21 
        22: 0000000000003dc0     0 SECTION LOCAL  DEFAULT   22 
        23: 0000000000003dc8     0 SECTION LOCAL  DEFAULT   23 
        24: 0000000000003fb8     0 SECTION LOCAL  DEFAULT   24 
        25: 0000000000004000     0 SECTION LOCAL  DEFAULT   25 
        26: 0000000000004010     0 SECTION LOCAL  DEFAULT   26 
        27: 0000000000000000     0 SECTION LOCAL  DEFAULT   27 
        28: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
        29: 0000000000001090     0 FUNC    LOCAL  DEFAULT   16 deregister_tm_clones
        30: 00000000000010c0     0 FUNC    LOCAL  DEFAULT   16 register_tm_clones
        31: 0000000000001100     0 FUNC    LOCAL  DEFAULT   16 __do_global_dtors_aux
        32: 0000000000004010     1 OBJECT  LOCAL  DEFAULT   26 completed.8061
        33: 0000000000003dc0     0 OBJECT  LOCAL  DEFAULT   22 __do_global_dtors_aux_fin
        34: 0000000000001140     0 FUNC    LOCAL  DEFAULT   16 frame_dummy
        35: 0000000000003db8     0 OBJECT  LOCAL  DEFAULT   21 __frame_dummy_init_array_
        36: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
        37: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c
        38: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
        39: 0000000000002194     0 OBJECT  LOCAL  DEFAULT   20 __FRAME_END__
        40: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
        41: 0000000000003dc0     0 NOTYPE  LOCAL  DEFAULT   21 __init_array_end
        42: 0000000000003dc8     0 OBJECT  LOCAL  DEFAULT   23 _DYNAMIC
        43: 0000000000003db8     0 NOTYPE  LOCAL  DEFAULT   21 __init_array_start
        44: 0000000000002020     0 NOTYPE  LOCAL  DEFAULT   19 __GNU_EH_FRAME_HDR
        45: 0000000000003fb8     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
        46: 0000000000001000     0 FUNC    LOCAL  DEFAULT   12 _init
        47: 0000000000001200     5 FUNC    GLOBAL DEFAULT   16 __libc_csu_fini
        48: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
        49: 0000000000004000     0 NOTYPE  WEAK   DEFAULT   25 data_start
        50: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@@GLIBC_2.2.5
        51: 000000000000116e    23 FUNC    GLOBAL DEFAULT   16 Run
        52: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   25 _edata
        53: 0000000000001208     0 FUNC    GLOBAL HIDDEN    17 _fini
        54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
        55: 0000000000004000     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
        56: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
        57: 0000000000004008     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
        58: 0000000000002000     4 OBJECT  GLOBAL DEFAULT   18 _IO_stdin_used
        59: 0000000000001190   101 FUNC    GLOBAL DEFAULT   16 __libc_csu_init
        60: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT   26 _end
        61: 0000000000001060    47 FUNC    GLOBAL DEFAULT   16 _start
        62: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
        63: 0000000000001149    37 FUNC    GLOBAL DEFAULT   16 main
        64: 0000000000004010     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
        65: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
        66: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.2

    其中UND(表示未定义),我们看图会发现两个.o对于对于调用的函数都是未定义,但是我们查看main.exe会发现对于缺失的函数地址已经进行重定位了,静态链接->.o合并,地址修正! 

    4.3 ELF文件的加载,ELF -> 进程

    4.3.1 我们知道只有当一个可执行程序执行时才会加载到内存,如果加载到内存他是否存在地址?

    • 加载模式

     过去我们对于一个可执行程序编址偏向于逻辑地址,访问时需要起始地址加偏移量,当代计算机工作的时候,都采用"平坦模式"进行工作。所以也要求ELF对自己的代码和数据进行统⼀编址,从反汇编的文件中我们可以看出这一点:

    
    main.exe:     file format elf64-x86-64
    
    
    Disassembly of section .init:
    
    0000000000001000 <_init>:
        1000:        f3 0f 1e fa                  endbr64 
        1004:        48 83 ec 08                  sub    $0x8,%rsp
        1008:        48 8b 05 d9 2f 00 00         mov    0x2fd9(%rip),%rax        # 3fe8 <__gmon_start__>
        100f:        48 85 c0                     test   %rax,%rax
        1012:        74 02                        je     1016 <_init+0x16>
        1014:        ff d0                        callq  *%rax
        1016:        48 83 c4 08                  add    $0x8,%rsp
        101a:        c3                           retq   
    
    Disassembly of section .plt:
    
    0000000000001020 <.plt>:
        1020:        ff 35 9a 2f 00 00            pushq  0x2f9a(%rip)        # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
        1026:        f2 ff 25 9b 2f 00 00         bnd jmpq *0x2f9b(%rip)        # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
        102d:        0f 1f 00                     nopl   (%rax)
        1030:        f3 0f 1e fa                  endbr64 
        1034:        68 00 00 00 00               pushq  $0x0
        1039:        f2 e9 e1 ff ff ff            bnd jmpq 1020 <.plt>
        103f:        90                           nop
    .............................................

    4.3.2 进程与ELF的联系 

     

    • 进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?从ELF各个segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start,end]等范围数据,另外在用详细地址,填充页表

    • CPU会通过Entry point address字段获取程序入口,进入CPU的地址是虚拟地址,EIP获取入口后得到虚拟地址,将虚拟地址交给CR3,通过查看页表的映射关系得到物理地址进行访问!

    所以:虚拟地址机制,不光光OS要支持,编译器也要支持!

    4.4 动态库如何与我们进程关联

    • 动态库需要被进程看见:动态库需要映射到进程的地址空间上!

    • 被进程调用:在进程的地址空间调转!

     

    整个过程:动态库加载-->物理内存-->页表映射(不会重复加载,多个进程也可以使用)-->共享库

    4.5 动态库如何加载

    • 动态链接不同于静态链接,他将链接的整个过程推迟到了程序加载时候进行!

    我们的可执行程序编译时:调用动态库中的函数方法时会先填充一个0地址,等待动态库加载到内存中,⼀旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。

    • 在我们的程序开始执行时我们第一个入口并不是main函数,程序的入口点是_start,这是⼀个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。

    _start的作用:

    1. 设置堆栈:创建一个初始的堆栈环境

    2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段

    3. 动态链接:这是关键的⼀步, _start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(sharedlibraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。

    动态链接器:

    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ niuma]$ ldd main
            linux-vdso.so.1 =>  (0x00007fff5d559000)
            libc.so.6 => /lib64/libc.so.6 (0x00007fa970340000)
            /lib64/ld-linux-x86-64.so.2 (0x00007fa97070e000)

    这个动态链接器会帮我们在程序运行时加载动态库,查找动态库会去对应的环境变量(LD_LIBRARY_PATH)或者配置文件(/etc/ld.so.conf.d/)中查找,但是每次查找都会消耗一定的资源时间,所以其内部还存在一个缓存文件用于保存已知的动态库相关信息(名称+路径等等),动态链接器会优先搜寻这个缓存文件!

    动态库也是一个ELF文件,加载到内存时也是平坦模式,然后通过页表映射关系映射到对应进程的共享区部分,但是我们知道此时进程的函数代码都保存在代码区,而代码区只是可读权限,那么怎么去修正我们调用函数的地址呢?

    4.5.1 全局偏移量表GOT(globaloffsettable)

    为了解决上面代码区不能修改的问题,我们在代码区预留了一个用来存放函数的跳转地址,它也被叫做全局偏移表GOT。

     

    • .got: 加载重定向表,GOT表对于每个进程的映射部分都是不一样的,因此每个进程都拥有一份独立的GOT表,进程之间的GOT表不可以共享!

    • 每次调用函数时,都会先进行查表跳转到对应的函数地址!

    • 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC = 相对编址 + GOT

    4.5.2 库与库之间的调用

    我们平时不止有进程对库进行调用,有时候还会出现库和库之间的调用,那么怎么理解库与库之间的关联呢?其实库中也存在.got,和可执行程序一样!这也是为什么可执行和库文件都是ELF文件!

     

    4.5.3 PLT

    • 由于动态链接在程序加载(每次加载动态库都会加载到内存的不同地址位置)的时候需要对大量函数进行重定位,这⼀步显然是非常耗时的。为了进⼀步降低开销,我们的操作系统还做了⼀些其他的优化,比如延迟绑定,或者也叫PLT(Procedure Linkage Table)->过程链接表。与其在程序⼀开始就对所有函数进行重定位,不如将这个过程推迟到函数第⼀次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间⼀次都不会被使用到。

    • 思路是:GOT中的跳转地址默认会指向⼀段辅助代码,它也被叫做桩代码/stup。在我们第⼀次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。


    http://www.kler.cn/a/584281.html

    相关文章:

  4. 重生之我在学Vue--第10天 Vue 3 项目收尾与部署
  5. Unity Lerp和InverseLerp函数用处
  6. 【C++】每日一练(用队列实现栈)
  7. 【fnOS飞牛云NAS本地部署跨平台视频下载工具MediaGo与远程访问下载视频流程】
  8. VS Code 配置优化指南
  9. 【TES817】基于XCZU19EG FPGA的高性能实时信号处理平台
  10. 【从零开始学习计算机科学】数据库系统(七)并发控制技术
  11. 元宇宙与数字孪生
  12. 如何查看mysql某个表占用的空间大小
  13. 深度学习 bert流程
  14. ClickHouse的数据引擎:解锁大数据分析的奥秘
  15. Netty基础—4.NIO的使用简介二
  16. 工控hmi医疗终端机的界面如何来设计?本文为你解答
  17. GolangTCP通信解决粘包问题
  18. JAVA中的多线程安全问题及解决方案
  19. 计算机网络-网络存储技术
  20. MySql数据库等级考试学习分享2(Day5)
  21. 深度学习----激活函数
  22. 什么是SWAP虚拟内存?使用服务器如何开启SWAP虚拟内存
  23. vue启动 localhost无法访问