Linux:库制作与原理

库的概念

库就是写好的代码,但这个代码又要经常使用,就把这个代码写入库中,我们就可以直接用现成的,就不需要从零开始写。

本质上库是一种可执行的二进制形式,可以被操作系统载到内存中执行。库两种:

动态库:在Linux下,后缀为".so";在windows下,后缀为".dll"

静态库:在Linux下,后缀为".a";在windows下,后缀为".lib"

库的命名

在Linux下,静态库:"libXXX.a",其中XXX是静态库的名字;动态库:"libXXX.so",其中XXX是动态库的名字。

简单设计两个库,为方便接下来的操作:

strlen_str.h

复制代码
#pragma once
int strlen_str(const char* str);

strlen_str.c

复制代码
 #include"mystring.h"
 
int strlen_str(const char* str)
 {
     int count=0;
     const char*end=str;
     while((*end)!='\0')
     {
         count++;
         end++;
     }
     return count;
 }

add.h

复制代码
#pragma once
int add(int x,int y);

add.c

复制代码
#include"add.h"
 int add(int x,int y)
 {
     return x+y;
 }

查看文件依赖的库

通过 ldd a.out(可执行文件)可以查询到该文件依赖的库

拿到了库的信息后,通过ls -l 库的信息。

我们会发现该库的信息其实是软链接,libc-2.17.so才是真正被执行的库文件。

通过file /libc/libc-2.17.so,我们可以知道库文件的属性。

该库文件是个动态库即共享的目标文件库。

静态库

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

我们的编译默认为动态链接库,只有在该库找不到动态.so的时候才会采用静态库。我们也可以通过gcc后面加-static强转成静态链接库。详细部分可看Linux基础开发工具链接部分。

打包

静态库实际上是把库源代码形成的obj打包整合成一个整体。形成静态库指令:

ar -rc libc.a *.o//把*.o文件打包进入libc.a文件

r(replace):如果库中已经存在同名的文件(.o),则用新的.o文件替换它;若不存在,则添加新文件到库里。

c(creat):如果指定的库文件不存在,则自动创建一个新的库文件(无需手动预先创建空文件)。

root@localhost lib# ar -tv libmy.a

rw-r--r-- 0/0 1240 Oct 11 21:35 2025 add.o

rw-r--r-- 0/0 1280 Oct 11 21:36 2025 strlen_str.o

t:列出静态库中的文件

v:详细情况

使用

makefile

复制代码
libmy.a:add.o strlen_str.o
    @ar -rc $@ $^
%.o:%.c
    @gcc -c $<
.PHONY:clean
clean:
    @rm -f libmy.a *.o  stdc*
.PHONY:output
output:
    @mkdir -p stdc/include //创建一个装头文件的新目录
    @mkdir -p stdc/lib//创建一个装静态库的新目录
    @cp -f *.h stdc/include//复制当前目录中的头文件进去
    @cp -f *.a stdc/lib//复制当前目录中的静态库进去
    @tar -czf stdc.tgz stdc//把stdc文件压缩成stdc.tgz

系统能调用的库一般都放在/lib路径中,所以库的安装本质上就是把库文件拷贝到/lib路径中,但是上面这个stdc/lib目录并没有放在系统查找的位置,系统找不到该库文件。

在C/C++语言中,可被使用的库=头文件+库文件(.a/.so),头文件应该放在系统默认路径下/usr/include,我们把这些都放在了第三方库中,所以当我们直接运行的时候,系统根本找不到这个库文件和头文件。

复制代码
//main.c
#include"add.h"
#include"strlen_str.h"
#include<stdio.h>
int main()
{
    int a,b;
    a=10;
    b=30;
    printf("a+b=%d\n",add(a,b));
    const char*str="hello world\n";
    printf("字符串str有%d个字符\n",strlen_str(str));
    return 0;
}

让文件能使用第三方库的有三种场景:

1.头文件和库文件在系统默认路径下:

gcc main.c -lmy

2.头文件和库文件和自己的源文件在同一个路径下,头文件不用表明。

gcc main.c -L. -lmy

3.头文件和库文件都有自己的独立路径

gcc main.c -I头文件路径 -L库文件路径 -lmy

-I(大i):表明头文件的路径位置

-L:表明库文件的路径位置

-l(小L):表明库的名(libmy.a的名字去除前缀lib 、去除后缀.a,得到文件名为my)

注:测试目标文件生成后,静态库删除,程序照样可以运行。

以场景2为例:

root@localhost d1# gcc main.c -L. -lmy

