api-ms-win-crt-runtime-l1-1-0.dll といった長い名前の DLL の実体は何か

「api-ms-win-crt-runtime-l1-1-0.dll」だとか「api-ms-win-crt-stdio-l1-1-0.dll」だとか、やたら長い名前の DLL が実行可能ファイルにリンクされていることがありますが、これは何なのでしょうか。

動作確認環境

  • Windows 11 Home 21H2
  • Visual Studio Community 2019

やたら長い名前の DLL

メモ帳がリンクしている DLL を見てみます。

C:\> dumpbin /dependents c:\windows\notepad.exe
Microsoft (R) COFF/PE Dumper Version 14.28.29913.0
......
  Image has the following dependencies:

    KERNEL32.dll
    GDI32.dll
    USER32.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-private-l1-1-0.dll
    api-ms-win-core-com-l1-1-0.dll
    api-ms-win-core-shlwapi-legacy-l1-1-0.dll
    api-ms-win-core-winrt-string-l1-1-0.dll
    api-ms-win-core-winrt-l1-1-0.dll
    api-ms-win-shcore-obsolete-l1-1-0.dll
    api-ms-win-shcore-path-l1-1-0.dll
    api-ms-win-shcore-scaling-l1-1-1.dll
    api-ms-win-core-rtlsupport-l1-1-0.dll
    api-ms-win-core-errorhandling-l1-1-0.dll
    api-ms-win-core-processthreads-l1-1-0.dll
    api-ms-win-core-processthreads-l1-1-1.dll
    api-ms-win-core-profile-l1-1-0.dll
    api-ms-win-core-sysinfo-l1-1-0.dll
    api-ms-win-core-interlocked-l1-1-0.dll
    api-ms-win-core-libraryloader-l1-2-0.dll
    api-ms-win-core-winrt-error-l1-1-0.dll
    api-ms-win-core-string-l1-1-0.dll
    api-ms-win-core-winrt-error-l1-1-1.dll
    COMCTL32.dll
......

「api-ms-」から始まる長い名前の DLL がたくさんリンクされています。
しかもリンクしている DLL が、PATH の通ったディレクトリに存在したりしなかったりします。

C:\> where api-ms-win-crt-runtime-l1-1-0.dll
C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\api-ms-win-crt-runtime-l1-1-0.dll

C:\> where api-ms-win-core-winrt-error-l1-1-1.dll
情報: 与えられたパターンのファイルが見つかりませんでした。

それなのにプログラムは実行できるという不思議。これは何なのでしょうか。

API セットコントラクト名

これは物理的な DLL 名ではなく、目的ごとにグルーピングされた API に付けられた論理的な名前であり、「API セットコントラクト名」(あるいは、API セット名、コントラクト名)と呼ばれているものです。
厳密には末尾の「.dll」を除いた名前、「api-ms-win-core-winrt-error-l1-1-1.dll」の場合は「api-ms-win-core-winrt-error-l1-1-1」が、コントラクト名になります。

プログラムの起動時、OS はコントラクト名を DLL 名に読み替えてロードします。
たとえば「api-ms-win-core-winrt-error-l1-1-1」を「combase.dll」に読み替えてロードします。
そのため、「api-ms-win-core-winrt-error-l1-1-1.dll」が存在しなくてもプログラムが実行できます。

コントラクト名を使うことで設計と実装が分離でき、HoloLens や Xbox への対応も容易になる、というのがマイクロソフトの説明です。
しかし大半のユーザーはそうした柔軟性を必要としておらず、過度に複雑になっただけのような気がしなくもありません。

どの DLL に読み替えられるのか

コントラクト名はどの DLL 名に読み替えられるのでしょうか。
オープンソースの Dependencies を使って見てみましょう。

GitHub の Dependenceis のページ [3] にて「Download here」のボタンを押し、ツールをダウンロードします。

「DependenciesGui.exe」を起動し、[File]-[Open] から「notepad.exe」を開きます。

メインのウィンドウに、「コントラクト名 -> DLL 名」の形で読み替え結果が表示されています。
たとえば、「api-ms-win-crt-runtime-l1-1-0.dll」は「C:\WINDOWS\system32\ucrtbase.dll」に読み替えられていることがわかります。

読み替えの過程を WinDbg で確認

コントラクト名の読み替えについて、WinDbg でも見てみましょう。

以下のプログラムで実験します。

#include <stdio.h>
int main()
{
    printf("Hello\n");
    return 0;
}

