Table of Contents:

编译

c++new 和 delete 在编译时生成如下内容:1.创建和释放内存 2.调用构造和析构函数

目标文件

COFF
ELF
PE

类型
- 目标文件 .o .obj
- 可执行文件
- 静态库 .a .lib
- 动态库 .so .dll
- 核心转储 core dump

Linux:ELF
文件头
描述文件属性
段表
保存各个段的基本属性
代码段
指令
符号表
与Symbol相关的信息
数据段
初始化的变量
.bss段
未初始化的变量,或初值为0的变量
重定位表
重定位的信息
字符串表
保存字符串
调试信息段

含义
.bss 未初始化数据(全局变量)
.comment 编译器版本信息
.ctors 全局构造函数指针
.data 已初始化数据(全局变量、静态变量)
.data.rel.ro 只读数据,与 .rodata 类似,不同的是它在重定位时会被改写,然后置为只读
.debug 调试信息,使用 gcc 的 -g 或 -ggdb 参数
.dtors 全局析构函数指针
.dynamic 动态链接信息,存储了动态链接的符号表地址、字符串表地址及大小、哈希表地址,共享对象的 SO-NAME、搜索路径,初始化代码地址,结束代码地址,依赖的共享对象文件名,动态链接重定位表地址、重定位入口数量等。
.dynstr 动态链接符号的符号名(字符串表)
.dynsym 与动态链接相关的符号表。需要注意,.symtab 中往往保存了所有符号,而 .dynsym 中只保存动态链接时需要的符号,不保存仅在模块内部使用的符号。
.eh_frame 与 C++ 异常处理相关
.eh_frame_hdr 与 C++ 异常处理相关
.fini 程序退出时执行的代码,相当于 main() 的“析构函数”
.fini_array 程序或共享对象退出时需要执行的函数指针
.gnu.version 动态链接符号版本,.dynsym 中的每个符号对应一项(该符号所需版本在 .gnu.version_d 中的序号)
.gnu.version_d 动态链接符号版本的定义(definitions),每个版本的标志位、序号、共享库名称、主次版本号
.gnu.version_r 动态链接符号版本的需求(requirements),依赖的共享库名称和版本序号
.got 全局偏移量表(用于动态链接的间接跳转或引用)
.got.plt Procedure Linkage Table,即运行时链接的“桩函数”
.hash 符号表的 hash 表,用于加快符号查找
.init main() 执行前的初始化代码,相当于 main() 的“构造函数”
.init_array 程序或共享对象初始化时需要执行的函数指针
.interp 动态链接器的文件路径
.line 调试用的行号信息,使用 gcc 的-g 或 -ggdb 参数
.note 编译器、链接器、操作系统加入的平台相关的额外信息
.note.ABI-tag 指定程序的 ABI
.preinit_array 早于初始化阶段前执行的函数指针,在 .init_array 之前执行
.rel.data 静态链接文件中,数据段的重定位表
.rel.dyn 动态链接文件中,对数据引用(.got、.data)的重定位表
.rel.plt 动态链接文件中,对函数引用(.got.plt)的重定位表
.rel.text 静态链接文件中,代码段的重定位表
.rodata 只读数据(常量、字符串常量)
.shstrtab 保存了各段名称的字符串表
.strtab 字符串表,通常是符号表中的符号名对应的字符串
.symtab 符号表,静态链接时需要的符号信息
.tbss 每个线程一份的未初始化数据(.bss 是各线程共享的)
.tdata 每个线程一份的已初始化数据(.data 是各线程共享的)
.text 代码段(为什么不叫 .code?)

Windows PE 文件格式

PE 文件是 Windows 下的一种可执行文件格式,包括可执行文件(.exe)、动态链接库(.dll)等。一个 PE 文件由各种节(Sections)组成,每个节都有特定的功能和用途。以下是一些常见的 PE 文件格式中的节及其含义:

PE 文件格式可能包含多个节,以下是一些常见的节及其可能的含义:
1. .text:包含程序的代码段。
2. .data:包含程序运行时使用的全局和静态变量的初始化数据。
3. .rdata:包含只读数据,如常量、字符串等。
4. .bss:包含未初始化的全局和静态变量(全局变量和静态变量的零初始化段)。
5. .edata:包含导出表,记录了程序内部的一些函数或符号,可以被其他程序或模块引用。
6. .idata:包含导入表,用于存储程序所需依赖的外部函数的引用信息。
7. .rsrc:包含资源数据,如图标、位图、字符串等。
8. .reloc:包含重定位表,记录了需要在加载时进行地址重定位的信息。
9. .tls:包含线程本地存储(Thread Local Storage)相关信息,用于多线程程序的线程本地数据存储。
10. .debug:包含调试信息,如符号表、源代码行号等。
11. .pdata:包含函数执行时异常处理相关的信息,如函数的异常处理程序。
12. .xdata:包含运行时异常处理的相关信息。
13. .arch:包含指定 CPU 架构信息的节。

静态链接

链接的接口——符号
- 符号修饰(函数签名)
- Calling Convention 的一部分
- extern "C"
- 强符号;弱符号
- 强符号: 定义的符号
- 弱符号:未定义的符号
- 强引用:链接时符号不存在就报错
- 弱引用:符号不存在不报错
- 作用:设计插件用

one-pass
- 只扫描一遍
- 需要程序员注意依赖库的顺序,被依赖的需要放在后边,即库的依赖关系的拓扑排序

two-pass
- 1.空间与地址分配:读取各目标文件->相似段合并->生成全局符号表.symbtab-->生成重定位表.rel.data.rel.text
- 2.符号解析与重定位
- 根据重定位表,把所有不确定要跳转地址的代码,根据符号表里面存储的地址,进行一次修正

