当前位置:   article > 正文

动静态库(编译的基本原理)_编译动态库

编译动态库

目录

科学名词 命令

进程地址空间的共享区

拓展:

System V共享内存:

Posix共享内存:

动静态库

动态库(Dynamic Link Library)

静态库(Static Library)

归档文件

ar

ldd

编写自己的静态库

文件

输出:

建立库的步骤

编写自己的动态库

文件

实验结果:

思考

error解决

LD_LIBRARY_PATH环境变量

/etc/ld.so.conf.d/配置文件

好用的第三方库:ncurses(了解)

静态编译生成可执行文件

编译过程

步骤

符号表

在编译过程中是否有指令的地址

链接过程

第七点中的其他必要信息

重定位表

怎么使用重定位表来进行地址重定位

平坦内存模型

运行可执行文件(二谈进程地址空间)

可执行文件中存放着?

程序的运行:

程序开始运行前

在程序运行过程中操作系统也没闲着:

指令执行

函数调用

动态编译生成可执行文件与运行

我认为 : 动态链接模糊了编译和执行的过程

进程地址空间的共享区

链接过程

运行动态库可执行文件

动态编译与静态编译的区别:


科学名词 命令

进程地址空间的共享区

在进程地址空间中,共享区是指多个进程之间共享的内存区域。共享内存是一种进程间通信和数据同步的技术,它允许不同进程之间在同一块内存区域中读取和写入数据。共享内存通常用于实现多线程或多进程之间的数据交换,以提高程序的执行效率和响应速度。

在进程地址空间中实现共享内存的方法通常是在操作系统中为多个进程分配一块共享内存区域,并允许这些进程访问该区域。共享内存的实现需要解决进程间的同步和互斥问题,以避免多个进程同时访问共享内存区域时发生数据竞争或冲突。

拓展:

在Linux中,进程地址空间的共享区通常是通过共享内存机制来实现的。共享内存允许多个进程共享同一块内存区域,以便它们可以直接访问共享的数据,从而实现高效的进程间通信。

Linux提供了多种方式来创建和管理共享内存区域,其中最常见的方法是使用System V共享内存和Posix共享内存

System V共享内存:
  1. 创建共享内存区域:使用shmget()系统调用来创建一个共享内存区域。该调用接受三个参数,分别是共享内存的键值、大小和标志。成功创建后,shmget()会返回一个唯一的标识符(shmid)。

  2. 连接共享内存:使用shmat()系统调用将共享内存区域连接到进程的虚拟地址空间。shmat()有两个参数,一个是shmid,另一个是指定连接方式的标志。成功连接后,shmat()会返回共享内存的起始地址。

  3. 使用共享内存:进程可以直接通过指针访问共享内存区域,就像访问本地内存一样。共享内存的访问并没有加任何额外的同步机制,因此进程需要自行实现进程间数据同步和互斥。

  4. 分离共享内存:进程使用shmdt()系统调用将共享内存区域从自己的地址空间中分离。成功分离后,该进程将不再能访问共享内存区域。

  5. 删除共享内存:使用shmctl()系统调用可以删除共享内存区域。该调用接受三个参数,分别为shmid,命令(IPC_RMID)和指向共享内存信息结构的指针。

Posix共享内存:

与System V共享内存不同,Posix共享内存采用了更加灵活的接口,更加符合POSIX标准,并且提供了更多的功能和操作。

  1. 创建共享内存区域:使用shm_open()函数创建一个共享内存对象。该函数接受两个参数,一个是共享内存的名称(类似文件名),另一个是标志。

  2. 设置共享内存大小:使用ftruncate()函数设置共享内存的大小,即划定共享内存区域的大小。

  3. 连接共享内存:使用mmap()函数将共享内存映射到进程的地址空间中。mmap()需要指定共享内存对象的文件描述符、映射的长度、映射的权限和映射的标志。

  4. 使用共享内存:进程可以直接通过指针访问共享内存区域,进行数据读写等操作。

  5. 分离共享内存:使用munmap()函数将共享内存区域从进程地址空间中解除映射。

  6. 删除共享内存:使用shm_unlink()函数可以删除共享内存对象。

总结:

Linux中的进程地址空间的共享区利用共享内存机制,允许多个进程共享同一块内存区域,从而实现高效的进程间通信。在使用共享内存时,需要注意进程之间的同步和互斥,以避免数据竞争和冲突。

静态库

动态库(Dynamic Link Library)和静态库(Static Library)是两种常见的软件库,它们在程序编译和运行时起着不同的作用。

动态库(Dynamic Link Library)

动态库在程序运行时被加载,它们不是直接编译到程序的二进制代码中,而是存在于程序的执行文件中的一份引用。操作系统负责在程序运行时将这些动态库加载到内存中。

动态库的优点包括:

  • 可共享:同一个动态库可以在多个程序之间共享,从而减少了内存占用和磁盘空间的需求。

  • 易于更新:更新动态库不需要重新编译或安装程序,只需替换旧的库文件即可。

  • 灵活性:动态库允许程序在运行时动态地加载和卸载,增加了程序的灵活性和可扩展性。

动态库的缺点包括:

  • 初始加载时间:动态库在程序启动时需要加载,可能会增加程序的启动时间。

  • 性能影响:每次程序运行时,操作系统都需要花费时间来加载动态库,这可能会导致微小的性能开销。

静态库(Static Library)

静态库在程序编译时被整合到程序的二进制中。这意味着,一旦程序编译完成,静态库的代码就会包含在可执行文件中。

静态库的优点包括:

  • 快速加载:由于库的代码已经包含在可执行文件中,因此程序启动时不需要额外的时间来加载库。

  • 性能优化:程序可以直接访问静态库的代码,无需操作系统介入,这可能会提高程序的运行效率。

静态库的缺点包括:

  • 不可共享:每个程序都有其自己的静态库副本,这会增加磁盘空间的使用,并且不利于资源的共享。

  • 更新困难:更新静态库通常需要重新编译和安装程序。

一个经常需要更新的用户界面库可能会更适合作为动态库使用,而一个性能关键的库可能会作为静态库提供。

归档文件

归档文件(Archive file)是指将多个文件或目录组合成一个单一的文件的一种文件格式。归档文件通常用于将相关文件捆绑在一起以进行便捷的传输、存储或归档。在Linux系统中,常见的归档文件格式是tar和zip。

Tar归档文件(.tar):

  • Tar(Tape Archive)是Linux中常用的归档工具。它将多个文件和目录组合成一个单一的文件,尽可能地保持目录结构和文件属性。

  • tar命令用于创建、提取和展示.tar文件。例如,使用以下命令创建.tar文件:

    tar -cvf archive.tar file1.txt file2.txt dir1/

Zip归档文件(.zip):

  • Zip是一种非常流行的归档文件格式,在Linux和Windows等多个操作系统上广泛使用。它可以压缩和归档多个文件和目录,并提供压缩和加密功能。

  • zip命令用于创建、提取和展示.zip文件。例如,使用以下命令创建.zip文件:

    zip archive.zip file1.txt file2.txt dir1/

在使用归档文件时,可以轻松地将多个文件或目录打包为一个单一文件,并进行传输、备份或共享。通过提取归档文件,你可以还原被打包的文件和目录到原始的目录结构中。

