DLL/OCX文件的注册与数据执行保护DEP
注册/反注册dll或ocx文件时,无论是用regsvr32还是dllregisterserver/dllunregisterserver,可能会遇到【内存位置访问无效】的问题:
此时把操作系统的数据执行保护(data execution prevention,下称dep)彻底关掉,重启,应该就能解决问题。操作:
- nt6.x系统:运行 bcdedit /set nx alwaysoff
- nt5.x系统:修改 %systemdrive%\boot.ini 文件,将当前操作系统条目的/noexecute参数的值改为alwaysoff,若没有则添加。若是多系统,要注意修改到正确的条目
本文主要是讨论,作为开发者,当需要在自己的程序中注册dll时(反注册的情况一样,下文只拿注册说事,其实适用于所有受dep影响的问题),如何避免改动系统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的开闭情况。
小结:
- 系统dep有4种,除了alwayson,其余3种都可以让(或已经是)自己的程序处于dep关闭状态;
- 系统dep可以用getsystemdeppolicy api获取,没有发现用于设置的api;
- 进程dep可以用getprocessdeppolicy获取,同时该函数还返回可否修改dep的信息,如果是可修改,则可以用setprocessdeppolicy更改自身dep状态;
- 一个简易的dephelper分享给大家:
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,我们往往会遇到来自另一方面的原因。
来自pe标记的原因
如果系统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 /nxcompat:no xxx.exe
如果安装vs时没装c++相关的东西,那你的电脑可能没有这个工具,可以选择把c++组件装上,或搜单独的vc build tools装上。
说回dll注册
上面说过如果是调用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就会遇到问题。
-文毕-