linux 库的理解与加载
什么是库我们知道在我们编写c语言代码或者c代码的时候会调用雷素与printf和cout之类的函数我们并没有具体实现这个函数而这个函数就是库里面的函数简称库函数库和我们的代码一样都是以二进制的形式存放在我们的磁盘上的本质上来说库是⼀种可执⾏代码的⼆进制形式可以被操作系统载⼊内存执⾏。库有两种静态库.a[Linux]、.lib[windows]动态库.so[Linux]、.dll[windows]我们可以来看看这上面就可以看到我们的程序是依赖于动态库。我们的可执行程序和库一样在磁盘中都是二进制程序那么操作系统怎么识别这个二进制程序是可执行程序或者库的呢目标文件在讲库的理解与加载之前我们先来说说程序是如果加载到内存中的cpu又是怎么寻址找到程序的库和程序本质是一个东西都是二进制文件让我们先来了解一下目标文件 目标文件就是.o文件我们深⼊探讨⼀下编译和链接的整个过程来更好的理解动静态库的使⽤原理。下面的图是我们编译链接的过程编译器最终会生成.o文件链接器会把.o文件和库函数进行链接最终形成可执行程序编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运⾏的机器 代码。⽐如在⼀个源⽂件hello.c⾥便简单输出hello world!并且调⽤⼀个run函数⽽这个函数被定义在另⼀个原⽂件code.c中。这⾥我们就可以调⽤gcc -c来分别编译这两个原⽂件。⽬标⽂件 是⼀个⼆进制的⽂件⽂件的格式是 ELF 是对⼆进制代码的⼀种封装。ELF⽂件在磁盘中库和我们的可执行程序还有.o目标文件一样都是以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)的描述。节SectionELF⽂件中的基本组成单位包含了特定类型的数据。ELF⽂件的各种信息和数据都存储在不同的节中如代码节存储了可执⾏代码数据节存储了全局变量和静态数据等。最常⻅的节代码节.text⽤于保存机器指令是程序的主要执⾏部分。数据节data保存已初始化的全局变量和局部静态变量。这个数据节的作用是在磁盘中保存全局变量假设我们在内存中需要申请50个int 的数据而在磁盘上不会真真的会存50个int而会以int 50的形式来保存以后在内存中申请的总数。ELF从形成到加载轮廓而每一个.o的文件都会以ELF格式存储而最终所有的ELF会最终形成一个ELF合并完所有 Section 后 把多个相邻、权限相同的 Section 打包成一个 SegmentELF形成可执⾏step-1将多份C/C源代码翻译成为⽬标.o⽂件 动静态库(ELF)step-2将多份.o⽂件section进⾏合并合并是在链接的时候进行合并但合并不是简单的合并还会包含库函数的合并ELF可执⾏⽂件加载⼀个ELF会有多种不同的Section在加载到内存的时候也会进⾏Section合并形成segment合并原则相同属性⽐如可读可写可执⾏需要加载时申请空间等.这样即便是不同的Section在加载到内存中可能会以segment的形式加载到⼀起很显然这个合并⼯作也已经在形成ELF的时候合并⽅式已经确定了具体合并原则被记录在了ELF的 程序头表(Program header table)中[thrlinux ~]$ readelf -S a There are 31 section headers, starting at offset 0x1ab0: 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 000000000000001c 0000000000000000 A 5 0 8 [ 5] .dynsym DYNSYM 00000000004002b8 000002b8 00000000000000f0 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 00000000004003a8 000003a8 0000000000000065 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 000000000040040e 0000040e 0000000000000014 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 0000000000400428 00000428 0000000000000020 0000000000000000 A 6 1 8 [ 9] .rela.dyn RELA 0000000000400448 00000448 0000000000000018 0000000000000018 A 5 0 8 [10] .rela.plt RELA 0000000000400460 00000460 00000000000000c0 0000000000000018 AI 5 24 8 [11] .init PROGBITS 0000000000400520 00000520 000000000000001a 0000000000000000 AX 0 0 4 [12] .plt PROGBITS 0000000000400540 00000540 0000000000000090 0000000000000010 AX 0 0 16 [13] .plt.got PROGBITS 00000000004005d0 000005d0 0000000000000008 0000000000000000 AX 0 0 8 [14] .text PROGBITS 00000000004005e0 000005e0 0000000000000252 0000000000000000 AX 0 0 16 [15] .fini PROGBITS 0000000000400834 00000834 0000000000000009 0000000000000000 AX 0 0 4 [16] .rodata PROGBITS 0000000000400840 00000840 0000000000000027 0000000000000000 A 0 0 8 [17] .eh_frame_hdr PROGBITS 0000000000400868 00000868 000000000000003c 0000000000000000 A 0 0 4 [18] .eh_frame PROGBITS 00000000004008a8 000008a8 0000000000000114 0000000000000000 A 0 0 8 [19] .init_array INIT_ARRAY 0000000000600e10 00000e10 0000000000000008 0000000000000008 WA 0 0 8 [20] .fini_array FINI_ARRAY 0000000000600e18 00000e18 0000000000000008 0000000000000008 WA 0 0 8 [21] .jcr PROGBITS 0000000000600e20 00000e20 0000000000000008 0000000000000000 WA 0 0 8 [22] .dynamic DYNAMIC 0000000000600e28 00000e28 00000000000001d0 0000000000000010 WA 6 0 8 [23] .got PROGBITS 0000000000600ff8 00000ff8 0000000000000008 0000000000000008 WA 0 0 8 [24] .got.plt PROGBITS 0000000000601000 00001000 0000000000000058 0000000000000008 WA 0 0 8 [25] .data PROGBITS 0000000000601058 00001058 0000000000000004 0000000000000000 WA 0 0 1 [26] .bss NOBITS 000000000060105c 0000105c 0000000000000004 0000000000000000 WA 0 0 1 [27] .comment PROGBITS 0000000000000000 0000105c 000000000000005a 0000000000000001 MS 0 0 1 [28] .symtab SYMTAB 0000000000000000 000010b8 00000000000006a8 0000000000000018 29 47 8 [29] .strtab STRTAB 0000000000000000 00001760 0000000000000244 0000000000000000 0 0 1 [30] .shstrtab STRTAB 0000000000000000 000019a4 000000000000010c 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[thrlinux ~]$ readelf -l a Elf file type is EXEC (Executable file) Entry point 0x4005e0 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 0x00000000000009bc 0x00000000000009bc R E 200000 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x000000000000024c 0x0000000000000250 RW 200000 DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28 0x00000000000001d0 0x00000000000001d0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x0000000000000868 0x0000000000400868 0x0000000000400868 0x000000000000003c 0x000000000000003c R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x00000000000001f0 0x00000000000001f0 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 .plt.got .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合并成segmnet答案是在磁盘和内存中最小存储单位是4kb假设每一个section只占用-2kb那么如果不进行合并会存在大量的资源浪费这样操作系统在加载程序时会将具有相同属性的section合并成⼀个⼤的 segment这样就可以实现不同的访问权限从⽽优化内存管理和权限访问控制。对于 程序头表 和 节头表 ⼜有什么⽤呢其实ELF⽂件提供 2 个不同的视图/视⻆来让我们理解这两个部分说白了就是一个在程序运行加载时起作用一个在程序链接时起作用这样ELF就形成好了下面我们来说说ELF如何加载到内存的ELF是怎么加载到内存的首先想一个问题操作系统是怎么找到他的答案是路径文件名有了路径很文件名操作系统就会很容易的找到elf所在的磁盘分区通过路劲解析查看struct dentry树对磁盘io对应的文件名和inode的映射最终找到elf那么ELF是如何转化为进程的我们想一个问题进程在没被加载到内存中时他有地址吗答案是有的这个地址不是内存地址是一个逻辑地址现代计算机的编址方法现代计算机的编址方法统一以一个叫做平坦模式的编址方法进行编址这个编址模式是怎么一回事他会从0地址开始编址线性递增供单一、无重叠的全局虚拟地址空间链接器可以直接给每组同权限 section 分配一段连续虚拟地址打包成独立 Segment操作系统直接按Segment 地址映射内存无需分段地址换算这个地址就是逻辑地址我们可以用反汇编查看一下我们可以看到这里的地址的确是从接近0号地址开始编址的这里我们的程序还没有运行这里的地址是在磁盘上的逻辑地址。程序是怎么确定哪一段内存对应的是代码段或者数据区的呢这里可以用起始地址加偏移量来确定如果每一个segment的开始地址都是0那么所有的可执行程序就是一个segment所有函数变量地址都是从0开始的磁盘上的可执行程序的编址其实就是虚拟地址的统一编址这里我们可以想到进程当中的的虚拟地址空间而虚拟地址空间不仅仅是进程看待内存的方式磁盘当中的可执行程序ELFELF 文件静态保存了构建该虚拟地址空间的映射规则也是ELF看待内存的方式逻辑地址和虚拟地址就相当于是一个硬币的两面ELF加载与进程地址空间关于这个问题我们需要画图看看我们看看上面的图片这里有个Entry point address 这个就是程序的入口地址我们把这个地址复制下来用objdump反汇编看看这个就是程序的入口地址,操作系统只需要找到这个地址就定位了程序的入口了下面再说说具体流程下面就是cpu怎么调度的了这个页表里面的虚拟地址就是磁盘里面存放的程序的逻辑地址这样整个体系结构就转起来了一句话总结内核先创建task_struct进程管理结构再加载磁盘 ELF 程序并搭建虚拟内存映射CPU 通过 EIP 存放虚拟地址借助 CR3 指向的页表经 MMU 转换为物理地址最终从物理内存读取指令以程序入口地址启动运行。有了以上对于程序的加载运行理解之后我们再来谈谈库静态库的链接与理解我们来看看下面的代码我们这两个代码里面分别写了main的实现和再main里面的对于func的调用head.h里面放了func的实现我们用readelf 去读取对应的.o文件发现func这个函数的地址是空的puts函数也是空的为什么因为这个时候还没与head.o进行链接还有对stdio库进行链接 其实puts就是printf我们把head.o 和test.o进行链接一下这时候对应的地址就有了静态链接的本质链接器从静态库归档包中抽取程序所需的目标文件与用户源码生成的.o文件合并各自同名 section整合为单一 ELF 可执行文件。静态库的加载和可执行程序一样他是包含再可执行程序里面了。动态库的链接与理解动态库的加载其实也可以和可执行程序进行关联动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存其中每个动 态库的加载地址都是不固定的操作系统会根据当前地址空间的使⽤情况为它们动态分配⼀段内存。 当动态库被加载到内存以后⼀旦它的内存地址被确定我们就可以去修正动态库中的那些函数跳转 地址了。我们的程序怎么和库具体映射起来的动态库也是⼀个⽂件要访问也是要被先加载要加载也是要被打开的让我们的进程找到动态库的本质也是⽂件操作不过我们访问库函数通过虚拟地址进⾏跳转访问的所以需要把动态库映射到进程的地址空间中我们可以看看下面的图片上面的图片清晰展示了库是如何记载到内存中的还有与程序地址1空间建立关系程序怎么才能在库对应的代码块中找到对应方法的地址呢这里还是用了起始地址加偏移量来计算的。那么我们知道库是再我们运行程序的时候才加载的那么我们知道再程序编译链接时库函数的地址是不确定的那么是怎么做到库加载的时候确定地址的呢是通过加载完库之后再去修改代码对应的地址吗答案是不是的这里就要引入一个东西了这个东西叫做全局偏移量表GOT所以动态链接采⽤的做法是在.data可执⾏程序或者库⾃⼰中专⻔预留⼀⽚区域⽤来存放函数 的跳转地址它也被叫做全局偏移表GOT表中每⼀项都是本运⾏模块要引⽤的⼀个全局变量或函数 的地址。如下图所示由于代码是只读的我们不能修改他所以就有了GOT表每个程序都独有一份为什么因为对应的每个程序地址空间对应的库的代码段的地址都是不一样的所以需要各自程序独有一份在调⽤函数的时候会⾸先查表然后根据表中的地址来进⾏跳转这些地址在动态库加载的时候会被修改为真正的地址。动态库内每个函数、全局变量相对于库基址的偏移在编译时就固定加载时操作系统确定库的虚拟起始基址动态链接器通过「基址 固定偏移」算出符号真实地址回填到编译阶段就已存在的 GOT 表中供代码访问全局符号这种⽅式实现的动态链接就被叫做PIC地址⽆关代码 。换句话说我们的动态库不需要做任何修改被加载到任意内存地址都能够正常运⾏并且能够被所有进程共享这也是为什么我们自己写动态库时编译器指定-fPIC参数的原因PIC相对编址GOT。