值得注意的是,归档文件通常只用于组合文件和目录,并不会改变它们的内容。如果需要进行文件的压缩,可以在归档文件创建后,再对归档文件进行压缩,例如使用gzip或bzip2等压缩程序。

ar

ar 命令是 Linux 中用于管理归档文件 (archive files) 的常用工具。可以创建、修改、删除和提取归档文件中的成员文件,还可以创建符号表和索引,以及查看归档文件的详细信息。

主要参数及其介绍:

  1. -c:创建新的空归档文件。

  2. -d:删除归档文件中的成员文件。

  3. -r将文件添加到归档文件中。

  4. -m:修改归档文件中成员文件的位置。

  5. -t列出归档文件中包含的文件。

  6. -x:从归档文件中提取成员文件。

  7. -p:查看归档文件中指定成员文件的内容。

  8. -q:将问号字符附加到归档文件的末尾。

  9. -o:将归档文件保存到指定的文件中。

  10. -f:指定要处理的文件名。

  11. -F:从标准输入中读取文件名。

  12. -j:为 JAR 文件创建索引。

  13. -J:显示 JAR 文件的索引。

  14. -M:显示归档文件的成员文件信息。

  15. -S:创建符号表。

  16. -s:删除符号表。

  17. -S:删除符号表。

  18. -x:从归档文件中提取成员文件。

  19. -l:查看符号表。

  20. -L:查看符号表中的所有符号。

  21. -n:显示归档文件中每个成员文件的文件名和大小。

  22. -N:显示归档文件中每个成员文件的文件名、大小和修改日期。

  23. -v:显示详细信息。

  24. -V:显示版本信息。

  25. 示例:

  • 创建一个新的空归档文件:

ar -c new_archive.ar
  • 将文件添加到归档文件中:

ar -rv old_archive.ar new_file.txt
  • 修改归档文件中成员文件的位置:

ar -mv old_archive.ar new_position.txt
  • 列出归档文件中包含的文件:

ar -t old_archive.ar
  • 从归档文件中提取成员文件:

ar -x old_archive.ar member_file.txt
  • 查看归档文件中指定成员文件的内容:

ar -p old_archive.ar member_file.txt
  • 为 JAR 文件创建索引:

ar -j old_archive.jar
  • 显示 JAR 文件的索引:

ar -J old_archive.jar

ldd

在Linux中,ldd命令用于显示可执行文件或共享库文件所依赖的动态链接库(shared libraries)信息。它会列出程序运行所需的共享库及其路径。

  • -d:显示库的调试信息,包括所需库的版本、路径等。

  • -r:递归显示依赖关系,即显示被依赖的库。

  • -u:显示未使用的直接依赖库。

  • -v:显示更详细的信息, 包括使用的共享库版本号和加载地址。

使用示例:

ldd /path/to/program

上述命令将显示程序所依赖的动态链接库列表和路径。

请注意,ldd命令只能用于可执行文件或共享库文件,而不能用于静态库文件(如.a文件)。

编写自己的静态库

文件

  1. 创建头文件(Header Files):mylibrary.h

#ifndef MYLIBRARY_H
#define MYLIBRARY_H
​
void sayHello();
int add(int a, int b);
​
class MyClass {
public:
    void printName();
};
​
#endif
  1. 实现代码(Implementation Files):mylibrary.cpp

#include "mylibrary.h"
#include <iostream>
​
void sayHello() {
    std::cout << "Hello from my library!" << std::endl;
}
​
int add(int a, int b) {
    return a + b;
}
​
void MyClass::printName() {
    std::cout << "This is MyClass." << std::endl;
}
  1. 构建静态库:

    编写Makefile文件

lib=libmylibrary.a
​
$(lib):mylibrary.o
    ar -rc $@ $^
mylibrary.o:mylibrary.cpp
    g++ -c $^
.PHONY:clean
clean:
    rm -rf *.o *.a lib
​
.PHONY:output
output:
    mkdir -p lib/include
    mkdir -p lib/mylibrarylib
    cp *.h lib/include
    cp *.a lib/mylibrarylib
​

首先,定义了两个变量:

  • lib:这个变量的值是 libmylibrary.a,它表示要编译的静态库的名称。

  • $(lib):这个变量引用 lib 变量的值,它将在后面的命令中使用。

然后,定义了一个规则来编译 mylibrary.o 文件:

  • mylibrary.o:这是要编译的目标文件。

  • mylibrary.cpp:这是要编译的源文件。

  • g++ -c $^:这是编译命令,它将 mylibrary.cpp 文件编译为 mylibrary.o 文件。

接下来,定义了一个规则来创建静态库:

  • libmylibrary.a:这是要创建的静态库文件。

  • $^:这是要链接的 object 文件列表,它将在后面使用。

  • ar -rc $@ $^:这是链接命令,它将 mylibrary.o 文件链接为 libmylibrary.a 文件。

最后,定义了两个规则来创建目录和复制文件:

  • lib/include:这是要创建的目录,用于存放头文件。

  • lib/mylibrarylib:这是要创建的目录,用于存放静态库文件。

  • cp *.h lib/include:这是复制,它将 *.h 文件复制到 lib/include 目录中。

  • cp *.a lib/mylibrarylib:这是复制命令,它将 *.a 文件复制到 lib/mylibrarylib 目录中。

这个 Makefile 示例中,使用 ar 命令来链接静态库文件。在链接过程中,使用了 -rc 参数,它将 libmylibrary.a 文件创建在当前目录下。

此外,这个 Makefile 示例中还定义了 clean 目标来删除所有 object 文件和静态库文件这有助于在开发过程中保持目录的整洁。

  1. 使用静态库:

    main.cpp

    #include"lib/include/mylibrary.h"
    #include<iostream>
    int main()
    {
        sayHello();
        std::cout<< "5+50 = "<<add(5,50)<<std::endl;
        MyClass a;
        a.printName();
        return 0;
    }
    • 使用该静态库的项目需要告诉编译器及链接器使用该静态库。例如,如果你正在使用 GCC 编译器,可以在编译命令中添加-I指定头文件所在的目录, -L 标志指定静态库的路径,-l 标志指定静态库的名称

输出:

 g++ main.cpp -o main -I ./lib/include/ -L ./lib/mylibrarylib/ -lmylibrary
​
[lzh@MYCAT Dynamic_statuc_library]$ ./main 
Hello from my library!
5+50 = 55
This is MyClass.
//删除静态库后,仍然可以执行main

注意:在-l选项中要把静态库的前缀和后缀都去掉

  1. 查看链接属性

[lzh@MYCAT Dynamic_statuc_library]$ ldd main
    linux-vdso.so.1 =>  (0x00007ffdd0fb1000)
    libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f3d4cc44000)
    libm.so.6 => /lib64/libm.so.6 (0x00007f3d4c942000)
    libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f3d4c72c000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f3d4c35e000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f3d4cf4c000)
​
  • linux-vdso.so.1:这是一个特殊的虚拟共享库,它提供了对Linux内核特定功能的访问。

  • libstdc++.so.6:这是C++标准库的动态链接库。

  • libm.so.6:这是数学函数库的动态链接库,提供了数学相关的函数和操作。

  • libgcc_s.so.1:这是GCC编译器的C和C++运行时支持库,提供了对C和C++语言的支持。

  • libc.so.6:这是C标准库的动态链接库,提供了对C语言的基础函数和操作的支持。

  • /lib64/ld-linux-x86-64.so.2:这是动态链接器的库文件,负责在程序运行时加载所需的共享库。

  1. 可以把库放到系统指定的目录下(/lib64/ 、/usr/include/)

