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

Linux修炼:库制作与原理(一)

         Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》、《数据结构与算法之美》、《C++修炼之路》、《Linux修炼:终端之内 洞悉真理》、《Git 完全手册:从入门到团队协作实战》

感谢你打开这篇博客!希望这篇博客能为你带来帮助,也欢迎一起交流探讨,共同成长。

目录

1、什么是库

2、静态库

创建库的头文件(mylibrary.h)

实现库的源文件(mylibrary.cpp)

使用库的主程序(main.cpp)

编译和链接

3、动态库

4、ELF文件

       4.1、认识ELF格式

ELF头部(ELF Header)

程序头表(Program Header Table)

节头表(Section Header Table)

节(Sections)

      4.2、谈链接过程

        静态链接的过程

        关键特点


 

1、什么是库

        库就是已经写好的可以复用的代码。库是一种可执行代码的二进制形式,可以被操作系统载入到内存执行。库有两种:

静态库 .a[Linux]、.lib[windows]

动态库 .so[Linux]、.dll[windows]

2、静态库

        程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。

        我们首先自己写一个库来理解一下什么是库:

        我们准备三个文件,一个是创建库的头文件,一个是实现库的源文件,还有一个是使用库的主程序。

创建库的头文件(mylibrary.h)

#ifndef MYLIBRARY_H
#define MYLIBRARY_Hnamespace MyLibrary {class Calculator {public:static int add(int a, int b);static int subtract(int a, int b);};void greet(const char* name);
}#endif // MYLIBRARY_H

实现库的源文件(mylibrary.cpp)

#include "mylibrary.h"
#include <iostream>namespace MyLibrary {int Calculator::add(int a, int b) {return a + b;}int Calculator::subtract(int a, int b) {return a - b;}void greet(const char* name) {std::cout << "Hello, " << name << "!" << std::endl;}
}

使用库的主程序(main.cpp)

#include "mylibrary.h"
#include <iostream>int main() {// 使用库中的类std::cout << "5 + 3 = " << MyLibrary::Calculator::add(5, 3) << std::endl;std::cout << "5 - 3 = " << MyLibrary::Calculator::subtract(5, 3) << std::endl;// 使用库中的函数MyLibrary::greet("World");return 0;
}

编译和链接

要将上述代码编译为库并链接到主程序,可以按照以下步骤操作:

  1.  编译库文件为对象文件(注意是把库的实现文件编译成.o)

    g++ -c mylibrary.cpp -o mylibrary.o
    
  2. 将对象文件打包为静态库

    ar rcs libmylibrary.a mylibrary.o
    
  3. 其中,ar是创建,修改,提取,静态库文件的命令,rcs是三个命令选项,其中r是替换或添加文件到归档中,c是静默模式,不显示警告信息,s是创建或更新归档索引。我们要把目标文件mylibrary.o打包成静态库libmylibrary.a。库必须以lib开头。

  4. 编译主程序并链接库:

    g++ main.cpp -L. -lmylibrary -o myprogram
    
  5. -L.是指定链接器搜索库文件的路径是当前目录。链接器会在当前目录寻找libmylibrary。-lmylibrary为我们要链接的库名。-l自动补全前缀lib和后缀。-o myprogram指生成的可执行文件名为myprogram

  6. 运行程序:

    ./myprogram
    

         库其实就是把程序员写好的源代码,编译成.o,然后打了个包。

        此时如果我们使用ldd命令去查看myprogram的依赖库信息,会发现查不到。这是因为静态库会把自己的代码合并到目标文件,即一旦形成可执行程序,就不再依赖静态库了。

        一般官方的库都被装在了系统的默认路径下,而gcc/g++编译程序时,一般会在默认路径下查找。所以,假设我们的可执行程序中只包含了stdio.h这个头文件,那么我们不需要-L来指示库的位置,因为C标准库就在系统的默认路径中。所谓的库的安装,主要就是把库文件拷贝到系统的默认路径下。

        又因为gcc是专门编译C语言的,所以他默认就要认识libc。因此在编译的时候,我们不需要带着-lc。第三方的库都需要带-l来表示库名称。

        假设我们现在想把自己写的静态库中的头文件在include的时用<>包含起来,我们需要怎么做呢?

        第一种方法就是在执行命令时,加上-I.,表示在当前路径搜索头文件。第二种方式就是把我们的头文件也拷贝到系统的指定目录下。我们把libmylibrary移动到默认路径下,再把头文件拷贝到系统的指定目录之后,我们就可以通过执行以下命令成功编译程序:

