0%

Windows原理与应用(四)——动态链接库

动态链接库(Dynamic Link Library)

关于动态链接库

链接

大多数高级语言都支持分别编译(compiling),程序员可以显式地把程序划分为独立的模块或文件,然后由编译器(compiler)对每个独立部分分别进行编译。在编译之后,由链接器(Linker)把这些独立编译单元链接(Linking)到一起

静态链接

在程序开发中,将各种目标模块(.OBJ)文件、运行时库(.LIB)文件,以及已编译的资源(.RES)文件链接在一起,以便创建Windows的.EXE文件,这种方式是静态链接

动态链接

在程序运行时,Windows把一个模块中的函数调用链接到库模块中的实际函数上的过程

二者区别

  • 静态链接会将相关的程序全部打包到最终的可执行文件中
  • 静态链接不会将程序打包到可执行文件中,而是在.exe执行时调用独立的.dll文件

dll地狱

  • dll地狱指因为系统文件被覆盖而让整个系统崩溃的现象
  • 例如多个系统程序依赖某个dll模块,而又有一些程序依赖该dll模块的新版本,而这个dll模块的新版本又不向下兼容,那么当你更新该dll模块后,则多个系统程序将无法工作
  • .Net平台中允许一个dll的多个版本同时在同一台机器上工作,从而解决了这一问题,该技术成为Side-by-Side[1]

Windows中主要的dll

名称 功能
KERNEL32.DLL 低级内核函数,用于内存管理、任务管理、资源控制等
USER32.DLL windows管理有关的函数,消息、菜单、光标、计时器、通信,钩子等
GDI32.DLL 图形设备接口库
ODBC32.DLL ODBC功能
Ws2_32.dll socket通信功能

dll中函数输入/输出参数(c#)

  • 传值
  • ref
  • out

其中,ref和out类似,但是ref需要自己赋初值,而out由dll负责赋值

dll的引用计数

  • DLL在内存中只有一个实例,系统为每个DLL维护一个线程级的引用计数,一旦一个线程载入了该DLL,引用计数将会加1。而程序终止或者引用计数变为0(仅指运行时动态链接库),DLL就会释放占用程序的虚地址空间。

Windows的虚地址映射

  • Windows将提供内部的地址映的工作,例如一个DLL文件被加载后在物理内存中只占一个固定区域,有多个进程使用同一个DLL文件,Windows将这个DLL的内存地址空间通过地址映射后提供给各个进程,进程代码地址与DLL映射后地址构成的是进程的虑地址空间,进程在自己的虚地址空间中好像是自己独自在使用这个DLL文件,使用DLL中的函数与程序自身的函数没有区别。

C#创建DLL类库(托管的动态链接库)

  • 一个没有main的.cs文件生成为dll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace csTestDLL
{
public class csDLLClass
{
double Pie = Math.PI;

public double PIE { get => Pie;}

public string say_hello()
{
return "Hello! This is csTestDLL.";
}

private double calc_power_two(double r)
{
return r * r;
}

public double calculate_circle_area(double r)
{
return Math.PI * calc_power_two(r);
}
}
}
  • 调用这个dll

image-20211207153830827

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Assembly csdll = Assembly.LoadFrom("csTestDLL.dll"); // 装载dll

public void say_hello_cs()
{
// 通过反射获取类型、函数、变量
foreach (Type t in csdll.GetTypes())
{
if (t.IsClass && !t.IsAbstract)
{
ConstructorInfo[] ci = t.GetConstructors(); // 获取构造列表
MethodInfo[] mis = t.GetMethods();// 获取函数列表
callback("构造函数:" + ci[0].ToString(), listname);
foreach (var item in mis)
{
callback("pulic函数:" + item.ToString(), listname);
}
mis = t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); // 一定要两个bindingflags同时有才能获取私有函数
foreach (var item in mis)
{
callback("private函数:" + item.ToString(), listname);
}
MethodInfo mi = t.GetMethod("say_hello"); // 调用say_hello函数
object obj = Activator.CreateInstance(t); // 创建实例
// Object obj = Activator.CreateInstance(typeof(String),new Object[] {13, 10 } );
// 有参构造的情况↑

string msg = mi.Invoke(obj, null) as String;
callback("welcome:" + msg, listname);
callback(split_line, listname);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void calc_circle_area(double r = 1)
{
foreach (Type t in csdll.GetTypes())
{
if (t.IsClass && !t.IsAbstract)
{
Type[] p = new Type[0]; // 无参构造
ConstructorInfo ci = t.GetConstructor(p); // 获取构造
object o = ci.Invoke(null); // 调用构造,和Activator.CreateInstance(t)效果是一样的
MethodInfo mi = t.GetMethod("calculate_circle_area"); // 获取计算面积函数
object[] para = new object[1] { r };
string msg = mi.Invoke(o, para).ToString();
callback("圆面积:" + msg, listname);
callback(split_line, listname);
}
}
}

image-20211207155521779

C++非托管动态链接库

头文件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "myHead.h"
#include "pch.h"


// 有def就不必写头文件了,没有def必须定义头文件,或者将导出定义在函数名上 ->
// extern "C" __declspec(dllexport) int __stdcall factorial(int n)
// 必须有__stdcall,否则调用报错


int __stdcall factorial(int n) {
int ans = 1;
while (n > 1) {
ans *= n;
n--;
}
return ans;
}
  • 关于__stdcall[2]
    • __stdcall是微软特有的修饰符
    • 它表示对这个程序要按照约定的方式调用,会将所有函数当做winapi调用
1
2
3
4
5
6
7
8
#pragma once

// 只是定义一个头并没有用,因为除了C/C++调用外,这个头并不会被编译
// 要将它放到pch.h头中才会编译

// __declspec(dllexport)声明导出函数
// extern "C" 用于非C/C++调用
extern "C" __declspec(dllexport) int __stdcall factorial(int n);
  • 调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// 自建cpp动态库
/// </summary>
[DllImport("../../../debug/cppTestDLL.dll")]
private static extern Int32 factorial(int n);


/// <summary>
/// c++动态库
/// </summary>
/// <param name="num"></param>
public void calc_factoria(int num)
{
callback("cppDLL开始调用", listname);
Int32 ans = factorial(num);
callback(num + "! = " + ans.ToString(), listname);
callback(split_line, listname);
}

使用def

1
2
3
4
5
LIBRARY library


EXPORTS
factorial

注意

1
2
3
4
5
[ DllImport( "kernel32.dll",EntryPoint="GetVersionEx" )] 
// your func name...

// 这种写法定义了EntryPoint,这样写的作用是可以自定义函数名
// 如果不写EntryPoint,则调用时的函数名必须为「GetVersionEx」