因此,我编写了以下代码:
using (var serviceController = new ServiceController(serviceName))
{
var serviceHandle = serviceController.ServiceHandle;
using (failureActionsStructure.Lock())
{
success = NativeMethods.ChangeServiceConfig2W(
serviceHandle,
ServiceConfigType.SERVICE_CONFIG_FAILURE_ACTIONS,
ref failureActionsStructure);
if (!success)
throw new Win32Exception();
}
}
P/Invoke声明如下:
[DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool ChangeServiceConfig2W(IntPtr hService, ServiceConfigType dwInfoLevel, ref SERVICE_FAILURE_ACTIONSW lpInfo);
ServiceConfigType
只是enum
,此特定成员的值为2。SERVICE_FAILURE_ACTIONSW
结构定义如下:[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct SERVICE_FAILURE_ACTIONSW
{
public int dwResetPeriod;
public string lpRebootMsg;
public string lpCommand;
public int cActions;
public IntPtr lpsaActionsPtr;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
public SC_ACTION[] lpsaActions;
class DataLock : IDisposable
{
IntPtr _buffer;
public DataLock(ref SERVICE_FAILURE_ACTIONSW data)
{
int actionStructureSize = Marshal.SizeOf(typeof(SC_ACTION));
// Allocate a buffer with a bit of extra space at the end, so that if the first byte isn't aligned to a 64-bit
// boundary, we can simply ignore the first few bytes and find the next 64-bit boundary.
_buffer = Marshal.AllocHGlobal(data.lpsaActions.Length * actionStructureSize + 8);
data.lpsaActionsPtr = _buffer;
// Round up to the next multiple of 8 to get a 64-bit-aligned pointer.
if ((data.lpsaActionsPtr.ToInt64() & 7) != 0)
{
data.lpsaActionsPtr += 8;
data.lpsaActionsPtr -= (int)((long)data.lpsaActionsPtr & ~7);
}
// Copy the data from lpsaActions into the buffer.
IntPtr elementPtr = data.lpsaActionsPtr;
for (int i=0; i < data.lpsaActions.Length; i++, elementPtr += actionStructureSize)
Marshal.StructureToPtr(data.lpsaActions[i], elementPtr, fDeleteOld: false);
}
public void Dispose()
{
Marshal.FreeHGlobal(_buffer);
}
}
internal IDisposable Lock()
{
return new DataLock(ref this);
}
}
(此类型在结构的末尾有一个额外的成员,它没有出现在 native 结构定义
lpsaActions
中,这简化了此结构的使用,并且仅导致在末尾编码了额外的数据-底层的API只会忽略它,因为它假定该结构已在内存中结束。)SC_ACTION
的定义如下:[StructLayout(LayoutKind.Sequential)]
struct SC_ACTION
{
public SC_ACTION_TYPE Type;
public int Delay;
}
..and
SC_ACTION_TYPE
是一个简单的enum
:enum SC_ACTION_TYPE
{
SC_ACTION_NONE = 0,
SC_ACTION_RESTART = 1,
SC_ACTION_REBOOT = 2,
SC_ACTION_RUN_COMMAND = 3,
}
我传入的结构是这样初始化的:
var failureActionsStructure =
new SERVICE_FAILURE_ACTIONSW()
{
dwResetPeriod = 60000, // 60 seconds
lpRebootMsg = "",
lpCommand = "",
cActions = 6,
lpsaActions =
new SC_ACTION[]
{
new SC_ACTION() { Type = SC_ACTION_TYPE.SC_ACTION_RESTART /* 1 */, Delay = 5000 /* 5 seconds */ },
new SC_ACTION() { Type = SC_ACTION_TYPE.SC_ACTION_RESTART /* 1 */, Delay = 15000 /* 15 seconds */ },
new SC_ACTION() { Type = SC_ACTION_TYPE.SC_ACTION_RESTART /* 1 */, Delay = 25000 /* 25 seconds */ },
new SC_ACTION() { Type = SC_ACTION_TYPE.SC_ACTION_RESTART /* 1 */, Delay = 35000 /* 35 seconds */ },
new SC_ACTION() { Type = SC_ACTION_TYPE.SC_ACTION_RESTART /* 1 */, Delay = 45000 /* 45 seconds */ },
new SC_ACTION() { Type = SC_ACTION_TYPE.SC_ACTION_NONE /* 0 */, Delay = 0 /* immediate, and this last entry is then repeated indefinitely */ },
},
};
当我在64位进程中运行此代码时,它工作正常。但是,当我在32位进程中运行它时(实际上,我只在32位Windows安装上测试了32位进程-我不确定在64位的32位进程中会发生什么Windows安装),我总是会收到
ERROR_INVALID_HANDLE
。我做了一些挖掘。
ChangeServiceConfig2W
API函数使用stdcall
调用约定,这意味着当函数中的第一个操作码即将执行时,堆栈应包括:但是,当我将本地调试器附加到我的32位C#进程上并在
ChangeServiceConfig2W
的第一条指令(技术上来说是_ChangeServiceConfig2WStub@12
的第一条指令)上设置断点时,我发现堆栈包括:我用一个简单的C++应用程序确认,
DWORD
的第二个和第三个[ESP]
应该是服务句柄和常数2。我尝试了各种其他P/Invoke声明,包括对前两个参数使用int
而不是IntPtr
和ServiceConfigType
,但无法获得其他任何行为。最后,我将第三个参数
ref SERVICE_FAILURE_ACTIONSW
改为直接采用IntPtr
,并使用failureActionsStruct
手动将Marshal.StructureToPtr
编码为分配了Marshal.AllocHGlobal
的块。通过此声明,第一个和第二个参数现在可以正确编码。所以,我的问题是,我在最初声明
ChangeServiceConfig2W
函数的方式上做错了什么,这可能导致前两个参数无法正确编码?可能性似乎很小,但我无法避免我在这里遇到P/Invoke(特别是编码(marshal)处理程序)中实际错误的可能性。奇怪的是,
DWORD
值0x0000AFC8
是我传入的Delay
中的SC_ACTION
结构之一的45,000 ms failureActionsStructure
。但是,它是该实例的最后一个成员,并且堆栈中0x00000001
后的0x0000AFC8
不会是以下Type
。即使是这样,我也看不到是什么导致这些值专门写入P/Invoke调用的参数区域。如果将整个结构序列化到内存中的错误位置并覆盖堆栈的一部分,那会不会导致内存损坏并可能终止进程?我很迷惑。 :-)
最佳答案
我相信我已经知道发生了什么事。在我看来,这是P/Invoke编码(marshal)处理程序中的错误,但是如果Microsoft的官方说法是“此行为是设计使然”,我不会感到惊讶。
当您将数组配置为在结构中编码(marshal)处理时,您的选择会受到很大限制。据我所知,您可以将UnmanagedType.SafeArray
用于任何VARIANT
基本类型,也可以使用UnmanagedType.ByValArray
,这需要您指定SizeConst
,即编码(marshal)编码(marshal)程序仅支持长度始终相同的嵌入式数组。
然后,Marshal.SizeOf
函数在计算结构的大小时会将数组的大小计算为SizeConst * Marshal.SizeOf(arrayElementType)
。不管实例指向的数组实际大小如何,情况总是如此。
该错误似乎是编码(marshal)拆封程序始终复制数组中的所有元素,即使该数字大于SizeConst
也是如此。因此,就像我的情况一样,如果将SizeConst
设置为1,但提供了一个包含6个元素的数组,那么它将基于Marshal.SizeOf
分配内存,后者为数组数据分配一个插槽,然后继续编码所有6个元素,写过去缓冲区的结尾和损坏的内存。
仅通过编码(marshal)处理程序在堆栈上为该序列化分配内存的原因,才能解释用于参数的堆栈插槽损坏的原因。通过溢出该缓冲区的末尾,它随后将数据覆盖整个堆栈,包括最终写入返回地址的位置以及已经放入堆栈中其插槽中的前两个参数。完成此编码(marshal)处理操作后,它随后将指向该堆栈缓冲区的指针写入第三个参数,解释了为什么第三个参数的值实际上是指向数据结构的有效指针。
我很幸运,使用我的特殊配置,第6个元素的结尾发生在破坏堆栈中其他元素之前,因为只有ChangeServiceConfig2W
的前两个参数被破坏了-代码能够在ChangeServiceConfig2W
返回后继续执行关于句柄无效的错误。对于较大的数组,或更简单的函数(编码(marshal)处理程序分配的缓冲区更靠近堆栈帧的末尾),它很可能会覆盖堆栈上方的重要数据,并产生ExecutionEngineException
,如@GSerg所看到的。
在64位系统上,堆栈上必须有更多的可用空间-一方面,所有指针现在都为64位宽,以便将所有内容隔开。这样,在缓冲区末尾进行写操作并不会使堆栈更远,也不会破坏ChangeServiceConfig2W
的第一个或第二个参数。这就是该代码在初始测试中的工作方式,并且看起来是正确的。
我认为,这是编码(marshal)处理程序中的错误;它具有足够的信息来避免损坏内存(只是不要编码超过SizeConst
元素,因为这就是您已分配的所有内存!),但是它继续进行,并且无论如何都会写入已分配缓冲区的末尾。我可以看到相反的原理,即“如果您告诉marshaler SizeConst
为1,则不要提供包含1个以上元素的数组。”但是在我阅读的任何文档中都没有明确警告说这样做会破坏执行环境。鉴于.NET为避免这种损坏所花费的时间,我不得不将其视为编码(marshal)程序中的错误。
我已经通过更新DataLock
类使代码工作,该类临时准备将lpsaActions
数据作为指向数组的指针(ChangeServiceConfig2W
需要,并且默认的P/Invoke编码(marshal)拆收器似乎不支持),以存放真实的lpsaActions
数组并将其替换为虚拟的1元素数组。这样可以防止编码(marshal)处理程序编码(marshal)1个以上的元素,并且不会发生内存损坏。当DataLock
对象为Dispose
d时,它将lpsaActions
恢复为其先前的值。
我将这个(有效的)代码与一个基本相同的C++版本放入了一个公共(public)GitHub存储库中,该版本用于比较在诊断期间代码流进入ChangeServiceConfig2W
函数时寄存器和堆栈的状态:
要重现我看到的问题,请从
WindowsAPI/SERVICE_FAILURE_ACTIONSW.cs
注释掉以下几行: // Replace the lpsaActions array with a dummy that contains only one element, otherwise the P/Invoke marshaller
// will allocate a buffer of size 1 and then write lpsaActions.Length items to it and corrupt memory.
_originalActionsArray = data.lpsaActions;
data.lpsaActions = new SC_ACTION[1];
然后,将程序作为32位进程运行。 (如果您使用的是64位操作系统,则可能需要调整构建配置,以使其指定“首选32位”或直接针对“x86”平台。)
Microsoft可能会也可能不会意识到并修复此错误。至少,Microsoft最好更新
UnmanagedType.ByValArray
的文档以包含有关这种可能情况的警告-我不确定如何将其传达给他们。但是,考虑到当前的.NET版本具有此功能,我认为在将结构编码为非托管代码时,最好避免提供长度不完全等于SizeConst
的数组。 :-)
关于c# - P/调用错误或我做错了吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39282182/