重用是软件工程的重要方法。软件系统中可重用的部分包括:数据,文档,算法,代码,设计模式,软件架构以及环境等多方面内容。根据不同的抽象层次,软件代码重用大致可分为三类: 软件实现层重用,即软件源代码的重用;软件设计层重用,即重用软件系统的设计信息;软件架构层重用,即重用软件系统的架构。共享库是软件源代码重用的方法,动态链接库是保存软件外部共享库的文件格式。
通过外部共享库实现软件重用的流程有三步:
1) 生成外部共享库文件,
2) 编译整合外部共享库与宿主程序,
3) 启动宿主程序,宿主程序进程加载外部共享库。
外部共享库格式
动态链接指操作系统将外部共享库从持久性存储区(persistent storage)复制到内存,通过填写跳转表和重新定位指针等操作,把外部共享库注入当前进程[2]。不同操作系统中实现动态链接方法以及使用的文件格式各不相同。 动态链接库(Dynamic-link library, DLL) 是Windows操作系统在动态链接时使用的外部共享库的文件格式。
根据编译和使用方式的不同,Windows操作系统的外部共享库分为两种: 动态链接库和静态链接库,如下表所示。在使用外部共享库时,首先要将外部共享库代码编译成动态链接库文件或静态链接库文件。然后将链接库文件和宿主程序代码编译成宿主可执行文件。 Windows操作系统中外部共享库主要使用两类文件,文件名后缀分别是: .lib 和.dll。
静态链接库 | 动态链接库 | |
---|---|---|
内存使用 | 每一个宿主软件运行时都有一份静态链接库 | 多个宿主软件运行时,内存只需要复制一个动态链接库 |
软件依赖 | 宿主程序运行时不依赖静态链接库文件 | 宿主程序运行时依赖动态链接库文件 |
文件 | .lib文件 | .lib文件, .dll文件 |
软件更新 | 整个软件都需要更新 | 宿主程序和库文件相互独立更新 |
版本冲突 | 无 | 非常严重 |
VS(Visual Studio)构造静态链接库时,导出函数的声明和实现都放在lib文件中。 静态链接库中函数代码插入宿主程序中。宿主程序运行时不依赖lib文件。
VS构造动态链接库时,会同时生成lib文件和dll文件。动态链接库中lib文件的功能相当于头文件,只包含导出函数的声明,函数的实现放在dll文件中。主程序运行时依赖dll文件。
一个动态链接库实例可以被多个程序共同使用。这能实现代码共享并且可以隐藏实现软件代码细节,便于软件升级。 但是,这也带来一定的副作用。多个程序依赖于同一个动态链接库,当这些程序升级步调不一致,造成软件版本冲突,会引发一系列灾难性后果,也就是所谓的"DLL Hell".
动态链接库的调用方式
宿主程序加载动态链接库的方法有两种: 显式加载 和 隐式加载
显式加载
宿主程序代码中通过LoadLibrary()函数 和 FreeLibrary()函数指定动态链接库的加载和卸载的时机。
宿主程序启动后,宿主程序进程在遇到LoadLibrary()函数时才将动态链接库加载到进程的内存空间。
通过显式加载方法使用动态链接库,在编译整合动态链接库与宿主程序时只有需要使用dll文件。
隐式加载
隐式加载的宿主程序代码中没有直接使用LoadLibrary()函数 和 FreeLibrary()函数的代码,因此称为隐式加载。
隐式加载的宿主程序在启动时搜索到dll文件以后,将动态链接库加载到宿主程序进程的内存空间。 隐式加载实际上也是通过LoadLibrary()函数实现加载工作。
通过隐式加载方法使用动态链接库,在编译整合动态链接库与宿主程序时需要使用lib文件和dll文件。
函数调用协定
由于操作系统,编译器,硬件等环境因素的不同,程序使用的函数调用方法在寄存器使用,内存堆栈管理,函数参数传递,函数名修饰符等方面会有较大差异。每一种函数调用方式称为函数调用协定(Function Calling Convention)。
函数调用协定主要规定调用函数时下列几方面事项:
- 函数参数在内存中的顺序
- 传递参数的方式
- 函数调用方要保留CPU寄存器
- 内存堆栈的使用方式
下面是运行于x86架构微处理器的程序常用的函数调用协定 [3] [4]
协议名称 | 函数参数在堆栈中的顺序 | 清理堆栈中的函数参数占用的内存空间 | 备注 |
---|---|---|---|
cdecl | RTL(right-to-left) | 函数调用方 | |
sdtcall | RTL | 函数被调用方 | |
Pascal | LTR(left-to-right) | 函数被调用方 | |
fastcall | LTR | 函数被调用方 | |
thiscall | RTL | 函数被调用方 | |
vectorcall | RTL |
在C语言源代码中,如果改变函数调用协定,需将协定的标识符放在函数名和函数返回值类型声明之间. 编译器根据调用协定生成相应的机器语言代码。汇编代码的助记符与机器语言指令一一对应。查看函数的汇编代码,能深入了解不同函数调用协定的实现细节。
下面通过几个样例展示不同函数调用协定之间的差异。 样例中使用的函数名为test, 它接受两个整数型参数,并返回这两个整数的和。在主函数main中,将整数1和3作为参数,调用函数test. 具体代码如下:
int test(int a, int b)
{
return (a + b);
}
int main()
{
test(1, 3);
return 0;
}
将上述源代码保存到名为foo.cc文件中. 生成汇编代码, 使用编译参数 /FA, 然后再控制台环境使用cl.exe编译:
cl /FA foo.cc
编译完成后,会生成可执行文件foo.exe. 编译过程中生成的汇编代码会保存在foo.asm中。
cdecl Calling Convention
本样例中,test函数使用cdecl调用协定,具体C语言源代码如下:
int cdecl test(int a, int b)
{
return (a + b);
}
int main()
{
test(1, 3);
return 0;
}