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