链接器把程序变成可执行文件之后,要装载器去执行程序就容易多了。

COMMON块
- 作用:处理多个不同类型的弱符号
- 原因:链接器无法判断符号类型

C++相关问题
- 重复代码消除
- 模版代码编译时会生成多份,链接时消除
- 全局构造与析构
- .init .fini
- ABI(Application Binary Interface)
- 比api更加严格,二进制上层次上的接口约定,二进制兼容

静态库链接
- 静态库:即把.o文件打包并建立索引
- 使用的工具 ar

装载

硬盘中的程序文件-->内存中的进程

程序装载面临的挑战

装载主要面临的其实是一个内存的问题

装载器需要满足两个要求:
第一,可执行程序加载后占用的内存空间应该是连续的。执行指令的时候,程序计数器是顺序地一条一条指令执行下去。这也就意味着,这一条条指令需要连续地存储在一起。
第二,我们需要同时加载很多个程序,并且不能让程序自己规定在内存中加载的位置。

虚拟内存

每个程序在自己来看都拥有连续的地址空间,独占整个操作的内存。
我们把指令里用到的内存地址叫作虚拟内存地址(Virtual Memory Address),实际在内存硬件里面的空间地址,我们叫物理内存地址(Physical Memory Address)**。

程序里有指令和各种内存地址,我们只需要关心虚拟内存地址就行了。对于任何一个程序来说,它看到的都是同样的内存地址,比如说都是从某个固定地址开始的地址,而它编译的程序中是不可能知道程序中的物理地址的,因为你不知道程序会被装载到哪里。我们维护一个虚拟内存到物理内存的映射表,这样实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行。

内存分段

这种找出一段连续的物理内存和虚拟内存地址进行映射的方法,我们叫分段(Segmentation)**。

内存交换(Memory Swapping): 把内存中的程序load到硬盘中,等到需要的时候再load到内存中。
虚拟内存、分段,再加上内存交换,看起来似乎已经解决了计算机同时装载运行很多个程序的问题。
不过,你千万不要大意,这三者的组合仍然会遇到一个性能瓶颈。硬盘的访问速度要比内存慢很多,而每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。所以,如果内存交换的时候,交换的是一个很占内存空间的程序,这样整个机器都会显得卡顿。

内存分页

既然问题出在内存碎片和内存交换的空间太大上,那么解决问题的办法就是,当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,这样就可以解决这个问题。这个办法,在现在计算机的内存管理里面,就叫作内存分页(Paging)。
和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小。而对应的程序所需要占用的虚拟内存空间,也会同样切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫(Page)。从虚拟内存到物理内存的映射,不再是拿整段连续的内存的物理地址,而是按照一个一个页来的。页的尺寸一般远远小于整个程序的大小。在 Linux 下,我们通常只设置成 4KB。你可以通过命令看看你手头的 Linux 系统设置的页的大小。

当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于 CPU 的缺页错误(Page Fault)。我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存里。这种方式,使得我们可以运行那些远大于我们实际物理内存的程序。

虚拟内存、内存交换和内存分页这三者结合之下,你会发现,其实要运行一个程序,“必需”的内存是很少的。CPU 只需要执行当前的指令,极限情况下,内存也只需要加载一页就好了。再大的程序,也可以分成一页。每次,只在需要用到对应的数据和指令的时候,从硬盘上交换到内存里面来就好了。以我们现在 4K 内存一页的大小,640K 内存也能放下足足 160 页呢,也无怪乎在比尔·盖茨会说出“640K ought to be enough for anyone”这样的话。
不过呢,硬盘的访问速度比内存慢很多,所有大内存还是很有用的。

进程虚存空间分布

image.png
image.png

装载过程

Linux 下的装载器只能解析 ELF 格式而不能解析 PE 格式。
如果我们有一个可以能够解析 PE 格式的装载器,我们就有可能在 Linux 下运行 Windows程序了。
Linux 下著名的开源项目 Wine,就是通过兼容PE 格式的装载器,使得我们能直接在 Linux 下运行 Windows 程序的。
而现在微软的Windows 里面也提供了 WSL,也就是 Windows Subsystem for Linux,可以解析和加载ELF 格式的文件。

  1. 创建一个独立的虚拟地址空间
    • 并不是创建空间而是创建映射函数所需要的相应的数据结构,在i386 的Linux下,创建虚拟地址空间实际上只是分配一个页目录(Page Directory)就可以了,甚至不设置页映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置
  2. 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
  3. 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
  4. 通过页错误机制从可执行文件加载数据
    • 上面的步骤执行完以后,其实可执行文件的真正指令和数据都没有被装入到内存中。操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚存之间的映射关系而已
    • CPU访问了空页面时产生页错误,并将控制权交给操作系统
    • 操作系统找到空页所在的VMA-->找出可执行文件的对应内容-->读取文件-->然后在物理内存分配一个页面并建立其和虚拟页的映射关系

Linux内核装载ELF过程