Visual Studio 2019 の x86 コマンドプロンプトから cl コマンドでビルドします。オプションの「/Od」は最適化抑止、「/MD」は C ライブラリを動的にリンク、「/Zi」はデバッグ情報を付加の意味です。

C:\tmp> cl /Od /MD /Zi hello.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:hello.exe
/debug
hello.obj

生成された「hello.exe」の依存関係を見てみます。

C:\tmp> dumpbin /dependents hello.exe
Microsoft (R) COFF/PE Dumper Version 14.28.29913.0
......
  Image has the following dependencies:

    KERNEL32.dll
    VCRUNTIME140.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-math-l1-1-0.dll
    api-ms-win-crt-locale-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
......

「api-ms-」で始まる「コントラクト名.dll」が複数リンクされています。

次に、GFlags コマンドを使って、「hello.exe」起動時の詳細な DLL 情報をデバッガに表示するよう設定します。「sls」は「Show loader snaps」の意味です。

C:\tmp> gflags /i hello.exe +sls

WinDbg 上で実行します。

C:\tmp> windbg hello.exe
Microsoft (R) Windows Debugger Version 10.0.22000.194 X86
......
ModLoad: 00380000 0038a000   hello.exe
ModLoad: 77750000 778f9000   ntdll.dll
......
ModLoad: 764a0000 76590000   C:\WINDOWS\SysWOW64\KERNEL32.DLL
......
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-rtlsupport-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ntdll.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-rtlsupport-l1-2-0.dll was redirected to C:\WINDOWS\SYSTEM32\ntdll.dll by API set
......
ModLoad: 76080000 762d2000   C:\WINDOWS\SysWOW64\KERNELBASE.dll
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-eventing-provider-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-processthreads-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-processthreads-l1-1-3.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-processthreads-l1-1-2.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-processthreads-l1-1-1.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-registry-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-heap-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-heap-l2-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-memory-l1-1-1.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-memory-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-memory-l1-1-2.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-handle-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-synch-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-synch-l1-2-1.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-synch-l1-2-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
1c0c:1d94 @ 603624281 - LdrpPreprocessDllName - INFO: DLL api-ms-win-core-file-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\kernelbase.dll by API set
......
1c0c:1d94 @ 603624390 - LdrpPreprocessDllName - INFO: DLL api-ms-win-crt-stdio-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ucrtbase.dll by API set
1c0c:1ae4 @ 603624390 - LdrpSearchPath - ENTER: DLL name: VCRUNTIME140.dll
ModLoad: 76710000 76822000   C:\WINDOWS\SysWOW64\ucrtbase.dll
......
1c0c:1d94 @ 603624406 - LdrpPreprocessDllName - INFO: DLL api-ms-win-crt-runtime-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ucrtbase.dll by API set
1c0c:1d94 @ 603624406 - LdrpPreprocessDllName - INFO: DLL api-ms-win-crt-math-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ucrtbase.dll by API set
......
1c0c:1d94 @ 603624406 - LdrpPreprocessDllName - INFO: DLL api-ms-win-crt-locale-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ucrtbase.dll by API set
......
1c0c:1d94 @ 603624406 - LdrpPreprocessDllName - INFO: DLL api-ms-win-crt-heap-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ucrtbase.dll by API set
......

「LdrpPreprocessDllName」の行が読み替えの情報です。たとえば最後の行からは、「api-ms-win-crt-heap-l1-1-0.dll」が「C:\WINDOWS\SYSTEM32\ucrtbase.dll」に読み替えられたことがわかります。

間接的なリンクも含めかなり多くの読み替えが行われていますが(上に示したのは一部です)、ほとんどのコントラクト名は「kernelbase.dll」に読み替えられており、結局のところメインルーチン開始直前のロードモジュールは以下の 5 つだけでした。

0:000> lm
......
6ef70000 6ef85000   VCRUNTIME140   (deferred)
76080000 762d2000   KERNELBASE   (deferred)
764a0000 76590000   KERNEL32   (deferred)
76710000 76822000   ucrtbase   (deferred)
77750000 778f9000   ntdll      (pdb symbols)
......

実験が終わったら、さきほどの設定をクリアしておきましょう。

C:\tmp> gflags /i hello.exe -sls

読み替えの過程をさらに詳しく

コントラクト名の読み替えの過程をさらに詳しく見てみます。

さきほどと同様、WindDbg 上で「hello.exe」を実行します。ただし「-xe ld」オプションを付加し、DLL ロードのタイミングで停止するよう指示します。

C:\tmp> windbg -xe ld hello.exe

