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

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

(此类型在结构的末尾有一个额外的成员,它没有出现在 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调用约定,这意味着当函数中的第一个操作码即将执行时,堆栈应包括:
  • (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/Invoke声明,包括对前两个参数使用int而不是IntPtrServiceConfigType,但无法获得其他任何行为。

    最后,我将第三个参数ref SERVICE_FAILURE_ACTIONSW改为直接采用IntPtr,并使用failureActionsStruct手动将Marshal.StructureToPtr编码为分配了Marshal.AllocHGlobal的块。通过此声明,第一个和第二个参数现在可以正确编码。

    所以,我的问题是,我在最初声明ChangeServiceConfig2W函数的方式上做错了什么,这可能导致前两个参数无法正确编码?可能性似乎很小,但我无法避免我在这里遇到P/Invoke(特别是编码(marshal)处理程序)中实际错误的可能性。

    奇怪的是,DWORD0x0000AFC8是我传入的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函数时寄存器和堆栈的状态:

  • https://github.com/logiclrd/TestServiceFailureActionChange

  • 要重现我看到的问题,请从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/

    相关文章:

    c# - 结构图和通用类型

    windows - Aptana Studio 3 中的 Git 存储库在哪里?

    c - 在Windows上用C编写自修改代码,无法编写补丁

    winapi - 如何获得英文的 boost::system::error_code::message?

    c# - Entity Framework - 迁移 - 代码优先 - 每次迁移播种

    c# - ReadOnlyCollection 类是不良设计的好例子吗?

    c# - 通过 protobufnet 从 Redis 反序列化大量用户定义对象时出现性能问题

    Windows内存管理: check if a page is in memory

    windows - 为什么在 64 位 Windows 上 64 位 DLL 转到 System32,而 32 位 DLL 转到 SysWoW64?

    windows - TerminateProcess() 返回 EINVAL