注册/反注册dll或ocx文件时,无论是用regsvr32还是DllRegisterServer/DllUnregisterServer,可能会遇到【内存位置访问无效】的问题:
此时把操作系统的数据执行保护(Data Execution Prevention,下称DEP)彻底关掉,重启,应该就能解决问题。操作:
bcdedit /set nx alwaysoff
本文主要是讨论,作为开发者,当需要在自己的程序中注册dll时(反注册的情况一样,下文只拿注册说事,其实适用于所有受DEP影响的问题),如何避免改动系统DEP,避免重启地把问题解决掉。其实这个问题的关键是,执行注册的进程是否启用了DEP,启用就不能注册,关闭就能,跟系统DEP没有直接关系,但进程DEP受系统DEP的影响。
系统DEP策略有4种,每种策略下对进程DEP的影响如下(注意,64位程序总是启用DEP,且不可禁用,不论系统DEP如何设置。所以下表和接下来说的都是32位程序的情况):
系统DEP策略 | 进程默认DEP | 能否更改进程DEP |
---|---|---|
OptIn (仅为基本的Windows程序和服务启用DEP;默认策略) | 关闭 | 允许更改 |
OptOut (除指定的程序外,全部启用DEP) | 开启 | 允许更改 |
AlwaysOn (全部启用DEP) | 开启 | 不允许更改 |
AlwaysOff (全部禁用DEP) | 关闭 | 不允许更改 |
我们的目的是关闭进程DEP。可以看到,系统DEP为OptIn和AlwaysOff时,进程DEP已经是默认关闭的。其余两种情况中,OptOut虽然是默认开启DEP,但它允许被关闭,所以这是一种可以抢救的情况,唯独AlwaysOn不可抢救,必须修改系统DEP策略并重启。
插播一下,系统默认的策略是OptIn,这种策略下普通程序的DEP是关闭的,但是系统程序例外,所以在程序中调用regsvr32进行注册会失败,因为实际执行注册的进程是regsvr32而非自己的程序,而regsvr32是系统程序,它在OptIn下是会被开启DEP的。可以在任务管理器进程页面中添加【数据执行保护】列,以呈现进程DEP的开闭情况。
小结:
1 using System;
2 using System.ComponentModel;
3 using System.Runtime.InteropServices;
4
5 namespace AhDung.Win32
6 {
7 /// <summary>
8 /// 数据执行保护(DEP)辅助类
9 /// </summary>
10 public static class DepHelper
11 {
12 /// <summary>
13 /// 获取进程DEP状态
14 /// </summary>
15 /// <param name="hProcess">进程句柄</param>
16 /// <param name="permanently">是否永久。该参数同时指示了是否可以更改进程DEP,为true表示不可修改</param>
17 public static ProcessDepFlag GetProcessDepPolicy(IntPtr hProcess, out bool permanently)
18 {
19 if (!GetProcessDEPPolicy(hProcess, out var lpFlags, out permanently))
20 {
21 throw new Win32Exception();
22 }
23
24 return (ProcessDepFlag)lpFlags;
25 }
26
27 /// <summary>
28 /// 设置进程DEP状态
29 /// </summary>
30 public static void SetProcessDepPolicy(ProcessDepFlag flag)
31 {
32 if (!Enum.IsDefined(typeof(ProcessDepFlag), flag))
33 {
34 throw new InvalidEnumArgumentException();
35 }
36
37 if (!SetProcessDEPPolicy((int)flag))
38 {
39 throw new Win32Exception();
40 }
41 }
42
43 /// <summary>
44 /// 获取系统DEP状态
45 /// </summary>
46 [DllImport("Kernel32.dll", EntryPoint = "GetSystemDEPPolicy")]
47 public static extern SystemDepPolicyType GetSystemDepPolicy();
48
49 [DllImport("kernel32.dll", SetLastError = true)]
50 static extern bool SetProcessDEPPolicy(int dwFlags);
51
52 [DllImport("kernel32.dll", SetLastError = true)]
53 static extern bool GetProcessDEPPolicy(IntPtr hProcess, out int lpFlags, out bool lpPermanent);
54 }
55
56 public enum SystemDepPolicyType
57 {
58 AlwaysOff,
59 AlwaysOn,
60 OptIn,
61 OptOut
62 }
63
64 public enum ProcessDepFlag
65 {
66 Disabled,
67 PROCESS_DEP_ENABLE,
68 PROCESS_DEP_DISABLE_ATL_THUNK_EMULATION
69 }
70 }
基本上,由于系统默认策略是OptIn,我们的32程序按说不太会遇到DEP的问题,但事情并没有这么简单,尤其作为.Neter,我们往往会遇到来自另一方面的原因。
如果系统DEP已经是OptIn或OptOut,程序也是32位,根据上面的表格,按说程序DEP应该是处于关闭或可以关闭的状态,但它偏偏处于开启状态,并且不可更改。这是因为,程序DEP除了会受系统DEP影响外,还会受程序文件PE头中的一个标记影响。
这个标记叫IMAGE_DLLCHARACTERISTICS_NX_COMPAT,有些地方叫NX Compatible。
标记的意思大概是标识该PE文件是否是DEP兼容,这里不必深究该标记的明确意义及其所在位置,只要知道拥有该标记的exe程序,会在除AlwaysOff外的情况下开启DEP且不可关闭,而据称在.net framework 2.0 SP1后,.net的编译器就会为生成的PE文件打上该标记,导致今天的.net程序,相信大多都是携带该标记的。
VC构建工具中有个叫editbin.exe的命令行工具,可以简单移除该标记:
editbin.exe /NXCOMPAT:NO xxx.exe
如果安装VS时没装c++相关的东西,那你的电脑可能没有这个工具,可以选择把c++组件装上,或搜单独的vc build tools装上。
上面说过如果是调用regsvr32来注册的,那就算搞掂程序的DEP也无济于事,需要考虑用regsvr32以外的办法进行注册,比如直接调用dll的DllRegisterServer函数,事实上regsvr32也应该是这么搞。下面是c#版的实现:
1 using System;
2 using System.ComponentModel;
3 using System.Runtime.InteropServices;
4
5 namespace AhDung.Win32
6 {
7 /// <summary>
8 /// COM组件注册辅助类
9 /// </summary>
10 public static class ComRegHelper
11 {
12 /// <summary>
13 /// 反注册
14 /// </summary>
15 public static void UnRegister(string file)
16 {
17 Register(file, false);
18 }
19
20 /// <summary>
21 /// 注册或反注册
22 /// </summary>
23 /// <param name="file">文件路径</param>
24 /// <param name="isRegister">true为注册,false为反注册</param>
25 /// <remarks>抄于https://limbioliong.wordpress.com/2011/08/11/programmatically-register-com-dlls-in-c/</remarks>
26 public static void Register(string file, bool isRegister = true)
27 {
28 file = Environment.ExpandEnvironmentVariables(file);
29
30 var hModuleDLL = LoadLibrary(file);
31
32 if (hModuleDLL == IntPtr.Zero)
33 {
34 throw new Win32Exception();
35 }
36
37 try
38 {
39 // Obtain the required exported API.
40 var pExportedFunction = GetProcAddress(hModuleDLL, isRegister
41 ? "DllRegisterServer"
42 : "DllUnregisterServer");
43
44 if (pExportedFunction == IntPtr.Zero)
45 {
46 throw new Win32Exception();
47 }
48
49 // Obtain the delegate from the exported function, whether it be
50 // DllRegisterServer() or DllUnregisterServer().
51 var pDelegateRegUnReg =
52 (DllRegUnRegAPI)Marshal.GetDelegateForFunctionPointer(pExportedFunction, typeof(DllRegUnRegAPI));
53
54 // Invoke the delegate.
55 var hResult = pDelegateRegUnReg();
56
57 if (hResult != 0)
58 {
59 throw new Win32Exception();
60 }
61 }
62 finally
63 {
64 FreeLibrary(hModuleDLL);
65 }
66 }
67
68 // All COM DLLs must export the DllRegisterServer()
69 // and the DllUnregisterServer() APIs for self-registration/unregistration.
70 // They both have the same signature and so only one
71 // delegate is required.
72 [UnmanagedFunctionPointer(CallingConvention.StdCall)]
73 delegate uint DllRegUnRegAPI();
74
75 [DllImport("Kernel32.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
76 static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string strLibraryName);
77
78 [DllImport("Kernel32.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
79 static extern int FreeLibrary(IntPtr hModule);
80
81 [DllImport("Kernel32.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
82 static extern IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);
83 }
84 }
如果不关DEP,LoadLibrary就会遇到问题。
-文毕-