Microsoft (R) Windows Debugger Version 10.0.22000.194 X86
......
ModLoad: 008a0000 008aa000   hello.exe
ModLoad: 77750000 778f9000   ntdll.dll
......

0:000> lm
start    end        module name
008a0000 008aa000   hello      (deferred)
77750000 778f9000   ntdll      (pdb symbols)
......

「ntdll.dll」のみをロードしたタイミングで停止しました。

メモリレイアウトを見てみます。

0:000> !address
......
  BaseAddr EndAddr+1 RgnSize ...... Usage
------------------------------------------------------------------
+        0   8a0000   8a0000 ...... Free       
+   8a0000   8a1000     1000 ...... Image      [hello; "hello.exe"]
......
+   940000   95f000    1f000 ...... Other      [API Set Map]
......
    bd7000   bda000     3000 ...... PEB        [4a64]
    bda000   bdc000     2000 ...... TEB        [~0; 4a64.5b98]
......
+   c00000   cfd000    fd000 ...... Stack      [~0; 4a64.5b98]
......
+ 77750000 77751000     1000 ...... Image      [ntdll; "ntdll.dll"]
......

「API Set Map」という文字列が見えます。ここが、コントラクト名と DLL 名の対応表が入っている領域です。

ダンプしてみましょう。

0:000> db  910000 L1f000
......
0091a300  18 00 00 00 61 00 70 00-69 00 2d 00 6d 00 73 00  ....a.p.i.-.m.s.
0091a310  2d 00 77 00 69 00 6e 00-2d 00 63 00 72 00 74 00  -.w.i.n.-.c.r.t.
0091a320  2d 00 73 00 74 00 64 00-69 00 6f 00 2d 00 6c 00  -.s.t.d.i.o.-.l.
0091a330  31 00 2d 00 31 00 2d 00-30 00 00 00 00 00 00 00  1.-.1.-.0.......
0091a340  00 00 00 00 00 00 00 00-d0 9f 00 00 18 00 00 00  ................
0091a350  61 00 70 00 69 00 2d 00-6d 00 73 00 2d 00 77 00  a.p.i.-.m.s.-.w.
0091a360  69 00 6e 00 2d 00 63 00-72 00 74 00 2d 00 73 00  i.n.-.c.r.t.-.s.
0091a370  74 00 72 00 69 00 6e 00-67 00 2d 00 6c 00 31 00  t.r.i.n.g.-.l.1.
0091a380  2d 00 31 00 2d 00 30 00-00 00 00 00 00 00 00 00  -.1.-.0.........
......

コントラクト名が確認できます。

この情報は、OS が「C:\Windows\System32\apisetschema.dll」の「.apiset」セクションから展開したものになります。

C:\> dumpbin /section:.apiset /rawdata c:\windows\system32\apisetschema.dll

Microsoft (R) COFF/PE Dumper Version 14.28.29913.0
......
SECTION HEADER #3
 .apiset name
   1E300 virtual size
    3000 virtual address (0000000180003000 to 00000001800212FF)
   1F000 size of raw data
......
  000000018000D300: 18 00 00 00 61 00 70 00 69 00 2D 00 6D 00 73 00  ....a.p.i.-.m.s.
  000000018000D310: 2D 00 77 00 69 00 6E 00 2D 00 63 00 72 00 74 00  -.w.i.n.-.c.r.t.
  000000018000D320: 2D 00 73 00 74 00 64 00 69 00 6F 00 2D 00 6C 00  -.s.t.d.i.o.-.l.
  000000018000D330: 31 00 2D 00 31 00 2D 00 30 00 00 00 00 00 00 00  1.-.1.-.0.......
  000000018000D340: 00 00 00 00 00 00 00 00 D0 9F 00 00 18 00 00 00  ........ミ.......
  000000018000D350: 61 00 70 00 69 00 2D 00 6D 00 73 00 2D 00 77 00  a.p.i.-.m.s.-.w.
  000000018000D360: 69 00 6E 00 2D 00 63 00 72 00 74 00 2D 00 73 00  i.n.-.c.r.t.-.s.
  000000018000D370: 74 00 72 00 69 00 6E 00 67 00 2D 00 6C 00 31 00  t.r.i.n.g.-.l.1.
  000000018000D380: 2D 00 31 00 2D 00 30 00 00 00 00 00 00 00 00 00  -.1.-.0.........
......

読み替えはさきほど見た通り「LdrpPreprocessDllName」関数で行われますので、ここにブレークポイントを仕掛けます。

0:000> bp ntdll!LdrpPreprocessDllName

0:000> bl
     0 e Disable Clear  77793e70     0001 (0001)  0:**** ntdll!LdrpPreprocessDllName

