为何不根据链接的SDK来改变API行为?

The Old New Thing (Raymond Chen) 工具

摘要

本文以Windows的CoInitializeSecurity为例,探讨了根据链接的SDK版本改变API行为的陷阱。讨论了DLL版本不匹配和尾调用优化等问题使这种方法复杂化。

<p>不久前,我注意到<a title="CoInitializeSecurity函数要求绝对安全描述符" href="https://devblogs.microsoft.com/oldnewthing/20240902-00/?p=110201"><code>CoInitializeSecurity</code>函数要求绝对安全描述符</a>,尽管Windows中许多函数生成的是自相对安全描述符,迫使你执行相对到绝对的转换,即使函数内部只是将其从绝对转换回相对。</p> <p><a href="https://devblogs.microsoft.com/oldnewthing/20240902-00/?p=110201&amp;commentid=141890#comment-141890">评论者tbodt写道</a>,</p> <blockquote class="q"><p>这个似乎很容易修复,采用Apple的技术:当程序链接旧SDK时,函数保留旧行为。</p></blockquote> <p>这听起来确实简单。如果你的程序链接了新SDK,那么它会获得新行为——接受自相对安全描述符。但如果链接旧SDK,则保留旧行为——要求绝对安全描述符。如果你想获得新行为,就链接新SDK。</p> <p>但这会带来一个微妙问题:如果链接了错误的SDK,一切仍能构建,但结果不同。传统上,Windows SDK是向前兼容的:你可以将旧程序链接到新SDK,它运行完全一样,因为旧程序只使用新SDK的向后兼容子集。如果行为根据链接的SDK版本改变,那么用户可能不易察觉行为变化是由于升级了SDK库导致的。</p> <p>另外,如果一个程序链接了某个SDK版本,但它使用的DLL链接了不同版本的SDK呢?也许你使用的UI框架库没有更新到新SDK。或者你的程序使用的是旧SDK,但UI框架库使用了新SDK。是让主程序的SDK版本决定函数的行为,即使DLL期望的是不同的行为?可怜的DLL将调用<code>CoInitializeSecurity</code>,但它的行为不会像DLL预期的那样。</p> <p>好吧,也许你决定函数的行为不基于程序链接的SDK版本,而是基于调用DLL的版本。但函数如何知道是哪个DLL调用了它?你可能会说:“嗯,你可以查看返回地址属于哪个DLL。”但在尾调用优化的情况下,这不起作用。</p> <pre>// DLL中的某个函数 HRESULT InitializeWidgets( UINT maxWidgets, const WIDGET_ID* ownerId, PCWSTR ownerDescription, PCWSTR countainerName, PCWSTR containerDescription, COLORREF defaultColor, UINT defaultWidth, UINT defaultHeight, bool isRemoteAccessible, bool isPersistent) { ⟦ 各种初始化步骤 ⟧ static BYTE sd[] = { 0x01, ⟦ 硬编码值 ⟧ }; return CoInitializeSecurity(sd, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IDENTIFY, nullptr, EOAC_NONE, nullptr); } </pre> <p>对<code>CoInitializeSecurity</code>的最终调用可能会被优化为尾调用,此时子程序调用指令变为无条件跳转,返回地址是<code>InitializeWidgets</code>调用者的地址。如果<code>CoInitializeSecurity</code>探查其返回地址,它会检查错误DLL的SDK版本。</p> <p>反过来,如果DLL中的函数只是一个包装器呢?</p> <pre>HRESULT CoInitializeSecuritywithLogging( _In_opt_ PSECURITY_DESCRIPTOR pSecDesc, _In_ LONG cAuthSvc, _In_reads_opt_(cAuthSvc) SOLE_AUTHENTICATION_SERVICE* asAuthSvc, _In_opt_ void* pReserved1, _In_ DWORD dwAuthnLevel, _In_ DWORD dwImpLevel, _In_opt_ void* pAuthList, _In_ DWORD dwCapabilities, _In_opt_ void* pReserved3) { if (dwCapabilities &amp; EOAC_APPID) { LogUuid("CoInitializeSecurity with APPID", (UUID*)pSecDesc); } else if (dwCapabilities &amp; EOAC_ACCESS_CONTROL) { Log("CoInitializeSecurity with IAccessControl"); } else { LogSecurityDescriptor("CoInitializeSecurity with security descriptor", pSecDesc); } HRESULT hr = CoInitializeSecurity(pSecDesc, cAuthSvc, asAuthSvc, pReserved1, dwAuthnLevel, dwImpLevel, pAuthList, dwCapabilities, pReserved3); Log("CoInitializeSecurity returned", hr); } </pre> <p>如果查看返回地址,你会找到包装器函数,并改变行为以匹配构建该包装器函数的版本,但该包装器函数只是从调用者传递参数。实际上我们想要匹配的是调用者的行为,而不是包装器。</p> <p>另外,如果库是静态库而不是DLL呢?它针对一个SDK版本编写,但链接到另一个版本,行为发生变化,即使函数检查返回地址,它也会获取DLL的地址并看到DLL的SDK版本,而不是库期望的版本。</p> <p>根据链接的SDK版本改变行为只有在程序是单体的情况下才有效。</p> <p><b>附加讨论</b>:更新到新SDK的<em>头文件</em>确实会导致行为变化,例如,带有显式大小成员的结构可能会被扩展以包含其他字段,而API使用大小成员的值来决定调用者使用的是哪个SDK版本。但这并不依赖于调用者链接的SDK,这是一件好事,因为它允许你将使用不同SDK头文件版本的静态库链接到一起,形成一个程序或DLL,它们仍能正常工作。</p> <p>本文最初发布于<a href="https://devblogs.microsoft.com/oldnewthing/20260506-00/?p=112303">《为何不根据链接的SDK来改变API行为?》</a>(原文链接:<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>)。</p>
查看原文
查看缓存全文

