链接装载库

一、内存、栈、堆

一般应用程序内存空间有如下区域:

  • 栈:由操作系统自动分配释放,存放函数的参数值、局部变量等的值,用于维护函数调用的上下文;
  • 堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收,用来容纳应用程序动态分配的内存区域;
  • 可执行文件映像:存储着可执行文件在内存中的映像,由装载器装载是将可执行文件的内存读取或映射到这里;
  • 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,如通常 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

编译链接过程

  1. 预编译(预编译器处理如 #include#define 等预编译指令,生成 .i 或 .ii 文件);
  2. 编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件);
  3. 汇编(汇编器把汇编码翻译成机器码,生成 .o 文件);
  4. 链接(连接器进行地址和空间分配、符号决议、重定位,生成 .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
2
3
4
Cmake_minimum_required (VERSION 3.10)
Project (MySharedLib)
Set (CMAKE_CXX_STANDARD 11)
Add_library (MySharedLib SHARED library. Cpp library. H)

Library. H:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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:

1
2
3
4
5
6
7
#include <iostream>
#include "library. H"

Void hello ()
{
    Std:: cout << "Hello, World!" << std:: endl;
}

So 共享库的使用

使用 VS Code 调用共享库,创建一个名为 TestSharedLib 的可执行项目:

CMakeLists. Txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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;
}

执行结果:

1
2
3
Hello, World!
1 + 2 = 3
1 + 2 + 3 = 6

Windows 应用程序入口函数

  • GUI(Graphical User Interface)应用,链接器选项:/SUBSYSTEM:WINDOWS
  • CUI(Console User Interface)应用,链接器选项:/SUBSYSTEM:CONSOLE

_tWinMain_tmain 函数声明:

1
2
3
4
5
6
7
8
9
10
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

用处

  • 扩展了应用程序的特性;
  • 简化了项目管理;
  • 有助于节省内存;
  • 促进了资源的共享;
  • 促进了本地化;
  • 有助于解决平台间的差异;
  • 可以用于特殊目的。

注意

  • 创建 DLL,事实上是在创建可供一个可执行模块调用的函数;
  • 当一个模块提供一个内存分配函数(malloc、new)的时候,它必须同时提供另一个内存释放函数(free、delete);
  • 在使用 C 和 C++ 混编的时候,要使用 extern “C” 修饰符;
  • 一个 DLL 可以导出函数、变量(避免导出)、C++ 类(导出导入需要同编译器,否则避免导出);
  • DLL 模块:cpp 文件中的 __declspec (dllexport) 写在 include 头文件之前;
  • 调用 DLL 的可执行模块:cpp 文件的 __declspec (dllimport) 之前不应该定义 MYLIBAPI。

加载 Windows 程序的搜索顺序

  1. 包含可执行文件的目录;
  2. Windows 的系统目录,可以通过 GetSystemDirectory 得到;
  3. 16 位的系统目录,即 Windows 目录中的 System 子目录;
  4. Windows 目录,可以通过 GetWindowsDirectory 得到;
  5. 进程的当前目录;
  6. PATH 环境变量中所列出的目录。

DLL 入口函数

DllMain 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
}

载入卸载库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 载入库
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 函数声明:

1
2
3
4
FARPROC GetProcAddress (
    HMODULE hInstDll,
    PCSTR pszSymbolName // 只能接受 ANSI 字符串,不能是 Unicode
);

DumpBin. Exe 查看 DLL 信息

在 VS 的开发人员命令提示符 使用 DumpBin. Exe 可查看 DLL 库的导出段(导出的变量、函数、类名的符号)、相对虚拟地址(RVA,relative virtual address)。如:

1
DUMPBIN -exports D:\mydll. Dll

DLL 库的编写(导出一个 DLL 模块)

DLL 库的编写(导出一个 DLL 模块) DLL 头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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 源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 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)

典型程序运行步骤

  1. 操作系统创建进程,把控制权交给程序的入口(往往是运行库中的某个入口函数);
  2. 入口函数对运行库和程序运行环境进行初始化(包括堆、I/O、线程、全局变量构造等等);
  3. 入口函数初始化后,调用 main 函数,正式开始执行程序主体部分;
  4. 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)

执行如下操作:

  1. 初始化和 OS 版本有关的全局变量;
  2. 初始化堆;
  3. 初始化 I/O;
  4. 获取命令行参数和环境变量;
  5. 初始化 C 库的一些数据;
  6. 调用 main 并记录返回值;
  7. 检查错误并将 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)。