windows apiset 研究

前言

众所周知,微软兼容了历史包袱,成功的实现了xp时代(更早的不是我的时代)编译的大部分程序在现在的win10最新版中都能运行,这在macos、linux等系统上是不能实现的。

但也是为了这种兼容,Microsoft C++ 从 VC6更新到vc2022,也一直在文件结构上做了几次尝试。也是令咱们广大程序员头疼,但之前的历史就不细说了,这里只研究下从vc2015开始的apiset模式。

Visual Studio 2015 开始分拆运行时库

从vc7开始微软的vc运行时库的名称就是 msvcr[N].dllmsvcp[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运行时库。

image.png

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的项目属性中可以切换。

image.png

文件关系对照表

文件 功能 所属板块
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文件,见下图:


image.png

这部分就对应上面提到的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(部分)。


image.png

但实测下来windows UWP 的app中的dll使用了更多的仅win10才有的api set,比如dbghelp.dll,win10商店里的dll导入如下:


image.png

而vs自带的如下:


image.png

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的原因)。

win10
win7中

反向转发即在非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”文件部分的内容可用于进程用户空间。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容