库制作与原理

什么是库

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:

  • 静态库 .a[Linux]、.lib[windows]
  • 动态库 .so[Linux]、.dll[windows]
cpp 复制代码
// ubuntu 动静态库
// C
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so
$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a
//C++
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linux-
gnu/9/libstdc++.so -> ../../../x86_64-linux-gnu/libstdc++.so.6
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
// Centos 动静态库
// C
$ ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so
[whb@bite-alicloud ~]$ ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a
// C++
$ ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 ->
libstdc++.so.6.0.19
$ ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l
-rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhat-
linux/4.8.2/libstdc++.a

预备工作,准备好代码,在任意新增"库文件"

cpp 复制代码
//file.c
#include<stdio.h>

int main()
{
  Myprint();
  return 0;
}

//hello.c
#include<stdio.h>
void Myprint()
{
  printf("hello\n ");
}

//hello.h
void Myprint();
bash 复制代码
//Makefile
libmyc.a:hello.o file.o
	ar -rc $@ $^
hello.o:hello.c
	gcc -c $< 
file.o:file.c
	gcc -c $<

.PHONY:output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp -f *.h lib/include
	cp -f *.a lib/mylib

.PHONY:clean
clean:
	rm -f *.o libmyc.a

上述代码实现是静态库的原理,原来只要我把.o文件,打包链接起来就可以达到库的效果!头文件包含了方法的声明,库包含了方法的实现,所有的库(无论是动还是静),本质是源文件对应的.o!

静态库

静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再

需要静态库,本质就是.o打了一个包。

  • 一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc的 -static 强转设置链接静态库。

ar 是 gnu 归档工具, rc 表示 (replace and create),.a静态库,本质是一种归档文件,不需要使用者解包,而用gcc/g++直接进行链接即可!

  • t: 列出静态库中的文件
  • v:verbose 详细信息

库的命名一般一lib开头,.a结尾,去首区尾就是库的名字

库的使用

1.指定参数

cpp 复制代码
//user.c
#include<stdio.h>

int main()
{
  Myprint();
  return 0;
}

如果我们要连接任何非C/C++标准库(包括其他外部或者我们自己写的) 都要指明 -l 或 -L

去哪里找(-L)找什么库(-l)

  • -L: 指定库路径
  • -I: 指定头文件搜索路径
  • -l: 指定库名
  • 测试目标文件生成后,静态库删掉,程序照样可以运行

或着在需要链接库的.c文件里面导包链接绝对路径

2.将库拷贝至Linux的默认路径下

因为是第三方库,还得带-l

所以,对库的安装,其实就是将对应的头文件,库文件拷贝到系统的指定文件下,将来编译时只要指定库名称,gcc自动找头文件和库文件

动态库

  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

动态库生成

  • shared: 表示生成共享库格式
  • fPIC:产生位置无关码(position independent code)
  • 库名规则:libxxx.so

与静态库不同的是,静态库使用ar归档类打包,动态库使用gcc打包(gcc既可以形成二进制文件,也可以形成动态库)

bash 复制代码
libmyc.so:hello.o file.o
	gcc -shared -o $@ $^
hello.o:hello.c
	gcc -fPIC -c $< 
file.o:file.c
	gcc -fPIC -c $<

.PHONY:output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp -f *.h lib/include
	cp -f *.so lib/mylib

.PHONY:clean
clean:
	rm -f *.o libmyc.so

动态库的使用

cpp 复制代码
//user.c
#include<stdio.h>
#include"hello.h"

int main()
{
  Myprint();
  return 0;
}

在同级目录下直接链接

库运行搜索路径

1.拷贝 .so 文件到系统共享库路径下

一般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等入,如静态库处理一样

2.向系统共享库路径下建立同名软连接

3.更改环境变量: LD_LIBRARY_PATH

将动态库的路径填进LD_LIBRARY_PATH环境变量里,系统也会在这里面找,像这种更改环境变量的操作,系统重启后就恢复了

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

创建一个.conf文件,将库的路径填入,如果不行就用nano,vim可能被配置过

