库制作与原理
1.什么是库?
库是一种预编译的代码集合,它们被设计用来提供常用的功能和算法,以便在多个程序中重复使用,大大提高了代码的重用性,模块化和开发效率。
库分为两种:
静态库:.a[Linux],.lib[windows]
动态库:. so[Linux],.dll[windows]
2.静态库的制作
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库
我们的编译默认执行的为动态链接库,只有在该库找不到动态.so的时候才会采用同名静态库,我们也可以采用-static强转设置链接静态库
2.1静态库的生成
libmystdio.a:my_stdio.o my_string.o@ar -rc $@ $^@echo "build $^ to $@ ... done"%.o:%.c@gcc -c $<@echo "compling $< to $@ ... done".PHONY:cleanclean:@rm -rf *.a *.o stdc*@echo "clean ... done".PHONY:outputoutput:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.a stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done''
静态库的本质就是对.o文件(已编译文件)进行的打包,本质是一种归档文件
ar是gnu归档工具,rc表示(replace and create)
t:列出静态库中的文件
v:verbose 详细信息
mystdio是静态库真正的名字
$ ar -tv libmystdio.arw-rw-r-- 1000 / 1000 2848 Oct 29 14 : 35 2024 my_stdio.orw-rw-r-- 1000 / 1000 1272 Oct 29 14 : 35 2024 my_string.o
2.2 静态库使用
以main.c文件为例#include "my_stdio.h"#include "my_string.h"#include <stdio.h>int main(){const char *s = "abcdefg";printf("%s: %d\n", s, my_strlen(s));mFILE *fp = mfopen("./log.txt", "a");if(fp == NULL) return 1;mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfclose(fp);return 0;}//场景1:gcc在编译main.c文件时链接mystdio$ gcc main.c -lmystdio//场景2:gcc在编译main.c文件在指定库文件路径链接mymath$ gcc main.c -L. -lmymath//场景3:gcc在编译main.c并在指定的头文件路径中搜索头文件,在指定的库文件路径中搜索并链接名为mymath的库
3.动态库
1. 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享库中的代码
2. 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库复制到内存中,这个过程称为动态链接
3.动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间
3.1 动态库生成
shared:表示生成共享库格式()
fPIC:产生位置无关码(有利于创建和共享动态链接库,提高系统安全性和灵活性以及特定平台的编译)
库名规则:libxxx.so
libmystdio.so:my_stdio.o my_string.ogcc -o $@ $^ -shared%.o:%.cgcc -fPIC -c $<.PHONY:cleanclean:@rm -rf *.so *.o stdc*@echo "clean ... done".PHONY:outputoutput:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.so stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
3.2 动态库的使用
//场景1:gcc编译main.c并链接mystdio
$ gcc main.c -lmystdio//场景2:gcc编译main.,c并链接指定的库文件$ gcc main.c -L. -lmymath// 场景3:查看库或者可执行的依赖$ ldd libmystdio.so//场景4:gcc编译main.c并在指定的头文件路径中搜索头文件,在指定的库文件路径中搜索并链接名为mymath的库$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
4.动态库和静态库的区别
4.1 链接方式
1.静态库:在编译链接过程中,静态库的内容会被完整地复制到可执行文件中
2.动态库:在编译链接过程中,动态库的引用会被放置到生成的可执行文件中,实际的代码不会复制到可执行文件中
4.2运行时行为
1.静态库:不需要依赖外部,使得编译成功的可执行文件可以独立运行
2.动态库:程序运行时需要加载动态库,并将其映射到内存中,动态库的使用增加了程序对外部环境的依赖。
4.3 命名方式
1.静态库:通常以.a或.lib作为文件扩展名
2.动态库:通常以.so或.dll作为文件扩展名
5.目标文件
编译:把程序的源代码翻译成CPU能够直接运行的机器代码
源文件.c经过编译后会变成扩展名为.o的文件,这就是目标文件,目标文件是一个二进制的文件,文件的格式是ELF,是对二进制代码的一种封装。
6.ELF文件
6.1 初步理解
一个ELF文件由以下四部分组成:
ELF头:描述文件的主要特征,基于文件开始的位置,主要目的是定位文件的其他部分
程序头表:列举了所有有效的段(segments)和他们的属性,表里记着每个段的开始的位置和位移,长度。
节头表:包含对节(section)的描述
节:ELF文件中的基本组成单位,包含了特定类型的数据,ELF文件的各种信息和数据都存储在不同的节中(最常见的节:1.代码节:用于保存机器指令,是程序的主要执行部分 2.数据节:保存已初始化的全局变量和局部静态变量)
6.2 ELF从形成到加载轮廓
6.2.1 ELF形成可执行
step 1:将多分C/C++源代码翻译成.o目标文件
step.2:将多份.o文件section进行合并
合并的过程是将同种类型的section进行合并,可以达到优化内存布局和提高内存的使用效率的目的,值得一提的是合并是发送在链接的过程中的。
6.2.2 ELF可执行文件加载
1.ELF会有多种不同的section,加载到内存的时候会进行section的合并,形成segment(为了减少页面碎片,提高内存的使用效率,还可以实现不同的访问权限,从而优化管理和权限访问控制)
2.合并原则:相同属性
3.具体的合并原则被记录在ELF的程序头表
4.readelf是一个用于显示ELF文件信息的工具
以下是主要的选项:
+ -h 显示ELF文件的文件头信息
+ -l 显示ELF文件的程序头信息
+ -S 显示ELF文件的节头表信息
+ -s 显示ELF文件的符号表信息
+ -r 显示ELF的重定位表信息
+ -d 显示ELF的动态节信息
+ -a 显示ELF文件的全部信息
6.2.2.1 程序头表和节头表
程序头表 | 节头表 | |
作用时机 | 运行加载时 | 链接时和调试时 |
描述内容 | 文件中各个段(segment)的信息,包括类址,大小等 | 文件中所有的节(section)的属性和位置 |
主要使用者 | 操作系统的加载器 | 链接器,调试器 |
1.程序头表
主要由Executable(可执行),Read&Write(读写),Read Only(只读)三个部分组成
可执行 | 读写 | 只读 | |
作用 | 包含了程序的代码,程序运行时会被CPU执行 | 程序运行时需要修改的数据 | 程序运行时不需要修改的数据 |
特性 | 具有可读和可执行的权限,通常不具备写 | 可读也可写,允许程序在运行时更新数据 | 这些段仅可读,不允许写 |
2.节头表
.text节:保存了程序代码指令的代码节
.data节:保存了初始化的全局变量和局部静态变量等数据
.rodata节:保存了只读的数据,由于该节是只读的,所以只能存在于一个可执行文件的只读段中。
.BSS节:为未初始化的全局变量和局部静态变量预留位置
.symtab节:Symbol Table符号表,就是源码里面那些函数名,变量名和代码的对应关系
.got.plt节:.got保存了全局偏移表,.got节和.plt节一起提供对导入的共享库函数的访问入口,由动态链接器在运行时进行修改
7.理解链接与加载
7.1 静态链接
研究静态链接,本质就是研究.o是如何链接的
objdump -d:将代码段(.text)进行反汇编查看
查看编译后的.o目标文件
$ objdump -d code.ocode.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <run>:0: f3 0f 1e fa endbr644: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f<run+0xf>f: e8 00 00 00 00 callq 14 <run+0x14>14: 90 nop15: 5d pop %rbp16: c3 retq$ objdump -d hello.ohello.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <main>:0: f3 0f 1e fa endbr644: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f<main+0xf>f: e8 00 00 00 00 callq 14 <main+0x14>14: b8 00 00 00 00 mov $0x0,%eax19: e8 00 00 00 00 callq 1e <main+0x1e>1e: b8 00 00 00 00 mov $0x0,%eax23: 5d pop %rbp24: c3 retq
$ cat hello.c# include<stdio.h>void run();int main(){printf("hello world!\n");run();return 0;}
$ cat code.c# include<stdio.h>void run(){printf("running...\n");}
(callq前面的00 00 00 00表示该指令的目标地址尚未确定)
由上可知hello.o中main函数不认识printf和run函数
code.o不认识printf函数
链接的时候,为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码块(.data)中还存在一个重定位表,这张表将在链接的时候,会根据表里的地址把callq前面的00 00 00 00地址修正,所以链接过程中会涉及到对.o中外部符号进行地址重定位
7.2 ELF加载与进程地址空间
7.2.1 虚拟地址/逻辑地址
ELF程序在没被加载到内存的时候就有了地址,下面是objdump -S进行反汇编后的代码
1.最左侧的就是就是ELF的虚拟地址,又可以称为逻辑地址(起始地址+偏移量),可以默认起始地址为0,在程序还没加载到内存时虚拟地址已对可执行程序进行统一编址了
2.mm_struct和vm_area_struct是Linux操作系统中用于进程内存管理的两个重要的数据结构,它们协作实现了对进程虚拟地址空间的有效管理和控制,在这两刚刚创建的时候,初始化数据从ELF各个segment中来
7.2.2 重新理解进程虚拟地址空间
ELF在被编译好之后,未来程序的入口地址会被记录在ELF header的Entry字段中
$ gcc *.o$ readelf -h a.outELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: DYN (Shared object file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x1060
下图就是 磁盘驱动程序在计算机中的执行流程
首先:可执行程序被加载到物理内存上,这个过程由OS负责,OS读取到可执行文件的内容并复制到内存中适当的位置
然后:当程序被加载到内存后,OS会读取ELF Header中的Entry字段,以确定程序的入口地址
接着:在程序执行之前,OS会建立虚拟内存到物理内存的映射关系,这是通过页表来实现的,页表的左边是虚拟地址,右边是物理地址,它记录二者相互的映射关系,当CPU需要访问某一个虚拟地址的时候,会通过页表找到对应的物理内存地址。
最后:cpu会从入口地址开始,按照指令的顺序执行程序,cpu会不断地访问内存以读取指令和数据,这些访问都是通过页表来实现的,以确保虚拟地址能够正确地映射到物理地址
磁盘中的segment提供信息给vm_area_struct然后,mm_struct根据相应的vm_area_struct来完成初始化,这个过程是在OS加载程序时完成的
7.3 动态链接与动态库加载
7.3.1 进程如何看到动态库
库函数的调用:
被进程发现和调用都是发生在mm_struct中的共享区
7.3.2 进程间如何共享库的
7.3.3 动态链接
7.3.3.1 概要
动态链接比静态链接要常用的多,静态链接会编译产生所有的目标文件,连同用到的各种库,合并形成一个独立的可执行文件,它不需要额外的依赖就可以运行,但这样会导致生成的文件体积大,并且相当耗费内存资源,而动态链接只需把共享的代码单独提取出来保存成一个独立的动态链接库,当程序运行时把它们加载到内存中就行了。
动态链接将链接的整个过程推迟到了程序加载的时候,动态链接分为加载时动态链接和运行时加载链接,前面的是指在程序加载到内存时,动态链接会同步加载所需的共享库,而后面的是运行时动态链接允许程序在运行中根据需要动态加载共享库
7.3.3.2 程序的执行
$ ldd /usr/bin/lslinux-vdso.so.1 (0x00007fffdd85f000)libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1(0x00007f42c025a000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f42c0068000)libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0(0x00007f42bffd7000)libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f42bffd1000)/lib64/ld-linux-x86-64.so.2 (0x00007f42c02b6000)libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0(0x00007f42bffae000)
由上面可知程序的执行与动态库和动态链接器有关
标红的地方是动态链接器,其余的都是动态库,而箭头后面对应的则是动态库的地址
程序的入口点是_start,这是由C运行时库或链接器提供的特殊函数,在_start函数中,会执行一系列的初始化操作:
1.设置堆栈:为程序创建一个初始的堆栈环境
2.初始化数据段:将程序的数据段(如全局变量和静态变量),从初始化数据复制到相应的内存位置,并清零未初始化的数据段
3.动态链接:_start函数会调用动态链接器的代码来解析和加载程序所依赖的动态库,动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库的实际地址
4.调用__libc_start_main:一旦动态链接完成,_start函数会调用__libc_start_main,它负责执行一些例如初始化线程库等额外的工作
5.调用main函数:__libc_start_main函数会调用程序的main函数,这时程序的执行控制权会交给用户编写的代码
6.处理main函数的返回值:__libc_start_main会负责处理这个返回值,并最终调用_exit函数来终止程序
7.3.3.3 程序如何跟库映射
7.3.3.4如何进行库函数的调用
我们的程序调用任意库函数只需要知道库的起始虚拟地址+库函数的偏移量即可定位对应的库函数
7.3.3.5 全局偏移量GOT
GOT表是一个数据表用于存储全局变量和函数的实际地址位于.got节,在动态链接的情况下,程序在运行时需要确定这些地址以便访问全局变量和调用函数。
作用:1.提高程序的模块化
2.优化存储空间利用
3.增强运行时的灵活性
当程序调用库函数时,先是跳转到plt表(过程链接表),然后通过GOT表来获取真实地址并跳转