root@localhost d1# ./a.out

a+b=40

字符串str有12个字符

root@localhost d1# rm -f libmy.a

root@localhost d1# ./a.out

a+b=40

字符串str有12个字符

可以通过ldd 库查找库的依赖:

动态库

动态库(.so):在程序运行的时候才去链接动态库的代码,多个程序可以共享一个动态库。

一个与动态链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目录文件的整个机器码。因为多个程序共享一个动态库,如果把所有的机器码加入到可执行文件中,那么就和静态库一样,但如果我们只要给该执行文件中加入用到函数的地址,内核就可以直接根据·地址找到函数使用,就不会占用很多内存。(后面详细讲解)

在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的动态库中复制到内存中,这个过程称为动态链接。所以使用动态库的可执行文件,是在内存中链接动态库的;使用静态库的可执行文件,是在磁盘中链接静态库。

动态库可以在多个程序间共享,所以动态库让可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省内存和磁盘空间。

打包

gcc -o libmyso.so *.o -shared

-shared:表示生成共享库格式

使用

makefile

libmy.a:add.o strlen_str.o

@gcc -o @ ^ -shared

%.o:%.c

@gcc -fPIC -c $<

.PHONY:clean

clean:

@rm -f libmy.so *.o stdc*

.PHONY:output

output:

@mkdir -p stdc/include

@mkdir -p stdc/lib

@cp -f *.h stdc/include

@cp -f *.so stdc/lib

@tar -czf stdc.tgz stdc

fPIC:产生位置无关码(position independent code)

共享库的特性是加载到内存的地址是"动态分配"的。-fPIC的作用的可以让动态库加载到内存的任意位置。

如果编译的时候没有加-fPIC,编译器会假设代码最终会被加载到内存中的某个"固定地址"(编译时编译器临时分配的),并在生成的机器指令中直接使用这个地址,但是动态库的加载地址是不确定的,因此这些动态库里的代码会失效。

和上面的文件使用第三方库的方式一样,有三种情况:

1.头文件和库文件在系统默认路径下:

gcc main.c -lmy

2.头文件和库文件和自己的源文件在同一个路径下,头文件不用表明。

gcc main.c -L. -lmy

3.头文件和库文件都有自己的独立路径

gcc main.c -I头文件路径 -L库文件路径 -lmy

可以通过ldd 库查找库的依赖:

复制代码
[root@localhost d2]# ldd libmyso.so
	linux-vdso.so.1 =>  (0x00007ffd0156d000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f31ef08e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f31ef65e000)

但是当我们用ldd+可执行文件查找文件所依赖的库时,我们发现文件中找不到依赖动态库的地址。

ldd提示找不到,本质是系统在预设的搜索路径中找不到对应的动态库文件。而且文件不能运行。

系统查找动态库的默认路径由 /etc/ld.so.conf 及其包含的配置文件(如 /etc/ld.so.conf.d/ 下的文件)定义,同时包含环境变量 LD_LIBRARY_PATH 中的路径。

若动态库安装在非默认路径(如自定义目录 /opt/lib),且未添加到上述配置中,ldd 会无法识别。

有四种方法可以在运行时,找到动态库:

1.把库直接拷贝到系统中/lib64

2.在系统/lib64内部,建立软链接

ln -s 绝对路径 系统路径

ln -s /home/d1/d2/libmyso.so /lib64/libmyso.so

3.配置LD_LIBRARY_PATH环境变量(临时方案)

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:绝对路径

4.通过配置文件添加库文件的绝对路径。

//查找配置文件( 全都是系统mo'r的系统目录

ls /etc/ld.so.conf.d/

.conf为动态库查找路径的配置文件

前两种方法比较常用。

目标文件

编译:将程序的源代码翻译成CPU额能读懂的机器代码。

.c文件通过编译(gcc -c XXX.c)后生成的扩展名为.o的文件,就叫做目标文件。如果我们修改了一个源文件,那么之需要单独编译目标文件即可,不需要浪费时间重新编译整个工程。目标文件是二进制文件,文件的格式是EIF,是对二进制代码的一种封装。

file命令:file+文件名(可以辨识文件类型)

ELF文件

编译链接的细节与ELF文件有关。

以下四种都是ELF文件:

1.可重定义文件(*.o):包含适合于与其他目标文件链接,去创建可执行文件或者共享目标文件的代码和数据。

2.可执行文件:可执行程序。

3.共享目标文件:动态库文件

4.内核转储:存放当前进程的执行上下文,用于dump信号触发。

