动态链接库

动态链接库（英语：Dynamic-link library，缩写为DLL）是微软公司在微软视窗操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是.DLL、.OCX（包含ActiveX控制的库）或者.DRV（旧式的系统驱动程序）。
所谓动态链接，就是把一些经常会共享的代码（静态链接的OBJ程序库）制作成DLL档，当可执行文件调用到DLL档内的函数时，Windows操作系统才会把DLL档加载存储器内，DLL档本身的结构就是可执行档，当程序有需求时函数才进行链接。透过动态链接方式，存储器浪费的情形将可大幅降低。静态链接库则是直接链接到可执行文件。
DLL的文件格式与视窗EXE文件一样——也就是说，等同于32位视窗的可移植执行文件（PE）和16位视窗的New Executable（NE）。作为EXE格式，DLL可以包括源代码、数据和资源的多种组合。
在更广泛的意义上说，任何同样文件格式的计算机文件都可以称作资源DLL。这样的DLL的例子有扩展名为ICL的图标库、扩展名为FON和FOT的字体文件。

背景

DLL的最初目的是节约应用程序所需的磁盘和内存空间。在一个传统的非共享库中，一部分代码简单地附加到调用的程序上。如果两个程序调用同一个子程序，就会出现两份那段代码。相反，许多应用共享的代码能够切分到一个DLL中，在硬盘上存为一个文件，在内存中使用一个实例（instance）。DLL的广泛应用使得早期的视窗能够在紧张的内存条件下运行。
DLL提供了如模块化这样的共享库的普通好处。模块化允许仅仅更改几个应用程序共享使用的一个DLL中的代码和数据而不需要更改应用程序自身。这种模块化的基本形式允许如Microsoft Office、Microsoft Visual Studio、甚至Microsoft Windows自身这样大的应用程序使用较为紧凑的补丁和服务包。
模块化的另外一个好处是插件的通用接口使用。单个的接口允许旧的模块与新的模块一样能够与以前的应用程序运行时无缝地集成到一起，而不需要对应用程序本身作任何更改。这种动态扩展的思想在ActiveX中发挥到了极致。
尽管有这么多的优点，使用DLL也有一个缺点：DLL地狱，也就是几个应用程序在使用同一个共享DLL库发生版本冲突。这样的冲突可以通过将不同版本的问题DLL放到应用程序所在的文件夹而不是放到系统文件夹来解决；但是，这样将抵消共享DLL节约的空间。当前，Microsoft .NET将解决DLL hell问题当作自己的目标，它允许同一个共享库的不同版本并列共存。由于现代的计算机有足够的磁盘空间和内存，这也可以作为一个合理的实现方法。

符号解析和绑定

DLL输出的每个函数都由一个数字序号唯一标识，也可以由可选的名字标识。同样，DLL引入的函数也可以由序号或者名字标识。对于内部函数来说，只输出序号的情形很常见。对于大多数视窗API函数来说名字是不同视窗版本之间保留不变的；序号有可能会发生变化。这样，我们不能根据序号引用视窗API函数。
按照序号引用函数并不一定比按照名字引用函数性能更好：DLL输出表是按照名字排列的，所以对半查找可以用来在在这个表中根据名字查找这个函数。另外一方面，只有线性查找才可以用于根据序号查找函数。
将一个可执行文件绑定到一个特定版本的DLL也是可能的，这也就是说，可以在编译时解析输入函数（imported functions）的地址。对于绑定的输入函数，链接工具保存了输入函数绑定的DLL的时间戳和校验和。在运行时Windows检查是否正在使用同样版本的库，如果是的话，Windows将绕过处理输入函数；否则如果库与绑定的库不同，Windows将按照正常的方式处理输入函数。
绑定的可执行文件如果运行在与它们编译所用的环境一样，函数调用将会较快，如果是在一个不同的环境它们就等同于正常的调用，所以绑定输入函数没有任何的缺点。例如，所有的标准Windows应用程序都绑定到它们各自的Windows发布版本的系统DLL。将一个应用程序输入函数绑定到它的目的环境的好机会是在应用程序安装的过程。

运行时通知DLL进程/线程加载

进程/线程加载时，可以通过DllMain函数通知DLL相关信息，提供对应处理的机会。
BOOL WINAPI DLLMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID fImpLoad){
  switch(fdwReason)
  { 
    case DLL_PROCESS_ATTACH:
        //当这个DLL第一次被映射到了这个进程的地址空间时。DLLMain函数的返回值为FALSE，说明DLL的初始化没有成功，系统就会终结整个进程，去掉所有文件映象，之后显示一个对话框告诉用户进程不能启动。
    break;
    case DLL_THREAD_ATTACH: 
        //一个线程被创建，新创建的线程负责执行这次的DllMain函数。系统不会让进程已经存在的线程以DLL_THREAD_ATTACH的值来调用DllMain函数。主线程永远不会以DLL_THREAD_ATTACH的值来调用DllMain函数。系统是顺序调用DllMain函数的，一个线程执行完DllMain函数才会让另外一个线程执行DllMain函数。
    break;
    case DLL_THREAD_DETACH:
       //如果线程调用了ExitThread来结束线程（线程函数返回时，系统也会自动调用ExitThread）。线程调用了TerminateThread，系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
    break;
    case DLL_PROCESS_DETACH:
        //这个DLL从进程的地址空间中解除映射。如果进程的终结是因为调用了TerminateProcess，系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。
    break;
  }
  return(TRUE);}

