为何不根据链接的SDK来改变API行为?
摘要
本文以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&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 & 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);
}
</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 账号上,讲述一些毫无实际用处的故事。
相似文章
如何对公共Web API进行版本管理?
关于API版本管理实践的讨论,质疑将URL路径(例如/v1/)与语义化版本控制耦合的做法,并探讨潜在的反模式。
为变更优化,而非应用性能
本文指出,软件团队常常过度优化微性能基准测试,却牺牲了开发者体验和工程吞吐量,而这两者才是长期交付速度与可维护性的真正瓶颈。
对于同时公开 MCP 和 CLI 的情况,这两种工具/命令是否应该暴露完全相同的功能?
作者讨论了同时设计 MCP 和 CLI 接口时的架构挑战,权衡了功能镜像化与利用各自独特优势(CLI 的可组合性,MCP 的安全性和可审计性)之间的利弊。
多提供商LLM API兼容性笔记:我们尝试的三种方法
工程笔记,比较了将多个LLM提供商(OpenAI、Anthropic、Google)的访问统一到单个内部接口的三种方法,讨论了API标准化、原生SDK使用和网关模式的权衡。
沙盒化令人抓狂
一篇技术博客,讨论实现安全沙盒技术的复杂性与挫败感。