这些EIF文件都由以下这四部分组成:

1.ELF头(EIF header):描述文件的主要特性。位于文件的开始位置,它的主要目的是定位文件的其他部份。

2.程序头表(Program header table):列举了所有有效的段(segments)和它们的属性。表里面记录每个段开始的位置和偏移量(offset)、长度。

3.节头表(Section header table):包含对节的描述。

4.节(Section):EIF文件的基本组成单位,包含了特定类型的数据。EIF文件的各种信息和数据都存储在不同的节中,比如代码节(.text)中存储了可执行代码,数据节(.data)中存储了已初始化的全局变量和局部静态变量等。

段是节的打包,比如代码段中包含.text节、.init节、.fini节。段通常在执行文件加载执行时发挥作用。

查找ELF文件中的节的类型和节的大小:size+ELF文件

查看ELF文件的ELF头的基本信息:readelf -h +ELF文件

ELF文件的组成格式:

注:.o文件中通常程序头表不存在或者不活跃。

ELF形成可执行文件

有两个步骤:

step-1:将多份源代码翻译成目标文件(.o文件)

step-2:将多份.o文件的相同属性的section进行合并。

可执行文件加载

一个ELF文件中会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment(段)。

合并是根据相同属性的原则进程合并。比如;可读,可写、可执行、需要加载时申请空间等。这样,即使是不同的节,但是属性相同,在加载到内存的时候,可能会以segment的形式,加载在一起。

合并方式在形成ELF的时候已经确定了,具体合并原则被记录在ELF的程序头表中。

查看节头表里节的名字和顺序:readelf -S+ELF文件

复制代码
[root@localhost d2]# readelf -S test_so
共有 30 个节头,从偏移量 0x1978 开始:

节头:
  [号] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       0000000000000038  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002d0  000002d0
       0000000000000108  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004003d8  000003d8
       000000000000007d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400456  00000456
       0000000000000016  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400470  00000470
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400490  00000490
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004004a8  000004a8
       0000000000000078  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         0000000000400520  00000520
       000000000000001a  0000000000000000  AX       0     0     4
    ......
   [29] .shstrtab         STRTAB           0000000000000000  0000186a
       0000000000000108  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

查看section合并的segment:readelf -l +可执行文件

复制代码
[root@localhost d2]# readelf -l test_so

Elf 文件类型为 EXEC (可执行文件)
入口点 0x4005a0
共有 9 个程序头,开始于偏移量64

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000008dc 0x00000000000008dc  R E    200000
  LOAD           0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
                 0x0000000000000244 0x0000000000000248  RW     200000
  DYNAMIC        0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
                 0x00000000000001e0 0x00000000000001e0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000007b4 0x00000000004007b4 0x00000000004007b4
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
                 0x0000000000000200 0x0000000000000200  R      1

 Section to Segment mapping:
  段节...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

第一个LOAD段表示代码段------在下面段节中,02段中包含的节有.text(代码节),第二个LOAD段表示数据段------在下面段节中,03段中包含的节有.data(数据节)。

为什么要把section合并成segment?

Section合并可以减少页面碎片,提高内存使用效率。假设页面的大小为4096字节(内存块基本大小,加载,管理的基本单位),.text部分为4097字节,.init部分为512字节,如果不合并,那么.text会占据两个页面,.init占据一个页面,一共占据三个。但是只要合并成segment,它们就只需要两个页面。

程序头表和节头表的作用是什么?从链接视图和执行视图讲解。

链接视图对应节头表(Section header table)

1.文件结构的粒度更细,将文件按功能模式的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解EIF文件中包含的各个部分的信息。

2.为了空间布局的效率,在链接目标文件时,链接器会把很多节合并,规整成可执行的段、可读写的段、只读的段等。合并后,空间利用率就高了。

执行视图对应程序头表(Program header table)

告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有program header table。

简单来说,就是节头表是在链接的时候起作用,程序头表是在运行加载的时候起作用。

从 链接视图 来看:

命令 readelf -S +ELF文件 可以帮助查看ELF文件的 节头表。

.text节 :是保存了程序代码指令的代码节。

.data节 :保存了已经初始化的全局变量和局部静态变量等数据。

.rodata节 :保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能是在text段(不是data段)中找到.rodata节。

.BSS节 :为未初始化的全局变量和局部静态变量预留位置。

.symtab节 : Symbol Table 符号表,就是源码里面那些函数名、变量名和代码的对应关系。

