静的リンク・動的リンク・明示的リンク・暗黙的リンク・遅延ロードの違い

混乱しがちな、静的リンク・動的リンク・明示的リンク・暗黙的リンク・遅延ロードの違いについて見てみます。Windows 前提です。

動作確認環境

  • Windows 11 Home 22H2
  • Visual Studio Community 2022 (Visual C++)

静的リンクと動的リンク

静的リンク (= スタティックリンク)では、プログラムのビルド時に、ライブラリを実行可能ファイル内に埋め込みます(DLL への埋め込みも可)。静的リンク用のライブラリを、静的ライブラリ(= スタティックライブラリ)と呼びます。拡張子は通常 .lib です。

動的リンク(= ダイナミックリンク)では、プログラムの実行時に、ライブラリをプロセス空間内にロードします。動的リンク用のライブラリを、動的ライブラリ(= ダイナミックライブラリ)あるいは動的リンクライブラリ(= ダイナミックリンクライブラリ)と呼びます。拡張子は通常 .dll です。

静的ライブラリと動的ライブラリは作り自体が異なっており、静的ライブラリを動的にリンクしたり、動的ライブラリを静的にリンクしたりはできません。

静的ライブラリを作る

C 言語のコマンドラインコンパイラで静的ライブラリを作ってみます。

引数にプラス 1 した値を返す関数です。

int MyInc(int i);
#include "MyLibInc.h"

int MyInc(int i)
{
    return i + 1;
}

コンパイルします。「/c」オプションはコンパイルはするがリンクはしない、「/Zl」オプションは既定のライブラリ名(LIBCMT, OLDNAMES)がオブジェクトファイルにディレクティブとして書き込まれるのを防ぐ(どの VC ランタイムライブラリを使うかをまだ指示しない)、の意味です。

C:\MyLib> cl /c /Zl MyLibInc.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.
......

オブジェクトファイル (.obj) が生成されました。この段階ではまだライブラリファイル (.lib) にはなっていません。

C:\MyLib> dir /b MyLibInc.*
MyLibInc.c
MyLibInc.h
MyLibInc.obj  ★ 生成されたオブジェクトファイル

もうひとつ、引数をマイナス 1 した値を返す関数を作ります。

int MyDec(int i);
#include "MyLibDec.h"

int MyDec(int i)
{
    return i - 1;
}

同様にコンパイルします。

C:\MyLib> cl /c /Zl MyLibDec.c

オブジェクトファイルが生成されました。

C:\MyLib> dir /b MyLibDec.*
MyLibDec.c
MyLibDec.h
MyLibDec.obj  ★ 生成されたオブジェクトファイル

2 つのオブジェクトを編成して、1 つの静的ライブラリを作ります。
使うのは、ライブラリアンと呼ばれる lib コマンドです。

C:\MyLib> lib /out:MyStaticLib.lib MyLibInc.obj MyLibDec.obj
Microsoft (R) Library Manager Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

静的ライブラリ「MyStaticLib.lib」が生成されました。

C:\MyLib> dir /b MyStaticLib.*
MyStaticLib.lib  ★ 生成された静的ライブラリファイル

ここまでの作業を図示すると次のようになります。

静的ライブラリの中身を見てみましょう。
2 つのオブジェクトが含まれていることが確認できます。

C:\MyLib> lib /list MyStaticLib.lib
......
MyLibInc.obj
MyLibDec.obj

静的ライブラリを逆アセンブルすると、「プラス 1 する関数」と「マイナス 1 する関数」の実体が確認できます。

C:\MyLib> dumpbin /disasm MyStaticLib.lib
......
_MyDec:  ★ マイナス 1 する関数
  00000000: 55               push      ebp
  00000001: 8B EC            mov       ebp,esp
  00000003: 8B 45 08         mov       eax,dword ptr [ebp+8]
  00000006: 83 E8 01         sub       eax,1  ★ マイナス 1
  00000009: 5D               pop       ebp
  0000000A: C3               ret

_MyInc:  ★ プラス 1 する関数
  00000000: 55               push      ebp
  00000001: 8B EC            mov       ebp,esp
  00000003: 8B 45 08         mov       eax,dword ptr [ebp+8]
  00000006: 83 C0 01         add       eax,1  ★ プラス 1
  00000009: 5D               pop       ebp
  0000000A: C3               ret
......

静的ライブラリを利用する

静的ライブラリを利用してみましょう。
「プラス 1 する関数」を呼び出すプログラムを書きます。

#include <stdio.h>
#include "MyLibInc.h"

int main()
{
    printf("Start\n");

    int i = MyInc(3); // ★ 「プラス 1 する関数」の呼び出し
    printf("i=%d\n", i);

    return 0;
}

ビルドします。このとき、引数に静的ライブラリ「MyStaticLib.lib」を指定します。「/MD」オプションは DLL 版の C ランタイムライブラリを利用する、「/Zi」オプションはデバッグ情報を付加する、の意味ですが、どちらも指定しなくても構いません。

