链接装载库
一、内存、栈、堆
一般应用程序内存空间有如下区域:
- 栈:由操作系统自动分配释放,存放函数的参数值、局部变量等的值,用于维护函数调用的上下文;
- 堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收,用来容纳应用程序动态分配的内存区域;
- 可执行文件映像:存储着可执行文件在内存中的映像,由装载器装载是将可执行文件的内存读取或映射到这里;
- 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,如通常 C 语言讲无效指针赋值为 0;(NULL),因此 0 地址正常情况下不可能有效的访问数据。
栈
栈保存了一个函数调用所需要的维护信息,常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),一般包含以下几方面:
- 函数的返回地址和参数;
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量;
- 保存上下文:包括函数调用前后需要保持不变的寄存器。
堆
堆分配算法:
- 空闲链表(Free List);
- 位图(Bitmap);
- 对象池。
“段错误(segment fault)” 或 “非法操作,该内存地址不能 read/write”
典型的非法指针解引用造成的错误。当指针指向一个不允许读写的内存地址,而程序却试图利用指针来读或写该地址时,会出现这个错误。
普遍原因:
- 将指针初始化为 NULL,之后没有给它一个合理的值就开始使用指针;
- 没用初始化栈中的指针,指针的值一般会是随机数,之后就直接开始使用指针。
编译链接
各平台文件格式
平台 | 可执行文件 | 目标文件 | 动态库/共享对象 | 静态库 |
Windows | exe | obj | dll | lib |
Unix/Linux | ELF、out | o | so | a |
Mac | Mach-O | o | dylib、tbd、framework | a、framework |
编译链接过程
- 预编译(预编译器处理如
#include
、#define
等预编译指令,生成.i
或.ii
文件); - 编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成
.s
文件); - 汇编(汇编器把汇编码翻译成机器码,生成
.o
文件); - 链接(连接器进行地址和空间分配、符号决议、重定位,生成
.out
文件)。
现在版本 GCC 把预编译和编译合成一步,预编译编译程序 cc1、汇编器 as、连接器 ld。
MSVC 编译环境,编译器 cl、连接器 link、可执行文件查看器 dumpbin。
目标文件
编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。
可执行文件(Windows 的
.exe
和 Linux 的ELF
)、动态链接库(Windows 的.dll
和 Linux 的.so
)、静态链接库(Windows 的.lib
和 Linux 的.a
)都是按照可执行文件格式存储(Windows 按照 PE-COFF,Linux 按照 ELF)。
目标文件格式
- Windows 的 PE(Portable Executable),或称为 PE-COFF,
.obj
格式; - Linux 的 ELF(Executable Linkable Format),
.o
格式; - Intel/Microsoft 的 OMF(Object Module Format);
- Unix 的
a.out
格式; - MS-DOS 的
.COM
格式。
PE 和 ELF 都是 COFF(Common File Format)的变种。
目标文件存储结构
段 | 功能 |
File Header | 文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等) |
.text section | 代码段,执行语句编译成的机器代码 |
.data section | 数据段,已初始化的全局变量和局部静态变量 |
.bss section | BSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间) |
.rodata section | 只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量 |
.comment section | 注释信息段,存放编译器版本信息 |
.note. GNU-stack section | 堆栈提示段 |
链接的接口 —— 符号
在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。
如下符号表(Symbol Table):
Symbol(符号) | Symbol Value(地址) |
main | 0x100 |
Add | 0x123 |
… | … |
Linux 的共享库(Shared Library)
Linux 下的共享库就是普通的 ELF 共享对象。
共享库版本更新应该保证二进制接口 ABI(Application Binary Interface)的兼容
命名
libname. So. X. Y. Z
- X:主版本号,不同主版本号的库之间不兼容,需要重新编译;
- Y:次版本号,高版本号向后兼容低版本号;
- Z:发布版本号,不对接口进行更改,完全兼容。
路径
大部分包括 Linux 在内的开源系统遵循 FHS(File Hierarchy Standard)的标准,这标准规定了系统文件如何存放,包括各个目录结构、组织和作用。
/lib
:存放系统最关键和最基础的共享库,如动态链接器、C 语言运行库、数学库等;/usr/lib
:存放非系统运行时所需要的关键性的库,主要是开发库;/usr/local/lib
:存放跟操作系统本身并不十分相关的库,主要是一些第三方应用程序的库。
动态链接器会在
/lib
、/usr/lib
和由/etc/ld. So. Conf
配置文件指定的,目录中查找共享库。
环境变量
LD_LIBRARY_PATH
:临时改变某个应用程序的共享库查找路径,而不会影响其他应用程序;LD_PRELOAD
:指定预先装载的一些共享库甚至是目标文件;LD_DEBUG
:打开动态链接器的调试功能。
So 共享库的编写
使用 VS Code 编写共享库,创建一个名为 MySharedLib 的共享库:
CMakeLists. Txt:
1 | Cmake_minimum_required (VERSION 3.10) |
Library. H:
1 |
|
Library. Cpp:
1 |
|
So 共享库的使用
使用 VS Code 调用共享库,创建一个名为 TestSharedLib 的可执行项目:
CMakeLists. Txt:
1 | Cmake_minimum_required (VERSION 3.10) |
Main. Cpp:
1 |
|
执行结果:
1 | Hello, World! |
Windows 应用程序入口函数
- GUI(Graphical User Interface)应用,链接器选项:
/SUBSYSTEM:WINDOWS
- CUI(Console User Interface)应用,链接器选项:
/SUBSYSTEM:CONSOLE
_tWinMain
与 _tmain
函数声明:
1 | Int WINAPI _tWinMain ( |
应用程序类型 | 入口点函数 | 嵌入可执行文件的启动函数 |
处理 ANSI 字符(串)的 GUI 应用程序 | _tWinMain (WinMain) |
WinMainCRTSartup |
处理 Unicode 字符(串)的 GUI 应用程序 | _tWinMain (wWinMain) |
wWinMainCRTSartup |
处理 ANSI 字符(串)的 CUI 应用程序 | _tmain (Main) |
mainCRTSartup |
处理 Unicode 字符(串)的 CUI 应用程序 | _tmain (wMain) |
wmainCRTSartup |
动态链接库(Dynamic-Link Library) | DllMain |
_DllMainCRTStartup |
Windows 的动态链接库(Dynamic-Link Library)
用处
- 扩展了应用程序的特性;
- 简化了项目管理;
- 有助于节省内存;
- 促进了资源的共享;
- 促进了本地化;
- 有助于解决平台间的差异;
- 可以用于特殊目的。
注意
- 创建 DLL,事实上是在创建可供一个可执行模块调用的函数;
- 当一个模块提供一个内存分配函数(malloc、new)的时候,它必须同时提供另一个内存释放函数(free、delete);
- 在使用 C 和 C++ 混编的时候,要使用 extern “C” 修饰符;
- 一个 DLL 可以导出函数、变量(避免导出)、C++ 类(导出导入需要同编译器,否则避免导出);
- DLL 模块:cpp 文件中的
__declspec (dllexport)
写在 include 头文件之前; - 调用 DLL 的可执行模块:cpp 文件的
__declspec (dllimport)
之前不应该定义 MYLIBAPI。
加载 Windows 程序的搜索顺序
- 包含可执行文件的目录;
- Windows 的系统目录,可以通过 GetSystemDirectory 得到;
- 16 位的系统目录,即 Windows 目录中的 System 子目录;
- Windows 目录,可以通过 GetWindowsDirectory 得到;
- 进程的当前目录;
- PATH 环境变量中所列出的目录。
DLL 入口函数
DllMain 函数:
1 | BOOL WINAPI DllMain (HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) |
载入卸载库
1 | // 载入库 |
显示地链接到导出符号
GetProcAddress 函数声明:
1 | FARPROC GetProcAddress ( |
DumpBin. Exe 查看 DLL 信息
在 VS 的开发人员命令提示符
使用 DumpBin. Exe
可查看 DLL 库的导出段(导出的变量、函数、类名的符号)、相对虚拟地址(RVA,relative virtual address)。如:
1 | DUMPBIN -exports D:\mydll. Dll |
DLL 库的编写(导出一个 DLL 模块)
DLL 库的编写(导出一个 DLL 模块) DLL 头文件:
1 | // MyLib. H |
DLL 源文件:
1 | // MyLibFile1. Cpp |
DLL 库的使用(运行时动态链接 DLL)
DLL 库的使用(运行时动态链接 DLL):
1 | // A simple program that uses LoadLibrary and |
运行库(Runtime Library)
典型程序运行步骤
- 操作系统创建进程,把控制权交给程序的入口(往往是运行库中的某个入口函数);
- 入口函数对运行库和程序运行环境进行初始化(包括堆、I/O、线程、全局变量构造等等);
- 入口函数初始化后,调用 main 函数,正式开始执行程序主体部分;
- Main 函数执行完毕后,返回到入口函数进行清理工作(包括全局变量析构、堆销毁、关闭 I/O 等),然后进行系统调用结束进程。
一个程序的 I/O 指代程序与外界的交互,包括文件、管程、网络、命令行、信号等。更广义地讲,I/O 指代操作系统理解为 “文件” 的事物。
Glibc 入口
_start -> __libc_start_main -> exit -> _exit
其中 main (argc, argv, __environ)
函数在 __libc_start_main
里执行。
MSVC CRT 入口
int mainCRTStartup (void)
执行如下操作:
- 初始化和 OS 版本有关的全局变量;
- 初始化堆;
- 初始化 I/O;
- 获取命令行参数和环境变量;
- 初始化 C 库的一些数据;
- 调用 main 并记录返回值;
- 检查错误并将 main 的返回值返回。
C 语言运行库(CRT)
大致包含如下功能:
- 启动与退出:包括入口函数及入口函数所依赖的其他函数等;
- 标准函数:有 C 语言标准规定的 C 语言标准库所拥有的函数实现;
- I/O:I/O 功能的封装和实现;
- 堆:堆的封装和实现;
- 语言实现:语言中一些特殊功能的实现;
- 调试:实现调试功能的代码。
C 语言标准库(ANSI C)
包含:
- 标准输入输出(stdio. H);
- 文件操作(stdio. H);
- 字符操作(ctype. H);
- 字符串操作(string. H);
- 数学函数(math. H);
- 资源管理(stdlib. H);
- 格式转换(stdlib. H);
- 时间/日期(time. H);
- 断言(assert. H);
- 各种类型上的常数(limits. H & float. H);
- 变长参数(stdarg. H);
- 非局部跳转(setjmp. H)。