链接装载库
本节部分知识点来自《程序员的自我修养——链接装载库》
内存、栈、堆
一般应用程序内存空间有如下区域:
- 栈:由操作系统自动分配释放,存放函数的参数值、局部变量等的值,用于维护函数调用的上下文
- 堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收,用来容纳应用程序动态分配的内存区域
- 可执行文件映像:存储着可执行文件在内存中的映像,由装载器装载是将可执行文件的内存读取或映射到这里
- 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,如通常 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 = 3
1 + 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 或 LoadLibraryEx
HMODULE 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)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。