动态链接库

一:DLL基础

  1. Windows API的所有函数都包含在dll中
  2. Windows最重要的三个dll:
    • Kernel32.dll:包含管理内存、进程和线程的函数
    • user32.dll:包含处理用户界面的一些函数
    • GDI32.dll:包含绘制图形和显示文本的函数
  3. 为什么要使用dll?
    1. 动态加载,应用程序可根据情况决定是否加载、加载哪些dll
    2. 可以采用多种语言编写和加载dll,如VB调用C++ DLL
    3. 可由不同的团队开发不同的模块(DLL),易于管理项目并提高开发效率
    4. 有助于节省内存。多个应用程序使用同一个DLL,则DLL的各内存页在RAM中只有一份拷贝,并且所有应用程序可以共享这些页面;链接到静态库,则同一份代码要多次存储在内存中
    5. 促进资源共享,DLL可以包含对话框模板、字符串、图标以及位图等资源,多个应用程序可以通过使用DLL来共享这些资源
    6. 有助于解决平台差异。不同版本的windows提供不同的函数,如果源代码已经包含了对新函数的调用,如果应用程序运行在不提供该函数支持的windows版本上,则操作系统的装载程序将拒绝执行装载,即使程序逻辑不会调用到该函数;如果这些函数封装到DLL中,则应用程序可以将其装载到旧版windows,当然,仍不能成功调用这些函数
    7. 可以实现特定的用途
  4. DLL中通常没有支持处理消息循环或创建窗口的代码
  5. 对于一个DLL, 必须要为链接程序设定/DLL开关选项,该选项使得链接程序向生成DLL的文件映像发出略有差异的信息,这样,操作系统装载程序就可以将该程序识别为一个DLL,而不是应用程序
  6. 将DLL映射到进程地址空间的方法:
    1. 隐式的装载时链接
    2. 显示的运行时链接
  7. 被DLL中的函数或代码所创建的任何对象都属于它的线程或进程——一个DLL并不拥有任何元素
  8. 当一个进程把一个DLL的映像文件映射到其他地址空间时,系统将同时创建全局数据变量和静态数据变量的实例
  9. 一个单独的地址空间由一个可执行模块和若干个DLL模块组成,一些模块可能链接到静态版本的C/C++运行时库,一些模块可能链接到DLL版本的运行时库,一些模块可能根本不需要C/C++运行时库

