1.上一节文件系统的软硬链接的末尾
unlinlk:删除硬链接。
硬链接智能给普通文件进行建立,Linux系统不支持建立硬链接。
但给当前目录和上级目录建立硬链接,为什么?如果给普通文件建立硬链接,就容易形成路径环问题。
独立的是软连接,硬链接就是一个文件名映射关系。
. 和 ..本质就是对目录的硬链接,方便上级文件寻找和当前文件寻址
2.什么是库
库是写好的现有的,成熟的,可以复⽤的代码。
3.静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执⾏⽂件中,程序运⾏的时候将不再 需要静态库。
1.动静态库,要不要包含main函数?
所有的库,本质都是原文件对应.o!!静态库的本质就是打包成一个包。把所有的.o文件打包成一个包。
2.ar -rc mylibc.a *.o:归档文件,把所有的.o文件打包成归档文件。
归档文件,不许要使用者解包,而是使用gcc 、g++直接进行解包即可!
3.-rc : replace and create:解包文件中,存在就替换最新的.o,不存就创建
4.静态库需要:.a结尾,lib开头,库的真正名字,去掉lib和.a。
5.gcc -o usercode usercode.o -L. -lmyc:
-L:告诉编译器存在那条路经上
-l:找什么库,+库的真正名字如果我们要连接任何非C/C++标准库,都需要指明-L,-l
mkdir -p lib.include在lib下创建路径
tar czf lib.tgz lib???
tar xzf lib.tgz???gcc -c usercode.c -I(da i) ./lib/include ???
lib/include 里面包含的是头文件
lib/mylib 里面包含的是静态库,libmyc.a
静态库:
拷贝到系统
静态库连接时,是直接把库的实现拷贝到可执行程序 /lib64 里面去,执行可执行程序时,不再依赖静态库。
sudo cp lib/include/* /usr/include/:把
ldd:查看当前用户关联哪个库。不安装:-I -l -L
安装到系统默认路径:
总结:静态库,就是把所有的.o文件包成一个包,然后再把静态库拷贝到默认系统文件中去,这样的话,用户再得到头文件,就可以通过默认路径找到备份的静态库,然后执行程序。
4.动态库
gcc -fPIC -c *.c:
不同ar,用gcc
gcc -shared -o libmyc.so *.o:生成libmyc.so的动态库.ldd a.out:
查所依赖的路径
实际上,只有gcc知道动态库在哪
系统则不知道你的动态库在哪?
那么怎么让系统找到动态库呢?
下面有四种方法:
1.拷贝到系统
静态库连接时,是直接把库的实现拷贝到可执行程序,执行可执行程序时,不再依赖静态库
而动态库,还需要找到动态库的实现。
拷贝到系统里就找到了
2.建立软连接 给默认路径建立软连接,指向我的库
3.环境变量
关掉再用
内存级别的,关了就没了。
4. 配置系统(暂时忽略)
总结:
那如果把动静态库都写在一起的话。
出现一下四种结论
拷⻉ .so ⽂件到系统共享库路径下,⼀般指 /usr/lib、/usr/local/lib、/lib64 或者开 篇指明的库路径等
• 向系统共享库路径下建⽴同名软连接
• 更改环境变量: LD_LIBRARY_PATH
• ldconfig⽅案:配置/ etc/ld.so.conf.d/ ,ldconfig更新
5.目标文件
.c文件在编译之后会⽣成两个扩展名为 .o 的⽂件,它们被称作⽬标⽂件。要注意的是如果我们 修改了⼀个原⽂件,那么只需要单独编译它这⼀个,⽽不需要浪费时间重新编译整个⼯程。⽬标⽂件 是⼀个⼆进制的⽂件,⽂件的格式是 ELF ,是对⼆进制代码的⼀种封装。
6.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):保存已初始化的全局变量和局部静态变量。
---------------------------------------------------------------------------------------------------------------------------------
7.ELF可执行文件的加载
readelf -S a.out:-S,读取section Header Table
里面的一个一个section内容
为什么要将section合并成为segment
• Section合并的主要原因是为了减少⻚⾯碎⽚,提⾼内存使⽤效率。如果不进⾏合并, 假设⻚⾯⼤⼩为4096字节(内存块基本⼤⼩,加载,管理的基本单位),如果.text部分 为4097字节,.init部分为512字节,那么它们将占⽤3个⻚⾯,⽽合并后,它们只需2个 ⻚⾯。
• 此外,操作系统在加载程序时,会将具有相同属性的section合并成⼀个⼤的 segment,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制。
未初始化程序变量存在bss里面:better save space
对于 程序头表Program header table 和 节头表 Section header table ⼜有什么⽤呢,其实ELF ⽂件提供2个不同的视图/视⻆来让我们理解这 两个部分:
• 链接视图(Linking view) -对应节头表 Section header table
◦ ⽂件结构的粒度更细,将⽂件按功能模块的差异进⾏划分,静态链接分析的时候⼀般关注的 是链接视图,能够理解ELF⽂件中包含的各个部分的信息。
◦ 为了空间布局上的效率,将来在链接⽬标⽂件时,链接器会把很多节(section)合并,规整 成可执⾏的段(segment)、可读写的段、只读段等。合并了后,空间利⽤率就⾼了,否 则,很⼩的很⼩的⼀段,未来物理内存⻚浪费太⼤(物理内存⻚分配⼀般都是整数倍⼀块给 你,⽐如4k),所以,链接器趁着链接就把⼩块们都合并了。
• 执⾏视图(execution view) -对应程序头表 Program header table
◦ 告诉操作系统,如何加载可执⾏⽂件,完成进程内存的初始化。⼀个可执⾏程序的格式中, ⼀定有 program header table 。
• 说⽩了就是:⼀个在链接时作⽤,⼀个在运⾏加载时作⽤。
-----------------------------------------------------------------------------------------------------------------------
symtable:符号表
把字符串放在一个数组里,只用记录偏移量也就是下标,根据小标就可以找到对应的符号
ELF Header:记录下面的管理信息存储在哪里。
readelf -h /usr/bin/ls
Magic:IO加载elf格式,首先就要先判定这是一个elf格式
记录字段大小
8. 理解链接和加载
8-1静态链接
objdump -d code.o对目标文件进行反汇编
e8:call命令对用的机器码,后面的是地址,反汇编,真实值是0。
并没有对.o文件进行连接,所以说是0.
这个地址会在哪个时候被修正?链接的时候!为了让链接器将来在链接时能够正确定位到这些被修正 的地址,在代码块(.data)中还存在⼀个重定位表,这张表将来在链接的时候,就会根据表⾥记录的 地址将其修正。
readelf -s code.c
UND undefine,没有被定义![]()
readelf -s code.c
UND undefine,没有被定义链接每个模块拿着自己未定义的模块去别的地方找。
形成一个可执行程序的时候,就会把call的地址为0的地址进行填充。
静态链接就是把库中的.o进⾏合并,和上述过程⼀样 所以链接其实就是将编译之后的所有⽬标⽂件连同⽤到的⼀些静态库运⾏时库组合,拼装成⼀个独⽴ 的可执⾏⽂件。其中就包括我们之前提到的地址修正,当所有模块组合在⼀起之后,链接器会根据我 们的.o⽂件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从⽽修正它们的地址。这 其实就是静态链接的过程。
所以,链接过程中会涉及到对.o中外部符号进⾏地址重定位。
.o可重定位目标文件:链接时,它的地址可以被修改。
.o合并,调用的该成目标地址
链接后,就可以找到了e8 后面对应的地址了。
没有链接之前,互相都不知道对方的存在。
readelf -s code.c
UND undefine,没有被定义链接每个模块拿着自己未定义的模块去别的地方找。
形成一个可执行程序的时候,就会把call的地址为0的地址进行填充。
8-2ELF加载与进程地址空间 
8-2-1虚拟地址/逻辑地址
问题:
• ⼀个ELF程序,在没有被加载到内存的时候,有没有地址呢?
• 进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?
最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量),但是我们 认为起始地址是0.也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执 ⾏程序进⾏统⼀编址了.
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?
从ELF各个 segment来,每个segment有⾃⼰的起始地址和⾃⼰的⻓度,⽤来初始化内核结构中的[start,end] 等范围数据,另外在⽤详细地址,填充⻚表. 所以:虚拟地址机制,不光光OS要⽀持,编译器也要⽀持.
磁盘上的地址叫做逻辑地址:
虚拟地址在创建mm_struct,vm_struct的时候初始化数据从ELF的各个segament来。
可执行程序在没有加载到物理内存的时候,有自己的地址
加载到内存,创建tast_struct结构体,创建代码区,有起始地址和偏移量,用可执行程序的线性地址进行初始化。
start 1060,在虚拟地址开辟代码块,可执行程序大小,也就是对应虚拟地址空间的偏移量。每一行代码有物理空间,也有自己对应的物理地址。
EIP:执行指令吓一跳指令的地址。
拿ELF的Entry point adderss地址填充EIP,调度当前的进程时,首先取EIP所得的地址,CPU拿到EIP后就可以根据下一条指令的地址,开始执行程序了
CR3:指向当前进程的页表
MMU:把虚拟地址转化物理地址,找到对应代码,load到CPU里面,进到CPU的地址全部都是虚拟地址。
在CPU虚拟地址,在磁盘叫逻辑地址。
8-3动态链接和动态链接加载
8-3-1动态库是怎么和地址空间关联的
对动态库作查找,
库函数也有自己的虚拟地址空间,
库函数的调用
1.被进程看到:动态库映射到进程的地址空间
2.被进程调用:在进程的地址空间进行跳转
2个进程:理解共享区域 ,不同的进程地址空间不一样,动态库加载的虚拟地址也不一样。
8-3-2动态库
动态链接其实远⽐静态链接要常⽤得多。⽐如我们查看下 hello 这个可执⾏程序依赖的动态库,会发 现它就⽤到了⼀个c动态链接库:
$ ldd hellolinux-vdso.so.1 => (0x00007fffeb1ab000)libc.so.6 => /lib64/libc.so.6 (0x00007ff776af5000)/lib64/ld-linux-x86-64.so.2 (0x00007ff776ec3000)# ldd命令⽤于打印程序或者库⽂件所依赖的共享库列表。
这⾥的libc.so是C语⾔的运⾏时库,⾥⾯提供了常⽤的标准输⼊输出⽂件字符串处理等等这些功能。
那为什么编译器默认不使⽤静态链接呢?
静态链接会将编译产⽣的所有⽬标⽂件,连同⽤到的各种 库,合并形成⼀个独⽴的可执⾏⽂件,它不需要额外的依赖就可以运⾏。照理来说应该更加⽅便才对 是吧?
静态链接最⼤的问题在于⽣成的⽂件体积⼤,并且相当耗费内存资源。随着软件复杂度的提升,我们 的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费⼤量的硬盘 空间。
这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成⼀个独 ⽴的动态链接库,等到程序运⾏的时候再将它们加载到内存,这样不但可以节省空间,因为同⼀个模块在内存中只需要保留⼀份副本,可以被不同的进程所共享。
动态链接到底是如何⼯作的??
⾸先要交代⼀个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。(也就是我们需要的时候才进行动态链接的加载)⽐如我们去运⾏ ⼀个程序,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使⽤情况为它们动态分配⼀段内存。 当动态库被加载到内存以后,⼀旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转 地址了。
我们的可执行程序被编译器动了手脚:
在C/C++程序中,当程序开始执⾏时,它⾸先并不会直接跳转到 main 函数。实际上,程序的⼊⼝点 是 _start ,这是⼀个由C运⾏时库(通常是glibc)或链接器(如ld)提供的特殊函数。 在 _start 函数中,会执⾏⼀系列初始化操作,这些操作包括:
1. 设置堆栈:为程序创建⼀个初始的堆栈环境。
2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位 置,并清零未初始化的数据段。
3. 动态链接:这是关键的⼀步, _start 函数会调⽤动态链接器的代码来解析和加载程序所依赖的 动态库(sharedlibraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调 ⽤和变量访问能够正确地映射到动态库中的实际地址。
动态链接器:
◦ 动态链接器(如ld-linux.so)负责在程序运⾏时加载动态库。
◦ 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。 环境变量和配置⽂件:
◦ Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置⽂件(如/etc/ld.so.conf及其⼦配置 ⽂件)来指定动态库的搜索路径。
◦ 这些路径会被动态链接器在加载动态库时搜索。 缓存⽂件:
◦ 为了提⾼动态库的加载效率,Linux系统会维护⼀个名为/etc/ld.so.cache的缓存⽂件。
◦ 该⽂件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会⾸先 搜索这个缓存⽂件。
![]()
找动态库:起始地址+库当中的偏移量。
绝对编址:
0x123:函数在库中的偏移量
0x1000:库的虚拟地址
0x1000 + 0x123:就可以找到我们想要的函数了,这也是加载到内存中函数的地址。
4. 调⽤ __libc_start_main :⼀旦动态链接完成, _start 函数会调⽤
__libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执⾏ ⼀些额外的初始化⼯作,⽐如设置信号处理函数、初始化线程库(如果使⽤了线程)等。
5. 调⽤ main 函数:最后, __libc_start_main 函数会调⽤程序的 main 函数,此时程序的执 ⾏控制权才正式交给⽤⼾编写的代码。
6. 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回 值,并最终调⽤ _exit 函数来终⽌程序。
跟可执行程序加载到内存是一样的。
跟加载可执行程序是一样的,的道理。
因为动态库也是一个文件。
总结:
注意:
• 库已经被我们映射到了当前进程的地址空间中
• 库的虚拟起始地址我们也已经知道了
• 库中每⼀个⽅法的偏移量地址我们也知道
• 所有:访问库中任意⽅法,只需要知道库的起始虚拟地址+⽅法偏移量即可定位库中的⽅ 法
• ⽽且:整个调⽤过程,是从代码区跳转到共享区,调⽤完毕在返回到代码区,整个过程完 全在进程地址空间中进⾏的
8-3-3-3全局偏移量表GOT(global offset table)
对上述进行解释:
调用put函数
首先put函数的偏移量,我们是已经知道的,
还差的就是库的起始虚拟地址,我们把
库的名字替换成库的起始虚拟地址,这样call就可以找到put函数的地址从而进行调用
• 也就是说,我们的程序运⾏之前,先把所有库加载并映射,所有库的起始虚拟地址都应该 提前知道
• 然后对我们加载到内存中的程序的库函数调⽤进⾏地址修改,在内存中⼆次完成地址设置 (这个叫做加载地址重定位)
• 等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?
所以:动态链接采⽤的做法是在 .data (可执⾏程序或者库⾃⼰)中专⻔预留⼀⽚区域⽤来存放函数 的跳转地址,它也被叫做全局偏移表GOT,表中每⼀项都是本运⾏模块要引⽤的⼀个全局变量或函数 的地址。
• 因为.data区域是可读写的,所以可以⽀持动态进⾏修改
$ readelf -S a.out ...[24] .got PROGBITS 0000000000003fb8 00002fb80000000000000048 0000000000000008 WA 0 0 8 ...$ readelf -l a.out # .got在加载的时候,会和.data合并成为⼀个segment,然后加载在⼀起 ...05 .init_array .fini_array .dynamic .got .data .bss ...
1. 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不 同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的 每个动态库都有独⽴的GOT表,所以进程间不能共享GOT表。
由于动态库是映射到进程的起始地址,所以各动态库的绝对地址和相当于地址都不相同
动态地址不同:
每次进程加载动态库时,系统都会为其分配不同的基地址
例如说:
在进程 A 中,libc.so 的起始地址可能是 0x11112222,而在进程 B 中,可能是 0x55556666
相对位置不同:
相对位置是指进程内部的代码段、数据段、GOT表等在动态库内部的相对偏移。
由于每个进程的动态库可能被加载到不同的基地址,库内部的数据结构(比如 GOT 表、代码段、数据段)相对于基地址的偏移可能不一样
GOT 表存储了动态链接库(如 libc.so)中函数或变量的地址。
由于每个进程的 GOT 表是独立的,因此即使多个进程加载了相同的 libc.so,它们的 GOT 表指向的 libc.so 代码段地址也可能不同
2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利⽤CPU的相对寻址来找 到GOT表。
3. 在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会 被修改为真正的地址。
4. 这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修 改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给 编译器指定-fPIC参数的原因,PIC=相对编址+GOT。