g++ main.cpp -lmylibrary -o myprogram

        库=头文件+库文件。库的使用需要搜索找到头文件(-I),库路径(-L),库是谁(-l)。库如果不想过多使用上面的选项,就需要把库安装到指定了系统特定路径。一般情况下,头文件拷贝到/user/include,而库文件拷贝到/lib64。

        现在我们正儿八经的用make构建制作静态库的流程:

libmyc.a:mymath.o mystdio.oar -rc $@ $^
%.o:%.cgcc -c $<.PHONY:clean
clean:rm -f *.a *.o.PHONY:output
output:mkdir mylibcmkdir -p mylibc/includemkdir -p mylibc/libcp *.h mylibc/includecp *.a mylibc/libtar czf mylibc.tgz mylibc     

       

        执行make,会自动生成静态库的压缩包。

3、动态库

如果想创建动态库(共享库),可以使用以下命令:

  1. 编译为动态库:

    g++ -shared -fPIC mylibrary.cpp -o libmylibrary.so
    
  2. 其中,-shared指示编译器生成动态链接库(共享库),而不是可执行文件。-fPIC指生成位置无关代码,这时动态链接库必需的,确保代码可以在内存中任意位置加载。动态库的名称一般以.so为后缀。

  3. 链接动态库:

    g++ main.cpp -L. -lmylibrary -o myprogram
    
  4. 运行前需要设置库路径:

    export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
    ./myprogram
    

        现在我们自动化构建动态库:

libmyc.so:mymath.o mystdio.ogcc -o $@ $^ -shared 
%.o:%.cgcc -fPIC -c $<.PHONY:clean
clean:rm -rf *.so *.o mylibc *.tgz.PHONY:output
output:mkdir mylibcmkdir -p mylibc/includemkdir -p mylibc/libcp *.h mylibc/includecp *.so mylibc/lib                                                                                                                                                                          tar czf mylibc.tgz mylibc

        使用libmyc.so时,我们需要指示库的路径,头文件的路径,以及库的名字:

gcc test.c -I mylibc/include. -L mylibc/lib/ -lmyc

        在我们配置编译器环境时,比如vs,它会自动给我们下载一堆东西,这堆东西中就有各种我们需要用的的库和头文件。

        接着我们运行程序,发现会报错,这是因为我们的动态链接要求在运行时也要能找到动态库。我们时刻需要访问动态库。而之前在编译时,我们只是告诉了编译器动态库在哪里,但是系统(中的加载器)并不知道动态库在哪。

        我们可以把动态库拷贝到lib64(即系统默认路径)中,这样系统和编译器就都能找到了。当然我们还有别的方法,我们还可以在libb64目录下,以软连接的方式进行关联。软连接的名字应当与库名称一样。其实一般情况下,我们所有的库都是放在lib64中的。

        除了这两种方式以外,还有一种方式就是通过环境变量。我们可以配置指定的环境变量,让系统找到指定的动态库。即上面第4条介绍的那种方式。这种方式是临时方式。

        除此之外,我们还可以将动态库查找路径全局有效,更改系统配置文件。在这里就不演示了,因为我们常用的是第一二种方法。

        动态库和静态库同时存在的时候,gcc/g++默认优先使用动态库。默认进行动态链接。一个可执行程序,可能会依赖多个库。如果我们只提供静态库,即使我们是动态链接,gcc也没有办法,只能对只提供静态库的库进行静态链接。

        我们在编译时可以使用--static选项,要求必须采用静态链接。这种情况下,静态库必须存在。

        大部分系统只安装了C/C++的动态库,没有安装静态库。我们可以通过以下指定手动安装静态库:

#C语言
sudo apt/yum install glibc-static
#C++
sudo apt/yum install libstdc++-static

4、ELF文件

       4.1、认识ELF格式

         在过去的一段时间里,我们接触过几种特殊的文件,包括可重定位文件(.o为后后缀),可执行文件,共享目标文件(.so为后缀),内核转储。以上这些文件都是ELF文件。

        以人类的眼光看,我们直接打开ELF文件,里面的内容都是乱码。其实这些乱码也是有固定的存储规则的。

        

