前言
众所周知,微软兼容了历史包袱,成功的实现了xp时代(更早的不是我的时代)编译的大部分程序在现在的win10最新版中都能运行,这在macos、linux等系统上是不能实现的。
但也是为了这种兼容,Microsoft C++ 从 VC6更新到vc2022,也一直在文件结构上做了几次尝试。也是令咱们广大程序员头疼,但之前的历史就不细说了,这里只研究下从vc2015开始的apiset模式。
Visual Studio 2015 开始分拆运行时库
从vc7开始微软的vc运行时库的名称就是 msvcr[N].dll
和 msvcp[N].dll
。但从vc2015开始,没有msvcr[N].dll
了,而是改成了vcruntime140.dll,并且编译的程序向后兼容新的vc版本。所以所有版本的vc库的名称都统一了,都是vcruntime140.dll、msvcp140.dll(后续版本又更新了msvcp140_1.dll、
msvcp140_2.dll)。它们也都有着调试版本(dll名称后面加d,如vcruntime140.dll)。
在vc2015中将crt拆分成了两个部分:VCRuntime和UniversalCRT。
VCRuntime
vcruntime 库包含特定于 Visual C++ CRT 实现的代码:异常处理和调试支持、运行时检查和类型信息、实现详细信息以及某些扩展库函数。 vcruntime 库版本需要与所使用的编译器版本匹配。
这部分是跟随Microsoft Visual Studio版本一起更新。可以在 项目属性中选择平台工具集来切换版本,不同版本区别有:对c++标准的支持程度不同、语法细节不同。选择使用哪个工具集编译的,最好就得使用比此版本新的vc运行时库。
UniversalCRT
(UCRT) 包含通过标准 C99 CRT 库导出的函数和全局函数。 UCRT 现为 Windows 组件,作为 Windows 10 及更高版本的一部分提供。 在windows 10版本升级时会同时升级这些dll,做为系统补丁一起升级。在win7到win8之间的版本,也有对应的更新包(Windows Update MSU packages)来安装UCRT。
在xp中没有系统更新来安装UCRT,所以VCRedist在安装时会安装。
在开发环境中,静态库、DLL 导入库和 UCRT 的头文件现在 Windows SDK 中提供。安装Windows SDK后在C:\Program Files (x86)\Windows Kits\10\Redist
中可以找到运行时文件,如果安装了多版本也会在目录中按版本号新增子目录。
vc2015之后的无论是哪个版本,都可以使用任意版本的Windows SDK。在Microsoft Visual Studio的项目属性中可以切换。
文件关系对照表
文件 | 功能 | 所属板块 |
---|---|---|
concrt140.dll | 并发运行时函数 concurrency: : 等 |
UCRT |
ucrtbase.dll | UCRT | |
api-ms-xxx.dll | UCRT | |
msvcp140.dll、msvcp140_1.dll、msvcp140_2.dll | VCRuntime | |
msvcp140_atomic_wait.dll、msvcp140_codecvt_ids.dll | VCRuntime | |
vcruntime140.dll | VCRuntime | |
vccorlib140.dll | VCRuntime |
软件分发及dll分布
软件在开发环境编译链接后(动态链接vcruntime),可以使用两种方式发布:
- 只发布自己开发的程序:在用户电脑上还需要安装VCRedist,VCRedist同时包含UCRT和VCRuntime,VCRedist会将dll文件安装到系统目录中。
- App-local deployment:将所有的ucrt dll 和 vcruntime 打包一起发布。一开始微软并不提供这种方式,在2015年9月11号才开始提供。
在win10中所有的dll都是安装到C:\Windows\WinSxS\中,并且在C:\Windows\System32\中添加硬链接。在系统升级时会更换硬链接到新版本中。
比如在我的当前机器上,下面两个文件是同一个:
- C:\Windows\WinSxS\amd64_microsoft-windows-ucrt_31bf3856ad364e35_10.0.22000.1_none_0204d2ed1e48fcf8\ucrtbase.dll
- C:\Windows\System32\ucrtbase.dll
而在win7中,安装vcredist后会直接将所有文件释放到C:\windows\system32中,但是仅释放了vcruntime系列软件,原来在系统中已经有了22个apiset文件,见下图:
这部分就对应上面提到的win7中使用系统补丁来安装ucrt,这些文件在。
ApiSet
在上面ucrt中包含很多个api-ms-win-xxx之类的dll,这些dll的原理是什么呢,查阅资料后得出以下知识点。
所有版本(不同设备、不同架构)的windows 10共享一个基础组件叫Core OS(有时叫OneCore),微软把原来的Win32函数分成了功能组,这些功能组叫ApiSets。区分Api set主要是为了分离调用者和实际提供api的dll之间的关系。
- 在有些设备上仅提供部分Win32 api,比如XBOX。这时通过Api set的名称(例:
if (!IsApiSetImplemented("ext-ms-win-session-wtsapi32-l1-1-0"))
)就能查询是否支持某些api。 - 在不同的设备中有时提供win32 api的dll名称不同,在检测 API 可用性时使用 API 集名称而不是 DLL 名称,然后延迟加载 API 时再提供正确的实现途径。
在所有设备中都可用的Api放在了OneCore.lib
中。
API set有固定的命名规则:
- 名称必须以字符串 api 或 ext-开头。
- 以 api 开头的名称表示保证存在于所有Windows版本上的 API。
- 以 ext- 表示 API 开头的名称,这些 API 可能不存在于所有Windows版本。
- 名称必须以序列 ln-n-n<<><>> 结尾,其中 n 由十进制数字组成。
- 名称正文可以是字母数字字符,也可以是 (-) 短划线。
- 此名称不区分大小写。
api set使用与链接
传统动态加载dll并获取函数地址时使用LoadLibrary or GetProcAddress。但由于win10中有反向转发,所以这些不能用于测试api set是否存在,即使系统中不提供该api也有可能返回一个有效的函数指针,但这时指针指向的Stub函数被调用时仅返回错误。这种情况下需要使用 IsApiSetImplemented 函数来查询给定 API 是否实现。
在windows 10 SDK中部分lib文件中的dll名称是api set的dll名称。比如:dumpbin /HEADERS "C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22000.0\um\x86\pathcch.lib"
的输出结果如下:
Version : 0
Machine : 14C (x86)
TimeDateStamp: FFFFFFFF
SizeOfData : 00000034
DLL name : api-ms-win-core-path-l1-1-0.dll
Symbol name : _PathCchAppendEx@16
Type : code
Name type : undecorate
Hint : 6
Name : PathCchAppendEx
再比如ucrt.lib里就包含了以下dll名称:
dumpbin /HEADERS "C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22000.0\ucrt\x86\ucrt.lib" | grep dll | uniq
DLL name : api-ms-win-crt-conio-l1-1-0.dll
DLL name : api-ms-win-crt-convert-l1-1-0.dll
DLL name : api-ms-win-crt-environment-l1-1-0.dll
DLL name : api-ms-win-crt-filesystem-l1-1-0.dll
DLL name : api-ms-win-crt-heap-l1-1-0.dll
DLL name : api-ms-win-crt-locale-l1-1-0.dll
DLL name : api-ms-win-crt-math-l1-1-0.dll
DLL name : api-ms-win-crt-multibyte-l1-1-0.dll
DLL name : api-ms-win-crt-process-l1-1-0.dll
DLL name : api-ms-win-crt-runtime-l1-1-0.dll
DLL name : api-ms-win-crt-stdio-l1-1-0.dll
DLL name : api-ms-win-crt-string-l1-1-0.dll
DLL name : api-ms-win-crt-time-l1-1-0.dll
DLL name : api-ms-win-crt-utility-l1-1-0.dll
所以生成的exe中就显示静态链接了这些dll(部分)。
但实测下来windows UWP 的app中的dll使用了更多的仅win10才有的api set,比如dbghelp.dll,win10商店里的dll导入如下:
而vs自带的如下:
Api set运行时加载
以上面程序为例,程序导入表中有一个api-ms-win-crt-runtime-l1-1-0.dll
,但是通过ProcMon监控,在程序运行时并不会真的去加载该dll,而是直接加载了ucrtbase.dll。并且如果程序目录同时有VCRuntime和UCRT,会加载目录下的VCRuntime,但是UCRT还是加载系统目录下的。
在官方文档API 集操作加载程序中提到,Api set依赖于系统的Library Loader使用api set名称进行运行时重定向到目标主机的二进制文件(Api实际实现的dll)。当加载器遇到api set时,使用API set schema来识别重定向目标dll,在不同设备这个映射关系是不一样的。
windows 10支持两种使用Api set的技术,直接转发和反向转发。
直接转发即使用api set的名称直接映射到一个dll上,根据本地配置(API set schema)将其转换为另外一个dll(不同设备可能不同名称),api set虽然以.dll结尾,但可以没有物理dll文件。如果用户自己新建一个api set名称的dll,比如api-ms-win-crt-runtime-l1-1-0.dll
,结果就是在win10系统中exe不能正常运行,因为在导入时系统已经强制切换了名称,并不会加载真实的dll。而在win7系统中会加载这个dll,但由于同时这dll提供了crt的一些函数,所以也会报错(win7中不加载此dll的原因是它Api set schema上只有35个映射,全是api-ms-win-core|Security|Service,不包括所有的api-ms-win-crt系列dll,这也就是上文中提到的win7系统目录下已经有的22个dll中包含crt系列dll但少了很多api-ms-win-core系列dll的原因)。
反向转发即在非windows电脑的设备上,当加载到一个windows pc中的dll名称,但该dll不存在于此设备中,加载器会查看配置中是否有一对应的反向转发器,反向转发器映射到一个对应功能的api set中,再由api set定位到该设备上实现该功能的dll上。
API set schema
上文提到的api set映射关系在系统的apisetschema.dll文件中,在开源项目Dependencies中有代码对其进行加载,使用Runtime DLL name resolution: ApiSetSchema - Part I (quarkslab.com)中提到的方法。
ApiSetSchema机制在系统启动的早期就激活了,winload.exe在 在系统启动时调用winload!OslpLoadAllModules and winload!OsLoadImage时就加载了 ApiSetSchema.dll,这时再加载windows kernel和HAL等重要模块。
总结一下我们目前所看到的:
Windows 使用一种机制将虚拟 DLL 重定向到逻辑(实现)DLL。
1.在启动时,Winload.exe加载“apisetschema.dll”文件。
2.在内核初始化阶段 1 期间,DLL 中的“.apiset”文件部分将映射到系统内核内存:将创建内核对象部分和此部分的视图。
3.启动流程时,将在流程地址中创建该部分的视图,并可通过_PEB访问。ApisetMap 字段:因此,“apisetschema.dll”文件中的“.apiset”文件部分的内容可用于进程用户空间。
参考资料
- 通用 CRT 简介 - C++团队博客 (microsoft.com)
- Windows API 集 - Win32 apps | Microsoft Learn
- windows-apisets兼容性 | Microsoft Learn
- winapi - 如何在运行时安全地动态加载 PathAllocCanonicalize - Stack Overflow
- Dependencies/Phlib.cpp at master · lucasg/Dependencies (github.com)
- 运行时 DLL 名称解析:ApiSetSchema - 第 I 部分 (quarkslab.com)
- 运行时 DLL 名称解析:ApiSetSchema - 第 II 部分 (quarkslab.com)