二:DLL的总体运行情况

  1. 创建DLL:
    1. 创建带有输出原型/结构/符号的头文件
    2. 创建实现输出函数/变量的C/C++源文件
    3. 编译器为每个C/C++源文件生成.obj文件
    4. 链接程序结合.obj模块生成DLL
    5. 如果至少一个函数/变量输出,则链接程序同时生成.lib文件
  2. 创建EXE:
    1. 创建带有导入原型/结构/符号的头文件
    2. 创建引用导入函数/变量的C/C++源文件
    3. 编译器为每个C/C++源文件生成.obj文件
    4. 链接程序结合.obj模块,使用.lib文件解析导入函数/变量的引用关系,生成.exe文件(包含所需要的DLL以及导入符号的导入表
  3. 运行应用程序:
    1. 装载程序为.exe文件创建地址空间
    2. 装载程序将所需要的DLL装载到地址空间,进程的主线程执行;应用程序开始运行
  4. 补充说明:
    1. .lib:
      • DLL的源代码至少导出一个函数或变量,就会生成一个单独的.lib文件
      • .lib很小,不包含任何函数和变量,只是简单的列出了所有被导出的函数和变量的符号名
      • 为创建可执行模块,该文件时必须的
    2. 在DLL的导出头文件中,用declspec(dllexport)来定义要导出的变量、函数或类;在加载DLL的可执行源代码中,用declspec(dllimport)定义要从DLL中导入的变量、函数或类
    3. 导出的符号要用extern "C"修饰符,告知编译器不要改变变量或函数名;若一个C++导出函数没有用extern "C"修饰,C++编译器在创建DLL是会改变其名字,当C编写的可执行模块时,C编译器不会修改函数名;当链接程序链接时,由于DLL和可执行模块得到的函数名不同,会提示符号找不到错误
    4. 可执行源码应将从DLL模块导入的函数和变量定义为__declspec(dllimport)
  5. 运行可执行模块步骤:
    1. 操作系统装载程序为该进程创建虚拟地址空间
    2. 装载程序将可执行模块映射到该进程地址空间
    3. 装载程序检查可执行模块的导入模块,并试图定位所需DLL,并将其映射到该进程的地址空间
    4. 装载程序检查每个DLL的导入部分,如果存在一个导入部分,则装载程序继续将额外所需的DLL模块映射到进程的地址空间(对每个DLL只装载一次,即使多个模块需要)

DLL高级技术

  1. LoadLibrary(Ex)这种加载方式属于显示加载
  2. .lib和.def并不用于显示链接
  3. 显示加载DLL的EXE文件由于没有对DLL导出符号的直接引用,所以不包含导入表
  4. 运行显示加载DLL的EXE:
    • 加载程序为.exe模块创建地址空间
    • 进程的主线程执行,应用程序开始运行
  5. 可以通过LoadLibraryEx及其dwFlag参数设定DLL的加载设置:
    • 将DLL作为数据文件加载,不运行DllMain初始化DLL
    • 不加载DLL依赖的DLL
    • 改变DLL的文件搜索算法(即加载程序定位DLL位置的算法)
  6. LoadLibrary为引用计数加1,FreeLibrary为引用计数减1
  7. DLL是通过使用DllMain函数来初始化它们自己的
  8. DllMain应当只执行简单的初始化工作,避免调用从其它DLL导入的函数,也应当避免调用LoadLibrary或FreeLibrary,因为这些DLL可能并没有加载,会导致循环依赖
  9. DLL_PROCESS_ATTCH通知:
    • 当把一个DLL初次映射到进程的地址空间时,系统调用DLL的DllMain函数,为其中的fdwReason参数传递DLL_PROCESS_ATTCH值;如果之后一个线程为已经映射地址空间的DLL调用LoadLibrary,则操作系统会简单的增加DLL的引用计数,不会使用DLL_PROCESS_ATTCH值再次调用DllMain函数
    • DllMain处理一个DLL_PROCESS_ATTCH通知时,DllMain返回值指明是否成功初始化DLL;对于其他fdwReason取值,系统将忽略DllMain返回值
      动态链接库_第1张图片
      **线程调用LoadLibrary函数时系统的执行步骤**
  10. DLL_PROCESS_DETACH通知:
    • 当一个DLL从进程的地址空间解除映射时,系统将调用DllMain函数,并为fdwReason的值传递DLL_PROCESS_DETACH;当一个DLL处理该值时,应当执行进程的清除工作
    • 如果DLL因为进程的终止而解除映射,则调用ExitProcess函数的线程将负责执行DllMain函数的代码
    • 如果DLL因为进程中的线程调用FreeLibrary或FreeLibraryAndExitThread函数而解除映射,则调用函数的线程将负责执行DllMain函数的代码。如果使用FreeLibrary,则线程并不立刻从该调用中返回,直到DllMain函数执行完DLL_PROCESS_DETACH之后
    • 一个DLL可以阻止进程的终止。例如,在DllMain函数接收到DLL_PROCESS_DETACH通知时,它只要进入到一个无限循环中,DllMain就无法返回,进程就不会结束
    • 如果一个进程因为某些线程调用TerminateProcess而终止,则系统将不以DLL_PROCESS_DETACH为参数调用DllMain函数。这意味着任何映射到进程地址空间的DLL,在进程终止前都没有机会来执行任何清除操作。这可能会导致数据丢失。因此,只有在迫不得已时才使用该函数。
      动态链接库_第2张图片
      **线程调用FreeLibrary函数时系统的执行步骤**
  11. 显示加载DLL的EXE运行过程:
    1. 创建新进程,系统为进程分配虚拟地址空间
    2. 将EXE和DLL文件映像映射到进程地址空间(包括EXE和DLL导入部分的DLL)
    3. 创建主线程,使用该线程并以DLL_PROCESS_ATTCH为参数调用每个DllMain以初始化DLL
    4. 若有DllMain调用失败,系统将终止进程,将所有的文件映像从地址空间中删除,并向用户显示一个消息框,说明进程无法启动;若所有的DllMain都调用成功,则执行模块的入口函数(main、wmain、WainMain等)
  12. DLL_THREAD_ATTCH通知:
    • 当在进程中创建一个线程时,系统将检查当前映射到进程地址空间的所有DLL的文件映像,并以DLL_THREAD_ATTCH为参数调用每个DLL的DllMain函数。新创建的线程负责执行DLL的所有DllMain函数中的代码。只有当所有的DLL都有机会处理该通知时,系统才允许新线程开始执行它的线程函数
    • 当把一个新创建的DLL映射到其地址空间时,如果进程内已经有一些线程在运行,则对于任何存在的线程而言,系统将不以DLL_THREAD_ATTCH为参数来调用DllMain函数。只有当DLL在新线程创建时映射到进程的地址空间,系统才会使用DLL_THREAD_ATTCH为参数调用DLL的DllMain函数
    • 对于进程的主线程而言,系统也不会以DLL_THREAD_ATTCH为参数来调用任何DllMain函数。当初次激活进程时,任何映射到进程地址空间的DLL都将接收到DLL_PROCESS_ATTCH通知,而不是DLL_THREAD_ATTCH通知
  13. DLL_THREAD_DETACH通知:
    • 终止线程运行的首选方法是使其线程函数返回。这将导致系统调用ExitThread函数来结束线程。ExitThread告诉系统该线程想要结束,但是系统并不立即结束该线程,而是获取将要结束的线程,并依据DLL_THREAD_DETACH通知,使其调用所有映射的DLL的DllMain函数。该通知告诉所有的DLL以执行各线程的清除操作
    • DLL可以阻止线程的终止。例如,当DllMain函数接收到DLL_THREAD_DETACH通知,它可以进入到一个无限循环中,DllMain就不会返回
    • 如果一个线程因为系统中的某个线程调用TerminateThread而终止,则系统将不以DLL_THREAD_DETACH为参数来调用DllMain函数。这意味着任何映射到进程地址空间的DLL,在进程终止前都没有机会来执行线程清除操作。这可能会导致数据丢失。只有在迫不得已的情况下才使用TerminateThread函数
  14. 可以调用DisableThreadLibraryCalls来使系统不将DLL_THREAD_ATTCH和DLL_THREAD_DETACH通知发送给特定的DllMain函数
  15. DllMain与互斥对象:
    • 当创建一个进程时,系统同时创建了一个互斥对象。每个进程都有它自己的互斥对象——多个进程间不共享互斥对象。当线程调用映射到进程地址空间DLL的DllMain函数时,该互斥对象将一个进程的所有线程同步
    • 当调用CreateThread函数时,系统将首次创建线程内核对象以及线程的栈。然后,它在内部调用WaitForSingleObject函数,传递进程互斥对象的句柄。一旦新线程拥有了互斥对象,系统将使新线程用DLL_THREAD_ATTCH去调用每个DLL的DllMain函数。只有在这时,系统才调用ReleaseMutex以撤销进程的互斥对象的所有权
  16. DllMain和C/C++运行时库:
    • 在默认情况下,当使用微软的链接程序并指定了/DLL开关时,链接程序假定入口函数为__DllMainCRTStartup。该函数包含在C/C++的运行时库文件中,并且在链接DLL时被静态链接到DLL的文件映像中。(即使使用C/C++运行时库的版本,该函数也是静态链接的)
    • 当把DLL文件映像映射到进程的地址空间时,系统实际上是调用这个__DllMainCRTStartup函数,而不是调用DllMain函数。__DllMainCRTStartup函数初始化C/C++的运行时库,并且确保当__DllMainCRTStartup函数接收到DLL_PROCESS_ATTCHA通知时,创建任何全局或者静态的C++对象。当所有C/C++运行时库的初始化都完成后,__DllMainCRTStartup函数将调用DllMain函数
    • 当DLL接收到一个DLL_PROCESS_DETACH通知时,系统将再次调用__DllMainCRTStartup函数。这一次,该函数调用DllMain函数,当DllMain返回时,__DllMainCRTStartup函数将为DLL的任何全局或者静态C++对象调用析构函数
  17. 如果在DLL的源代码中没有实现自己的DllMain函数,则默认使用C/C++运行时库的DllMain函数;C/C++运行时库不关心DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知;它调用了DisableThreadLibraryCalls
  18. 延迟加载DLL:
    • 一个延迟加载的DLL是这样一个DLL,它被隐式链接,但实际上并没有加载,直到代码试图引用包含在DLL中的一个符号时才加载
    • 延迟加载DLL有用的场合:
      1. 如果应用程序使用了若干个DLL,则其初始化的时间可能会比较长,因为加载程序要将所有需要的DLL都映射到进程的地址空间中。缓解此问题的一个办法是在进程的执行过程中,分开加载DLL。延迟加载DLL可以更加方便的实现此方法
      2. 如果在代码中调用一个新函数,并且试图在不包含该函数的旧版本的系统上运行应用程序,则加载程序将报错,并且不允许程序继续执行。需要一种方法以允许应用程序执行,如果发现(在运行时)应用程序运行在一个旧版本系统上,则不用调用缺少的函数。

你可能感兴趣的:(动态链接库)