- 链接装载库
- 内存、栈、堆
- 栈
- 堆
- “段错误(segment fault)” 或 “非法操作,该内存地址不能 read/write”
- 编译链接
- 各平台文件格式
- 编译链接过程
- 目标文件
- 目标文件格式
- 目标文件存储结构
- 链接的接口————符号
- Linux 的共享库(Shared Library)
- 命名
- 路径
- 环境变量
- so 共享库的编写
- so 共享库的使用(被可执行项目调用)
- Windows 应用程序入口函数
- Windows 的动态链接库(Dynamic-Link Library)
- 用处
- 注意
- 加载 Windows 程序的搜索顺序
- DLL 入口函数
- 载入卸载库
- 显示地链接到导出符号
- DumpBin.exe 查看 DLL 信息
- LoadLibrary 与 FreeLibrary 流程图
- LoadLibrary
- FreeLibrary
- DLL 库的编写(导出一个 DLL 模块)
- DLL 库的使用(运行时动态链接 DLL)
- 运行库(Runtime Library)
- 典型程序运行步骤
- glibc 入口
- MSVC CRT 入口
- C 语言运行库(CRT)
- C语言标准库(ANSI C)
- 内存、栈、堆
链接装载库
内存、栈、堆
一般应用程序内存空间有如下区域:
- 栈:由操作系统自动分配释放,存放函数的参数值、局部变量等的值,用于维护函数调用的上下文
- 堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收,用来容纳应用程序动态分配的内存区域
- 可执行文件映像:存储着可执行文件在内存中的映像,由装载器装载是将可执行文件的内存读取或映射到这里
- 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,如通常 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 共享库的编写
使用 CLion 编写共享库
创建一个名为 MySharedLib 的共享库
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)project(MySharedLib)set(CMAKE_CXX_STANDARD 11)add_library(MySharedLib SHARED library.cpp library.h)
library.h
#ifndef MYSHAREDLIB_LIBRARY_H#define MYSHAREDLIB_LIBRARY_H// 打印 Hello World!void hello();// 使用可变模版参数求和template <typename T>T sum(T t){return t;}template <typename T, typename ...Types>T sum(T first, Types ... rest){return first + sum<T>(rest...);}#endif
library.cpp
#include <iostream>#include "library.h"void hello() {std::cout << "Hello, World!" << std::endl;}
so 共享库的使用(被可执行项目调用)
使用 CLion 调用共享库
创建一个名为 TestSharedLib 的可执行项目
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)project(TestSharedLib)# C++11 编译set(CMAKE_CXX_STANDARD 11)# 头文件路径set(INC_DIR /home/xx/code/clion/MySharedLib)# 库文件路径set(LIB_DIR /home/xx/code/clion/MySharedLib/cmake-build-debug)include_directories(${INC_DIR})link_directories(${LIB_DIR})link_libraries(MySharedLib)add_executable(TestSharedLib main.cpp)# 链接 MySharedLib 库target_link_libraries(TestSharedLib MySharedLib)
main.cpp
#include <iostream>#include "library.h"using std::cout;using std::endl;int main() {hello();cout << "1 + 2 = " << sum(1,2) << endl;cout << "1 + 2 + 3 = " << sum(1,2,3) << endl;return 0;}
执行结果
Hello, World!1 + 2 = 31 + 2 + 3 = 6
Windows 应用程序入口函数
- GUI(Graphical User Interface)应用,链接器选项:
/SUBSYSTEM:WINDOWS - CUI(Console User Interface)应用,链接器选项:
/SUBSYSTEM:CONSOLE
_tWinMain 与 _tmain 函数声明
Int WINAPI _tWinMain(HINSTANCE hInstanceExe,HINSTANCE,PTSTR pszCmdLine,int nCmdShow);int _tmain(int argc,TCHAR *argv[],TCHAR *envp[]);
| 应用程序类型 | 入口点函数 | 嵌入可执行文件的启动函数 |
|---|---|---|
| 处理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)
知识点来自《Windows核心编程(第五版)》
用处
- 扩展了应用程序的特性
- 简化了项目管理
- 有助于节省内存
- 促进了资源的共享
- 促进了本地化
- 有助于解决平台间的差异
- 可以用于特殊目的
注意
- 创建 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 函数
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){switch(fdwReason){case DLL_PROCESS_ATTACH:// 第一次将一个DLL映射到进程地址空间时调用// The DLL is being mapped into the process' address space.break;case DLL_THREAD_ATTACH:// 当进程创建一个线程的时候,用于告诉DLL执行与线程相关的初始化(非主线程执行)// A thread is bing created.break;case DLL_THREAD_DETACH:// 系统调用 ExitThread 线程退出前,即将终止的线程通过告诉DLL执行与线程相关的清理// A thread is exiting cleanly.break;case DLL_PROCESS_DETACH:// 将一个DLL从进程的地址空间时调用// The DLL is being unmapped from the process' address space.break;}return (TRUE); // Used only for DLL_PROCESS_ATTACH}
载入卸载库
LoadLibrary、LoadLibraryExA、LoadPackagedLibrary、FreeLibrary、FreeLibraryAndExitThread 函数声明
// 载入库HMODULE WINAPI LoadLibrary(_In_ LPCTSTR lpFileName);HMODULE LoadLibraryExA(LPCSTR lpLibFileName,HANDLE hFile,DWORD dwFlags);// 若要在通用 Windows 平台(UWP)应用中加载 Win32 DLL,需要调用 LoadPackagedLibrary,而不是 LoadLibrary 或 LoadLibraryExHMODULE LoadPackagedLibrary(LPCWSTR lpwLibFileName,DWORD Reserved);// 卸载库BOOL WINAPI FreeLibrary(_In_ HMODULE hModule);// 卸载库和退出线程VOID WINAPI FreeLibraryAndExitThread(_In_ HMODULE hModule,_In_ DWORD dwExitCode);
显示地链接到导出符号
GetProcAddress 函数声明
FARPROC GetProcAddress(HMODULE hInstDll,PCSTR pszSymbolName // 只能接受 ANSI 字符串,不能是 Unicode);
DumpBin.exe 查看 DLL 信息
在 VS 的开发人员命令提示符 使用 DumpBin.exe 可查看 DLL 库的导出段(导出的变量、函数、类名的符号)、相对虚拟地址(RVA,relative virtual address)。如:
DUMPBIN -exports D:\mydll.dll
LoadLibrary 与 FreeLibrary 流程图
LoadLibrary 与 FreeLibrary 流程图
LoadLibrary

FreeLibrary

DLL 库的编写(导出一个 DLL 模块)
DLL 库的编写(导出一个 DLL 模块) DLL 头文件
// MyLib.h#ifdef MYLIBAPI// MYLIBAPI 应该在全部 DLL 源文件的 include "Mylib.h" 之前被定义// 全部函数/变量正在被导出#else// 这个头文件被一个exe源代码模块包含,意味着全部函数/变量被导入#define MYLIBAPI extern "C" __declspec(dllimport)#endif// 这里定义任何的数据结构和符号// 定义导出的变量(避免导出变量)MYLIBAPI int g_nResult;// 定义导出函数原型MYLIBAPI int Add(int nLeft, int nRight);
DLL 源文件
// MyLibFile1.cpp// 包含标准Windows和C运行时头文件#include <windows.h>// DLL源码文件导出的函数和变量#define MYLIBAPI extern "C" __declspec(dllexport)// 包含导出的数据结构、符号、函数、变量#include "MyLib.h"// 将此DLL源代码文件的代码放在此处int g_nResult;int Add(int nLeft, int nRight){g_nResult = nLeft + nRight;return g_nResult;}
DLL 库的使用(运行时动态链接 DLL)
DLL 库的使用(运行时动态链接 DLL)
// A simple program that uses LoadLibrary and// GetProcAddress to access myPuts from Myputs.dll.#include <windows.h>#include <stdio.h>typedef int (__cdecl *MYPROC)(LPWSTR);int main( void ){HINSTANCE hinstLib;MYPROC ProcAdd;BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;// Get a handle to the DLL module.hinstLib = LoadLibrary(TEXT("MyPuts.dll"));// If the handle is valid, try to get the function address.if (hinstLib != NULL){ProcAdd = (MYPROC) GetProcAddress(hinstLib, "myPuts");// If the function address is valid, call the function.if (NULL != ProcAdd){fRunTimeLinkSuccess = TRUE;(ProcAdd) (L"Message sent to the DLL function\n");}// Free the DLL module.fFreeResult = FreeLibrary(hinstLib);}// If unable to call the DLL function, use an alternative.if (! fRunTimeLinkSuccess)printf("Message printed from executable\n");return 0;}
运行库(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)