细节:动静态库g++/gcc默认使用动态库,非静态链接只能-static,一旦-static,就必须得存在对应的静态库,只存在静态库,可执行程序对于该库,只能静态链接了

在Linux系统下,默认安装大部分库,默认优先安装动态库,库:应用程序=1:n,vs不仅可以形成可执行程序,也能形成动静态库

目标文件

编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常⽅便,但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这一系列操作。

在编译之后会生成两个扩展名为 .o 的文件,它们被称作目标⽂件。要注意的是如果我们

修改了一个原文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件,文件的格式是 ELF ,是对二进制代码的一种封装。

bash 复制代码
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
## file命令⽤于辨识⽂件类型。

ELF文件

要理解编译链链接的细节,我们不得不了解⼀下ELF文件。其实有以下四种文件其实都是ELF文件:

  • 可重定位文件(Relocatable File) :即 xxx.o 文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
  • 可执行文件(Executable File) :即可执行程序。
  • 共享目标文件(Shared Object File) :即 xxx.so文件。
  • 内核转储(core dumps) ,存放当前进程的执行上下文,用于dump信号触发。一个ELF文件由以下四部分组成:
  • ELF头(ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
  • 程序头表(Program header table) :列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。
  • 节头表(Section header table) :包含对节(sections)的描述。
  • 节(Section ):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。最常见的节:代码节(.text):用于保存机器指令,是程序的主要执行部分。数据节(.data):保存已初始化的全局变量和局部静态变量。

ELF从形成到加载轮廓

ELF形成可执行:

  • step-1:将多份 C/C++ 源代码,翻译成为目标 .o 文件
  • step-2:将多份 .o 文件section进行合并

一个ELF存着各种数据,链接就是把每个ELF合并起来重新形成一个ELF,合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并,此处不做过多追究

ELF可执行文件加载

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

合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。

这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起很显然,这个合并工作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的**程序头表(Program header table)**中

查看可执行程序的section: readelf -S xxx

bash 复制代码
[xm@hcss-ecs-68d7 study_06]$ readelf -S user
There are 30 section headers, starting at offset 0x1918:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 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
       000000000000003c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002d8  000002d8
       00000000000000f0  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004003c8  000003c8
       000000000000006e  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400436  00000436
       0000000000000014  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400450  00000450
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400470  00000470
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400488  00000488
       0000000000000048  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         00000000004004d0  000004d0
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004004f0  000004f0
       0000000000000040  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000400530  00000530
       0000000000000182  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         00000000004006b4  000006b4
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         00000000004006c0  000006c0
       0000000000000010  0000000000000000   A       0     0     8
  [16] .eh_frame_hdr     PROGBITS         00000000004006d0  000006d0
       0000000000000034  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000400708  00000708
       00000000000000f4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000600e00  00000e00
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000600e08  00000e08
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000600e10  00000e10
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000600e18  00000e18
       00000000000001e0  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000030  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         0000000000601030  00001030
       0000000000000004  0000000000000000  WA       0     0     1
  [25] .bss              NOBITS           0000000000601034  00001034
       0000000000000004  0000000000000000  WA       0     0     1
  [26] .comment          PROGBITS         0000000000000000  00001034
       000000000000002d  0000000000000001  MS       0     0     1
  [27] .symtab           SYMTAB           0000000000000000  00001068
       00000000000005e8  0000000000000018          28    46     8
  [28] .strtab           STRTAB           0000000000000000  00001650
       00000000000001bf  0000000000000000           0     0     1
  [29] .shstrtab         STRTAB           0000000000000000  0000180f
       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)

NR编号就好比是一个数组存放不同的Section, 属性(Type),地址(Address),以及偏移量(Offset),通过程序头表(Program header table)存放的起始地址和偏移量(Offset),就能找到对于的Section ,也是通过这种方式划分不同的section节点,每个节点都存放着二进制文件的不同信息

读取可执行程序合并后的ELF:readelf -l

bash 复制代码
[xm@hcss-ecs-68d7 study_06]$ readelf -l user

Elf file type is EXEC (Executable file)
Entry point 0x400530
There are 9 program headers, starting at offset 64

