本文是对C++应用程序在Windows下的编译、链接的深入理解和分析,文章的目录如下:
我们先看第一章概述部分。
1概述
1.1编译工具简介
cl.exe是windows平台下的编译器,link.exe是Windows平台下的链接器,C++源代码在使用它们编译、链接后,生成的可执行文件能够在windows操作系统下运行。cl.exe和link.exe集成在Visual Studio中,随着开发工具Visual Studio的安装,它们也被安装到与VC相关的目录下。
使用该编译器的方式有两种,一种是在Visual Studio开发环境中,直接点击命令按钮,通过Visual Studio启动编译器;另外一种方式是在命令行窗口中通过c l命令编译C++源代码文件。
在集成开发环境Visual Studio中,已经设定好了c l命令的各种默认参数,当使用Visual Studio编译C++源代码的时候,最终会调用到这个编译工具,并且使用这些事先设定好的默认参数。
在安装Visual Studio的时候,安装程序在命令行工具“Visual Studio 2008 Command Prompt”中设定了编译器(cl.exe)和链接器(link.exe)需要的各种参数和变量,因此,在“Visual Studio 2008 Command Prompt”工具的命令行窗口中,可以使用c l命令编译C++源代码。Visual Studio 2008 Command Prompt工具的路径是:开始-》所有程序-》Visual Studio-》Visual Studio Tools-》Visual Studio 2008 Command Prompt。
在编译C++源代码的时候,编译器需要使用到三个环境变量,它们分别是:
- Path,用于设定编译器cl.exe的路径,以及该编译器所依赖的一个动态链接库(mspdb80.dll)所在的路径。设定了这个环境变量以后,就可以在命令行窗口直接键入c l命令,而不需要把当前目录定位到cl.exe的安装目录;比如:“C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin;C:\Program Files (x86)\Microsoft Visual Studio 9.0\Team Tools\Performance Tools”。前一个地址指定cl.exe所在的路径,后一个地址指定mspdb80.dll的路径。
- Include,用于设定C运行库头文件的路径。设定了这个环境变量以后,在C++源代码中,就可以使用“#include <stdio.h>”的形式引入运行库的头文件;如果不设定这个变量,就必须使用“#include<C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include\stdio.h>”的形式引入运行库的头文件,否则在编译的时候,编译器就无法找到这些要被引入的头文件。比如:“C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include”,该路径指定了C运行库头文件的位置。
- Lib,用于设定C运行库目标文件的路径。链接器使用该环境变量定位C运行库的目标文件。比如:“C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\lib”,该路径指定了C运行库目标文件的位置。
如果我们在系统环境变量中设定了这三个环境变量,那么就可以在普通的命令行窗口中使用cl命令编译C++源代码,而不是使用Visual Studio的集成工具“Visual Studio 2008 Command Prompt”。
1.2应用程序示例
1.2.1C++源代码
本文将以如下应用程序示例展开论述,通过对应用程序的编译,链接过程的介绍,着重讲解PE文件的数据格式,以及在应用程序加载的过程中,操作系统是如何进行“重定基地址”,以及执行各个DLL之间的“动态链接”。
示例应用程序各模块之间的调用关系如下图所示:
在示例应用程序中,各源代码文件的说明如下表:
序号 |
文件名称 |
描述 |
1 |
DemoDef.h |
定义函数的导入,导出;定义全局变量和全局函数 |
2 |
DemoMath.h |
数学操作类的定义 |
3 |
DemoOutPut.h |
信息输出类的定义 |
4 |
DemoMath.cpp |
数学操作类的实现,全局函数的定义,全局变量的定义 |
5 |
DemoOutPut.cpp |
信息输出类的实现 |
6 |
main.cpp |
主函数 |
示例应用程序的源代码如下:
------------------------------------main.cpp-------------------------------------------- #include "DemoDef.h" #include "DemoMath.h" #include <iostream> using namespace std; int nGlobalData = 5; int main() { DemoMath objMath; objMath.AddData(10,15); objMath.SubData(nGlobalData,3); objMath.DivData(10,0); objMath.DivData(10,nGlobalData); objMath.Area(2.5); int ntimes = GetOperTimes(); cout << "操作次数为:" << ntimes << endl; //用于停止命令行 int k = 0; cin >> k; } ----------------------------------------DemoDef.h------------------------------------ #ifndef _DemoDef_H #define _DemoDef_H #include <stdio.h> //定义函数的导入,导出 #ifdef DEMODLL_EXPORTS #define DemoDLL_Export _declspec(dllexport) #else #define DemoDLL_Export _declspec(dllimport) #endif
//文件作用域中的符号常量,将要执行常量折叠 const double PI = 3.14;
//声明全局变量,记录操作的次数 extern int nOperTimes;
//声明全局函数,返回操作的次数 int DemoDLL_Export GetOperTimes(); #endif -------------------------------------DemoMath.h------------------------------------- #ifndef _DemoMath_H #define _DemoMatn_H
#include "DemoDef.h"
class DemoOutPut;
class DemoDLL_Export DemoMath { public: DemoMath(); ~DemoMath();
void AddData(double a,double b); void SubData(double a,double b); void MulData(double a,double b); void DivData(double a,double b); void Area(double r);
private: DemoOutPut * m_pOutPut; }; #endif
--------------------------------------------------DemoOutPut.h----------------------------------------- #ifndef _DemoOutPut_H #define _DemoOutPut_H
//执行信息输出 class DemoOutPut { public: DemoOutPut(); ~DemoOutPut();
//输出数值 void OutPutInfo(double a); //输出字符串 void OutPutInfo(const char* pStr); }; #endif
-----------------------------------------------------DemoMath.cpp----------------------------------------- #include "DemoMath.h" #include "DemoOutPut.h"
//全局变量的定义 int nOperTimes = 0;
//全局函数的定义 int GetOperTimes() { return nOperTimes; }
//类方法的实现 DemoMath::DemoMath() { m_pOutPut = new DemoOutPut(); }
DemoMath::~DemoMath() { if(m_pOutPut != NULL) { delete m_pOutPut; m_pOutPut = NULL; } }
void DemoMath::AddData(double a, double b) { nOperTimes++; m_pOutPut->OutPutInfo(a + b); }
void DemoMath::SubData(double a, double b) { nOperTimes++; m_pOutPut->OutPutInfo(a - b); }
void DemoMath::MulData(double a, double b) { nOperTimes++; m_pOutPut->OutPutInfo(a * b); }
void DemoMath::DivData(double a, double b) { if (b == 0) { m_pOutPut->OutPutInfo("除数不能为零"); return; }
nOperTimes++; m_pOutPut->OutPutInfo(a / b); }
void DemoMath::Area(double r) { nOperTimes++; m_pOutPut->OutPutInfo( r * r * PI); }
---------------------------------------------------------DemoOutPut.cpp--------------------------------------------- #include <iostream> #include "DemoOutPut.h"
DemoOutPut::DemoOutPut() { }
DemoOutPut::~DemoOutPut() { }
void DemoOutPut::OutPutInfo(double a) { std::cout << "计算的结果为:" << a << std::endl; }
void DemoOutPut::OutPutInfo(const char *pStr) { std::cout << pStr << std::endl; } |
1.2.2Visual Studio对C++源代码的支持
在编写C++源代码的时候,如果要使用一个类库,那么就必须引入这个类库的头文件,就必须知道这个类型头文件的具体路径。在上面的代码示例中,使用了“#include <stdio.h>”这种形式引入了一个C运行库的头文件。在这个引用中,我们没有设定该头文件的具体路径,也没有在其他位置设定该头文件的具体路径,但是集成开发环境Visual Studio能够找到该文件的具体位置。具体原因是这样的:在安装Visual Studio的时候,安装程序已经在Visual Studio中设定了C运行库头文件的具体位置。通过菜单“Tool-Options->Projects and Solutions->VC++Directories”可以查看到这些事先设定的信息。具体情况如下图:
在上图中,通过下拉窗口“Show directories for”,可以选择要设定的路径的类型,包括:头文件的路径(Include files),lib文件的路径(Library files),源代码文件(Source files)的路径等。
在头文件路径的设定中,一共设定了四类头文件的路径,分别是C运行库头文件的路径,MFC类库头文件的路径,Win32API开发相关的头文件路径,以及与FrameWork相关的头文件的路径。
除了系统事先设定好的各种路径外,我们也可以在该窗口中设定我们需要的各种其他路径。
1.3C++源代码的编译过程
1.3.1编译过程概述
在编译C++源代码的时候,整个编译过程可以划分为两个阶段,分别是编译阶段和链接阶段。在编译阶段,以程序员编写的C++源代码(头文件+源文件)为输入,经过编译器的处理后,输出COFF格式的二进制目标文件;在链接阶段,以编译阶段输出的目标文件为输入,经过链接器的链接,输出PE格式的可执行文件。整个编译的过程如下图所示:
编译阶段又可以进一步细分为三个子阶段,分别是:预编译,编译和汇编。在每一个子阶段中,都会对应不同的工作内容,以及输出不同的输出物。
由于程序员是在C/C++运行库的基础上开发出来的C++应用程序,所以在链接阶段,除了要将编译阶段输出的目标文件进行链接外,还要加入对C/C++运行库中相关目标文件的链接。这种链接分为两种情况。一种情况是:由于C++源代码中显式地调用了C/C++运行库中的函数而引起的链接。例如:在C++源代码中调用了C/C++运行库中的函数:printf(),那么在链接的时候,就需要把printf()所在的目标文件也链接进来。另外一种情况是隐式地,由链接器自动完成。在C++应用程序运行的时候,它必须要得到C/C++运行库的支持,因此在链接的时候,那些支持C++应用程序运行的库文件也被链接器自动地链接过来。无论哪种情况,C/C++运行库都必须被链接到C++应用程序中。
在命令行窗口中,可以使用c l命令对C++源代码进行编译。在编译的时候,可以设定不同的编译选项,进而获得不同的输出结果。比如:可以一步完成编译工作,直接获得PE格式的可执行文件。在这种情况下,cl.exe在完成编译后,会自动调用link.exe执行链接工作。也可以通过分阶段编译的方式获得不同阶段的编译结果,通过设定不同的c l命令选项,可以将编译过程细分。比如:/C命令表示只编译,不链接,通过这个命令就可以获得目标文件;/P命令表示只执行预编译,通过它可以查看预编译的结果;/Fa命令表示执行汇编操作,通过它可以获得汇编语言格式的程序文件。
在C++源代码的编译过程中,各个步骤的详细描述如下表所示:
序号 |
步骤 |
输入 |
输出 |
描述 |
C l命令 |
1 |
预编译 |
C++源文件 |
.i文件 |
输出经过预处理后的文件。 |
Cl /P xxx.cpp |
2 |
编译 |
C++源文件 |
.asm文件 |
输出汇编文件。 |
Cl /Fa xxx.cpp |
3 |
汇编 |
C++源文件 |
.obj文件 |
输出目标文件 |
Cl /C xxx.cpp |
4 |
链接 |
.obj文件 |
.exe文件 |
输出可执行文件 |
Cl xxx.obj |
1.3.2编译阶段
1.3.2.1概述
每一种高级编程语言都有它自己的编译器,在特定的操作系统平台上,编译器为该编程语言提供运行库的支持,并且将该编程语言编写的源文件编译成目标文件。
通过提供C/C++运行库的方式,Cl编译器支持C/C++应用程序的开发。C/C++运行库是由编译器厂商提供的,每支持在一个操作系统系下的编译,编译器就需要提供一个能够在该操作系统下运行的C/C++运行库。通过对操作系统API的封装,C/C++运行库实现了C/C++标准库的接口。由于标准库的接口是统一的,原则上来说,使用C++语言开发出来的应用程序是可以运行在不同操作系统平台上的。只需要针对该操作系统实现其运行库。
不同的CPU硬件可能会要求不同的指令格式。编译器在将高级语言翻译成机器语言的时候,是依赖于计算机系统硬件的。根据不同的硬件,会产生不同的指令格式。编译器屏蔽了计算机系统硬件的细节。
对上支持高级语言的程序编写工作,对下封装计算机系统硬件的细节,编译器负责将高级语言编写的源程序翻译成底层计算机系统硬件能够识别的二进制机器代码,并将这些二进制机器代码以统一的格式输出,这个文件格式就是COFF格式。
通过产生统一格式的COFF文件,使编译和链接能够互相隔离。也就是说,链接器的实现不会依赖具体的编译器。链接器只关注COFF格式的目标文件,只要目标文件的格式统一,那么链接器就可以链接由不同编译器编译出来的目标文件。
1.3.2.2预编译
在预编译阶段,主要是处理那些源代码文件中以“#”开头的预编译指令,如:“#include”,“#define”等,主要的处理规则描述如下:
- 将所有的“#define”指令删除,并且将宏定义展开;
- 处理所有的条件编译指令;
- 处理#include预编译指令,将被包含的头文件插入到预编译指令的位置。这可能是一个递归操作,如果被包含的头文件中又包含其他头文件;
- 删除所有的注释;
- 添加行号和文件标识;
- 保留所有的#program编译器指令,后续的编译步骤中要用到该指令。
经过预编译的处理以后,头文件被合并到源文件中,并且所有的宏定义都被展开。
示例一:对源文件“DemoOutPut.cpp”进行预编译操作,命令格式如下:
Cl /P DemoOutPut.cpp |
执行预编译以后,将会输出“demooutput.i”文件,该文件的部分内容如下:
#line 2 "demooutput.cpp" #line 1 "e:\\demo\\DemoOutPut.h" class DemoOutPut { public: DemoOutPut(); ~DemoOutPut(); void OutPutInfo(double a); void OutPutInfo(const char* pStr); }; #line 18 "e:\\demo\\DemoOutPut.h" #line 3 "demooutput.cpp" DemoOutPut::DemoOutPut() { } DemoOutPut::~DemoOutPut() { } void DemoOutPut::OutPutInfo(double a) { std::cout << "计算的结果为:" << a << std::endl; } void DemoOutPut::OutPutInfo(const char *pStr) { std::cout << pStr << std::endl; } |
在“demooutput.i”文件中,除了加入了行号信息外,类DemoOutPut的头文件和源文件已经合并到了一起。在编写C++源代码的时候,如果我们无法确定宏定义是否正确,那么就可以输出“.i”文件,进而确定问题。
1.3.2.3编译
以预编译的输出为输入,将C++源代码翻译成计算机系统应将能够识别的二进制机器指令,并将编译的输出结果存储在COFF格式的目标文件中。在编译的中间过程中,还可以通过c l命令选择性地输出汇编语言格式的中间文件。
编译器在编译的时候,一般会分为如下步骤,具体情况如下表描述:
序号 |
步骤 |
描述 |
1 |
词法分析 |
扫描C++源代码,识别各种符号。这些被识别的符号包括:C++系统关键字,函数名称,变量名称,字面值常量,以及特殊字符。函数名称,变量名称将被保存到符号表中,字面值常量将被保存到文字表中。 |
2 |
语法分析 |
将词法分析阶段产生的各种符号进行语法分析,产生语法树。每个语法树的节点都是一个表达式。 |
3 |
语义分析 |
此阶段开始分析C++语句的真正意义。编译器只能进行静态语义分析,包括:声明和类型的匹配,类型转换等。经过语义分析,语法树的表达式都被标识了类型。 |
4 |
源代码级优化 |
执行源代码级别的优化。比如:表达式3+8会被求值成11。 将语法树转换成中间代码,它是语法树的顺序表达。这个中间代码已经非常接近目标代码了,但是它和目标机器以及运行时环境是无关的。比如:不包含数据的尺寸,变量的地址,寄存器的名称等。 中间代码将编译器划分成两部分,第一部分负责产生与机器无关的中间代码;第二部分将中间代码转化成目标机器代码。 |
5 |
目标代码生成及优化 |
将中间语言代码转化成目标机器相关的机器代码。同时执行一些优化。 |
1.3.2.4COFF文件中的段种类
在执行编译的时候,编译器以“.cpp”文件为单位,对于每一个“.cpp”文件,编译器都会输出一个目标文件。在COFF格式的目标文件中,按照二进制文件内容的功能和属性的不同,会将文件内容划分成不同的段。COFF文件所包含的段种类如下图所示:
各个主要段的详细信息描述如下表:
序号 |
段名 |
描述 |
1 |
.text |
在该段中包含C++程序的源代码,这些源代码已经被编译成计算机系统硬件能够识别的二进制指令。每一个二进制指令都必须对应一个虚拟内存地址 |
2 |
.data |
已初始化的全局变量,静态变量存储在该段中 |
3 |
.bss |
未初始化的全局变量存储在该段中 |
4 |
.rdata |
只读的数据存储在该段中 |
5 |
.debug$S |
包含与调试符号相关的调试信息 |
6 |
.debug$T |
包含与类型相关的调试信息 |
7 |
.drectve |
包含链接指示信息,如采用哪个版本的运行库,以及函数的导出等。 |
85 |
重定位表 |
在该段中存储着属于其他段的重定位信息。在编译阶段,某些二进制指令的虚拟内存地址是暂时无法确定的,在重定位段将会记录这些无法确定虚拟内存地址的位置。在链接阶段,将使用这些重定位信息。在重定位段中,主要的信息字段包括:需要重定位的位置,重定位地址的类型。对应的符号表索引等 |
9 |
行号表 |
在行号表中存储的信息描述了二进制代码和C++源代码之间的对应关系,应用于程序调试。 |
10 |
符号表 |
在编译的时候,函数名称,变量名称都会被当作符号来处理。编译器将C++源代码中出现的符号统一地存储在符号表中。链接阶段需要使用符号表中的信息。 |
11 |
字符串表 |
字符串表用于辅助符号表。如果符号表中符号名的长度超过8个字节,那么这个名称将被保存到符号表中。而在符号表中,符号名称的位置保存了字符串表中相关项的地址。 |
1.3.3链接阶段
1.3.3.1链接的目标
在C++程序的开发过程中,程序代码是以“.cpp“文件为单位来组织的。在各个文件之间又会存在调用关系。比如:A.CPP文件调用B.CPP文件中的函数。
在C++程序的编译阶段,编译器是以“.CPP”文件为单位进行编译的。也就是说,对于每一个“.CPP”文件,都会生成一个“.obj”目标文件。在目标文件中,对于每一条指令或者指令要操作的数据,都应该生成一个虚拟内存的地址。如果一个目标文件中要使用的函数或者数据被定义在另外一个目标文件中,如:在A.obj文件中调用了B.obj文件中定义的函数。在将A.CPP生成A.obj的过程中,是无法马上确定该被调用函数的地址的。因为该函数的地址记录在B.obj文件中。
链接器执行链接的过程就是将多个目标文件合并在一起,形成可执行文件的过程。在形成可执行文件的过程中,链接器需要将在编译阶段无法确定的被调用符号(函数,变量)的虚拟内存地址确定下来。这就是链接的主要目标。
注:关于每个指令的虚拟内存地址,在目标文件中,该地址以相对于文件某个位置的偏移来表示;直到PE文件生成的时候,才会将这些偏移值转换成虚拟内存地址。
1.3.3.2链接的类型
首先看一个示例,在使用Visual Studio开发C++应用程序的时候,首先会建立一个解决方案,然后在解决方案中包含若干个项目,这些C++源代码是以项目的形式组织在一起的。它们的关系如下图所示:
解决方案“DemoDLL”中包含了两个项目,分别是:“DemoDLL”,“DemoExe”。在项目“DemoDLL”中包含了两个源文件,分别是:“DemoMath.cpp”,“DemoOutPut.cpp”。项目“DemoExe”引用了项目“DemoDLL”中的函数。在编译的时候,项目“DemoDLL”被编译成了动态链接库;项目“DemoExe”被编译成了可执行文件。在编译这两个项目的时候,C运行库和C++运行库也被链接了进来。
由上一节的描述可以得知,链接的主要目的是确定被调用函数的地址。即:使主调函数知道被调用函数的位置。在处理这个问题的时候,可以采用不同的方式和方法,因此也就有了不同的链接类型,具体的链接分类如下图所示:
链接可以被分为静态链接和动态链接两种情况。而动态链接又被进一步划分为隐式动态链接和显式动态链接。
在上面的示例中,将源文件“DemoMath.cpp”和源文件“DemoOutPut.cpp”编译成动态链接库的时候,这两个源文件之间采用的链接类型是静态链接。静态链接的特点描述如下:
- 在编译时刻完成目标文件之间的链接;
- 所有的目标文件的内容都被合并到一起,包括:代码,数据等,然后将这些合并后的内容输出成一个PE格式的文件。在上面的示例中,在“DemoMath.cpp”中调用了“DemoOutPut.cpp”中的函数,在执行链接的时候,主调函数的代码和被调用函数的定义都被写入到了同一个文件中,即:DemoDLL。
在上面的示例中,在项目“DemoExe”中调用了项目“DemoDLL”中的函数,在编译的时候,这两个项目之间的链接类型是动态链接。动态链接的特点描述如下:
- 在编译时刻,仅将被调用函数的符号写入到主调函数所在的文件中,主调函数和被调用函数分别位于不同的文件中。在上面的示例中,主调函数位于可执行文件“DemoExe.exe”,而被调用函数位于“DemoDLL.dll”中。
- 在程序发布的时候,需要将可执行文件和动态链接库一同发布,缺一不可。在上面的示例中,“DemoExe.exe”和“DemoDLL.dll”必须一同提供给用户,否则程序运行不起来;
- 在程序加载的时候,由操作系统的加载器完成最终的链接。也就是说,在程序加载的时候,主调函数才能确定被调用函数的地址。所以,这种链接方式才叫动态链接,而“DemoDLL.dll”才被叫做动态链接库。
- 这种链接方式也叫做隐式动态链接,是默认的动态链接类型。
一般情况下,在同一个项目中,比如项目“DemoDLL”中,由程序员编写的C++源代码之间的链接方式是静态链接;在多个项目之间,比如:项目“DemoExe”和项目“DemoDLL”之间,采用的链接方式是动态链接。
由程序员开发出来的可执行程序或动态链接库,在运行的时候,它们是需要C/C++运行库支持的。这些项目和运行库之间的链接方式可以是静态链接,也可以是动态链接。可以在编译源程序的时候进行设定,确定是采用静态链接方式还是采用动态链接方式。如果采用静态链接方式,C/C++运行库中的相关函数的代码被加入到目标项目中,然后合并成一个文件发布,这个文件相对较大;如果是采用动态链接,只是将C/C++运行库中的相关函数的符号写入到了目标项目中。在程序发布的时候,需要将生成可执行文件和C/C++运行库的动态链接库文件一同发布。这时候生成的可执行文件相对较小。
在Visual Studio中,可以通过如下方式更改C++应用程序与C/C++运行库的链接方式,具体情况如下图所示。
该窗体的打开路径如下:在解决方案中选择一个项目,然后鼠标右键选择“属性”选项,在弹出的窗体中,选择C/C++标签中的“代码生成”项。
在上图“运行时库”项目中,可以设定要链接的方式。一共有四种可以被选择的链接方式,分别是:多线程静态链接,多线程静态链接调试版,多线程动态链接,多线程动态链接调试版。默认链接的类型为多线程动态链接。
如果选择了动态链接方式,将会使用C/C++运行库的动态链接版本,使用工具Dependency将生成的可执行文件打开后,各个组件之间的关系如下图所示:
MSVCR90.dll是C运行库所在的动态链接库,MSVCP90.dll是C++运行库所在的动态链接库,Kerner32.dll和NTDLL.dll是操作系统的组件,它们以动态链接库的形式提供。C/C++运行库与Kerner32.dll之间采用动态链接的方式。在上图中,可执行文件DemoExe除了与DemoDll进行了动态链接外,还与C运行库,C++运行库,以及组件Kerner32.dll进行了动态链接;由程序员开发出来的动态链接库DemoDLL.dll也与C运行库,C++运行库,以及组件Kerner32.dll进行了动态链接。C/C++运行库又动态链接了组件Kerner32.dll,组件Kerner32.dll动态链接了组件NTDLL.dll。
如果选择了静态链接方式,将会使用C/C++运行库的静态链接版本。使用工具Dependency将可执行文件打开,各个组件之间的关系如下图所示:
由于设定了静态链接的方式,DemoExe和DemoDLL与C/C++运行库之间的链接方式变成了静态链接。但是DemoExe与DemoDLL之间的链接方式,已经C/C++运行库与组件Kerner32.dll之间的链接方式依然是动态链接。所以,在上图中可以看出,C/C++运行库的相关代码已经被合并到DemoDLL.dll以及DemoExe中,已经看不到MSVCR90.dll和MSVCP90.dll的存在。但是由于C/C++运行库与组件Kerner32.dll之间是动态链接,所以DemoExe和DemoDLL继承了种链接方式,它们与组件Kerner32.dll之间的链接方式依然是动态链接。
由上面的分析可以看出,在Visual Studio中设定的链接方式,只能影响应用程序与C/C++运行库之间的链接。程序员开发出来的C++应用程序与其他组件之间的关系如下图所示:
程序员开发的应用程序受到C/C++运行库的支持,而C/C++运行库在实现C/C++标准库接口的时候,是需要受到操作系统组件的支持的。在Windows平台上,它们分别是Kerner32.dll,以及NTDLL.dll。这些组件包含了对win32API的封装,也就是说,在实现C/C++标准库接口的时候,C/C++运行库调用了Win32API中的相关函数。
动态链接的另外一种方式是显式动态链接。当进行这种动态链接的时候,只要当真正执行函数的调用的时候,才会确定被调用函数的地址。隐式动态链接与显式动态链接的区别是:隐式动态链接在程序加载的时候确定被调用函数的地址,而显式动态链接将这个过程推后到具体函数调用的时候。可以使用函数LoadLibrary和函数GetProcAddress实现显示动态链接。
1.3.3.3PE文件中的段种类
在执行了链接以后,将多个目标文件合并在一起,输出了可执行文件或者是动态链接库。可执行文件和动态链接库的二进制内容是以PE格式存储的。在PE文件中所包含的段的种类如下图所示:
各个主要段的详细信息描述如下表:
序号 |
段名 |
描述 |
1 |
.text |
在该段中包含C++程序的源代码,这些源代码已经被编译成计算机系统硬件能够识别的二进制指令。每一个二进制指令都必须对应一个虚拟内存地址 |
2 |
.data |
已初始化的全局变量,静态变量存储在该段中 |
3 |
.textbss |
该段为代码段,在PE文件中不占用存储空间,在虚拟内存中占用虚拟内存的地址空间。在执行增量链接的时候,新修改过的函数的代码可能会被放到该段中。用于debug模式下。 |
4 |
.rdata |
只读的数据存储在该段中,例如:字符串文本。导入,导出表会被合并到该段中。 |
5 |
.idata |
导入表。在创建release版本的时候,该节经常被合并到.rdata节中。 |
6 |
.edata |
导出表。在创建一个包含导出 API 或数据的时候,链接器会生成一个 .EXP 文件。这个 .EXP 文件包含一个最终会被添加到可执行文件里的 .edata 节。和 .idata 节一样,.edata 节经常会被合并到 .text 或 .rdata 节中。 |
7 |
.rsrc |
资源节,该节只读,不能被合并到其他节。 |
8 |
reloc |
基址重定位节。 |
9 |
.crt |
为支持 C++ 运行时(CRT)而添加的数据。比如,用来调用静态 C++ 对象的构造器和析构器的函数指针 |