运行时显式链接

对于一个进程，每个被加载的DLL在进程的PEB中有一个引用计数器，在当前进程中每多一次加载该计数器便加一。LoadLibrary 与 FreeLibrary 指令影响每一个进程内含的计数器；动态链接则不影响。因此借由调用FreeLibrary 而从存储器卸载一DLL 是很重要的。一个进程可以从它自己的VAS 注销此计数器。该计数器变为0时，这个DLL从当前进程中卸载，但并不意味着会从物理内存中卸载，因为其它进程可能使用这个DLL。
DLL 文件能够在运行时使用LoadLibrary（或者 LoadLibraryEx）API 函数进行显式调用，这个的过程微软简单地称为运行时动态调用。如果在上述API的参数中指明了库文件的全路径，则不再搜索这个库文件。API 函数GetProcAddress 查找具有某名称的输出函数、FreeLibrary卸载 DLL（实际上是引用计数器减一）。这些函数类似于POSIX 标准 API 中的dlopen、dlsym、和 dlclose。
注意微软简单称为“运行时动态链接”的运行时隐式链接，如果不能找到链接的DLL 文件，Windows 将提示一个错误消息并且调用应用程序失败（准确地说是不能创建进程）。应用程序开发人员不能通过编译链接来处理这种缺少DLL 文件的隐式链接问题，而且在更改了实现后还必须重新编译链接整个程序——默认的增量链接模式会一直保留着那条缺少的函数引用，只有重新编译链接才能去掉。相反，虽然显式链接的代码量增多了，但开发人员有机会提供一个完善的出错处理机制。
运行时显式链接的过程在所有语言中都是相同的，因为它依赖于Windows API 而不是语言结构。只要一种语言能够调用上述的LoadLibrary 等函数，就能执行运行时显示链接。

Win16下的DLL

16位Windows，所有进程共享同一个内存地址空间。如果一个DLL被多个进程使用，它只被加载到内存中一次，只有一份数据节（data segment）。也就是说，DLL是系统全局而不是进程内的；进程不能得到DLL的一份独立的拷贝。每个DLL在内存中只有一份实例（instance）。
如果一个EXE文件同时执行多次，那么在内存中只有该EXE的一套只读拷贝（如代码或资源），但这个EXE的每个进程都有自己的一套数据段，即有多份实例（instance）。实际上，进程的实例句柄就是data segment（存放进程的全局变量）的内存起始地址。
模块（module）是指一个硬盘文件，可被加载到内存中。模块句柄是一个数据结构，表示这个硬盘文件的各部分（section）出自哪里，是否已经加载到内存中。
所以进程只能用实例句柄标识，而不能用模块句柄标识。
Win32的进程使用自己专用的逻辑内存空间。进程的全局变量不再是跨进程边界可见的。实例句柄与模块句柄相同，都是指向模块加载后的内存基地址。这样规定实际上也兼容了Win16时实例句柄与模块句柄的含义。

资源加载

EXE和DLL都有其自己的资源（如对话框资源），而且这些资源的ID可能重复，默认使用EXE的资源。 如果需要加载、使用DLL中的资源，需要通过DLL加载后的实例句柄（HINSTANCE）来找到DLL的资源。
应用程序进程本身及其调用的每个DLL模块都具有一个全局唯一的HINSTANCE句柄，它们代表了EXE或DLL模块在进程逻辑地址空间中的起始地址。进程本身的模块句柄一般为0x400000，而DLL模块默认加载地址为0x10000000。如果程序同时加载了多个DLL，则每个DLL模块都会有不同的HINSTANCE。
几种可行的办法：
方法1:
// in MFC DLLvoid CDLL::ShowDlg(void){
       AFX_MANAGE_STATE(AfxGetStaticModuleState());
       CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
       dlg.DoModal();}
