目录:
源程序变成可执行文件的过程
以C语言为例,程序的生成过程基本需要经过以下几个过程:C源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执行文件
1.编译预处理 读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。
伪指令主要包括以下四个方面:
(1)宏定义指令,如# define Name TokenString,#undef等。对于前一个伪指令,预编译所要作得的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
(3)头文件包含指令,如#include “FileName”或者#include <>等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号(”“)。
(4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。
2.编译阶段
经过预编译得到的输出文件中,将只有常量。如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,},+,-,*,\,等等。预编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
3.优化阶段
优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。上图中,我们将优化阶段放在编译程序的后面,这是一种比较笼统的表示。
对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。
后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。
4.汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
目标文件由段组成。通常一个目标文件中至少有两个段:
代码段 该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
数据段 主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
5.链接程序
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
(1)静态链接 在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
(2)动态链接 在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
经过上述过程,C源程序就最终被转换成可执行文件了。
静态编译与动态编译
1 静态链接库的优点
(1) 代码装载速度快,执行速度略比动态链接库快;
(2) 只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。
2 动态链接库的优点
(1) 更加节省内存并减少页面交换;
(2) DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
(3) 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
(4)适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
关于 DLL 地狱:
DLL 地狱(DLL Hell)是指因为系统文件被覆盖而让整个系统像是掉进了地狱。
简单地讲,DLL地狱是指当多个应用程序试图共享一个公用组件时,如某个DLL或某个组件对象模型(COM)类,所引发的一系列问题。
最典型的情况是,某个应用程序将要安装一个新版本的共享组件,而该组件与机器上的现有版本不向后兼容。虽然刚安装的应用程序运行正常,但原来依赖前一版本共享组件的应用程序也许已无法再工作。在某些情况下,问题的起因更加难以预料。比如,当用户浏览某些web站点时会同时下载某个Microsoft ActiveX控件。如果下载该控件,它将替换机器上原有的任何版本的控件。如果机器上的某个应用程序恰好使用该控件,则很可能也会停止工作。在许多情况下,用户需要很长时间才会发现应用程序已停止工作。结果往往很难记起是何时的机器变化影响到了该应用程序。
这些问题的原因是应用程序不同组件的版本信息没有由系统记录或加强。而且,系统为某个应用程序所做的改变会影响机器上的所有应用程序—现在建立完全从变化中隔离出来的应用程序并不容易
静态链接与动态链接的实现过程(开发环境:vs2015+win10)
一、静态链接
静态库的调用方法
第一种:项目设置中引用,在项目的属性中设置。
第二种:在代码中使用 #pragma comment(lib,”lib文件名”)
第一种方法:
步骤一:
右键单击项目—>属性—>配置属性—>链接器—>常规—>附加库目录
在其中填入lib库的目录,也可以是相对或绝对路径。
步骤二:
右键单击项目—>属性—>配置属性—>链接器—>输入—>附加依赖项
在其中填入lib库的名称,如:Test.lib
其等价于程序中的#pragma comment(lib,”*.lib”)
第二种方法:
’#progma comment(lib,”lib文件名”); 中lib文件名也可以是带相对路径或是绝对路径的lib。
‘#pragma comment(lib,”..\\lib文件名”)
如果是不带路径的lib文件名,则可以通过第一种方法的步骤一的方式来指定该lib的路径。
下面以一个实际例子,分别使用上述两种方法,以供大家作为对比。
1.生成静态库
(1)分别编写.h文件和.cpp文件
//StaticMath.h
#pragma once
class StaticMath
{
public:
StaticMath(void);
~StaticMath(void);
static double add(double a, double b);
static double sub(double a, double b);
static double mul(double a, double b);
static double div(double a, double b);
};
//StaticMath.cpp
class StaticMath
{
public:
StaticMath(void);
~StaticMath(void);
static double add(double a, double b);
static double sub(double a, double b);
static double mul(double a, double b);
static double div(double a, double b);
};
(2)生成解决方案,会在Debug文件夹下生成.lib静态库文件
2.调用静态库
(1)创建控制台程序,测试调用静态库
// TestStaticLibrary.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include "StaticLibrary.h"
int main()
{
double a = 1.2;
double b = 2.4;
std::cout << "a+b=" << StaticMath::add(a, b) << std::endl;
system("pause");
return 0;
}
(2)
此处采用方法一通过使用更改配置完成
配置头文件引用目录:项目属性->C/C++->常规->附加包含目录->静态库头文件所在的目录。
配置静态库的目录:项目属性->链接器->命令行->其它选项->添加静态库的绝对路径
此处采用方法二调用静态库,即使用#progma comment(lib,”lib文件名”);在使用该语句前,需要将生成的静态库文件放在当前工作路径下如图所示,包括StaticLibrary.h和StaticLibrary.lib文件
通过上述两种方法均可完成静态库调用,运行结果如下图所示
二、动态链接
1.生成动态库
创建新工程生成.dll,首先创建DynamicLirary.h和DynamicLirary.cpp文件,具体如下图所示。
#pragma once
//DynamicMath.h
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 DYNAMICMATH_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// DYNAMICMATH_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
//当定义了符号TESTDLL_EXPORTS,TESTDLL_API被设置为 __declspec(dllexport) 修饰符,
//若未定义则TESTDLL_API被设置为__declspec(dllimport),当DLL项目生成时,TESTDLL_EXPORTS默认是定义的,
//所以默认设置的是__declspec(dllexport) 修饰符。
#ifdef DYNAMICMATH_EXPORTS
#define DYNAMICMATH_API __declspec(dllexport)
#else
#define DYNAMICMATH_API __declspec(dllimport)
#endif
#ifdef __cplusplus // 为了让函数名更规范
extern "C" { // 创建dll过程中使用C编译器来编译函数,这样DLL文件中的函数名和原dll工程中的函数名就一致了。
#endif //当使用C++编译器时会出现这样问题,只需要extern "C"即可解决
__declspec(dllexport) int sub(int a, int b)
{
return a*b;
}
#ifdef __cplusplus
}
#endif
// 此类是从 DynamicMath.dll 导出的
class DYNAMICMATH_API CDynamicMath {
public:
CDynamicMath(void);
// TODO: 在此添加您的方法。
static double add(double a, double b);
static double sub(double a, double b);
static double mul(double a, double b);
static double div(double a, double b);
};
//示例导出变量声明
extern DYNAMICMATH_API int nDynamicMath;
//示例导出函数声明
DYNAMICMATH_API int fnDynamicMath(void);
// DynamicMath.cpp : 定义 DLL 应用程序的导出函数。
//
#include "stdafx.h"
#include "DynamicMath.h"
// 这是导出变量的一个示例
DYNAMICMATH_API int nDynamicMath = 0;
// 这是导出函数的一个示例。
DYNAMICMATH_API int fnDynamicMath(void)
{
return 12;
}
// 这是已导出类的构造函数。
// 有关类定义的信息,请参阅 DynamicMath.h
CDynamicMath::CDynamicMath()
{
return;
}
//我自己添加的导出函数
double CDynamicMath::sub(double a, double b)
{
return a*b;
}
生成解决方案,会在Debug文件夹下生成DynamicLirary.lib和DynamicLirary.dll文件,如图
2.隐式链接动态库
(此处采用静态加载,即隐式链接的方式加载动态库,需要.h文件 、动态链接库、导入库这三件套)
建立控制台程序,调用动态库文件,创建测试程序
#include "stdafx.h"
#include "DynamicMath.h"
#include <iostream>
int main()
{
int num = fnDynamicMath();//自动生成的示例导出函数
std::cout << num << std::endl;
double a = 1.2;
double b = 2.5;
std::cout << CDynamicMath::sub(a, b) << std::endl;
std::cout << nDynamicMath << std::endl;
system("pause");
return 0;
}
更改属性配置选项,修改C/C++附加包含目录
修改链接器附加库目录
在链接器附加依赖项中加入.lib文件
在VC++目录下的库目录中添加Debug路径
最后,将.lib文件和.dll文件分别放在项目目录下UseDynamic和Debug文件夹下
运行后结果如图
2.显示链接动态库
显式链接是应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。
动态库的显示调用方法:
1、创建一个函数指针,其指针数据类型要与调用的 DLL 引出函数相吻
合。
2、通过 Win32 API 函数LoadLibrary()显式的调用DLL,此函数返回
DLL 的实例句柄。
3、通过 Win32 API 函数GetProcAddress()获取要调用的DLL 的函数地
址,把结果赋给自定义函数的指针类型。
4、使用函数指针来调用 DLL 函数。
5、最后调用完成后,通过 Win32 API 函数FreeLibrary()释放DLL 函数
新建项目,不需要特殊配置,添加cpp文件,需要将.dll文件放在Debug文件夹下
// Usedynamic1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <Windows.h>//注意此头文件位置,如果放在第一个位置会发生错误
using namespace std;
int main()
{
typedef double(*pSub)(double a, double b);//TEXT("MyDLL.dll")
HMODULE hDLL = LoadLibrary(TEXT("DynamicLirary.dll")); //加载dll文件
if (hDLL != NULL)
{
pSub fp2 = pSub(GetProcAddress(hDLL, "sub")); //得到dll中标示为"?..."的函数,C++编译器考虑了函数的参数
if (fp2 != NULL)
{
cout << fp2(5.5, 2.5) << endl;
system("pause");
}
else
{
cout << "Cannot Find Function " << "sub" << endl;
system("pause");
}
FreeLibrary(hDLL);
}
else
{
std::cout << "Cannot Find " << "DynamicLirary" << std::endl;
}
return 1;
}
运行后结果如图
本人的第一篇技术博客,学习的同时进行总结,收获颇丰,能力有限难免有错,欢迎指教!