在linux系统的bash下执行一个命令到底做了什么?
1. 首先在用户层面,bash进程会调用fork()系统调用创建一个新的进程
2. 新的进程调用 execxx()系统的库函数,去执行程序
3. 再调用 execve() 系统调用执行指定的ELF文件
1. 查找被执行的文件,如果找到文件,则读取文件的前128个字节。目的是判断文件的格式,如:可执行程序、shell脚本、python脚本等
2. 然后调用 search_binary_handle() 去搜索和匹配合适的可执行文件装载处理过程,即 load_elf_binary()
- ELF可执行文件的装载处理过程叫做 load_elf_binary()
- 装载可执行脚本程序的处理过程叫做 load_script()
3. 检查ELF可执行文件格式的有效性
4. 寻找动态链接的“.interp”段,设置动态链接器路径
5. 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据
6. 初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址
7. 将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,
- 对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中 e_entry 所指的地址;
- 对于动态链接的ELF可执行文件,程序入口点是动态链接器
- 运行动态链接器,加载动态库,并进行动态链接
- 设置返回地址为入口地址,并开始执行程序
4. 原先的 bash 进程调用 waitpid()等待刚才启动的新进程结束,然后继续等待用户输入命令

Windows PE的装载

  1. 先读取文件的第一个页,在这个页中,包含了DOS头、PE文件头和段表。
  2. 检查进程地址空间中,目标地址是否可用,如果不可用,则另外选一个装载地址。这个问题对于可执行文件来说基本不存在,因为它往往是进程第一个装入的模块,所以目标地址不太可能被占用。主要是针对DLL文件的装载而言的
  3. 使用段表中提供的信息,将PE文件中所有的段一一映射到地址空间中相应的位置。
  4. 如果装载地址不是目标地址,则进行Rebasing。
  5. 装载所有PE文件所需要的DLL文件。
  6. 对PE文件中的所有导入符号进行解析。
  7. 根据PE头中指定的参数,建立初始化栈和堆。
  8. 建立主线程并且启动进程。

动态链接

问题及解决方案

新问题:静态库的缺陷
- 静态库浪费内存和磁盘空间,模块在内存中有多个重复副本
- 静态库更新不方便,每个模块的改动都需要重新链接整个程序

基本思想
- 将链接过程推迟到运行时再执行,代码段共享,数据段独享
- 在动态链接的过程中,我们想要“链接”的,不是存储在硬盘上的目标文件代码,而是加载到内存中的共享库(Shared Libraries)。

新问题:加载位置不固定
- 若动态的加载,则加载的位置会不固定,那动态库中就不能存在绝对地址

装载时重定位
- -shared 链接参数, 即动态库中的符号在确定加载后的位置后,再对符号进行重定位,规则为:基址 + 偏移量

新问题:地址无关代码,绝对符号地址引用的问题
- 要想要在程序运行的时候共享代码,也有一定的要求,就是这些机器码必须是“地址无关”的。也就是说,我们编译出来的共享库文件的指令代码,是地址无关码(Position-Independent Code),就是地址不能写死,比如分支的跳转地址,函数的调用地址等,不能像静态链接的地址一样是个绝对的地址。换句话说就是,这段代码,无论加载在哪个内存地址,都能够正常执行。如果不是这样的代码,就是地址相关的代码。
- 装载时重定位是解决动态模块中有绝对地址引用的办法之一,但是它有一个很大的缺点是指令部分无法在多个进程之间共享(因为不同的程序对同一库的加载的地址可能不一样),这样就失去了动态链接节省内存的一大优势

地址无关代码(PIC,Position-independent Code), 链接参数 -fPIC
- 希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本
- 四种情况:
1. 模块内部的函数调用、跳转等。 --> 使用相对寻址,即符号相对当前地址的偏移
2. 模块内部的数据访问,比如模块中定义的全局变量、静态变量。
3. 模块外部的函数调用、跳转等。 --> 间接访问,即增加GOT表,先访问GOT,再访问真正的地址
4. 模块外部的数据访问,比如其他模块中定义的全局变量

image.png

新问题:加载速度问题
- 若把所有符号都在启动时动态链接,那么会增加程序启动的时间。于是就有了延迟绑定

延迟绑定(PLT)
目的:提升动态链接程序加载时的性能
当函数首次被使用时才进行绑定(符号查找、重定位等),否则不绑定,绑定以后,之后再访问此符号就相当于直接访问了。

ELF将GOT拆分成了两个表叫做“.got”和“.got.plt”。其中“.got”用来保存全局变量引用的地址,“.got.plt”用来保存函数引用的地址,也就是说,所有对于外部函数的引用全部被分离出来放到了“.got.plt”中。

当调用一个动态库的函数时

call   400550 <show_me_the_money@plt>

这里后面有一个 @plt 的关键字,代表了我们需要从 PLT,也就是程序链接表(Procedure Link Table)里面找要调用的函数。对应的地址呢,则是 400550 这个地址。程序链接表是在编译期间就确定的了。

那当我们把目光挪到上面的 400550 这个地址,你又会看到里面进行了一次跳转,这个跳转指定的跳转地址,你可以在后面的注释里面可以看到,GLOBAL_OFFSET_TABLE+0x18。这里的 GLOBAL_OFFSET_TABLE,就是GOT。而 GOT 表里的数据,则是加载一个个共享库的时候写进去的。。

  400550:       ff 25 12 05 20 00       jmp    QWORD PTR [rip+0x200512]        # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18>


首次会走到第四步,当计算出动态库函数符号地址后,就把真正的地址写入到got表中,之后只需走到第三步就可以直接调用动态库中的函数了。

动态链接相关结构

动态链接的步骤

显式运行时链接

共享库

核心问题:如何维护共享库

共享库版本

符号版本

共享库的组织和路径

共享库查找过程

动态链接的模块所依赖的模块路径保存在“.dynamic”段里面,由DT_NEED类型的项表示。动态链接器对于模块的查找有一定的规则:如果DT_NEED里面保存的是绝对路径,那么动态链接器就按照这个路径去查找;如果DT_NEED里面保存的是相对路径,那么动态链接器会在/lib、/usr/lib和由/etc/ld.so.conf配置文件指定的目录中查找共享库。为了程序的可移植性和兼容性,共享库的路径往往是相对的。

