64 位 Windows API 结构对齐导致命名管道出现拒绝访问错误

64-bit Windows API struct alignment caused Access Denied error on named pipe

我花了两天时间,但我终于缩小了 ERROR_ACCESS_DENIED (5) 尝试 CallNamedPipe 结构对齐问题时错误的来源。我们有一个 32 位服务和一个 32 位应用程序,我正在尝试将该服务更新为 64 位服务。奇怪的是,在 32 位模式下一切正常,但在 64 位模式下,32 位应用程序 CallNamedPipe 报告拒绝访问错误。

该服务已经在设置 ​​SECURITY_ATTRIBUTES 结构并使用正确初始化的 PSECURITY_DESCRIPTOR 填充 lpSecurityDescriptor 成员。并且在传递给 CreateNamedPipe 时没有报告任何错误。我仍然不知道为什么它不报告错误;也许不良的安全属性会默默地回退到默认值而不是失败。

经过多次折腾(包括一些早期的 incomplete/incorrect 尝试更改结构对齐 - 调试服务启动代码并不容易),我开始意识到将默认结构对齐设置为 1 字节的项目设置 ( /Zp1) 引起了问题。当我最终在所有出现的 #include <windows.h>#pragma pack(pop) 之后使用 #pragma pack(push,8) 时,事情开始起作用了。

我现在的问题是为什么这是必要的。我看到 Windows API 中有许多头文件通过包括 pshpack1.hpshpack2.hpshpack4.hpshpack8.h 显式设置结构对齐和 poppack.h。我怎么知道什么时候 Windows API 控制自己的打包,什么时候我有责任设置正确的打包级别?每个 Windows API 关心结构对齐的声明不应该设置正确的包装所以我不必筛选系统中的所有代码以查找包括 Windows [=53 在内的任何内容=] 头文件?一种替代方法是更改​​项目设置以使用默认结构对齐,但我必须假设这样做是因为我们的系统中依赖 1 字节结构对齐的代码比我们依赖 Windows API.

这是服务器端代码的样子:

BOOL OpenMyPipe()
{
   SECURITY_ATTRIBUTES sa;
   PSECURITY_DESCRIPTOR pSD;

   printf("sizeof(SECURITY_ATTRIBUTES) == %d\n", sizeof(SECURITY_ATTRIBUTES));
   pSD = (PSECURITY_DESCRIPTOR)GlobalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH);
   if (pSD == NULL)
      return FALSE;

   if (!InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION))
      return FALSE;

   if (!SetSecurityDescriptorDacl(pSD, TRUE, (PACL)NULL, FALSE))
      return FALSE;

   sa.nLength = sizeof(sa);
   sa.lpSecurityDescriptor = pSD;
   sa.bInheritHandle = FALSE;

   char szPipeName[MAX_PATH];
   sprintf(szPipeName, "\\.\pipe\%s%s", "__SQLTST_",
      "MAINMR");

   hPipe = CreateNamedPipe(szPipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
      PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
      1, 0, 0, NMPWAIT_WAIT_FOREVER, &sa);

   if (hPipe == INVALID_HANDLE_VALUE)
      return FALSE;
   return TRUE;
}

为了简单起见,我用一个小 VB.NET 客户端验证了这一点:

Sub Main()
  Dim pipes = System.IO.Directory.GetFiles("\.\pipe\")

  Using pipe As New System.IO.Pipes.NamedPipeClientStream(".", "__SQLTST_MAINMR")
     Dim message(16) As Byte
     pipe.Connect(3000)
     Array.Copy(BitConverter.GetBytes(Process.GetCurrentProcess().Id), message, 4)
     pipe.Write(message, 0, 16)
  End Using
End Sub

我相信只有当服务器端代码是 运行 在不同的帐户(如 SYSTEM 帐户)下时才会发生错误。不过,我不知道如何轻松地测试它。我所知道的是,上面的代码 not 即使没有 SECURITY_ATTRIBUTES 设置也会失败,当它全部 运行 在与常规应用程序代码相同的帐户下时。另外,当然,你必须在服务器代码中将结构对齐设置为1字节才能看到错误。

Windows SDK 要求打包为 8 个字节。来自 Using the Windows Headers

Projects should be compiled to use the default structure packing, which is currently 8 bytes because the largest integral type is 8 bytes. Doing so ensures that all structure types within the header files are compiled into the application with the same alignment the Windows API expects. It also ensures that structures with 8-byte values are properly aligned and will not cause alignment faults on processors that enforce data alignment.

这是确保数据结构按系统预期对齐所必需的。我怀疑不明确这样做的原因是他们想要默认值,所以 why ask for anything else。改变包装的情况相对较少,应仅限于特定情况。如果 Microsoft 在每个头文件中添加 #pragma pack(push,8),他们将隐含地表示更改对齐方式是正常的。

未对齐的结构保存 space,但会降低性能,因为访问未对齐的成员时会生成 alignment faults

Windows SDK 确实会出于多种原因更改结构的对齐方式。一种可能适用于需要读取 32 位或 64 位数据结构的文件格式。例如,可以使用 IMAGE_THUNK_DATA64IMAGE_THUNK_DATA32 读取 PE 文件格式。前者需要 8 字节填充,而后者需要 4 字节填充。类似地,Wininet.h 将根据它是为 32 位代码还是 64 位代码编译而不同地打包数据结构。这些是包装上的合法更改,但有特定原因。