AFX_MANAGE_STATE(AfxGetStaticModuleState());必须作为接口函数的第一条语句。功能是在栈上创建一个AFX_MAINTAIN_STATE2类的实例，利用其构造函数和析构函数对_afxThreadState.GetData()所指向内存存储块的模块状态（AFX_MODULE_STATE类型）设置现场（AfxGetModuleState函数返回的值）及恢复现场。  
方法2：
// in MFC DLLvoid CDLL::ShowDlg(void){
       HINSTANCE save_hInstance = AfxGetResourceHandle();    //当前资源句柄
       AfxSetResourceHandle(theApp.m_hInstance);             //设置为当前DLL的实例句柄所对应的资源句柄
       CDialog dlg(IDD_DLL_DIALOG);                          //打开指定ID的对话框
       dlg.DoModal();
       AfxSetResourceHandle(save_hInstance);     
}
方法3：
// in EXEvoid CEXE::OnButtonClick(){
       HINSTANCE exe_hInstance = GetModuleHandle(NULL);
      HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll"); 
      AfxSetResourceHandle(dll_hInstance); //切换状态
       ShowDlg();
      AfxSetResourceHandle(exe_hInstance); //恢复状态}

Delphi

在源文件的开头使用关键词library而不是program，在文件的末尾输出函数使用exports排列。
Delphi不需要LIB文件以从DLL中输入函数。为了链接一个DLL，在函数声明中使用关键词external。

微软 Visual Basic 及 .Net 框架

在Visual Basic（VB）中只支持运行时链接；但是除了使用LoadLibrary和GetProcAddress这两个API函数之外，允许使用输入函数的声明来引入DLL函数，如果找不到DLL文件，VB将产生一个运行时异常。开发人员可以捕获该异常并且进行适当的处理。
Visual Basic 6这样的较老的语言，只能调用__stdcall调用约定修饰的函数，并需要在声明DLL的输出函数时使用Alias，否则就会出现在DLL中找不到函数入口点的错误。例如：
Public Declare Function test2 Lib "PackingDLL.dll" Alias "_test2@4" (ByVal param As Integer) As Integer
也可以用def文件来定义dll输出函数的名字。。另外需要注意，VB6调用的dll，如果还依赖其他的dll，那么这些间接依赖的dll必须在VB6程序的dll搜索路径上，否则会报无法找到（被直接依赖的）dll的错误。
为了兼容，Declare Function 的方式一直沿用到VB.net。不过在.net 中，平台调用提供了一种新的声明DLL中的输出函数方式，通过 System.Runtime.InteropServices.DllImportAttribute 提供。例如：
<DllImportAttribute("user32.dll", EntryPoint:="MessageBoxW", SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)>Public Function MessageBox(hWnd As Integer, lpText As String, lpCaption As String, uType As UInteger) As IntegerEnd Function
在同为.net 语言的C# 中，这是仅有的处理方式。当然托管C/C++ 就不必受此限制，见下节。

创建DLL输出函数

下面的例子展示了与特定语言相关的从DLL输出符号表的方法。
Delphi
 library Example;
 
 // Function that adds two numbers
 function AddNumbers(a, b: Double): Double; cdecl;
 begin
     AddNumbers := a + b
 end;
 
 // Export this function
 exports
     AddNumbers;
 
 // DLL initialization code: no special handling needed
 begin
 end.
C 或 C++
 #include <windows.h>
 
 // Export this function
 extern "C" __declspec(dllexport) double AddNumbers(double a, double b);
 
 // DLL initialization function
 BOOL APIENTRY DllMain(HANDLE hModule, [[DWORD]] dwReason, LPVOID lpReserved)
 {
 return TRUE;
 }
 
 // Function that adds two numbers
 double AddNumbers(double a, double b)
 {
 return a + b;
 }

运行时使用显式调用

下面的例子展示了如何使用不同语言特有的WIN32 API绑定进行运行时的调用和链接。
Microsoft Visual Basic
 Option Explicit
 Declare Function AddNumbers Lib "Example.dll" (ByVal a As Double, ByVal b As Double) As Double
 
 Sub Main()
     Dim Result As Double
     Result = AddNumbers(1, 2)
     Debug.Print "The result was: " & Result
 End Sub
C 或 C++
 #include <windows.h>
 #include <stdio.h>
 
 // DLL function signature
 typedef double (*importFunction)(double, double); 

 int main(int argc, char **argv)
 {
     importFunction addNumbers;
     double result;
 
     // Load DLL file
     HINSTANCE hinstLib = LoadLibrary("Example.dll");
     if (hinstLib == NULL) {
         printf("ERROR: unable to load DLL\n");
         return 1;
     }
 
     // Get function pointer
     addNumbers = (importFunction)GetProcAddress(hinstLib, "AddNumbers");
     if (addNumbers == NULL) {
         printf("ERROR: unable to find DLL function\n");
         return 1;
     }
 
     // Call function.
     result = addNumbers(1, 2);
 
     // Unload DLL file
     FreeLibrary(hinstLib);
 
     // Display result
     printf("The result was: %f\n", result);
 
     return 0;
 }

组件对象模型

组件对象模型（COM）将DLL概念扩充到了面向对象编程。对象能够从另外一个进程调用或者在另外一台机器上运行。COM对象有一个唯一的GUID并且能够实现强大的后台以简化如Visual Basic和ASP这样的GUI前台应用。它们也可以使用脚本语言编程。COM对象的创建和使用比DLL更为复杂。