缓存时间: 2026/05/16 03:32

# 为何不应让 API 行为依赖所链接的 SDK 版本? - 《老调重弹》 来源:https://devblogs.microsoft.com/oldnewthing/20260506-00?p=112303 不久前我曾指出,`CoInitializeSecurity` 函数要求使用绝对安全描述符(原文链接:https://devblogs.microsoft.com/oldnewthing/20240902-00/?p=110201),尽管 Windows 中许多函数生成的是自相关安全描述符,迫使你进行相对到绝对的转换,而该函数内部又将其从绝对转换回相对。 评论者 tbodt 写道(原文链接:https://devblogs.microsoft.com/oldnewthing/20240902-00/?p=110201&commentid=141890#comment-141890): > 这个问题似乎很容易解决,只需采用 Apple 的技巧:当程序链接旧版 SDK 时,函数保持旧行为。 这听起来确实简单。如果你的程序链接了新版 SDK,那么它就能获得接受自相关安全描述符的新行为。但如果它链接了旧版 SDK,则沿用要求绝对安全描述符的旧行为。如果你想要新行为,那就链接新版 SDK。 但这产生了一个微妙问题:如果你选错了要链接的 SDK 版本,一切仍能编译通过,但结果却不同了。传统上,Windows SDK 是向前兼容的:你可以拿一个旧程序去链接新版 SDK,它运行起来完全一样,因为旧程序只使用了新版 SDK 中向后兼容的子集。如果根据链接的 SDK 版本来改变行为,那么当你经历行为变化时,可能并不清楚这是否源于升级了 SDK 库。 此外,如果一个程序是用某个版本的 SDK 链接的,而它使用的某个 DLL 却是用不同版本的 SDK 链接的,那该怎么办?也许你正在使用一个 UI 框架库,该库认为没有必要升级到新版 SDK。或者你的程序使用的是旧版 SDK,但 UI 框架库使用的是新版。你是否让主程序的 SDK 版本来决定函数行为,即使 DLL 期望的是不同的行为?这个可怜的 DLL 会调用 `CoInitializeSecurity`,而它的表现却不符合其预期。 好吧,也许你决定函数的改变不基于程序链接的 SDK 版本,而是基于调用方 DLL 的版本。但函数如何知道是哪个 DLL 调用了它?你可能会说:“嗯,你可以查看返回地址属于哪个 DLL。” 但在尾调用优化的情况下,这行不通。 ```cpp // DLL 中的某个函数 HRESULT InitializeWidgets( UINT maxWidgets, const WIDGET_ID* ownerId, PCWSTR ownerDescription, PCWSTR countainerName, PCWSTR containerDescription, COLORREF defaultColor, UINT defaultWidth, UINT defaultHeight, bool isRemoteAccessible, bool isPersistent) { /* 各种初始化步骤 */ static BYTE sd[] = { 0x01, /* 硬编码的值 */ }; return CoInitializeSecurity(sd, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IDENTIFY, nullptr, EOAC_NONE, nullptr); } ``` 最后的那个 `CoInitializeSecurity` 调用可能会被优化为尾调用,此时子例程调用指令变成了无条件分支,返回地址变成了 `InitializeWidget` 的调用者的地址。如果 `CoInitializeSecurity` 探查其返回地址,它就会检查错误 DLL 的 SDK 版本。 反过来,如果 DLL 中的函数只是一个包装器呢? ```cpp HRESULT CoInitializeSecuritywithLogging( _In_opt_ PSECURITY_DESCRIPTOR pSecDesc, _In_ LONG cAuthSvc, _In_reads_opt_(cAuthSvc) SOLE_AUTHENTICATION_SERVICE* asAuthSvc, _In_opt_ void* pReserved1, _In_ DWORD dwAuthnLevel, _In_ DWORD dwImpLevel, _In_opt_ void* pAuthList, _In_ DWORD dwCapabilities, _In_opt_ void* pReserved3) { if (dwCapabilities & EOAC_APPID) { LogUuid("CoInitializeSecurity with APPID", (UUID*)pSecDesc); } else if (dwCapabilities & EOAC_ACCESS_CONTROL) { Log("CoInitializeSecurity with IAccessControl"); } else { LogSecurityDescriptor("CoInitializeSecurity with security descriptor", pSecDesc); } HRESULT hr = CoInitializeSecurity(pSecDesc, cAuthSvc, asAuthSvc, pReserved1, dwAuthnLevel, dwImpLevel, pAuthList, dwCapabilities, pReserved3); Log("CoInitializeSecurity returned", hr); } ``` 如果你查看返回地址,你会发现包装器函数,并据此更改行为以匹配包装器函数所构建的版本。但这个包装器函数只是从其调用者那里传递参数。实际上,我们希望匹配的是调用者的行为,而不是包装器的。 如果库是静态库而不是 DLL 呢?它是为某个版本的 SDK 编写的,但你链接了另一个版本,行为就发生了变化。即使函数检查返回地址,它也会得到 DLL 的地址,从而看到 DLL 的 SDK 版本,而不是库所期望的版本。 根据链接的 SDK 版本来改变行为,仅在程序是单一整体时才有效。 **额外闲谈**:改用新版 SDK 的*头文件*确实会导致行为变化,例如,带有显式大小成员的结构体可能会被扩展以包含额外字段,而 API 会使用该 size 成员的值来判断调用者使用的是哪个版本的 SDK。但这并不依赖于调用者所链接的 SDK,这是一件好事,因为你可以将使用不同版本 SDK 头文件的静态库链接在一起,形成一个程序或 DLL,它们仍然可以正常工作。 ### 分类 ### 主题 ## 作者 Raymond Chen Raymond 参与 Windows 的发展已超过 30 年。2003 年,他创办了一个名为“The Old New Thing”的网站,其受欢迎程度远超他最疯狂的想象,这一发展至今仍让他感到不安。该网站衍生出了一本书,巧合的是,书名也叫《The Old New Thing》(Addison Wesley, 2007)。他偶尔会出现在 Windows Dev Docs 的 Twitter 账号上,讲述一些毫无实际用处的故事。

相似文章

为变更优化,而非应用性能

Hacker News Top

本文指出,软件团队常常过度优化微性能基准测试,却牺牲了开发者体验和工程吞吐量,而这两者才是长期交付速度与可维护性的真正瓶颈。

沙盒化令人抓狂

Lobsters Hottest

一篇技术博客,讨论实现安全沙盒技术的复杂性与挫败感。