asp网站开发实训报告,高端室内设计工作室,电商网站设计与制作总结,设计海报的软件目录
一.什么是库
二.静态库
2.1创建静态库 我们在之前的路径下新建lib使用我们自己的库
2.2 使用makefile生成静态库 三.动态库
3.1动态库生成
3.2动态库使用
3.3库运行搜索路径
四.目标文件
五.ELF文件
六.ELF从形成到加载轮廓
6.1ELF形成可执行
6.2 ELF可执行文…目录
一.什么是库
二.静态库
2.1创建静态库 我们在之前的路径下新建lib使用我们自己的库
2.2 使用makefile生成静态库 三.动态库
3.1动态库生成
3.2动态库使用
3.3库运行搜索路径
四.目标文件
五.ELF文件
六.ELF从形成到加载轮廓
6.1ELF形成可执行
6.2 ELF可执行文件加载
七.理解链接和加载
7.1静态链接 7.2ELF加载与进程地址空间
7.2.1虚拟地址/逻辑地址 7.2.2重新理解进程虚拟地址空间
编辑 7.3动态链接与动态库加载
7.3.1进程如何看到动态库
7.3.2进程间共享库 7.3.3动态链接
7.3.3.1概要
7.3.3.2我们的可执行程序被编译器动了手脚 7.3.3.3动态库中的相对地址
7.3.3.4我们的程序怎么和库具体映射起来的
7.3.3.5我们的程序怎么进行库函数调用
7.3.3.6全局偏移量表GOT(global offset table)
7.3.3.7库间依赖
7.3.4总结 一.什么是库
库是写好的现有的成熟的可以复用的代码。现实中每个程序都要依赖很多基础的底层库不可能每个人的代码都从零开始因此库的存在意义非同寻常。 本质上来说库是一种可执行代码的二进制形式可以被操作系统载入内存执行。库有两种 • 静态库 .a[Linux]、.lib[windows] • 动态库 .so[Linux]、.dll[windows] 预备代码第三节
简单封装的libc库
二.静态库 • 静态库.a程序在编译链接的时候把库的代码链接到可执行文件中程序运行的时候将不再 需要静态库。 • 一个可执行程序可能用到许多的库这些库运行有的是静态库有的是动态库而我们的编译默 认为动态链接库只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc 的 -static 强转设置链接静态库。 2.1创建静态库
我创建一个目录里面有文件其中.h里面是声明.c是方法是实现usercode.c是我们调用的方法。 我们知道多文件执行形成.o文件进行链接后才能使用 我们不想让别人看到我自己函数的实现就可以用这种方式发给被人让别人去链接。
命令ar 把当前目录的.o文件全部放在libmyc.a文件里。这样就算是打包好了 新开一个目录 把我们上面打包好的库和.h文件拿过来 不需要解包我们在进行链接时需要指明在哪里找库因为我们的是第三方库OS只会在系统找这里我们就需要指明在哪个库-L .在当前路径叫做myc的库 我们在之前的路径下新建lib使用我们自己的库
把.h放在lib里的include下把.o应该是libmyc.a写错了放在lib里的mylib下 之后放在一个压缩包里 在其他地方就可以直接下载下来 解压 解压完毕就是需要形成.o文件这里需要使用-I选项来表明头文件在哪里 注意把lib/mylib里面的.o应该是libmyc.a文件上面弄错了。 下面就是执行的过程 • -L: 指定库路径 • -I: 指定头文件搜索路径 • -l: 指定库名 • 测试目标文件生成后静态库删掉程序照样可以运行 • 关于 -static 选项稍后介绍 • 库文件名称和引入库的名称去掉前缀 lib 去掉后缀 .so , .a 如 libc.so - c 2.2 使用makefile生成静态库 • ar 是 gnu 归档工具 rc 表示 (replace and create) • t: 列出静态库中的文件 • v:verbose 详细信息 三.动态库 • 动态库.so程序在运行的时候才去链接动态库的代码多个程序共享使用库的代码。 • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表而不是外部函数所在目标文件的整个机器码 • 在可执行文件开始运行以前外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中 这个过程称为动态链接dynamic linking • 动态库可以在多个程序间共享所以动态链接使得可执行文件更小节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用节省了内存和磁盘空间。 3.1动态库生成
初始文件 生成.o时要用fPIC 注意动态库和静态库不同这里不需要用归档工具ar直接用gcc就行带上选项-shared makefile也就只需要修改一点 • shared:表示生成共享库格式 • fPIC产生位置无关码(position independent code) • 库名规则libxxx.so 3.2动态库使用
动态库的使用和静态库的使用是一样的 但是当我运行的时候 虽然我们在上面的指令中带了选项指明我们的动态库的位置这个“指明”是给gcc指明。静态库能运行是因为静态库直接在编译时直接链接到可执行文件中的库。而动态库是在运行时才被加载的即可执行文件依赖于动态库的存在。
这里查看也是not found 3.3库运行搜索路径 • 拷贝 .so 文件到系统共享库路径下, 一般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等 • 向系统共享库路径下建立同名软连接 • 更改环境变量 LD_LIBRARY_PATH • ldconfig方案配置/ etc/ld.so.conf.d/ ldconfig更新 第一种比较简单看第二种 之后别忘了解除 第三种是环境变量 把我们动态库的路径加上去 这里就有了 还有第四种更改系统的配置文件 在这里面创建一个文件 往里面写上我们动态库的地址 但还是找不到 最后一步重新加载搜索路径 此时就有了。
结论 四.目标文件
编译和链接这两个步骤在Windows下被我们的IDE封装的很完美我们一般都是一键构建非常方便 但一旦遇到错误的时候呢尤其是链接相关的错误很多⼈就束手无策了。 编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器 代码。 比如在一个源文件 hello.c 里便简单输出hello world!并且调用一个run函数而这个函数被 定义在另一个原文件 code.c 中。这里我们就可以调用 gcc -c 来分别编译这两个原文件。
在编译之后会生成两个扩展名为 .o 的文件它们被称作目标文件。要注意的是如果我们修改了一个原文件那么只需要单独编译它这一个而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件文件的格式是 ELF 是对二进制代码的一种封装。 五.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从形成到加载轮廓
6.1ELF形成可执行 • step-1将多份 C/C 源代码翻译成为目标 .o 文件 • step-2将多份 .o 文件section进行合并 实际合并是在链接时进行的但是并不是这么简单的合并也会涉及对库合并。
6.2 ELF可执行文件加载 • 一个ELF会有多种不同的Section在加载到内存的时候也会进行Section合并形成segment • 合并原则相同属性比如可读可写可执行需要加载时申请空间等. • 这样即便是不同的Section在加载到内存中可能会以segment的形式加载到一起 • 很显然这个合并工作也已经在形成ELF的时候合并方式已经确定了具体合并原则被记录在了 ELF的程序头表(Program header table) 中 下面这个读的是ls命令的section headers 下面是读的 program headers 上面的0-8就是我们可以合并的段比如.data .bss就合并到一起了。 为什么要将section合并成为segment • Section合并的主要原因是为了减少页面碎片提高内存使用效率。如果不进行合并 假设页面大小为4096字节磁盘中8个扇区合并后的块大小内存块基本大小加载管理的基本单位如果.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节一起提供了对导入的共享库函数的访问入口由动态链接器在运行时进行修改。对于GOT的理解 ◦ 使用 readelf 命令查看 .so 文件可以看到该节。 从 执行视图 来看 • 告诉操作系统哪些模块可以被加载进内存。 • 加载进内存之后哪些分段是可读可写哪些分段是只读哪些分段是可执行的。 我们可以在 ELF头 中找到文件的基本信息以及可以看到ELF头是如何定位程序头表和节头表的。例 如我们查看下ls的主要信息 七.理解链接和加载
7.1静态链接 • 无论是自己的.o, 还是静态库中的.o本质都是把.o文件进行链接的过程 • 所以研究静态链接本质就是研究.o是如何链接的 首先一个目录里有main.exe是两个.o文件链接之后的 在两个.o文件在进行链接之前先看一下它们的汇编代码 • objdump -d 命令将代码段.text进行反汇编查看 • hello.o 中的 main 函数不认识 printf和run 函数 上面可以看出在每单个的文件中调用函数的e8后面的地址跟的都是全0其实就是在编译 hello.c 的时候编译器是完全不知道 printf 和 run 函数的存在的比如他们位于内存的哪个区块代码长什么样都是不知道的。因此编辑器只能将这两个函数的跳转地址先暂 时设为0
查看它们的符号表也是UND未定义 当它们链接之后main.s就有了地址 查看main.exe的符号表 最终 1. 两个.o的代码段合并到了一起并进行了统一的编址
2. 链接的时候会修改.o中没有确定的函数地址在合并完成之后进行相关call地址完成代码调用所以叫做可重定位目标文件 静态链接就是把库中的.o进行合并和上述过程⼀样
所以链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合拼装成一个独立的可执行文件。其中就包括我们之前提到的地址修正当所有模块组合在一起之后链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量从而修正它们的地址。这 其实就是静态链接的过程。 7.2ELF加载与进程地址空间
7.2.1虚拟地址/逻辑地址
• 一个ELF程序在没有被加载到内存的时候,本来就有地址当代计算机工作的时候都采用平坦 模式进行工作。所以也要求ELF对自己的代码和数据进行统一编址下面是 objdump -S 反汇编 之后的代码 最左侧的就是ELF的虚拟地址其实严格意义上应该叫做逻辑地址(起始地址偏移量),但是我们 认为起始地址是0.也就是说其实虚拟地址在我们的程序还没有加载到内存的时候就已经把可执 行程序进行统一编址了.
• 进程mm_struct、vm_area_struct在进程刚刚创建的时候初始化数据从哪里来的从ELF各个 segment来每个segment有自己的起始地址和自己的长度用来初始化内核结构中的[start, end] 等范围数据另外在用详细地址填充页表.
所以虚拟地址机制不光光OS要支持编译器也要支持. 7.2.2重新理解进程虚拟地址空间
ELF 在被编译好之后会把自己未来程序的入口地址记录在ELF header的Entry字段中 EIP存的是起始地址就是上面的Entry字段里面的地址MMU是页表的起始地址。 7.3动态链接与动态库加载
7.3.1进程如何看到动态库 7.3.2进程间共享库 7.3.3动态链接
7.3.3.1概要
动态链接其实远比静态链接要常用得多。比如我们查看下 main.exe 这个可执行程序依赖的动态库会发现它就用到了一个c动态链接库 这里的libc.so是C语言的运行时库里面提供了常用的标准输入输出文件字符串处理等等这些功能。 那为什么编译器默认不使用静态链接呢静态链接会将编译产生的所有目标⽂件连同用到的各种 库合并形成一个独立的可执行文件它不需要额外的依赖就可以运行。照理来说应该更加方便才对是吧 静态链接最大的问题在于生成的文件体积大并且相当耗费内存资源。随着软件复杂度的提升我们的操作系统也越来越臃肿不同的软件就有可能都包含了相同的功能和代码显然会浪费大量的硬盘空间。 这个时候动态链接的优势就体现出来了我们可以将需要共享的代码单独提取出来保存成⼀个独立的动态链接库等到程序运行的时候再将它们加载到内存这样不但可以节省空间因为同一个模块在内存中只需要保留一份副本可以被不同的进程所共享。 动态链接到底是如何工作的 首先要交代一个结论动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存其中每个动态库的加载地址都是不固定的操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。 当动态库被加载到内存以后一旦它的内存地址被确定我们就可以去修正动态库中的那些函数跳转 地址了。 7.3.3.2我们的可执行程序被编译器动了手脚 在C/C程序中当程序开始执行时它首先并不会直接跳转到 main 函数。实际上程序的入口点是 _start 这是一个由C运行时库通常是glibc或链接器如ld提供的特殊函数。
在 _start 函数中会执行一系列初始化操作这些操作包括 1. 设置堆栈为程序创建一个初始的堆栈环境。 2. 初始化数据段将程序的数据段如全局变量和静态变量从初始化数据段复制到相应的内存位 置并清零未初始化的数据段。 3. 动态链接这是关键的一步 _start 函数会调用动态链接器的代码来解析和加载程序所依赖的 动态库shared libraries。动态链接器会处理所有的符号解析和重定位确保程序中的函数调 用和变量访问能够正确地映射到动态库中的实际地址。 动态链接器 ◦ 动态链接器如ld-linux.so负责在程序运行时加载动态库。 ◦ 当程序启动时动态链接器会解析程序中的动态库依赖并加载这些库到内存中。 环境变量和配置文件 ◦ Linux系统通过环境变量如LD_LIBRARY_PATH和配置文件如/etc/ld.so.conf及其子配置文件来指定动态库的搜索路径。 ◦ 这些路径会被动态链接器在加载动态库时搜索。 缓存文件 ◦ 为了提高动态库的加载效率Linux系统会维护一个名为/etc/ld.so.cache的缓存文件。 ◦ 该文件包含了系统中所有已知动态库的路径和相关信息动态链接器在加载动态库时会首先搜索这个缓存文件。 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 函数来终止程序退出码。 7.3.3.3动态库中的相对地址
动态库为了随时进行加载为了支持并映射到任意进程的任意位置对动态库中的方法统一编址 采用相对编址的方案进行编制的(其实可执行程序也一样都要遵守平坦模式只不过exe是直接加载的)。
7.3.3.4我们的程序怎么和库具体映射起来的 • 动态库也是一个文件要访问也是要被先加载要加载也是要被打开的 • 让我们的进程找到动态库的本质也是文件操作不过我们访问库函数通过虚拟地址进行跳转访问的所以需要把动态库映射到进程的地址空间中 7.3.3.5我们的程序怎么进行库函数调用 • 库已经被我们映射到了当前进程的地址空间中 • 库的虚拟起始地址我们也已经知道了 • 库中每一个方法的偏移量地址我们也知道 • 所以访问库中任意方法只需要知道库的起始虚拟地址方法偏移量即可定位库中的方法 • 而且整个调用过程是从代码区跳转到共享区调用完毕在返回到代码区整个过程完 全在进程地址空间中进行的. 7.3.3.6全局偏移量表GOT(global offset table) • 也就是说我们的程序运行之前先把所有库加载并映射所有库的起始虚拟地址都应该提前知道 • 然后对我们加载到内存中的程序的库函数调用进行地址修改在内存中二次完成地址设置 (这个叫做加载地址重定位) • 等等修改的是代码区不是说代码区在进程中是只读的吗怎么修改能修改吗 所以动态链接采用的做法是在 .data 可执行程序或者库自己中专门预留一片区域来存放函数 的跳转地址它也被叫做全局偏移表GOT表中每一项都是本运行模块要引用的一个全局变量或函数的地址。
• 因为.data区域是可读写的所以可以支持动态进行修改
# .got在加载的时候会和.data合并成为一个segment然后加载在一起 1. 由于代码段只读我们不能直接修改代码段。但有了GOT表代码便可以被所有进程共享。但在不同进程的地址空间中各动态库的绝对地址、相对位置都不同。反映到GOT表上就是每个进程的每个动态库都有独立的GOT表所以进程间不能共享GOT表。 2. 在单个.so下由于GOT表与 .text 的相对位置是固定的我们完全可以利用CPU的相对寻址来找到GOT表。 3. 在调用函数的时候会首先查表然后根据表中的地址来进行跳转这些地址在动态库加载的时候会被修改为真正的地址。 4. 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说我们的动态库不需要做任何修改被加载到任意内存地址都能够正常运行并且能够被所有进程共享这也是为什么之前我们给编译器指定-fPIC参数的原因PIC相对编址GOT。 7.3.3.7库间依赖 plt是什么 • 不仅仅有可执行程序调用库 • 库也会调用其他库库之间是有依赖的如何做到库和库之间互相调用也是与地址无关的呢 • 库中也有.GOT,和可执行一样这也就是为什么大家为什么都是ELF的格式 • 由于动态链接在程序加载的时候需要对大量函数进行重定位这一步显然是非常耗时的。为了进一步降低开销我们的操作系统还做了一些其他的优化比如延迟绑定或者也叫PLT过程连接表 Procedure Linkage Table。与其在程序一开始就对所有函数进行重定位不如将这个过程推迟到函数第一次被调用的时候因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。
思路是GOT中的跳转地址默认会指向一段辅助代码它也被叫做桩代码/stup。在我们第一次调用函数的时候这段代码会负责查询真正函数的跳转地址并且去更新GOT表。于是我们再次调用函数的时候就会直接跳转到动态库中真正的函数实现。 总而言之动态链接实际上将链接的整个过程比如符号查询、地址的重定位从编译时推迟到了程序的运行时它虽然牺牲了一定的性能和程序加载时间但绝对是物有所值的。因为动态链接能够更有效的利用磁盘空间和内存资源以极大方便了代码的更新和维护更关键的是它实现了二进制级别的代码复用。
7.3.4总结
• 静态链接的出现提高了程序的模块化水平。对于一个大的项目不同的人可以独立地测试和开发自己的模块。通过静态链接生成最终的可执行文件。
• 我们知道静态链接会将编译产生的所有目标文件和用到的各种库合并成一个独立的可执行文件 其中我们会去修正模块间函数的跳转地址也被叫做编译重定位(也叫做静态重定位)。
• 而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存其中每个动态库的加载地址都是不固定的但是无论加载到什么地方都要映射到进程对应的地址空间然后通过.GOT方式进行调用(运行重定位也叫做动态地址重定位)。