sudo cp lib/include/mylibrary.h  /usr/include/
sudo cp lib/malibrary/libmylibrary.a /lib64
​
g++ main.cpp -lmylibrary

还是需要在这里注明要链接的静态库名mylibrary。

  1. 可以建立软连接在系统目录下

sudo ln -s /home/lzh/tmp/Dynamic_statuc_library/lib/include/mylibrary.h  /usr/include/mylibrary.h
sudo ln -s /home/lzh/tmp/Dynamic_statuc_library/lib/mylibrarylib/libmylibrary.a  /lib64/libmylibrary.a 

建立库的步骤

创建自己的静态库可以帮助你更好地组织和管理代码,并且可以方便地将其分享给其他人使用。

步骤:

  1. 编写代码:编写你的库的代码,并将其保存为多个文件。确保你的代码遵循良好的编程规范和风格。

  2. 创建文件夹:创建一个文件夹来存放你的静态库,并为文件夹命名。

  3. 将文件放入文件夹:将你的代码文件放入文件夹中,并为每个文件命名。确保文件名清晰地表示文件的内容。

  4. 构建静态库:使用构建工具(如Makefile或CMake)来构建静态库。这将生成一个可执行文件或库文件,其中包含你的代码。

  5. 命名库文件:为生成的库文件命名,并确保它与你的库的功能和用途匹配。

  6. 打包静态库:将生成的库文件打包为一个静态库文件(如.a或.lib文件),以便其他人可以使用它。

  7. 编写文档:为静态库编写文档,并将其与静态库一起分享。

  8. 版本控制:使用版本控制系统(如 Git)来跟踪你的库的更改历史,并确保代码的正确性和一致性。

  9. 发布和维护:

    • 将你的静态库发布到公共存储库(如 GitHub 上的公共仓库)或将其分发给其他人使用。定期更新和维护你的库,以确保其功能和性能保持最佳状态。

除了以上步骤,还有一些其他事项需要考虑:

兼容性和跨平台性:

  • 确保你的库在多个平台和编译器上都能正常工作。考虑使用跨平台库或针对特定平台进行优化。

错误处理和日志记录

  • 在你的代码中添加适当的错误处理和日志记录机制,以便在出现问题时能够轻松追踪和解决问题。

依赖项管理:

  • 如果你的库依赖于其他库或框架,确保正确管理这些依赖项,并确保它们与你的库兼容。

编写自己的动态库

文件

我们使用上边的源代码

创建新的头文件和源文件:

//mylibrary1.h
#ifndef MYLIBRARY1_H
#define MYLIBRARY1_H
int sub(int a, int b);
​
#endif
​
//mylibrary1.cpp
#include"mylibrary1.h"
#include<iostream>
int sub (int a,int b)
{
return a-b;
}
​
​
//mylibrary2.h
#ifndef MYLIBRARY2_H
#define MYLIBRARY2_H
int mul(int a, int b);
​
#endif
​
​
//mylibrary2.cpp
#include"mylibrary1.h"
#include<iostream>
int mul(int a,int b)
{
return a*b;
}
​
//mylibrary.cpp
//mylibrary.h
//见上文
​
​
//main.cpp
#include"lib/include/mylibrary.h"
#include<iostream>
#include"mylibrary1.h"
#include"mylibrary2.h"
int main()
{
    sayHello();
    std::cout<< "5+50 = "<<add(5,50)<<std::endl;
    MyClass a;
    a.printName();
std::cout<<"500/52= "<< sub(500,52)<<std::endl;
std::cout<<"500*52= "<< mul(500,52)<<std::endl;
​
        return 0;
}

改变一下Makefile文件

static_lib=libmylibrary.a
dynamic_lib=libmylibrary0.so
.PHONY:all
all:$(static_lib) $(dynamic_lib)
​
​
​
$(static_lib):mylibrary.o
        ar -rc $@ $^
mylibrary.o:mylibrary.cpp
        g++ -c $^
​
$(dynamic_lib):mylibrary1.o mylibrary2.o
        g++ -shared -o $@ $^
mylibrary1.o:mylibrary1.cpp
        g++ -fPIC -c $^
mylibrary2.o:mylibrary2.cpp
        g++ -fPIC -c $^
​
.PHONY:clean
clean:
         rm -rf *.o *.a lib *.so
​
.PHONY:output
output:
        mkdir -p lib/include
        mkdir -p lib/mylibrarylib
        cp *.h lib/include
        cp *.a lib/mylibrarylib
        cp *.so lib/mylibrarylib
​

实验结果

[lzh@MYCAT Dynamic]$ make
g++ -c mylibrary.cpp
ar -rc libmylibrary.a mylibrary.o
g++ -fPIC -c mylibrary1.cpp
g++ -fPIC -c mylibrary2.cpp
g++ -shared -o libmylibrary0.a mylibrary1.o mylibrary2.o
[lzh@MYCAT Dynamic]$ ll
total 60
drwxrwxr-x 4 lzh lzh   41 Feb  5 20:57 lib
-rwxrwxr-x 1 lzh lzh 8392 Feb  5 20:57 libmylibrary0.so
-rw-rw-r-- 1 lzh lzh 3262 Feb  5 20:55 libmylibrary.a
-rw-rw-r-- 1 lzh lzh  321 Feb  5 20:51 main.cpp
-rw-rw-r-- 1 lzh lzh  523 Feb  5 20:57 Makefile
-rw-rw-r-- 1 lzh lzh   80 Feb  5 20:49 mylibrary1.cpp
-rw-rw-r-- 1 lzh lzh   74 Feb  5 20:33 mylibrary1.h
-rw-rw-r-- 1 lzh lzh 2232 Feb  5 20:55 mylibrary1.o
-rw-rw-r-- 1 lzh lzh   80 Feb  5 20:50 mylibrary2.cpp
-rw-rw-r-- 1 lzh lzh   73 Feb  5 20:52 mylibrary2.h
-rw-rw-r-- 1 lzh lzh 2232 Feb  5 20:55 mylibrary2.o
-rw-rw-r-- 1 lzh lzh  246 Feb  5 19:37 mylibrary.cpp
-rw-rw-r-- 1 lzh lzh  139 Feb  5 19:36 mylibrary.h
-rw-rw-r-- 1 lzh lzh 3072 Feb  5 20:55 mylibrary.o
​
[lzh@MYCAT Dynamic]$ make output
mkdir -p lib/include
mkdir -p lib/mylibrarylib
cp *.h lib/include
cp *.a lib/mylibrarylib
cp *.so lib/mylibrarylib
[lzh@MYCAT Dynamic]$ tree lib
lib
├── include
│   ├── mylibrary1.h
│   ├── mylibrary2.h
│   └── mylibrary.h
└── mylibrarylib
    ├── libmylibrary0.so
    └── libmylibrary.a