一連のロード処理の中盤あたりの動作を調べるため「g」コマンドを 30 回程度実行します。
そして、「LdrpPreprocessDllName」のブレークポイントで止まった状態で次のように入力すると、コントラクト名と読み替え後の DLL 名が一組分確認できます。

ntdll!LdrpPreprocessDllName:
77793e70 8bff            mov     edi,edi

0:000> r ecx, edx
ecx=00cff158 edx=00cfeffc
 
0:000> dt _UNICODE_STRING  cff158   ★ ecx レジスタの値
ntdll!_UNICODE_STRING
 "api-ms-win-core-io-l1-1-0.dll"
   +0x000 Length           : 0x3a
   +0x002 MaximumLength    : 0x100
   +0x004 Buffer           : 0x00cff160  "api-ms-win-core-io-l1-1-0.dll"

0:000> gu   ★ LdrpPreprocessDllName 関数を抜けるまで実行
......
ntdll!LdrpLoadDependentModuleInternal+0xe0:
777db7c3 8bf0            mov     esi,eax

0:000> dt _UNICODE_STRING cfeffc   ★ 関数呼び出し時の edx レジスタの値
ntdll!_UNICODE_STRING
 "C:\WINDOWS\SYSTEM32\kernelbase.dll"
   +0x000 Length           : 0x44
   +0x002 MaximumLength    : 0x100
   +0x004 Buffer           : 0x00cff004  "C:\WINDOWS\SYSTEM32\kernelbase.dll"

LdrpPreprocessDllName 関数呼び出し時、ecx レジスタにはコントラクト名が入った _UNICODE_STRING 型構造体へのポインタが、edx レジスタには読み替え後の DLL 名を格納する _UNICODE_STRING 構造体へのポインタがセットされています。上記操作で、これらのレジスタが指す情報を表示しています。

実在する「コントラクト名.DLL」は何か

コントラクト名は DLL 名に読み替えられるのだとしたら、実在する「コントラクト名.DLL」、たとえば「api-ms-win-crt-stdio-l1-1-0.dll」は何なのでしょうか。

この DLL のエクスポート関数を見ると、処理をほかの DLL に丸投げするフォワーダーであることがわかります。

C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE> 
dumpbin /exports api-ms-win-crt-stdio-l1-1-0.dll

Microsoft (R) COFF/PE Dumper Version 14.28.29913.0
......
        126   7D          fopen (forwarded to ucrtbase.fopen)
        127   7E          fopen_s (forwarded to ucrtbase.fopen_s)
        128   7F          fputc (forwarded to ucrtbase.fputc)
        129   80          fputs (forwarded to ucrtbase.fputs)
        130   81          fputwc (forwarded to ucrtbase.fputwc)
        131   82          fputws (forwarded to ucrtbase.fputws)
        132   83          fread (forwarded to ucrtbase.fread)
        133   84          fread_s (forwarded to ucrtbase.fread_s)
        134   85          freopen (forwarded to ucrtbase.freopen)
        135   86          freopen_s (forwarded to ucrtbase.freopen_s)
        136   87          fseek (forwarded to ucrtbase.fseek)
        137   88          fsetpos (forwarded to ucrtbase.fsetpos)
        138   89          ftell (forwarded to ucrtbase.ftell)
        139   8A          fwrite (forwarded to ucrtbase.fwrite)
        140   8B          getc (forwarded to ucrtbase.getc)
        141   8C          getchar (forwarded to ucrtbase.getchar)
        142   8D          gets (forwarded to ucrtbase.gets)
......

プログラムの起動時、OS のバージョンが古い等の理由で「Api Set Map」に当該「コントラクト名.DLL」の情報がなく読み替えができなかった場合、OS は「コントラクト名.DLL」そのものをロードしようとします。そして、「コントラクト名.DLL」が存在し、かつ、フォワード先の DLL の適切なバージョンも存在していれば、プログラムは正常に動作することになります。
つまり、ひとつのプログラムをさまざまな環境で動かせるようにするために「コントラクト名.DLL」が用意されていると考えられます。

参考文献

[1] Microsoft. Windows API セット.
https://docs.microsoft.com/ja-jp/windows/win32/apiindex/windows-apisets

[2] ALEX IONESC. Hooking Nirvana. 2015.
http://publications.alex-ionescu.com/Recon/Recon%202015%20-%20Hooking%20Nirvana%20-%20Stealthy%20Instrumentation%20Techniques.pdf

[3] Dependencies.
https://github.com/lucasg/Dependencies