如果动态链接器在每次查找共享库时都去遍历这些目录,那将会非常耗费时间。所以Linux系统中都有一个叫做 ldconfig 的程序,这个程序的作用是为共享库目录下的各个共享库创建、删除或更新相应的SO-NAME(即相应的符号链接),这样每个共享库的SO-NAME就能够指向正确的共享库文件;并且这个程序还会将这些SO-NAME收集起来,集中存放到 /etc/ld.so.cache 文件里面,并建立一个SO-NAME的缓存。当动态链接器要查找共享库时,它可以直接从 /etc/ld.so.cache 里面查找。而/etc/ld.so.cache 的结构是经过特殊设计的,非常适合查找,所以这个设计大大加快了共享库的查找过程

所以理论上讲,如果我们在系统指定的共享库目录下添加、删除或更新任何一个共享库,或者我们更改了/etc/ld.so.conf的配置,都应该运行 ldconfig 这个程序,以便调整SO-NAME和/etc/ld.so.cache。

环境变量
- LD_LIBRARY_PATH:共享库查找路径
- LD_LIBRARY_PATH对于共享库的开发和测试来说十分方便,但是它不应该被滥用。也就是说,普通用户在正常情况下不应该随意设置LD_LIBRARY_PATH来调整共享库搜索目录。随意修改LD_LIBRARY_PATH并且将其导出至全局范围,将可能引起其他应用程序运行出现的问题;
- LD_LIBRARY_PATH也会影响GCC编译时查找库的路径,它里面包含的目录相当于链接时GCC的“-L”参数。
- LD_PRELOAD:指定预先装载的共享库或者目标文件
- 在LD_PRELOAD里面指定的文件会在动态链接器按照固定规则搜索共享库之前装载,它比LD_LIBRARY_PATH里面所指定的目录中的共享库还要优先
- 由于全局符号介入这个机制的存在,LD_PRELOAD里面指定的共享库或目标文件中的全局符号就会覆盖后面加载的同名全局符号,这使得我们可以很方便地做到改写标准C库中的某个或某几个函数而不影响其他函数,对于程序的调试或测试非常有用。
- 系统配置文件中有一个文件是/etc/ld.so.preload,它的作用与LD_PRELOAD一样。
- LD_DEBUG:打开动态链接器的调试功能
- 当我们设置这个变量时,动态链接器会在运行时打印出各种有用的信息,对于我们开发和调试共享库有很大的帮助。可以为如下值:
- “bindings”显示动态链接的符号绑定过程。
- “libs”显示共享库的查找过程。
- “versions”显示符号的版本依赖关系。
- “reloc”显示重定位过程。
- “symbols”显示符号表查找过程。
- “statistics”显示动态链接过程中的各种统计信息

windows下共享库

动态库相关的文件

静态库(Static Library):
函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件)。
- 使用静态库的优势在于依赖明确,不依赖于外部的 DLL 文件,但会增加程序的大小。

动态链接库(Dynamic Linked Library):
Windows为应用程序提供了丰富的函数调用,这些函数调用都包含在动态链接库中。其中有3个最重要的DLL,
- Kernel32.dll,它包含用于管理内存、进程和线程的各个函数;
- User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;
- GDI32.dll,它包含用于画图和显示文本的各个函数。

导入库(Import Library):
在使用动态链接库的时候,往往提供两个文件:一个引入库和一个DLL。引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问DLL中导出的函数。
- 导入库是针对动态链接库的一种特殊类型的库文件,它是编译器用来在链接时定位和解析动态链接库中函数的符号的。
- 当你编译一个程序并使用动态链接库时,通常需要导入库文件来告诉编译器在链接时如何定位动态链接库中的函数。
- .lib 文件通常被称为导入库,它不包含实际的代码,而是包含了动态链接库中函数的符号信息以及程序运行时所需的元数据。

EXP文件(Export File)
- 作用:这是一个中间文件,用于存储DLL项目中所有导出函数的列表。该文件由链接器在创建DLL时使用,以建立函数导出表。
- 这个文件的主要使用者是链接器。在链接过程中,链接器使用.exp文件来解析DLL中哪些符号需要被外部访问,并据此构建DLL的导出表。
- 应用:主要用于链接过程中生成DLL文件,确保DLL中的导出表正确设置。
- 在项目发布的时候需要包含exp文件吗?
- 一旦DLL文件被成功构建,.exp文件在运行时就不再需要了。
- 在项目进行发布的时候,.exp文件(Export File)通常不需要被包含在发布包中。.exp文件是在链接(Linking)过程中生成的一个中间文件,其主要目的是在构建阶段帮助创建DLL(动态链接库)文件时使用,存储有关如何处理来自DLL的导出的信息。

PDB文件(Program Database)
- 作用:存储程序编译时生成的调试信息。如函数名、变量名和行号等。使得开发者在调试程序时可以进行源代码级的调试。
- 应用:非常关键的调试工具,用于程序崩溃分析、性能分析等。

发布时应包含哪些文件?
当你发布一个项目(尤其是包含DLL的项目)时,通常需要包含以下类型的文件:
1. .dll 文件 - 动态链接库文件本身,包含所有实现的代码,是程序运行所必需的。
2. .lib 文件 - 如果你的DLL设计为由其他程序动态链接(dynamic linking)使用,那么相关的导入库(Import Library)文件.lib也需要被包含。这个文件是用来在编译时解析DLL中公开的符号的。
3. .pdb 文件 - 如果你希望最终用户能够对程序进行调试(通常在beta测试阶段或开发者版中可能需要),那么程序数据库文件(Program Database)也可能被包含。这个文件包含了源代码级调试的信息。
4. 相关的配置文件和资源 - 根据项目的不同,可能还需要包括其他资源,如配置文件、数据文件、图像等。