​
2 directories, 5 files
[lzh@MYCAT Dynamic]$  g++ main.cpp -o main.exe -I ./lib/include/ -L ./lib/mylibrarylib/ -lmylibrary -lmylibrary0
[lzh@MYCAT Dynamic]$ ./main.exe 
./main.exe: error while loading shared libraries: libmylibrary0.so: cannot open shared object file: No such file or directory
​
[lzh@MYCAT Dynamic]$ ldd main.exe 
    linux-vdso.so.1 =>  (0x00007ffd5b511000)
    libmylibrary0.so => not found
    libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fa2a3aac000)
    libm.so.6 => /lib64/libm.so.6 (0x00007fa2a37aa000)
    libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fa2a3594000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fa2a31c6000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa2a3db4000)
​

思考

error解决

遇到了error:error while loading shared libraries: libmylibrary0.so: cannot open shared object file: No such file or directory

  1. 将共享库文件放到默认的共享库搜索路径下: - 打开终端并切换到共享库文件所在的目录。 - 使用 sudo 命令将共享库文件复制到 /lib/usr/lib 目录:(或者/lib目录)

    sudo cp libmylibrary0.so /usr/lib
    g++ main.cpp -o main.exe -I ./lib/include/ -L ./lib/mylibrarylib/ -lmylibrary -lmylibrary0
    ​
    ./main.exe 
    ​
    Hello from my library!
    5+50 = 55
    This is MyClass.
    500/52= 9
    500*52= 26000

  2. 在系统目录中建立软连接。和第一种原理相同。

    sudo ln -s /home/lzh/tmp/Dynamic_statuc_library/Dynamic/lib/mylibrarylib/libmylibrary0.so /lib64/libmylibrary0.so
    ​
    g++ main.cpp -o main.exe -I ./lib/include/ -L ./lib/mylibrarylib/ -lmylibrary -lmylibrary0
    ​
    ./main.exe 
    ​
    ​

  3. 通过设置 LD_LIBRARY_PATH 环境变量: - 打开终端并切换到当前工作目录。 - 运行以下命令,将共享库所在目录添加到 LD_LIBRARY_PATH 环境变量中:

export LD_LIBRARY_PATH=./lib/mylibrarylib:$LD_LIBRARY_PATH  
  • 运行 ./main.exe,现在应该能够找到共享库文件了。

4.通过设置运行时链接器参数 -rpath:修改编译命令,加入 -Wl,-rpath 参数,指定共享库文件的路径:

g++ main.cpp -o main.exe -I ./lib/include/ -L ./lib/mylibrarylib/ -lmylibrary -lmylibrary0 -Wl,-rpath,'$ORIGIN/lib/mylibrarylib'
  • 运行 ./main.exe,现在应该能够找到共享库文件了。

如下:

[lzh@MYCAT Dynamic]$ g++ main.cpp -o main.exe -I ./lib/include/ -L ./lib/mylibrarylib/ -lmylibrary -lmylibrary0 -Wl,-rpath,'$ORIGIN/lib/mylibrarylib'
[lzh@MYCAT Dynamic]$ ./main.exe 
Hello from my library!
5+50 = 55
This is MyClass.
500/52= 9
500*52= 26000
[lzh@MYCAT Dynamic]$ ldd main.exe 
    linux-vdso.so.1 =>  (0x00007ffebb1c8000)
    libmylibrary0.so => ./lib/mylibrarylib/libmylibrary0.so (0x00007f25a97cf000)
    libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f25a94c7000)
    libm.so.6 => /lib64/libm.so.6 (0x00007f25a91c5000)
    libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f25a8faf000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f25a8be1000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f25a99d1000)
​

5.修改配置文件/etc/ld.so.conf.d/

[lzh@MYCAT Dynamic]$ cd /etc/ld.so.conf.d/
[lzh@MYCAT ld.so.conf.d]$ ls
bind-export-x86_64.conf             libiscsi-x86_64.conf
dyninst-x86_64.conf                 mysql-x86_64.conf
kernel-3.10.0-1160.el7.x86_64.conf
[lzh@MYCAT ld.so.conf.d]$ vim my.conf
[sudo] password for lzh: 
/home/lzh/tmp/Dynamic_statuc_library/Dynamic/lib/mylibrarylib/
[lzh@MYCAT ld.so.conf.d]$ sudo ldconfig
[lzh@MYCAT ld.so.conf.d]$ cd -
[lzh@MYCAT Dynamic]$  g++ main.cpp -o main.exe -I ./lib/include/ -L ./lib/mylibrarylib/ -lmylibrary -lmylibrary0
[lzh@MYCAT Dynamic]$ ./main.exe 
Hello from my library!
5+50 = 55
This is MyClass.
500/52= 9
500*52= 26000

LD_LIBRARY_PATH环境变量

LD_LIBRARY_PATH 用于指定动态链接器在运行时搜索共享库的路径。当程序在运行时需要动态加载共享库时,动态链接器会在该环境变量指定的路径中寻找依赖的共享库文件。

使用 LD_LIBRARY_PATH 环境变量,可以临时性地修改运行时共享库的搜索路径,以满足特定程序或脚本的需求。

示例用法:

export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH

以上命令将 /path/to/library 添加到 LD_LIBRARY_PATH 环境变量中,让动态链接器在运行时将该路径作为共享库的搜索路径。

请注意,/path/to/library 应替换为实际的共享库所在路径。如果需要指定多个路径,可以使用冒号 : 分隔它们,例如:/path/1:/path/2:/path/3

请注意,LD_LIBRARY_PATH 是一个临时设置,只在当前终端会话中有效。

/etc/ld.so.conf.d/配置文件

/etc/ld.so.conf.d/ 目录下的配置文件通常用于指定系统动态链接器的搜索路径。这些配置文件以 .conf 为扩展名,每个文件对应一个搜索路径。

每个配置文件中的每一行都包含一个需要添加到动态链接器搜索路径的目录。动态链接器在运行时会在这些路径中查找共享库文件。

例如,假设 /etc/ld.so.conf.d/myconfig.conf 是一个配置文件,其内容如下:

# Example configuration file
​
# Add custom library directory
/usr/local/lib
​
# Add another library directory
/opt/myapp/lib

上述示例配置文件指定了两个搜索路径 /usr/local/lib/opt/myapp/lib。这些路径都会被添加到动态链接器的搜索路径中。

完成对 /etc/ld.so.conf.d/ 目录下配置文件的编辑后,需要运行以下命令以使更改生效:

sudo ldconfig

这将重新加载动态链接器的配置,并使其读取最新的搜索路径。

好用的第三方库:ncurses(了解)

ncurses 是一个非常有用的库,可以在终端环境中创建文本用户界面(TUI)应用程序。它提供了一组函数和工具,可以在终端上创建交互式的文本界面,包括文本窗口、菜单、对话框、按钮、输入框等。

使用 ncurses,你可以编写终端应用程序并实现以下功能:

  1. 创建多窗口和面板:可以在终端上创建多个交互式窗口,并对其进行管理和控制。

  2. 绘制和控制文本界面:可以在窗口中绘制文本、边框、颜色等,并对其进行修改和更新。

  3. 处理用户输入和交互:可以接收用户的按键输入,并根据输入进行相应的操作和响应。

  4. 创建菜单和对话框:可以创建菜单和对话框,使用户能够进行选择、输入等操作。

  5. 控制屏幕刷新:可以手动控制屏幕的刷新,以实现动画效果或更高级的用户界面。