ELF文件由以下部分组成:

ELF头部(ELF Header)

描述文件的基本属性,如文件类型(可执行、目标文件等)、目标架构(如x86、ARM)和程序入口地址。

程序头表(Program Header Table)

仅存在于可执行文件和共享库中,定义如何将文件加载到内存。每个条目描述一个段(如代码段、数据段)的内存布局。

节头表(Section Header Table)

包含文件中所有节(section)的描述信息,如代码节(.text)、数据节(.data)和符号表(.symtab)。目标文件通常依赖此表进行链接。

节(Sections)

存储实际内容,例如:

  • .text:可执行代码。
  • .data:已初始化的全局变量。
  • .bss:未初始化的全局变量(不占文件空间)。
  • .rodata:只读数据(如字符串常量)。

        我们可以通过readelf命令来获取ELF文件的各种信息,比如我们可以查看ELF Header中的信息:         

        ELF(Executable and Linkable Format)文件的开头是一个固定大小的头部结构(ELF Header),它描述了整个文件的基本属性和布局。以下是 ELF Header 中存储的关键信息:

  1. Magic Number

    • 前 4 字节为 0x7F'E''L''F',用于标识文件是否为 ELF 格式。
    • 后续字节包含 ELF 文件的类别(32/64 位)、字节序(大端/小端)和版本信息。
  2. 文件类型

    • 2 字节字段,标识文件类型,如:
      • ET_REL(可重定位文件,如 .o 文件)
      • ET_EXEC(可执行文件)
      • ET_DYN(共享库,如 .so 文件)
  3. 机器架构

    • 2 字节字段,指定目标机器的 CPU 架构,例如:
      • EM_X86_64(x86-64)
      • EM_ARM(ARM)
      • EM_RISCV(RISC-V)
  4. ELF 版本

    • 4 字节字段,通常为 EV_CURRENT(当前版本)。
  5. 程序入口地址

    • 8 字节字段(64 位)或 4 字节字段(32 位),指定可执行文件的入口点(即 _start 函数的地址)。
  6. Program Header 和 Section Header 的位置

    • e_phoff:Program Header 表在文件中的偏移量。
    • e_shoff:Section Header 表在文件中的偏移量。
  7. 标志与对齐信息

    • 处理器特定的标志(如 ABI 版本)。
    • Program Header 和 Section Header 的条目大小及数量。