C:\MyLib> cl /MD /Zi MyMain.c MyStaticLib.lib

実行します。

C:\MyLib> MyMain.exe
4

想定通り、引数 3 にプラス 1 された 4 が表示されました。

実行可能ファイル「MyMain.exe」を逆アセンブルすると、静的ライブラリ「MyStaticLib.lib」に含まれていた「プラス 1 する関数」の実体が埋め込まれていることが確認できます。

C:\MyLib> dumpbin /disasm MyMain.exe
......
_main:  ★ main 関数
  ......
  00401401: 6A 03            push      3
  00401403: E8 61 FC FF FF   call      @ILT+100(_MyInc)  ★ MyInc 関数呼び出し
  00401408: 83 C4 04         add       esp,4
  ......
  00401424: C3                 ret
......
_MyInc:  ★ 静的ライブラリにあったプラス 1 する関数
  004014E0: 55               push      ebp
  004014E1: 8B EC            mov       ebp,esp
  004014E3: 8B 45 08         mov       eax,dword ptr [ebp+8]
  004014E6: 83 C0 01         add       eax,1  ★ プラス 1
  004014E9: 5D               pop       ebp
  004014EA: C3               ret
......

最初に書いた図から抜き出すと、こういうことです。

なお、ライブラリを編成する「MyLibInc.obj」と「MyLibDec.obj」の 2 つのオブジェクトのうち、後者は利用していないため、実行可能ファイルには埋め込まれていません。

動的ライブラリを作る

次に動的ライブラリを作ってみます。いろいろな作り方が考えられますが、さきほど書いた「プラス 1 する関数」を、モジュール定義ファイル(.def ファイル)を利用して DLL 化してみます。

以下、.def ファイルです。ライブラリ名や公開する関数の名前を書きます。

LIBRARY MyLibInc
EXPORTS
    MyInc

ビルドします。「/LD」オプションで DLL の生成を、.def ファイルで公開する関数の名前を指示しています。

C:\MyLib> cl /LD /MD /Zi MyLibInc.c MyLibInc.def

DLL ファイルやインポートライブラリなどが生成されました。

C:\MyLib> dir /b MyLibInc.*
MyLibInc.c
MyLibInc.def
MyLibInc.dll  ★ 生成された DLL
MyLibInc.exp
MyLibInc.h
MyLibInc.ilk
MyLibInc.lib  ★ 生成されたインポートライブラリ
MyLibInc.obj
MyLibInc.pdb

ここまでの作業を図示します。

DLL には、「プラス 1 する関数」の実体が含まれています。

C:\MyLib> dumpbin /disasm MyLibInc.dll
......
_MyInc:  ★ プラス 1 する関数
  10001350: 55               push      ebp
  10001351: 8B EC            mov       ebp,esp
  10001353: 8B 45 08         mov       eax,dword ptr [ebp+8]
  10001356: 83 C0 01         add       eax,1  ★ プラス 1
  10001359: 5D               pop       ebp
  1000135A: C3               ret
......

dumpbin /exports で DLL が公開している(エクスポートしている)関数名が表示されます。

C:\MyLib> dumpbin /exports MyLibInc.dll
......
Dump of file MyLibInc.dll
File Type: DLL
......
    ordinal hint RVA      name
          1    0 00001055 MyInc = @ILT+80(_MyInc)  ★ MyInc が公開されている
......

DLL と同時に生成されたインポートライブラリは、拡張子こそ静的ライブラリと同じ .lib ですが、中身はまったくの別物です。
関数の実体は含まれていません。

C:\MyLib> dumpbin /disasm MyLibInc.lib
......
Dump of file MyLibInc.lib
★ MyInc がない
......

含まれているのは、DLL が公開している関数名などの情報のみです。

C:\MyLib> dumpbin /exports MyLibInc.lib
......
Dump of file MyLibInc.lib
File Type: LIBRARY
......
       ordinal    name
                  _MyInc
......

インポートライブラリは、後述の「暗黙的リンク」や「遅延ロード」でビルド時に使います。

動的ライブラリを利用する

最初のほうに書いた動的リンクの図を再掲します。

これはこれで間違ってはいませんが、ロードの指示方法やタイミングによって、動的リンクはさらに 3 種類に分類できます。

  • 明示的リンク(Explicit Link): プログラミングで明示的にロードを指示。
  • 暗黙的リンク(Implicit Link): プロセス起動直後に自動的にロード。
  • 遅延ロード(Delay Load): 暗黙的リンクの一種。DLL 内の関数呼び出し時に自動的にロード。

図示します。

どのパターンを使うかによって、呼び出し側のソースの書き方やビルド方法が変わります。

明示的リンクで DLL を利用する

明示的リンクで DLL を利用するには、DLL をロードし、その中から目的の関数を探し出すという処理を、Windows API の LoadLibrary 関数や GetProcAddress 関数を使って自前で組まなければなりません。
以下、サンプルです。

#include <stdio.h>
#include <windows.h>

typedef int (*MYINC)(int i);