ncurses 是一个强大且广泛使用的库,在开发控制台应用程序、命令行工具和交互式终端界面等方面非常有用。它是跨平台的,可在多个操作系统上使用,包括Linux、Unix、macOS等。

静态编译生成可执行文件

以下基本以C语言文件执行为例

程序的静态编译链接是将源代码、库文件和其他资源文件进行编译和链接,生成一个可执行文件的过程。这个过程包括以下几个步骤:

  1. 预处理:编译器会读取源代码文件,对其进行预处理,包括将预处理器指令(如#include、#define等)处理为对应的代码或文本。

  2. 编译:预处理后的代码会被编译器编译成目标代码(通常是机器语言或汇编语言)。编译器会检查源代码中的语法错误,并生成目标文件。

  3. 汇编:如果源代码是高级语言,还需要将目标代码汇编成汇编语言才能被计算机理解并执行。

  4. 链接:链接器会将编译后的目标文件和其他库文件进行合并,解决函数调用和全局变量的引用问题,生成一个可执行文件。链接器还会进行一些优化,如去除重复的代码段和数据段,优化程序的内存布局等。

编译过程

在程序的静态编译过程中,会发生以下几个重要的事情:

编译过程是将源代码转换成机器语言的过程,它包含了多个步骤,旨在生成可以被计算机执行的指令。

步骤
  1. 预处理(Preprocessing)

    • 预处理器读取源代码,并根据预处理器指令(如#include#define#ifdef等)进行。

    • 预处理器会扩展宏定义,包含头文件,并处理条件编译指令。

  2. 词法分析(Lexical Analysis)

    • 编译器的词法分析器(又称扫描器或lexer)读取处理后的源代码,将其分解成一系列的词法单元(tokens)。

    • 词法单元是源代码中的基本元素,如关键字、标识符、常量、运算符等。

  3. 语法分析(Syntax Analysis)

    • 编译器的语法分析器(又称解析器或parser)接收词法单元,并根据语言的语法规则将它们组合成抽象语法树(Abstract Syntax Tree,AST)。

    • AST是一种树形的数据结构,它表示源代码的语法结构。

  4. 生成符号表

    在生成抽象语法树(AST)过程中,编译器会收集源代码中出现的所有符号,包括变量名、函数名等生成符号表。

    过程主要包括以下几个步骤:

    1. 符号定义:编译器会识别源代码中的符号定义,如变量的声明、函数的定义等,并提取出符号的名称、类型、作用域等信息。

    2. 符号引用:编译器会识别源代码中对符号的引用,如变量的使用、函数的调用等,并建立对应的符号引用。

    3. 符号解析:编译器会对符号进行解析,确定符号的定义和引用位置。

    4. 符号表生成:编译器会将所有收集到的符号信息存储到符号表中。符号表通常是一个数据结构,可以是数组、哈希表或其他形式。

  5. 语义分析(Semantic Analysis)

    • 编译器的语义分析器检查AST中的节点,确保源代码的语义正确。

    • 语义分析包括检查变量是否已定义、类型兼容性、作用域规则等。

  6. 中间代码生成(Intermediate Code Generation)

    • 编译器将AST转换成中间代码,中间代码是一种低级、与机器无关的代码表示形式。

    • 中间代码通常以三地址码的形式出现,每个指令包含最多三个操作数。

  7. 优化(Optimization)

    • 编译器对中间代码进行优化,以提高程序的执行效率。

    • 优化包括消除冗余代码、改进算法、优化数据访问路径等。

  8. 目标代码生成(Code Generation)

    • 编译器将优化后的中间代码转换成目标机器上的机器语言。

    • 目标代码可以是汇编语言,也可以是机器语言的二进制表示。

  9. 代码排放(Code Emission)

    • 编译器将生成的目标代码排放到目标文件中,这些文件通常具有可执行的二进制格式。

编译器在执行上述步骤时,还会进行各种错误检查和警告,确保程序的正确性和可靠性。

符号表

符号表(Symbol table)是编译过程中生成的重要数据结构之一,用于记录程序中定义的符号(如变量、函数、类等)的信息。符号表在语法分析、语义分析和代码生成等阶段发挥重要作用。编译器可以使用符号表来进行语义检查、类型推断、符号解析和代码生成等操作,对于调试和符号查找也非常有用。

符号表包含了以下重要信息:

  1. 符号名称(Symbol name):记录符号的名称,以字符串的形式表示。每个符号都有一个唯一的名称,用于在代码中引用和定义。

  2. 符号类型(Symbol type):指示符号的类型,如变量、函数、类、枚举等。符号类型描述了符号在程序中的角色和用途。

  3. 符号属性(Symbol attributes):记录符号的额外属性信息,如访问修饰符、存储类别、作用域等。这些属性描述了符号的访问权限和范围。

  4. 符号位置(Symbol location):表示符号在程序中定义或引用的位置信息。可以是源代码文件和行号(用于调试),也可以是相对于目标文件的偏移量(用于链接过程)。

  5. 符号大小(Symbol size):对于变量和数据符号,记录其占用的内存大小。对于函数符号,记录函数的指令大小。

  6. 符号作用域(Symbol scope):指示符号的可见范围,即在哪些部分可以访问到该符号。常见的作用域包括全局作用域、文件作用域和局部作用域。

在编译过程中是否有指令的地址

在编译过程中,编译器并不生成指令的绝对地址。编译器生成的是指令的机器代码,这些机器代码是在抽象的指令集上操作的,而不是在具体的物理内存地址上。编译器也不知道这些指令最终将被加载到内存中的哪个位置。

在编译过程中,编译器会处理符号和引用,例如函数调用和变量声明,但这些处理并不涉及实际的内存地址。编译器会为变量和函数生成符号,并在编译后的代码中使用这些符号来引用它们,而不是使用实际的地址。

链接过程

注意静态库是已经经过编译和链接后的文件,里边有符号表等等。

在程序的静态编译链接过程中,会发生以下几个重要的事情:

  1. 库文件静态链接:如果程序使用了静态库,链接器会将静态库中的目标代码直接复制到文件中。这样,可执行文件会包含静态库中所有所需的函数和数据。

  2. 符号解析:在链接过程中,链接器会对源代码中使用的函数和全局变量进行符号解析,找到它们的定义。如果找不到定义或者存在多个定义,链接器会报错。链接器会根据编译生成的符号表中的信息来确定每个符号所在的位置。

  3. 符号合并和重复消除:链接器将各个目标文件中的符号进行合并,消除重复的符号。如果多个目标文件中的符号名称相同,链接器会根据不同的链接规则(如静态链接和动态链接)来决定使用哪个符号。

    符号表的汇总:链接器会将来自多个目标文件的符号信息进行汇总,生成一个统一的符号表。这个符号表包含了所有链接的目标文件中定义和引用的符号信息。

  4. 段合并:链接器会将不同的目标文件中的代码和数据段进行合并,生成一个统一的代码段和数据段,并根据需要进行代码和数据的对齐、填充等操作。这包括合并代码段、数据段、只读数据段、堆栈段等。

  5. 重定位表生成:根据符号表中的信息链接器会生成重定位表,记录需要进行重定位的位置和相关信息,这些位置包括了对其他目标文件中符号的引用和需要修正的地址。

  6. 地址重定位(符号重定位):由于各个目标文件是独立地进行编译的,所以它们可能引用了其他目标文件中的符号。链接器会根据符号表和重定位表的信息,对引用的符号进行地址重定位,即修改它们的地址,使其指向正确的目标地址。这样可以将代码和数据中的相对地址转换为绝对地址,确保引用的符号能够正确连接和执行。

  7. 生成可执行文件:在链接过程的最后,链接器会根据汇总后的代码段和数据段,以及其他必要的信息,生成最终的可执行文件。这个可执行文件包含了程序的指令、数据和其他资源,可以被操作系统加载和执行。

最终,静态编译链接过程会将所有的目标文件、库文件以及其他资源文件合并成一个独立的可执行文件。这个可执行文件包含了程序的所有指令和数据,可以直接在目标平台上运行。由于所有的依赖都被静态链接到可执行文件中,因此可执行文件具有很好的可移植性和独立性,可以在不同的系统上运行,而无需依赖外部的库文件。

第七点中的其他必要信息

在链接过程的最后,链接器会根据汇总后的代码段和数据段,以及其他必要的信息,生成最终的可执行文件。这些必要的信息包括:

  1. 程序入口点:可执行文件的起始执行地址,通常是main函数或其他指定的初始化代码的地址。

  2. 指令和数据:代码段中包含程序的机器指令,数据段中包含程序的数据变量。

  3. 重定位信息:这些信息用于在程序加载时修正地址引用,确保程序中的相对地址能够转换为正确的绝对地址。

  4. 符号表:包含程序中所有符号的列表,包括函数、变量等,以及它们的属性信息,如是否可外部访问等。

  5. 动态链接信息:如果程序使用了动态链接,链接器会包含动态链接所需的信息,如导入表(Import Table)和导出表(Export Table)。导入表记录了程序依赖的共享库和函数,而导出表则记录了程序提供给其他共享库调用的函数。

  6. 全局偏移表(GOT)或动态链接表(DT)项:这些项在动态链接时使用,用于存储全局变量和函数的地址,或者记录动态链接所需的库和其他信息。

  7. 节表:节表是可执行程序中不同节(sections)的信息,链接器会为程序的不同部分创建节,如代码节、数据节、BSS节(未初始化数据节)等,每个节都包含自己的数据和大小等信息,并为每个节分配唯一的节号。例如:

  8. 堆和栈的配置:链接器会确定程序的堆(Heap)和栈(Stack)的配置,包括它们的起始虚拟地址和大小。

  9. 调试信息:调试信息用于在程序运行时进行调试和错误排查。这些信息可能包括源代码文件名、行号、变量名称、函数名称等。

  10. 其他系统特定的信息:根据操作系统的要求,链接器可能会包含其他特定的信息,如程序的运行权限、文件属性等。

生成的可执行文件是一个完整的程序副本,它包含了所有必要的信息,可以在操作系统上运行。当操作系统加载可执行文件时,它会根据这些信息初始化程序的执行环境,然后开始执行程序的入口点指令。

重定位表

重定位表中通常不存储绝对地址,而是存储了足够的信息,帮助程序在运行时找到符号表中符号的绝对地址。重定位表的这种设计允许程序在编译和链接时不必知道符号的确切地址,因为这些地址在程序运行时才会被确定。

重定位表(Relocation Table)是在链接过程中生成的一种数据结构,用于记录需要进行符号地址重定位的位置和相关信息。重定位表主要包含以下几个重要的字段:(他可能在执行程序时候发生作用,也可能在平坦模型下链接过程直接发生作用)

  1. 符号信息(Symbol Information):包含了需要进行重定位的符号的相关信息,如符号的名称、绑定类型(局部或全局)等。

  2. 偏移地址(Offset):指示需要进行重定位的位置在目标文件或可执行文件中的偏移量。偏移地址可指示代码区域、数据区域或其他重定位节区。

  3. 重定位类型(Relocation Type):表示需要执行的重定位操作的类型。不同的平台和架构有不同类型的重定位操作,如绝对重定位、相对重定位、PC相对重定位等。

  4. 重定位目标地址(Target Address):指示进行重定位时需要修改的绝对地址、相对地址或其他目标地址。

怎么使用重定位表来进行地址重定位

在执行可执行文件之前,操作系统会负责加载可执行文件到内存中,并对其中需要进行地址重定位的位置进行修正。操作系统使用重定位表来进行最终的地址重定位,实际的步骤如下:

  1. 加载可执行文件:操作系统根据可执行文件的文件格式和头部信息,将可执行文件的代码段、数据段等加载到内存中的适当位置。这些位置通常是由链接器在链接过程中进行分配的。

  2. 解析重定位表:操作系统解析可执行文件中的重定位表,找到需要进行地址重定位的位置。

  3. 计算修正值:对于每一个需要重定位的位置,操作系统会根据重定位表中的信息计算出修正值。修正值表示需要加或减的偏移量,以便将原始的地址修正为正确的地址。

  4. 应用修正值:操作系统将计算得到的修正值应用到需要重定位的位置,即将原始的地址加上修正值,得到最终的正确地址。

通过重定位表进行地址重定位的过程确保了可执行文件中引用的符号能够正确链接和执行。操作系统在加载可执行文件时,会根据重定位表对代码和数据的地址进行修正,使其与其他模块中定义的符号的地址相对应。

平坦内存模型

在某些架构中,比如x86架构,存在平坦内存模型(Flat Memory Model),在这种模型中,程序的所有代码和数据都从虚拟地址0开始,没有段的概念,地址之间没有显式的分隔符或段描述符。。在这种模型中,地址重定位通常在编译或链接阶段完成,因为所有的地址都是扁平的,不需要在运行时进行重定位。但是在其他架构和操作系统中,可能会有更复杂的内存模型和重定位机制,需要在运行时由操作系统或硬件来完成地址重定位。

在平坦内存模型中,代码和数据可以自由地访问彼此,无需通过段选择器或段寄存器进行地址转换。因此,程序中的指令可以直接使用相对于基地址0的偏移量来访问内存中的数据。

平坦内存模型的特点是简化了内存访问和地址转换的过程,提高了程序的执行效率。它可以使编程和调试过程更加方便,因为所有的内存访问可以使用简单的相对地址来表示,无需复杂的段选择和地址转换操作。

平坦内存模型在一些处理器架构中被广泛使用,如x86架构中的实模式和保护模式下的32位和64位模式。在这些模式下,程序的所有代码和数据都被视为位于相同的地址空间中,无需进行段选择和段切换。

总而言之,平坦内存模型是将程序的所有内存视为一个连续的地址空间,消除了段的概念和地址转换的复杂性。它在提高程序执行效率和简化编程过程方面具有优势。

运行可执行文件(二谈进程地址空间)

可执行文件中存放着?

在生成可执行文件中的第七点我们有所介绍

重点解释一下机器代码或字节码:这是源代码经过编译或解释后生成的指令集,即可执行程序的实际执行代码。

其中每个指令都有地址,这个地址是什么?存在文件中的地址是逻辑地址(虚拟地址),不是内存中的地址。

在可执行程序的机器代码中,每个指令都是通过相对地址或绝对地址来表示的。这个地址是指令在内存中的位置,用于指示程序在执行过程中应该执行的操作。(在平坦内存模型下,所有的程序的所有代码和数据都从虚拟地址0开始,指令中存放的就是相对于起始入口点的偏移量,不需要使用重定向表就能定位指令)也就是说平坦模式下相对地址和绝对地址是相同的。

指令地址是由操作系统和硬件共同控制的。操作系统在加载可执行程序时会分配内存区域,并将程序的指令按地址顺序存储在这些区域中。硬件负责将这些指令读取到指令寄存器中,并根据地址逐条执行。


我们知道程序有自己的进程地址空间,每个空间地址的差别很大,其实进程的地址空间在内存上是连续的,这意味着进程可以访问一个连续的内存区域,这个区域被操作系统分配给该进程。


程序的运行:

程序开始运行前
  1. 在内核中创建PCB:当操作系统接收到一个新的进程创建请求时,它会为该进程分配一个唯一的进程标识符(Process Identifier, PID)并为其分配一个独立的PCB。PCB包含有关进程的所有重要信息,如进程状态、程序计数器、寄存器、内存限制、打开的文件描述符。

  2. 分配内存空间:操作系统为新进程分配一块内存空间,在这块内存中,加载可执行文件的代码段、数据段、堆和栈等。

  3. 加载可执行文件:操作系统会从存储设备(如硬盘)中加载可执行文件到内存中。加载过程包括将可执行文件的代码段、数据段、堆和栈等部分复制到适当的内存位置。

  4. 设置执行环境:操作系统为进程设置执行环境,包括设置进程的内存空间布局、创建进程的页表(Page Table)用于地址映射,以及设置运行时所需的其他环境变量,页表的虚拟地址栏目全部都填好了,物理地址可能没有被加载(设置缺页位),也可能被填好了。(采用延迟分配的策略:在进程中的页表中有介绍)

  5. 分配寄存器和初始化上下文:操作系统会分配和设置进程的寄存器,以及初始化进程的上下文。这包括保存和加载寄存器的值以及设置程序计数器(PC)的初始值,使进程从正确的位置开始执行。

  6. 权限和访问控制:操作系统会根据进程的访问权限,验证进程对系统资源的访问请求。这包括对文件、设备以及网络接口等的权限控制。

  7. 初始化PCB:操作系统在PCB中填写进程的标识符(PID)和其他必要的信息,如进程的状态、优先级、打开的文件列表等。同时,PCB会记录进程的上下文(如寄存器的状态)。PCB中还会存储程序的入口地址。

  8. 启动进程执行:操作系统将控制权交给进程,使其开始执行。程序从指定的入口点(如主函数)开始执行,根据指令的执行顺序逐步执行。

在程序运行过程中操作系统也没闲着:

它在监视着程序

  1. 处理中断和异常:操作系统会监视进程的执行并处理发生的中断、异常或系统调用。这可能包括处理硬件中断、处理陷阱或异常、以及提供对操作系统服务的访问。

  2. 内存管理:操作系统负责管理进程的内存,包括为进程分配和回收内存,处理页面调度和分页等任务。

  3. 调度和资源分配:操作系统根据特定的调度算法决定何时切换进程以及如何分配处理器时间和其他资源,以保持系统的高效运行。

  4. 文件操作和I/O管理:操作系统负责管理进程的文件操作和输入输出(I/O)操作,包括打开、关闭、读取、写入等文件操作。此外,操作系统还会管理进程的网络连接和通信操作。

  5. 进程间通信:操作系统支持进程之间的通信和同步操作,如管道、信号、共享内存等。这些机制可以实现进程之间的数据交换和同步执行。

  6. 资源管理:操作系统负责管理系统资源,如CPU、内存、磁盘空间、网络带宽等。它根据进程的需求和系统资源的情况,合理地分配和调度资源,以提高系统的性能和效率。

  7. 错误处理和崩溃恢复:操作系统会处理进程运行过程中可能出现的各种错误,如访问违法、内存溢出等。当进程崩溃或异常退出时,操作系统会记录崩溃原因并采取相应的措施进行恢复,如重新启动进程或重新加载可执行文件等。

  8. 安全管理:操作系统负责管理系统的安全,包括身份认证、访问控制、加密等。它确保进程只能访问其权限范围内的资源,以保护系统的安全性和稳定性。

  9. 系统维护和优化:操作系统会定期进行系统维护和优化操作,如垃圾回收、内存碎片整理、磁盘整理等,以保证系统的高效运行和稳定性。

指令执行

指令加载到物理内存中后,除了他本身的地址(虚拟地址),它还有自己在内存中的物理地址。我们只需要知道指令的起始地址就能运行程序。指令有许多种类,大小也不同,但是CPU能够识别并且正确执行,一条接一条的运行,这些地址通常是连续的,可以通过指针或引用来访问和执行指令。在大多数情况下,编译器或解释器会将函数的入口点(函数调用时的地址)和函数的返回地址(函数执行完毕后的返回地址)也存储在内存中,以便后续的调用和返回操作。

CPU将程序的入口地址放入寄存器中执行,因为入口地址是虚拟地址,所以要通过页表映射到物理地址,如果这时候物理地址没有被加载(内存没有加载)或者页表项的缺页位为真,发生缺页中断,这时候就会通过inode从磁盘中加载数据到物理地址中。由于程序的局部性原理,我们加载到内存中的还有该程序的其他的指令,这样就把新加载的指令物理地址初始化到页表中。(注意:指令本身有两个地址,根据他在源文件中的虚拟地址初始化页表中的逻辑地址,根据他加载到内存中的绝对地址初始化页表中的物理地址)

函数调用

指令有许多类型,有的指令含有数据,有的指令含有函数调用(的地址),也就是说,这个指令的参数中有地址。这个地址也是虚拟地址。

有可能我们之前加载到内存的页中包含了这个偏移量后的物理地址,这样就能直接在页表中找到对应的物理地址继续将函数中的指令加载到寄存器中。如果发生缺页中断,就再进行类似最初知道指令虚拟地址入口,找物理地址映射的情景。

(这里涉及到栈的操作)

在函数调用时,调用函数会将它的返回地址压入栈中,然后跳转到被调用函数的入口点。被调用函数会在函数执行的过程中使用栈来存储局部变量、参数和临时数据。当函数执行完毕后,它会从栈中弹出返回地址,并将程序的控制流恢复到该地址,即返回到调用函数的位置继续执行。

在底层,返回地址是通过栈指针(stack pointer)和栈帧(stack frame)来管理的。栈帧包含了函数的参数、局部变量和其他相关信息。返回地址通常存储在栈帧中的特定位置,如偏移量(offset)或者在某些体系结构中的固定位置(如x86架构中的EBP寄存器指向的位置)。

当函数执行完成时,它会从栈中取出返回地址,并使用该地址来指示程序的下一条指令的位置。通过这种方式,程序可以返回到调用函数的位置,继续执行后续的代码。

动态编译生成可执行文件与运行

从上文可以得出,静态库在链接阶段会加入到可执行文件中,所以动态库编译与静态库编译在这之前都是一样的操作,我们重点讲链接过程

我认为 : 动态链接模糊了编译和执行的过程

动态链接在某种程度上模糊了编译和执行的过程,它允许程序在运行时动态地加载和链接共享库。这个过程与传统的编译过程有所不同,在传统编译过程中,程序的所有部分在编译时就被组合在一起,形成一个不可分割的整体。

静态编译(Static Linking)是指在程序编译时进行链接,生成的可执行文件包含了所有依赖库的符号解析和重定位信息。在生成可执行文件的过程中,链接器会生成重定位表,记录需要修正的地址和相关信息。重定位表中的信息在生成可执行文件时就已经确定,并且在程序运行期间不会发生变化。

动态编译(Dynamic Linking)是指在程序运行时进行链接,生成的可执行文件不包含所有依赖库的符号解析和重定位信息。在程序运行时,动态链接器会根据可执行文件和动态库的引用信息,加载和链接相应的库文件,并进行符号解析和重定位操作。重定位表是在程序运行时再进行更新的,需要根据动态库的加载和引用情况动态地生成和更新。

动态库是一个编译后的文件,其中的变量和函数采用起始地址加偏移量的方式来描述,所以动态库可以在虚拟内存(进程地址空间)中的任意位置加载。(是否记得-fPIC参数,产生与位置无关码)。

进程地址空间的共享区

在操作系统中,进程地址空间中的共享区是指多个进程之间共享的内存区域。这种共享区域允许不同的进程之间进行数据交换和通信。

共享区通常包含以下几个重要的部分:

  1. 共享库(Shared Libraries):多个进程可以使用同一个共享库(动态链接库或共享对象文件),从而避免每个进程都需要加载和执行一份独立的代码。这种共享库可以包含一组函数、类、变量等可重用的代码和资源。

  2. 内存映射文件(Memory-Mapped Files):内存映射文件允许多个进程将同一个文件映射到各自的地址空间中。这使得多个进程可以直接在内存中读写共享的文件内容,从而实现了高效的共享文件访问。

  3. 共享内存(Shared Memory):共享内存是一种进程间通信的机制,允许多个进程通过将一部分内存区域映射到它们的地址空间中来实现数据的共享。这种共享内存区域可以用于高效地进行进程间的数据传递和共享。

通过共享区域,不同的进程可以共享资源、共同访问文件、进行进程间通信等。这种共享机制可以提高系统的性能和效率,同时也增加了进程间竞争和同步的复杂性。因此,在使用共享区域时需要进行适当的同步和互斥操作,以确保数据的一致性和正确性。

链接过程

与静态链接不同,动态链接并不在编译时将库文件的代码合并到可执行文件中,而是在程序运行时动态地加载和链接库文件。可以说动态库的链接过程和执行文件过程是类似的,都会去内存中找动态库的数据。

  1. 库加载、共享库的内存映射:动态链接器根据需要加载依赖的共享库(动态链接库)。它会在指定的路径中搜索库文件,将库文件加载到内存(进程地址空间的共享区)中,并将共享库文件的代码和数据映射到这些内存区域。

  2. 初始化和引用计数:动态链接器会完成一些初始化工作,并更新库文件的引用计数。引用计数是跟踪哪些库文件在使用的机制,以便在不再需要时正确地卸载这些库文件。

  3. 符号解析:动态链接器首先会解析可执行文件和依赖的动态链接库中的符号引用。识别出每个目标文件中的符号引用(函数调用、全局变量等)和符号定义(函数定义、全局变量声明等)。

  4. 段合并、符号表汇总与重复擦除、生成重定位表、地址(符号)重定位:与静态链接过程相同或类似。

  5. 控制权交还(生成可执行文件):最后,动态链接器将控制权交还给可执行文件的入口点(也称为启动例程),从而可以启动可执行文件的执行。

  6. 动态链接库的卸载:当一个库不再被引用时,动态链接器会将其卸载,以释放内存和其它资源。这个过程通常由引用计数机制触发,当引用计数降至零时,动态链接器会将其卸载。

  7. 动态链接库的替换:当一个动态链接库被卸载后,如果需要使用其功能,新的库可以被动态加载并替换它。

  8. 错误处理:动态链接器在加载和解析库文件时,可能会遇到错误,如文件找不到、内存不足等。如果出现错误,动态链接器会尝试恢复,或者报告错误给运行时环境。

通过动态链接,不同的程序可以共享使用相同的库文件,这样可以减小可执行文件的体积并提高代码的重用性。如果库文件发生更新或修复,只需要更新库文件本身即可,不需要重新编译和链接所有使用该库的程序。此外,动态链接还可以在程序运行时提供灵活的模块加载和卸载的能力。

运行动态库可执行文件

重点就是在执行过程原本是得到虚拟地址从而知晓映射的物理地址,变成了通过库文件的引用加文件中的偏移量得到数据映射的物理地址。

  1. 程序运行前:

    1. 编译:在编译动态库文件时,编译器会生成目标模块和符号表,并将符号表保留在动态库中。

    2. 链接:动态库文件中包含了函数和数据的位置信息,但是实际的代码和数据并不在库文件中。在编译可执行文件时,链接器会将对动态库的引用标记为未解析的引用,并在可执行文件的符号表中添加对动态库的依赖。

  2. 程序运行时:

    1. 加载与链接:当程序运行时,操作系统的动态链接器(通常是ld.so或ld-linux.so)会根据可执行文件的依赖关系找到所需的动态库文件,并进行加载和链接。加载过程包括将动态库的代码和数据加载到内存中,解析符号表,并为未解析的引用进行符号解析,将其指向实际的代码和数据。

    2. 执行: 当程序需要使用动态库中的某个函数或变量时,它会通过调用动态链接器的函数来执行这个函数或变量。链接器会根据符号表找到这个函数或变量的位置,并执行它。

    3. 写实拷贝:如果对动态库的数据进行修改,会发生写实拷贝。

  3. 程序结束后:

    1. 卸载: 当动态库不再需要时,可以使用系统调用(如dlclose)来卸载它。这一步通常发生在程序结束时,以确保系统不会为不再使用的动态库分配资源。

动态编译与静态编译的区别:

  1. 时间上的分离:在静态编译中,编译器需要在链接阶段就处理所有的符号引用和重定位。而在动态链接中,这些操作可以在程序运行时由动态链接器来完成。这意味着,程序的某些部分可以在生成可执行文件后,执行可执行文件才被加载和链接。

  2. 空间上的分离:静态编译的程序通常将所有代码和数据都打包在一起,占据固定的内存空间。动态链接允许不同的程序实例共享相同的库副本,从而可以节省内存空间。

  3. 灵活性:动态链接使得程序可以更容易地更新和维护。例如,只需更新共享库而不必重新编译或重新安装整个程序,就可以引入新的功能或修复错误。

  4. 动态扩展:动态链接还允许程序在运行时加载额外的模块,从而提供扩展性和灵活性。这种机制在插件系统、扩展功能等方面非常有用。

  5. 性能和资源使用:静态编译通常比动态编译具有更好的性能,因为它在编译时解决了所有符号引用和重定位,避免了运行时加载和链接的开销。此外,静态编译的程序通常占用更少的内存,因为它不需要动态加载库。

  6. 安全性:动态链接允许程序在运行时加载新的库,这可能引入安全风险。例如,如果恶意用户能够控制要加载的库,他们可能会利用它来执行恶意代码。静态编译可以减少这种风险,因为它在编译时解决了所有符号引用。

  7. 代码可维护性:静态编译通常使得代码更易于理解和维护,因为它不需要动态加载库。这使得代码更易于审查和调试。

  8. 可移植性:动态链接允许程序在不同的操作系统或硬件平台上运行,而无需重新编译。这对于跨平台应用程序特别有用。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号