Program Headers:
  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
                 0x00000000000007fc 0x00000000000007fc  R E    200000
  LOAD           0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
                 0x0000000000000234 0x0000000000000238  RW     200000
  DYNAMIC        0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
                 0x00000000000001e0 0x00000000000001e0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000006d0 0x00000000004006d0 0x00000000004006d0
                 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:
  Segment Sections...
   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 

将不同section合并成一个section,在内存上加载就在一块了

为什么要将section合并成为segment?

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

对于 程序头表和节头表又有什么用呢,其实 ELF文件提供 2 个不同的视图/视角来让我们理解这

两个部分:
链接视图(Linking view) - 对应节头表 Section header table

  • 文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。
  • 为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并了后,空间利用率就高了,否则,很小的很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一块给你,比如4k),所以,链接器趁着链接就把小块们都合并了。

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

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

说白了就是:一个在链接时作用,一个在运行加载时作用

从链接视图来看:

  • 命令 readelf -S hello.o 可以帮助查看ELF文件的节头表。
  • .text节 :是保存了程序代码指令的代码节。
  • .data节 :保存了初始化的全局变量和局部静态变量等数据。
  • .rodata节 :保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所
    以只能存在于一个可执行文件的只读段中。因此,只能是在text段(不是data段)中到.rodata节。
  • .BSS节 :为未初始化的全局变量和局部静态变量预留位置
  • .symtab节 : Symbol Table 符号表,就是源码里面那些函数名、变量名和代码的对应关系。
  • .got.plt节 (全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节一起提供
    了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。使用readelf 命令查看 .so文件可以看到该节。

从执行视图来看:

  • 告诉操作系统哪些模块可以被加载进内存。
  • 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。我们可以在ELF头 中找到文件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。例如我们查看下hello.o这个可重定位文件的主要信息:
bash 复制代码
[xm@hcss-ecs-68d7 study_06]$ readelf -h user
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400530
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6424 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

理解连接与加载

静态链接

无论是自己的.o, 还是静态库中的.o,本质都是把.o文件进行连接的过程,所以:研究静态链接,本质就是研究.o是如何链接的

链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在一起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程

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

ELF加载与进程地址空间

虚拟地址/逻辑地址

一个ELF程序,在没有被加载到内存的时候,有没有地址呢?进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?

一个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用"平坦

模式"进行工作。所以也要求ELF对自己的代码和数据进行统一编址,下面是 objdump -S 反汇编

之后的代码

最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量), 但是我们

认为起始地址是0.也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执

行程序进行统一编址了.
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?

从ELF各个segment来,每个segment有自己的起始地址和自己的长度,⽤来初始化内核结构中的[start, end]等范围数据,另外在用详细地址,填充页表.
所以:虚拟地址机制,不光光OS要支持,编译器也要支持.

进程如何看到动态库

相关推荐
涛ing6 分钟前
【Linux “less“ 命令详解】
linux·运维·c语言·c++·人工智能·vscode·bash
Blossom.1186 分钟前
KWDB创作者计划—深度解析:AIoT时代的分布式多模型数据库新标杆
数据库·分布式·架构·边缘计算·时序数据库·持续部署·ai集成
林木木木木木木木木木2 小时前
【随身WiFi】随身WiFi Debian系统优化教程
linux·运维·debian·随身wifi
mr_cmx2 小时前
Nodejs数据库单一连接模式和连接池模式的概述及写法
前端·数据库·node.js
不剪发的Tony老师2 小时前
WhatTheDuck:一个基于浏览器的CSV查询工具
数据库
临观_2 小时前
打靶日记 zico2: 1
linux·网络安全
痆古酊旳琲伤3 小时前
Linux驱动开发1 - Platform设备
linux·驱动开发
淬渊阁3 小时前
Hello world program of Go
开发语言·后端·golang
Pandaconda3 小时前
【新人系列】Golang 入门(十五):类型断言
开发语言·后端·面试·golang·go·断言·类型
周Echo周3 小时前
16、堆基础知识点和priority_queue的模拟实现
java·linux·c语言·开发语言·c++·后端·算法