.got.plt节 (全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。使用 readelf -S命令查看 .so 文件可以看到该节。

从 执行视图来看:

1.告诉操作系统哪些模块可以被加载进内存。

2.加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。

静态链接

静态链接本质上就是把依赖的所有.o文件中需要的函数和引用变量直接拷贝到可执行文件中。

hello.c文件:

复制代码
#include<stdio.h>
int main()
{
    printf("hello world!!!\n");
    return 0;
}

具体拷贝过程:

1.收集依赖的目标文件。把.c文件编译成.o文件,静态链接器会把收集的所有需要的目标文件,包括自己写的.o文件和程序依赖的静态库。

2.解析符号并拷贝代码和数据。链接器会读取每个目标文件的符号表(.symtab段),找到程序调用函数和引用的变量对应的符号,然后从静态库中,将这些符号对应的.text代码段、.data数据段直接拷贝到最终的可执行文件中。

objdump -d命令:将代码段进行反汇编

hello.o中main函数不认识printf函数。

进行静态链接:gcc -o hello hello -static

3.重定位地址。完成拷贝后,链接器会修正代码中的地址引用。因为目标文件中的代码地址是"相对地址",拷贝到可执行文件后,需要将这些地址修改成可执行文件中的"绝对虚拟地址"。

objdump -d hello

还没链接前找不到函数printf函数的地址,所以相对地址为e8 00 00 00 00 ,链接后修改成了绝对虚拟地址e8 c4 0e 00 00。

4.生成独立的可执行文件。生成的可执行文件,不再依赖任何外部静态库。

readlf -s +ELF文件:读取ELF文件的字符表

目标文件:

puts表示printf函数的实现

UND就是:undefine,表示未定义,说白了就是本.o文件找不到

链接后:

0000000000401eb0:其实是地址

FUNC:表示printf符号类型是函数

6:就是printf函数所在的section被合并成最终的可执行文件中的那个section中,6就是下标。通过readelf -S hello查询。

动态链接和加载

动态链接就是把来链接部分推迟到程序加载的时候。比如我们运行一个程序,我们会把程序的代码和数据连同它要依赖的一系列库先加载到内存,但是每个动态库的加载地址是不固定的,操作系统会根据当前地址空间的使用情况去给它们动态分配一块内存。

当动态库被加载到内存后,一旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址。

进程间如何看到动态库?

通过虚拟地址可以看到共享库的起始地址。

在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到main函数。实际上,程序的入口点是_start,这是一个由C运行时库或链接器(如ld)提供的特殊函数。

在_start函数中,会执行一系列操作:

1.设置堆栈

2.初始化数据段:将程序的数据段从初始化数据段复制到对应的内存位置,并清零未初始化的数据段。

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

动态链接器负责在程序运行时加载动态库。当程序启动时,动态链接器会解析程序中依赖的动态库,并加载这些库到内存中。

总结:动态链接就是把动态库加载到内存后,一旦确定了动态库的地址,就可以找到动态库中每个函数的地址,在程序运行时,通过"内存重定位"修改可执行文件在内存中的"地址引用",让其指向动态库函数的实际内存地址。

程序怎么和库具体映射?

如何找到动态库中的函数?

在编译形成动态库的时候,每个库中的函数的偏移量已经有了,所以当我们知道了共享区的起始地址,就可以根据起始地址加上偏移量就可以找到需要的函数。

我们形成我们的程序,进行连接的时候,库名称保留,通过库名字@偏移量的方式来修改我们自己程序中的调用函数。比如libc.so@0x123456

动静态链接的区别

阶段 静态链接 动态链接
链接时操作 拷贝函数代码,写入实际地址 记录符号引用,不写入实际地址
重定位时机 链接阶段完成(一次性) 程序运行时(首次调用函数时)
可执行文件 包含所有依赖代码,体积大 不含动态库代码,体积小
库更新影响 需重新编译链接才能生效 替换动态库文件即可,无需重新编译

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

我们在程序运行的时候,会重定位函数的地址,那就是要修改可执行文件中的代码,但是代码区并不能修改, 在进程中只能读,所以,动态链接在.data(可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或者 函数的地址。

.data放在数据区是可读写的,所以支持动态修改。

所以我们寻找库中函数的地址就需要用GOT表来保存函数的偏移量。

注:

1.每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表

2.在单个动态库文件中,由于GOT表与.text的相对位置是固定的,我们可以完全可以利用CPU的相对寻址来寻找GOT表。

3.在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改成真正的地址。

相关推荐
大树8813 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠13 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质13 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush414 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52014 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz14 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工15 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智15 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩15 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_15 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化