返回 2026-05-07
⚙️ 工程

为何不使 API 行为变更依赖于链接的 SDK?Why not have changes in API behavior depend on the SDK you link against?

文章探讨微软是否应让 API 的行为随链接的 SDK 版本而变化,以增强兼容性控制。作者认为静态库在这种机制下处于劣势,难以适配不同 SDK 的变更。最终结论是,动态链接或模块化设计更适合实现此类灵活性,而静态库架构限制了版本隔离能力。

Raymond Chen

不久前,我注意到 Co­Initialize­Security 函数要求一个绝对的安全描述符,尽管 Windows 中的许多函数都会生成自相对安全描述符,迫使你执行从相对到绝对的转换,即使该函数内部只是将其从绝对转换回相对。

评论者 tbodt 写道:

这似乎很容易通过苹果的技巧来解决——当程序链接到旧 SDK 时,让函数恢复旧行为。

听起来确实很简单。如果你的程序链接的是新 SDK,那么它就会获得接受自相对安全描述符的新行为;而如果链接的是旧 SDK,则仍需要绝对安全描述符的旧行为。如果你想使用新行为,那就链接新 SDK。

但这会带来一个微妙的问题:如果你选错了要链接的 SDK,程序仍然可以构建,但结果却不同。传统上,Windows SDK 是向前兼容的:你可以用旧程序链接一个新 SDK,它仍能完全正常工作,因为旧程序只使用了新 SDK 中向后兼容的子集。但如果根据所链接的 SDK 版本改变行为,你可能不会意识到当前的行为变化正是由于升级了 SDK 库所致。

此外,如果一个程序链接的是某个版本的 SDK,但它使用的一个 DLL 链接的是另一个版本的 SDK,又该如何处理?也许你正在使用一个 UI 框架库,它觉得没必要更新到新 SDK。或者反过来,你的主程序使用的是旧版 SDK,而 UI 框架库用的是新版。这时,你是让主程序的 SDK 版本决定函数行为,即使 DLL 期望的是不同的行为?这样一来,那个可怜的 DLL 调用 Co­Initialize­Security 时,其行为将不符合预期。

好吧,也许你会决定:函数的行为不应基于主程序链接的 SDK 版本,而应基于调用它的 DLL 的版本。但问题是,函数如何知道是哪一个 DLL 调用了它?有人可能会说:“哦,你可以查看返回地址属于哪个 DLL。”但这在尾调用优化(tail call optimization)的情况下不起作用。

// some function in a 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)
{
    ⟦ various initialization steps ⟧

    static BYTE sd[] = { 0x01, ⟦ hard-coded values ⟧ };

    return CoInitializeSecurity(sd, -1, nullptr, nullptr,
                                RPC_C_AUTHN_LEVEL_DEFAULT,
                                RPC_C_IMP_LEVEL_IDENTIFY,
                                nullptr, EOAC_NONE, nullptr);
}

对 Co­Initialize­Security 的最后一次调用可能被优化为尾调用——此时子例程调用指令会变成无条件跳转,返回地址变为 Initialize­Widget 调用者的地址。如果 Co­Initialize­Security 检查自己的返回地址,它查到的将是错误 DLL 的 SDK 版本。

相反,如果 DLL 中的函数只是一个包装器(wrapper)呢?

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

如果你查看返回地址,会发现是包装器函数,于是你根据其构建时的 SDK 版本调整行为;但这个包装器只是将从调用者那里接收到的参数原样传递出去。我们真正想匹配的其实是调用者本身的行为,而不是包装器。

那如果这个库是静态库而非 DLL 呢?它原本是为某个 SDK 版本编写的,但你链接到了另一个版本,导致行为发生变化。即便函数检查返回地址,它获取的也是 DLL 的地址及其对应的 SDK 版本,而不是该库原本期望的版本。

只有当程序是单体结构时,才可能根据所链接的 SDK 版本来改变行为。

补充说明:改用新版 SDK 的头文件确实会导致行为变化,例如某些带有显式大小成员的结构体可能会被扩展以包含额外字段,而 API 会利用该大小成员的数值来判断调用方使用的是哪个版本的 SDK。但这并不取决于调用方链接的是哪个 SDK,这是个优点,因为它允许你将使用不同版本 SDK 头文件的静态库全部链接到同一个程序或 DLL 中,它们仍能正常工作。

作者

Raymond 参与 Windows 系统演进已有三十余年。2003 年,他创建了一个名为《The Old New Thing》的网站,其受欢迎程度远远超出了他的想象,这一发展至今仍让他感到些许不安。该网站催生了一本同名书籍——《The Old New Thing》(Addison Wesley,2007)。他偶尔也会出现在 Windows Dev Docs 的 Twitter 账号上,讲述一些毫无实用价值的故事。

需要完整排版与评论请前往来源站点阅读。