dumpbin查看库文件

下面是打印的例子:
如果库里面各个程序段都很全,那说明它是一个动态库或静态库。
如果是导入库,里面会有多个.idata节,它用于存储导入表(Import Table)信息。导入表是一个数据结构,用于描述程序需要从其他模块(通常是 DLL 文件)中导入的函数或符号。

结论:一般成对出现的库(.dll .lib都有)为动态链接库,仅有一个.lib的库为静态库

######## 动态库 ########
C:\Users\sunxv\Desktop\0\depends22_x64>dumpbin OpenThreads.dll

Dump of file OpenThreads.dll

File Type: DLL
  Summary
        1000 .data
        1000 .pdata
        4000 .rdata
        1000 .reloc
        1000 .rsrc
        3000 .text

######## 导入库 ########
C:\Users\sunxv\Desktop\0\depends22_x64>dumpbin OpenThreads.lib

Dump of file OpenThreads.lib

File Type: LIBRARY
  Summary
          CF .debug$S
          14 .idata$2
          14 .idata$3
           8 .idata$4
           8 .idata$5
          10 .idata$6

######## 静态库 ########
C:\Users\sunxv\Desktop\0\depends22_x64>dumpbin mysqlclient.lib

Dump of file mysqlclient.lib

File Type: LIBRARY

  Summary
           8 .CRT$XCU
        8EF8 .bss
      117079 .data
          30 .debug$F
      58BC90 .debug$S
        7580 .debug$T
        4FD6 .drectve
       116D7 .rdata
        2623 .rdata$r
           C .sxdata
       78390 .text
          18 .text$yc
          54 .xdata$x

查看lib库是32位还是64位

dumpbin /headers lua51.lib|findstr machine

VS中编译共享库

image.png

Linux下共享库

编译静态库

将源文件打包为静态链接库的过程很简单,只需经历以下 2 个步骤:
1) 将所有指定的源文件,都编译成相应的目标文件:
2) 然后使用 ar 压缩指令,将生成的目标文件打包成静态链接库,其基本格式如下:

ar rcs 静态链接库名称 目标文件1 目标文件2 ...

例子:

gcc -c sub.c add.c div.c
ar rcs libmymath.a add.o sub.o div.o

使用静态库

GCC 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,GCC 编译器才会选择相应的静态链接库。如果两种都没有(或者 GCC 编译器未找到),则链接失败。

方式一:

gcc -static main.o libmymath.a

其中,-static 选项强制 GCC 编译器使用静态链接库。

方式二:

gcc main.o -static -L /root/demo/ -lmymath

其中,-L(大写的 L)选项用于向 GCC 编译器指明静态链接库的存储位置
-l(小写的 L)选项用于指明所需静态链接库的名称,libmymath.a要写出 -lmymath

当以第一种写法完成链接操作时,GCC 编译器只会在当前目录中(这里为 demo 目录)查找 libmymath.a 静态链接库;反之,如果使用 -l(小写的 L)选项指明了要查找的静态库的文件名,则 GCC 编译器会按照如下顺序,依次到指定目录中查找所需库文件:
1. 如果 gcc 指令使用 -L 选项指定了查找路径,则 GCC 编译器会优先选择去该路径下查找所需要的库文件;
2. 再到 Linux 系统中 LIBRARY_PATH 环境变量指定的路径中搜索需要的库文件;
3. 最后到 GCC 编译器默认的搜索路径(比如 /lib、/lib64、/usr/lib、/usr/lib64、/usr/local/lib、/usr/local/lib64 等,不同系统环境略有差异)中查找。

如果使用第一种方法完成链接操作,但 GCC 编译器提示找不到所需库文件,表明所用库文件并未存储在当前路径下,解决方案就是手动找到库文件并将其移至当前路径,然后重新执行链接操作。

反之,如果使用的是第二种方法,也遇到了 GCC 编译器提示未找到所需库文件,表明库文件的存储路径不对,解决方案有以下 3 种:
- 手动找到该库文件,并在 gcc 指令中用 -L 选项明确指明其存储路径。比如 libmymath.a 静态库文件存储在 /usr 目录下,则完成链接操作的 gcc 指令应为gcc -static main.c -L/usr -lmymath -o main.exe
- 将库文件的存储路径添加到 LIBRARY_PATH 环境变量中。仍以库文件存储在 /usr 目录下,则通过执行export LIBRARY_PATH=$LIBRARY_PATH:/usr指令,即可将 /usr 目录添加到该环境变量中(此方式仅在当前命令行窗口中有效);
- 将库文件移动到 GCC 编译器默认的搜索路径中。

编译动态库

so文件的源文件中不需要有main函数,即使有也不会被执行
编译的时候gcc需要加-fPIC选项,这可以使gcc产生与位置无关的代码。
链接的时候gcc使用-shared选项,指示生成一个共享库文件。

共享库文件名要以lib开头,扩展名为.so。按照命名惯例,每个共享库有三个文件名:real name、soname和linker name。
- 真实名(Real Name):共享库的实际文件名,包含完整的共享库版本号。
- 共享库名(SONAME):共享库的标识名,只包含主版本号,用于表示库的 API 兼容性,主版本号一致即可保证库函数的接口一致。
- 链接器名(Linker Name):一个符号链接,指向真实名或SONAME,用于方便链接器查找库。