int main()
{
    printf("Start\n");

    // DLL を明示的にロードする
    HMODULE hmodule = LoadLibrary("MyLibInc.dll");
    if (hmodule == NULL)
    {
        printf("LoadLibrary, err=%d\n", GetLastError());
        return 1;
    }

    // DLL の中にある特定の関数のアドレスを取得する
    MYINC fnMyInc = (MYINC)GetProcAddress(hmodule, "MyInc");
    if (fnMyInc == NULL)
    {
        printf("GetProcAddress, err=%d\n", GetLastError());
        return 1;
    }

    // DLL の中にある特定の関数を呼び出す
    int i = fnMyInc(7);
    printf("i=%d\n", i);

    // DLL をアンロードする
    BOOL bSuccess = FreeLibrary(hmodule);
    if (! bSuccess)
    {
        printf("FreeLibrary, err=%d\n", GetLastError());
        return 1;
    }

    return 0;
}

ビルドします。

C:\MyLib> cl /MD /Zi MyMain.c

実行します。DLL 内の関数が正しく呼び出されています。

C:\MyLib> MyMain.exe
Start
i=8

詳細を見るため、WinDbg 経由で実行してみます。
プロセスの起動後、LoadLibrary 関数呼び出し前までは、「MyLibInc.dll」がロードされていませんが、

LoadLibrary 関数を通過したタイミングでロードされたことがわかります。

ちなみに、DLL ファイルを削除してプログラムを実行すると、LoadLibrary 関数が詳細エラーコード 126 (ERROR_MOD_NOT_FOUND, 指定されたモジュールが見つかりません)で失敗します。

C:\MyLib> MyMain.exe
Start
LoadLibrary, err=126

暗黙的リンクで DLL を利用する

暗黙的リンクでは DLL のロードを意識する必要がありません。呼び出し側のプログラムは、静的ライブラリと利用する場合と同様、シンプルです。

#include <stdio.h>
#include "MyLibInc.h"

int main()
{
    printf("Start\n");

    int i = MyInc(9); // ★ 「プラス 1 する関数」の呼び出し
    printf("i=%d\n", i);

    return 0;
}

ただし、ビルド時に、前述のインポートライブラリを指定する必要があります。

C:\MyLib> cl /MD /Zi MyMain.c MyLibInc.lib

実行します。DLL 内の関数が正しく呼び出されています。

C:\MyLib> MyMain.exe
Start
i=10

今度は WinDbg 経由で実行してみます。
プロセスの起動直後、main 関数に入る前に、すでに「MyLibInc.dll」がロードされていることが確認できます。

ちなみに DLL ファイルを削除してプログラムを起動すると、DLL が見つからない旨のエラーメッセージが表示されて、プログラムは終了します。

遅延ロードで DLL を利用する

遅延ロードは暗黙的リンクの一種です。呼び出し側のプログラムの書き方も、暗黙的リンクの場合と同じです。

#include <stdio.h>
#include "MyLibInc.h"

int main()
{
    printf("Start\n");

    int i = MyInc(13); // ★ 「プラス 1 する関数」の呼び出し
    printf("i=%d\n", i);

    return 0;
}

ただし、ビルド時に、DLL のインポートライブラリと Visual C++ に含まれる「delayimp.lib」と「/DELAYLOAD」オプションを指定する必要があります。

C:\MyLib> cl /MD /Zi MyMain.c MyLibInc.lib delayimp.lib /link /DELAYLOAD:MyLibInc.dll

実行します。DLL 内の関数が正しく呼び出されています。

C:\MyLib> MyMain.exe
Start
i=14

今度は WinDbg 経由で実行してみます。
プロセスが起動し main 関数に入った後、「プラス 1 する関数」である MyInc 関数呼び出し前までは「MyLibInc.dll」がロードされていませんが、

MyInc 関数呼び出しのタイミングで当該 DLL が自動的にロードされました。

ちなみに DLL ファイルを削除してプログラムを実行すると、MyInc 関数呼び出しのタイミングで例外 0xC06D007E(指定されたモジュールが見つかりません)が発生します。
この例外は、構造化例外処理 (__try ~ __except)で捕捉したり、遅延読み込みヘルパー関数(__pfnDliNotifyHook2, __pfnDliFailureHook2)で事前に検出したりできます。

おわりに

  • ライブラリのリンク方法には、ビルド時にリンクする「静的リンク」と、実行時にリンクする「動的リンク」があること、
  • 静的リンクには「静的ライブラリ」を、動的リンクには「動的ライブラリ」を使い、作りが別であること、
  • 「動的リンク」は、ロードの指示方法やタイミングによって、さらに「明示的リンク」「暗黙的リンク」と「遅延ロード」に分けられること、

を見てきました。

分類や用語に気になる点もありますが(「動的ロード」は「動的リンク」に含めてよいのかとか、「遅延ロード」を「暗黙的リンク」と同列に扱うのは違和感があるので「暗黙的リンク(遅延ロード)」としてはどうかとか)、本稿では上記のようにまとめてみました。