我们可以通过指令来读一下节头表中的内容:

        其中包含了各个节的名称,比如text(文本),rodate(已初始化全局数据区)等等。Section Header会记录一共有多少个节,并且记录每个节的大小、偏移量和权限等等。

        节的大小不一定是4KB,但操作系统每次读取数据都是以4KB为单位的。而且,不同的节也可能会有相同的属性。所以,我们每次IO的时候,都需要将多个属性相同的section合并,对齐4KB(对齐4KB的倍数)。合并之后的数据节叫数据段(segment)。没错,我们之前学过的什么BSS段,数据段,代码段,都是从ELF文件中加载进来的。ELF加载到内存的时候,会被OS自动合并成为多个segment,加载到内存中。而程序头表(Program Header Table)就是合并成为segment的方法表。也就是说,合并的方式在ELF文件被创建的时候就已经规定好了,就像所有命运馈赠的礼物早就被暗中标好了价格。

        Section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并, 假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分 为4097字节,.init部分为512字节(.init和.text属性相同),那么它们将占用3个页面,而合并后,它们只需2个页面。此外,操作系统在加载程序时,会将具有相同属性的section合并成⼀个大的 segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问。

        我们可以通过指令查看程序头表(Program Header Table)中的内容:

        合并之后就变成了八个数据段,分别对应编号01到08,最后合并成的数据段的个数是不固定的,不同文件可能不一样。

        一个ELF文件要有两种视角,一种视角是编译器视角(section),另一种是系统视角(segment)。

      4.2、谈链接过程

        链接其实就是把多个ELF文件中对应的属性相同的section合并到一起:

        当然实际上的链接要复杂的多。

        静态链接的过程

        符号解析
        链接器读取所有输入的目标文件,检查每个目标文件中的符号定义与引用。确保每个符号有且仅有一个定义,解决所有未解析的符号引用。

        地址与空间分配
        为输出文件中的代码段、数据段等分配运行时内存地址。合并所有目标文件的同类段(如.text.data),并确定每个段在最终可执行文件中的大小和位置。

        重定位
        修改目标文件中的符号引用地址,使其指向正确的运行时地址。根据段合并后的新基址,调整代码和数据中的相对地址或绝对地址引用。

        生成可执行文件
        将重定位后的代码、数据及必要的头部信息(如ELF头、段表)写入输出文件,形成可直接加载运行的静态可执行文件。

        关键特点

  • 所有外部符号在链接时已解析并绑定。
  • 最终文件独立运行,无需运行时加载动态库。
  • 文件体积较大,因库代码被完整复制到可执行文件中。

        所以,链接过程中会涉及到对.o中外部符号进行地址重定位。

        一个ELF可执行程序,在没有加载到内存的时候,就已经有地址了!Linux系统编译形成可执行程序的时候,需要对代码和数据进行编址。当代CPU和计算机和操作系统,在对ELF编址的时候采用的做法都是平坦模式进行的,我们仍可以看作是段起始地址+偏移量的方式,只不过段起始地址是0。

        这种从全零到全F的线性编址得到的地址起始就是我们之前讲的虚拟地址。在磁盘中的可执行文件,它的地址是以逻辑地址的方式表示的,而在内存中,它是以虚拟地址的方式表示的。尽管在磁盘中是逻辑地址,在内存中是虚拟地址,但是他们都是从全0到全F的。

        当我们把可执行程序加载到内存中时,我们的程序内部就建立起了物理地址到虚拟地址的映射关系。这个映射关系被记录在页表之中。cpu可以根据存储在EIP中的程序地址入口(即ELF Header中的entry point address)去访问物理地址。需要注意的是,这个程序地址入口存储的地址是程序在内存中的虚拟地址。当cpu拿到虚拟地址之后,在根据页表得到物理地址再进行访问。当CPU需要将虚拟地址转换为物理地址时,会从CR3寄存器获取页表基址开始查找。

        CR3 寄存器为 MMU 提供了页表的起始地址。在 x86 分页机制中,虚拟地址到物理地址的转换需要多级页表(如页目录和页表)。CR3 存储的是页目录的物理地址,MMU 使用 CR3 的值作为起点,逐级查找页表项,最终完成地址转换。转换完成后,cpu和内存又通过系统总线连接起来,进行数据的传输。

        好了,今天的内容就分享到这,我们下期再见!

 

http://www.dtcms.com/a/532288.html

相关文章:

  • 本地安装yolo算法环境的步骤
  • 8.1 时钟树
  • perl网站建设南宁网站定制
  • 计算机网络自顶向下方法6——应用层 进程通信与运输服务
  • HTTP 常考问题简洁回答(速记版)
  • MQTT 与 HTTP 协议对比
  • 商城网站建设视频教程wordpress教程cms
  • SZU大学物理A2实验报告-汇总链接-free
  • IOT项目——电源入门系列-第四章
  • ① leetcode刷题汇总(数组 / 字符串)
  • 网站名称填写什么晋中品牌网站建设建设
  • 宫殿记忆术AI训练系统:可扩展的终身记忆框架
  • 掌握机器学习算法及其关键超参数
  • 网站建设收费价目表织梦制作网站如何上线
  • 【传奇开心果系列】基于Flet框架实现的窗口加载显示本地图像示例自定义模板实现原理深度解析
  • 机器学习算法常用算法
  • Gorm(七)关联的Tag写法
  • 零基础理解k8s
  • *Python基础语法
  • 广东卫视你会怎么做网站化妆品网站的建设方案
  • WPF 静态样式与动态样式的定义及使用详解
  • 有没有专业做盐的网站手机wap网站程序
  • 【线程同步系列6】一个基于VC封装的多线程类CMyThread(类似QT中的QThread类的run方法)
  • python+vue旅游购票管理系统设计(源码+文档+调试+基础修改+答疑)
  • 宠物管理|宠物店管理|基于SSM+vue的宠物店管理系统(源码+数据库+文档)
  • 站内关键词自然排名优化制作图片的免费软件
  • Cline中模型识别任务与clinerules相关性的实现逻辑
  • Linux 进程面试考点:进程状态、通信方式、信号量等关键问题速记
  • 网站建设有哪些类型西昌网站建设公司
  • 风中有朵雨做的云网站观看美容网站开发