P /调用错误或我做错了吗?

时间:2016-09-01 23:16:18

标签: c# windows winapi pinvoke win32-process

所以,我写了下面的代码:

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);
    }
}

(此类型在结构的末尾有一个额外的成员,它不会出现在本机结构定义lpsaActions中,这简化了这个结构的使用,并且只会导致额外的数据被编组在最后 - 底层API忽略的数据,因为它假定结构已经在内存中结束。)

SC_ACTION的定义如下:

[StructLayout(LayoutKind.Sequential)]
struct SC_ACTION
{
    public SC_ACTION_TYPE Type;
    public int Delay;
}

..而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位进程中发生了什么-bit Windows安装),我总是得到ERROR_INVALID_HANDLE

我做了一些挖掘。 ChangeServiceConfig2W API函数使用stdcall调用约定,这意味着当函数中的第一个操作码即将执行时,堆栈应包含:

  • (DWORD PTR)返回地址
  • (DWORD PTR)服务处理
  • (DWORD)服务配置类型(2)
  • (DWORD PTR)指向失败操作结构的指针

但是,当我将本机调试器连接到我的32位C#进程并在ChangeServiceConfig2W的第一条指令(技术上是_ChangeServiceConfig2WStub@12的第一条指令)上放置断点时,我发现堆栈包括:

  • (DWORD PTR)返回地址
  • (DWORD PTR)始终为值0x0000AFC8
  • (DWORD)值0x00000001,而不是预期的2
  • (DWORD PTR)实际上是指向失败动作结构的有效指针

我在一个简单的C ++应用程序中确认,DWORD 的第二个和第三个[ESP]应该是服务句柄和常量值2.我尝试了各种替代P /调用声明,包括使用int代替IntPtrServiceConfigType作为前两个参数,但无法获得任何其他行为。

最后,我更改了第三个参数ref SERVICE_FAILURE_ACTIONSW,直接取IntPtr,然后使用failureActionsStruct手动将Marshal.StructureToPtr编组到一个分配了{Marshal.AllocHGlobal的块中1}}。通过此声明,第一个和第二个参数现在可以正确编组。

所以,我的问题是,我是否以最初声明ChangeServiceConfig2W函数的方式做错了什么,这可以说明前两个参数没有正确编组?可能性似乎很小,但我无法摆脱我在P / Invoke(特别是封送器)中遇到实际错误的可能性。

奇怪的是,DWORD0x0000AFC8是来自Delay SC_ACTION内的failureActionsStructure个结构中的0x000000010x0000AFC8的45,000 ms。但是,它是该实例的最后一个成员,堆栈中Type后面的SC_ACTION不是以下{{1}}的{​​{1}}。即使它是,我也看不出会导致这些值专门写入P / Invoke调用的参数区域的原因。如果它将整个结构序列化到内存中的错误位置并覆盖部分堆栈,那么会不会导致内存损坏并可能终止进程?

我很困惑。 : - )

1 个答案:

答案 0 :(得分:1)

我相信我已经知道发生了什么。在我看来, P / Invoke封送程序中的一个错误,但如果微软的官方专线是,那我就不会感到惊讶,&#34;这种行为是设计的&#34;

当您在结构中配置要封送的数组时,您的选项会受到很大限制。据我所知,您可以将UnmanagedType.SafeArray用于任何VARIANT基元类型,也可以使用UnmanagedType.ByValArray,这需要您指定SizeConst - 即,编组程序仅支持总长度完全相同的嵌入式数组。

Marshal.SizeOf函数,在计算结构大小时,将数组的大小计为SizeConst * Marshal.SizeOf(arrayElementType)。无论实例指向的数组的实际大小如何,情况总是如此。

该错误似乎是封送程序始终复制数组中的所有元素,即使该数字大于SizeConst。所以,在我的情况下,如果你将SizeConst设置为1,但是提供一个包含6个元素的数组,那么它会根据Marshal.SizeOf分配内存,它为数组数据分配一个插槽,然后继续编组所有6个元素,写入缓冲区的末尾并破坏内存。

参数的堆栈槽以这种方式被破坏的原因只能由编组器为堆栈上的序列化分配内存来解释。通过溢出该缓冲区的末尾,它然后在堆栈中进一步覆盖数据,包括最终写入返回地址的位置以及已经放入堆栈中的插槽的前两个参数。在这个封送操作之后,它然后将指向该堆栈缓冲区的指针写入第三个参数,解释了为什么第三个参数的值实际上是指向数据结构的有效指针。

我很幸运,在我的特定配置下,第6个元素的结尾发生在腐蚀堆栈中的其他元素之前,因为只有ChangeServiceConfig2W的前两个参数被损坏 - 代码能够在ChangeServiceConfig2W返回有关句柄无效的错误后继续执行。使用更大的数组或更简单的函数,其中由编组器分配的缓冲区更接近堆栈帧的末尾,它可能很好地覆盖堆栈中的重要数据,并导致ExecutionEngineException为@GSerg锯。

在64位系统上,堆栈上必须有更多的可用空间 - 首先,所有指针现在都是64位宽,这样就可以解决问题。然后,写入缓冲区的末尾,并没有在堆栈中向上移动,并且没有设法破坏ChangeServiceConfig2W的第一个或第二个参数。这是这段代码在初始测试中的工作方式,看起来是正确的。

在我看来,这是封送者的一个错误;它有足够的信息来避免破坏内存(只是不要编组超过SizeConst个元素,因为这是你分配的所有内存!),但它会继续并写入超过{无论如何,分配的缓冲区。我可以看到相反的哲学,即&#34;如果你告诉编组者SizeConst是1,那么就不要提供一个包含1个以上元素的数组。&#34;但是没有明确的警告,这样做会破坏我阅读的任何文档中的执行环境。考虑到.NET为避免这种类型的腐败所需的长度,我不得不将其视为封送器中的一个错误。

我已经通过更新DataLock类来使我的代码正常工作,该类暂时准备lpsaActions数据作为指向数组的指针(ChangeServiceConfig2W需要和默认的P / Invoke marshaler似乎不支持),隐藏真实的lpsaActions数组并用虚拟的1元素数组替换它。这可以防止封送处理器编组超过1个元素,并且不会发生内存损坏。当DataLock对象为Dispose d时,它会将lpsaActions恢复为之前的值。

我已经将这个(工作)代码放入一个公共GitHub仓库,以及一个基本相同的C ++版本,我用它来比较寄存器和堆栈的状态,因为代码流在诊断过程中输入了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位操作系统,那么您可能需要调整构建配置,以便指定&#34;首选32位&#34;或者定位&#34; x86&#34;平台直接。)

Microsoft可能会也可能不会发现并修复此错误。至少,微软更新UnmanagedType.ByValArray的文档以包含有关这种可能情况的警告是件好事 - 我不确定如何向他们传达这些信息。鉴于.NET的当前版本具有它,我认为最好在将结构编组为非托管代码时避免提供长度不完全等于SizeConst的数组。 : - )