# 基础使用
gcc -fPIC -c a.c
gcc -fPIC -c b.c
gcc -shared -Wl -o libmyab.so a.o b.o

# 一步到位使用
gcc -fpic -shared 源文件名... -o 动态链接库名

# 加soname的使用
gcc -shared -Wl -soname libmyab.so.1 -o libmyab.so.1.0.1 a.o b.o

编译动态库的makefile例子:

.SUFFIXES:.c .o

CC=gcc
SRCS=test.c

EXEC=libtest.so

OBJS=$(SRCS:.c=.o)

start:$(OBJS)
    $(CC) -shared -o $(EXEC) $(OBJS)

.c.o:
    $(CC) -g -fPIC -o $@ -c $<

clean:
    rm -f $(OBJS)

使用动态库

例子:

gcc main.c  libmymath.so -o main.exe
gcc main.o  -L /root/demo/ -lmymath
  1. 为了让linux能找到so文件的位置,需要在.bash_profile中添加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. 或者将so文件放入linux的系统目录/usr/lib/
  2. 在c文件中使用so文件,首先需要 #inluce相关头文件。
  3. gcc连接时添加 –L 参数指明so文件存放路径, -l 参数指明so文件名
  4. 为了使我们编写的so文件同时可以被C或者C++调用,我们需要修改一下函数申明部分增加带有__cplusplus的预编译指令,extern "C"{}

动态库找不到的问题

在linux系统下报如下的错误是因为公司库没找到:

error while loading shared libraries: libmysqlpp.so.3: cannot open shared object file: No such file or directory

事实上,当 GCC 编译器运行可执行文件时,会按照如下的路径顺序搜索所需的动态库文件:
1. 如果在生成可执行文件时,用户使用了-Wl,-rpath=dir(其中 dir 表示要查找的具体路径,如果查找路径有多个,中间用 : 冒号分隔)选项指定动态库的搜索路径,则运行该文件时 GCC 会首先到指定的路径中查找所需的库文件;
2. GCC 编译器会前往 LD_LIBRARY_PATH 环境变量指明的路径中查找所需的动态库文件;
3. GCC 编译器会前往 /ect/ld.so.conf 文件中指定的搜索路径查找动态库文件;
4. GCC 编译器会前往默认的搜索路径中(例如 /lib、/lib64、/usr/lib、/usr/lib64 等)中查找所需的动态库文件。

注意,可执行文件的当前存储路径,并不在默认的搜索路径范围内,因此即便将动态库文件和可执行文件放在同一目录下,GCC 编译器也可能提示“找不到动态库”。

解决办法如下:
0、最粗暴的方法,把缺失的动态库复制到可执行程序的目录,程序可能可能会在当前目录下查找动态库。但也不一定。或者把动态库复制到将系统库目录下(例如 /usr/lib、/usr/lib64、/lib、/lib64)

1、如果共享库文件安装到了 /lib/usr/lib 目录下, 那么需执行一下 ldconfig 命令
ldconfig命令的用途, 主要是在默认搜寻目录( /lib/usr/lib )以及动态库配置文件/etc/ld.so.conf内所列的目录下, 搜索出可共享的动态链接库(格式如lib.so), 进而创建出动态装入程序(ld.so)所需的连接和缓存文件. 缓存文件默认为/etc/ld.so.cache, 此文件保存已排好序的动态链接库名字列表,目的是来提高共享库的查找速度.

如果不执行 ldconfig 或者没有 /etc/ld.so.cache 文件,系统将不会更新共享库的缓存,这可能导致一些问题,包括但不限于:
性能问题
缓存的目的是提高共享库的查找速度。如果没有更新缓存,系统在运行时可能会花费更多时间来查找和加载共享库,从而导致性能下降。
共享库不可用
如果新安装了共享库但没有运行 ldconfig,系统可能无法找到或加载新安装的库。这可能导致应用程序无法启动或者出现运行时错误,因为它们无法找到所需的库。

2、如果共享库文件安装到了 /usr/local/lib (很多开源的共享库都会安装到该目录下)或其它"非/lib或/usr/lib"目录下, 那么在执行 ldconfig命令前, 还要把新共享库目录加入到共享库配置文件/etc/ld.so.conf中, 如下:

# cat /etc/ld.so.conf
include ld.so.conf.d/*.conf
# echo "/usr/local/lib" >> /etc/ld.so.conf  
# ldconfig

3、如果共享库文件安装到了其它"非/lib或/usr/lib" 目录下, 但是又不想在/etc/ld.so.conf中加路径(或者是没有权限加路径). 那可以export一个全局变量 LD_LIBRARY_PATH , 然后运行程序的时候就会去这个目录中找共享库。也可以在 .bashrc.bash_profile 或shell里加入如下类似语句:

export LD_LIBRARY_PATH=/usr/local/mysql/lib:$LD_LIBRARY_PATH   

一般来讲这只是一种临时的解决方案, 在没有权限或临时需要的时候使用.

显示调用C/C++动态链接库

总的来讲,动态链接库的调用方式有 2 种,分别是:
- 隐式调用(静态调用):将动态链接库和其它源程序文件(或者目标文件)一起参与链接;
- 显式调用(动态调用):手动调用动态链接库中包含的资源,同时用完后要手动将资源释放。

显式调用动态链接库的过程,类似于使用 malloc() 和 free()(C++ 中使用 new 和 delete)管理动态内存空间,需要时就申请,不需要时就将占用的资源释放。由此可见,显式调用动态链接库对内存的使用更加合理。

使用步骤:
1、引入<dlfcn.h> 头文件

2、打开该库文件

void *dlopen (const char *filename, int flag);

其中,filename 参数用于表明目标库文件的存储位置和库名;flag 参数的值有 2 种:
1.RTLD_NOW:将库文件中所有的资源都载入内存;
2.RTLD_LAZY:暂时不降库文件中的资源载入内存,使用时才载入。

值得一提的是,对于 filename 参数,如果用户提供的是以 / 开头,即以绝对路径表示的文件名,则函数会前往该路径下查找库文件;反之,如果用户仅提供文件名,则该函数会依次前往 LD_LIBRARY_PATH 环境变量指定的目录、/etc/ld.so.cache 文件中指定的目录、/usr/lib、/usr/lib64、/lib、/lib64 等默认搜索路径中查找。

3、借助 dlsym() 函数可以获得指定函数在内存中的位置

void *dlsym(void *handle, char *symbol);

4、和dlopen() 相对地,借助 dlclose() 函数可以关闭已打开的动态链接库。

5、借助 dlerror() 函数,我们可以获得最近一次 dlopen()、dlsym() 或者 dlclose() 函数操作失败的错误信息

6、编译时加上 -ldl,可执行程序需要 libdl.so 动态库的支持

gcc main.c -ldl -o main.exe

debug or release

linux 下编译,区分程序、库是debug还是release版本。
在linux里面,使用编译器的时候,默认没有 -g 的选项,也就是默认编译成release版本。

使用gdb命令

使用 gdb filename //filename这里指的是你想要产看的程序,或者库
如果是debug版本的话,最后会提示:Reading symbols from filename...done 相关信息 //filename这里指的是你想要产看的程序,或者库
如果是release版本的话,最后会提示:no debugging symbols found

使用file命令

file <your_program>

通常情况下,如果程序是 release 版本,输出可能会包含 "stripped" 或 "not stripped" 等字样,表示调试信息被删除或保留。而 debug 版本则可能会包含 "debug" 或 "with debug_info" 等字样,表示调试信息被保留。

程序运行

程序的内存布局

image.png

栈与调用惯例

栈帧结构:
image.png

i386下的函数调用
1. 把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。
2. 把当前指令的下一条指令的地址压入栈中。
3. 跳转到函数体执行。

其中第2步和第3步由指令call一起执行,跳转到函数体之后即开始执行函数。

i386函数体的标准开头
1. push ebp:把ebp压入栈中(称为old ebp)。把ebp压入栈中,是为了在函数返回的时候便于恢复以前的ebp值
2. mov ebp, esp:ebp = esp(这时ebp指向栈顶,而此时栈顶就是oldebp)。
3. 【可选】sub esp, XXX:在栈上分配XXX字节的临时空间。
4. 【可选】push XXX:如有必要,保存名为XXX寄存器(可重复多个)。原因是:在于编译器可能要求某些寄存器在调用前后保持不变,那么函数就可以在调用开始时将这些寄存器的值压入栈中,在结束后再取出

i386函数体的标标准结尾
1. 【可选】pop XXX:如有必要,恢复保存过的寄存器(可重复多个)。
2. mov esp, ebp:恢复ESP同时回收局部变量空间。
3. pop ebp:从栈中恢复保存的ebp的值。
4. ret:从栈中取得返回地址,并跳转到该位置。

image.png
image.png

调用约定(Calling Convention)”通常会规定如下几方面内容:
- 函数参数的传递顺序和方式(使用栈/寄存器传参?);
- 栈的维护方式(由哪一方来清理栈?);
- 名字修饰(Name-mangling)的策略(为了在链接时对不同的调用惯例进行区分)。

C++ thiscall 调用惯例:专用于类成员函数的调用。

在调用 C++ 非静态成员函数时使用此约定。基于所使用的编译器和函数是否使用可变参数,有两个主流版本的 thiscall。 对于 GCC 编译器,thiscall 几乎与 cdecl 等同:调用者清理堆栈,参数从右到左传递。差别在于 this 指针,thiscall 会在最后把 this 指针推入栈中,即相当于在函数原型中是隐式的左数第一个参数。

函数返回值的处理

大对象返回值的处理步骤:
1. 首先main函数在栈上额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这里称为temp。
2. 将temp对象的地址作为隐藏参数传递给return_test函数。
3. return_test函数将数据拷贝给temp对象,并将temp对象的地址用eax传出。
4. return_test返回之后,main函数将eax指向的temp对象的内容拷贝给n。

image.png

堆与内存管理

用来分配堆空间的系统调用:
- brk():通过设置进程数据段的结束地址,即把数据段的结束地址向高地址移动,来扩大空间。并将这部分扩大出来的空间来作为堆空间使用;
- mmap():向操作系统申请一段虚拟地址空间,这部分空间可以映射到某个文件。也可以不映射到具体文件而成为匿名空间,以作堆空间之用。

常用的堆分配算法:
- 空闲链表:把堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个链表,直到找到大小合适的块并将它拆分。当用户释放空间时将它合并到空闲链表中;
- 位图:将整个堆划分为大量的块,每个块大小相同。用户请求内存时,总是分配整数个块给用户,第一个块称为已分配区域的头,其余称为已分配区域的主体。可以用一个整数数组来记录块的使用情况,用两位数表示一个块的状态(头\主体\空闲)。
- 对象池:如果每一次分配的空间大小都一样,那么就可以按照这个每次请求分配的大小作为一个单位,把整个堆空间划分为大量的小块,每次请求的时候只需要找到一个小块就可以了。

运行库

入口函数和程序初始化

一个典型的程序运行步骤:
- 操作系统创建进程后,把控制权交到了程序入口,该入口通常为运行库中的某个入口函数
- 入口函数对运行库和程序运行环境进行初始化,包括:堆、IO、线程、全局变量构造等;
- 入口函数在完成初始化后,调用 main() 函数,正式开始执行程序主体部分;
- main 函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭 IO 等,然后进行系统调用结束进程;

C/C++运行库

一个C语言运行库大致包含了如下功能:
- 启动与退出:包括入口函数及入口函数所依赖的其他函数等。
- 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现。
- I/O:I/O功能的封装和实现,参见上一节中I/O初始化部分。
- 堆:堆的封装和实现,参见上一节中堆初始化部分。
- 语言实现:语言中一些特殊功能的实现。
- 调试:实现调试功能的代码

系统调用

系统中断

现代的CPU常常可以在多种截然不同的特权级别下执行指令,分别为用户模式(UserMode)和内核模式(Kernel Mode),也被称为用户态内核态。由于有多种特权模式的存在,操作系统就可以让不同的代码运行在不同的模式上,以限制它们的权力,提高稳定性和安全性。普通应用程序运行在用户态的模式下,诸多操作将受到限制,这些操作包括访问硬件设备、开关中断、改变特权模式等。

系统调用是运行在内核态的,而应用程序基本都是运行在用户态的。

用户态的程序如何运行内核态的代码呢?
操作系统一般是通过中断(Interrupt)来从用户态切换到内核态。

什么是中断呢?
中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作转手去处理更加重要的事情。举一个例子,当你在编辑文本文件的时候,键盘上的键不断地被按下,CPU如何获知这一点的呢?当键盘上有键被按下时,键盘上的芯片发送一个信号给CPU,CPU接收到信号之后就知道键盘被按下了,然后再去询问键盘被按下的键是哪一个。 这样的信号就是一种中断。
中断的时机:cpu每执行完一个指令后回去检查相应的中断相关的寄存器,有中断了则执行相应的中断程序。

中断一般具有两个属性,一个称为中断号(从0开始),一个称为中断处理程序(Interrupt Service Routine, ISR)。
不同的中断具有不同的中断号,而同时一个中断处理程序一一对应一个中断号。在内核中,有一个数组称为中断向量表(Interrupt Vector Table),这个数组的第n项包含了指向第n号中断的中断处理程序的指针。
当中断到来时,CPU会暂停当前执行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成之后,CPU会继续执行之前的代码。一个简单的示意图如图12-2所示

image.png

linux下中断处理流程

  1. 触发中断, 即通过 int $0x80 的汇编代码来触发中断
  2. 切换堆栈
    • 用户态和内核态使用的是不同的栈,两者各自负责各自的函数调用,互不干扰。但在应用程序调用0x80号中断时,程序的执行流程从用户态切换到内核态,这时程序的当前栈必须也相应地从用户栈切换到内核栈。从中断处理函数中返回时,程序的当前栈还要从内核栈切换回用户栈
  3. 中断处理程序
image.png
image.png
image.png

系统调用

系统调用是应用程序(运行库也是应用程序的一部分)与操作系统内核之间的接口,它决定了应用程序是如何与内核打交道的。无论程序是直接进行系统调用,还是通过运行库,最终还是会到达系统调用这个层面上。

Windows系统是完全基于DLL机制的,它通过DLL对系统调用进行了包装,形成了所谓的Windows API。应用程序所能看到的Windows系统的最底层的接口就是Windows API,比如fread最终还是到了 ReadFile 这个API。于是Windows的程序相当于在运行库与系统调用之间又多了一层API,不过无论如何,API最终还是通过系统调用.

系统调用完成了应用程序和内核交流的工作,因此理论上只需要系统调用就可以完成一些程序,但是:
理论上,理论总是成立的。
事实上,包括Linux,大部分操作系统的系统调用都有两个特点:
- 使用不便。操作系统提供的系统调用接口往往过于原始,程序员须要了解很多与操作系统相关的细节。如果没有进行很好的包装,使用起来不方便。
- 各个操作系统之间系统调用不兼容。首先Windows系统和Linux系统之间的系统调用就基本上完全不同,虽然它们的内容很多都一样,但是定义和实现大不一样。即使是同系列的操作系统的系统调用都不一样,比如Linux和UNIX就不相同。

为了解决这个问题,第1章中的“万能法则”又可以发挥它的作用了。“解决计算机的问题可以通过增加层来实现”,于是运行库挺身而出,它作为系统调用与程序之间的一个抽象层可以保持着这样的特点:
- 使用简便。因为运行库本身就是语言级别的,它一般都设计相对比较友好。
- 形式统一。运行库有它的标准,叫做标准库,凡是所有遵循这个标准的运行库理论上都是相互兼容的,不会随着操作系统或编译器的变化而变化。

运行时库将不同的操作系统的系统调用包装为统一固定的接口,使得同样的代码,在不同的操作系统下都可以直接编译,并产生一致的效果。这就是源代码级上的可移植性。

但是运行库也有运行库的缺陷,比如C语言的运行库为了保证多个平台之间能够相互通用,于是它只能取各个平台之间功能的交集。比如Windows和Linux都支持文件读写,那么运行库就可以有文件读写的功能;但是Windows原生支持图形和用户交互系统,而Linux却不是原生支持的(通过XWindows),那么CRT就只能把这部分功能省去。因此,一旦程序用到了那些CRT之外的接口,程序就很难保持各个平台之间的兼容性了

image.png

参考连接

youzhonghui/MiniCRT: 俞甲子的MiniCRT代码,以及我将他的代码修改,移植到64位linux系统上的源码