x64 アセンブリ言語で、Windows API や C 言語のライブラリを呼び出す .exe, .lib, .dll を開発する (Windows, MASM)

前回は簡単な x64 アセンブリ言語プログラムを作りました。今回は次のプログラムを作ります。

  • C 言語のライブラリや Windows の API を呼び出すプログラム (.exe)
  • 足し算をするだけのスタティックライブラリ (.lib)
  • C 言語のライブラリや Windows の API を呼び出すスタティックライブラリ (.lib)
  • 足し算をするだけの DLL (.dll)
  • C 言語のライブラリや Windows の API を呼び出す DLL (.dll)

使用するアセンブラは Visual Studio に付属の 64bit 版 MASM(マイクロソフトマクロアセンブラ)です。Visual Studio Community に入っており、無料で使えます。

動作確認環境

  • Windows 11 Home 23H2
  • Visual Studio Community 2022 (MASM for x64, Visual C++)

C 言語のライブラリや Windows API を呼び出すプログラム (.exe)

まずは、C 言語のライブラリや Windows API を呼び出すプログラムのサンプルです。

; 外部関数の宣言
; Windows の関数
EXTERN MessageBeep: PROC
EXTERN MessageBoxA: PROC
; C ランタイムライブラリの関数
EXTERN scanf: PROC
EXTERN printf: PROC
EXTERN sprintf_s: PROC

; 定数の定義
MB_OK               EQU     0
MB_ICONINFORMATION  EQU     40h
MSGBUFLEN           EQU     64

; 初期化したデータのセグメント
; BYTE は DB(Define BYTE の意)という書き方もあり。
.data
SCANPROMPT      BYTE    "Enter a number: ", 0
SCANFORMAT      BYTE    "%d", 0
PRINTFORMAT     BYTE    "number=%d, str=%s", 0Dh, 0Ah, 0
HELLOSTR        BYTE    "Hello!", 0
MSGTITLE        BYTE    "Sample2", 0

; 初期化しないデータのセグメント
; DWORD は DD(Define DWORD の意)という書き方もあり。
.data?
SCANVAL         DWORD   ?
MSGBUF          BYTE    MSGBUFLEN dup (?)

; コードセグメント
.code

; void myFunc(void) 関数の定義
; デフォルトは PUBLIC
myFunc PROC PRIVATE
    ;------ スタックの確保と整列
    ;       関数内に call がある場合は、
    ;       「8 バイト x 引数の個数の最大値」分のスタックが必要。
    ;       ローカル変数を使う場合は、その分のスタックも必要。
    ;       スタックを 10h バイト単位に整列する必要もあり
    ;       (関数に入った直後のスタックのアドレスの末尾は 8h)。
    sub     rsp, 28h

    ;       関数内で RBX, RBP, RDI, RSI, 
    ;       R12-R15, XMM6-XMM15 を書き換える場合は
    ;       退避・復帰が必要。

    ;------ MessageBeep(MB_OK);
    mov     ecx, MB_OK
    call    MessageBeep

    ;------ printf("Enter a number:");
    lea     rcx, SCANPROMPT
    call    printf

    ;------ scanf("%d", &SCANVAL);
    lea     rdx, SCANVAL
    lea     rcx, SCANFORMAT
    call    scanf

    ;------ printf("number=%d, str=%s\r\n", SCANVAL, &HELLOSTR);
    lea     r8, HELLOSTR
    mov     edx, SCANVAL
    lea     rcx, PRINTFORMAT
    call    printf

    ;------ sprintf_s(&MSGBUF, MSGBUFLEN, "number=%d, str=%s\r\n", SCANVAL, &HELLOSTR);
    ;       引数が 5 個なので、28h バイトのスタックが必要(確保済み)。
    ;       最初の 4 つまでの引数は、
    ;       左から順に rcx, rdx, r8, r9 レジスタにセットする。
    ;       (小数のときや型が不明なときは別。)
    ;       5 個目およびそれ以降の引数は、スタックの次の位置に格納する。
    ;       [rsp + 20h], [rsp + 28h], [rsp + 30h], ......
    lea     rax, HELLOSTR
    mov     qword ptr [rsp + 20h], rax
    mov     r9d, SCANVAL
    lea     r8, PRINTFORMAT
    mov     edx, MSGBUFLEN ; 上位 32bit はゼロ拡張される
    lea     rcx, MSGBUF
    call    sprintf_s

    ;------ MessageBoxA(0, &MSGBUF, &MSGTITLE, 0);
    mov     r9d, MB_ICONINFORMATION
    lea     r8, MSGTITLE
    lea     rdx, MSGBUF
    xor     ecx, ecx
    call    MessageBoxA

    ;------ スタックの解放
    add     rsp, 28h
    ret
myFunc ENDP

; main 関数の定義
; デフォルトは PUBLIC なので「PUBLIC」は省略可
main PROC PUBLIC
    ;------ スタックの確保と整列
    sub     rsp, 28h

    ;------ myFunc();
    ;       ここに直接処理を書いてもよいが、
    ;       内部 (PRIVATE) 関数の呼び出しにしてみる。
    call    myFunc

    ;------ 戻り値の設定
    mov     eax, 200

    ;------ スタックの解放
    add     rsp, 28h
    ret
main ENDP

END

ポイントは次の通りです。

  • 呼び出す関数は「EXTERN <関数名>: PROC」のように宣言する。
  • 関数の呼び出し方法は「x64 での呼び出し規則」[1] に従う(※ 普通は「呼び出し規約」というが機械翻訳のため「呼び出し規則」になっている)。
  • 「main」という名前の関数を定義する。

ビルドしましょう。

Windows のスタートメニューから「x64 Native Tools Command Prompt for VS 2022」を起動します。

ソースファイルのあるディレクトリ(ここでは「c:\tmp」とする)に移動し、64bit 版 MASM である「ml64.exe」を呼び出してビルドします。「/link」以降に指定しているのは利用するライブラリです。

C:\tmp> ml64 sample2.asm /link user32.lib msvcrt.lib legacy_stdio_definitions.lib

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample2.asm
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:sample2.exe
sample2.obj
user32.lib
msvcrt.lib
legacy_stdio_definitions.lib

リンカーは、プログラム内に「main」という名前の関数を見つけると、サブシステムを「console」に、プログラムのエントリーポイントを C 言語ランタイムライブラリの初期化ルーチンである「mainCRTStartup」に自動的に設定します(「mainCRTStartup」は「main」を呼び出します)。
また、リンカーには既定のライブラリを自動的にリンクする機能があります。
そういったリンカーのお任せ機能に頼らずにオプションをすべて明示すると、次のように書けます。

C:\tmp> ml64 sample2.asm /link /nodefaultlib kernel32.lib user32.lib msvcrt.lib vcruntime.lib ucrt.lib legacy_stdio_definitions.lib legacy_stdio_wide_specifiers.lib /subsystem:console /entry:mainCRTStartup

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample2.asm
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:sample2.exe
sample2.obj
/nodefaultlib
kernel32.lib
user32.lib
msvcrt.lib
vcruntime.lib
ucrt.lib
legacy_stdio_definitions.lib
legacy_stdio_wide_specifiers.lib
/subsystem:console
/entry:mainCRTStartup

いずれにせよ、実行ファイル「sample2.exe」が生成されます。

実行します。

「scanf」関数が数の入力を求めてくるので、適当に「1234」と入力して [Enter] キーを押します。

「printf」関数がテキストを、「MessageBoxA」関数がメッセージを表示しました。

プログラムの終了コードは「%errorlevel%」で取得できます。

mov 命令で設定した終了コード「200」が取得できています。

コンソールプログラムではなく Windows プログラムを作りたいときは、「main」ではなく「WinMain」という名前の関数を定義します。

<前略(前のプログラムと同じ)>

; WinMain 関数の定義
; デフォルトは PUBLIC なので「PUBLIC」は省略可
WinMain PROC PUBLIC
    ;------ スタックの確保と整列
    sub     rsp, 28h

    ;------ myFunc();
    ;       ここに直接処理を書いてもよいが、
    ;       内部 (PRIVATE) 関数の呼び出しにしてみる。
    call    myFunc

    ;------ 戻り値の設定
    mov     eax, 200

    ;------ スタックの解放
    add     rsp, 28h
    ret
WinMain ENDP

END

コンソールプログラムのときと同じようにビルドします。

C:\tmp> ml64 sample2.asm /link user32.lib msvcrt.lib legacy_stdio_definitions.lib

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample2.asm
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:sample2.exe
sample2.obj
user32.lib
msvcrt.lib
legacy_stdio_definitions.lib

リンカーは、プログラム内に「WinMain」という名前の関数を見つけると、サブシステムを「windows」に、プログラムのエントリーポイントを C 言語ライブラリの Windows 版初期化ルーチンである「WinMainCRTStartup」に自動的に設定します(「WinMainCRTStartup」は「WinMain」を呼び出します)。
リンカーのお任せ機能に頼らずにオプションをすべて明示すると、次のように書けます。

C:\tmp> ml64 sample2.asm /link /nodefaultlib kernel32.lib user32.lib msvcrt.lib vcruntime.lib ucrt.lib legacy_stdio_definitions.lib legacy_stdio_wide_specifiers.lib /subsystem:windows /entry:WinMainCRTStartup

 Assembling: sample2.asm
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:sample2.exe
sample2.obj
/nodefaultlib
kernel32.lib
user32.lib
msvcrt.lib
vcruntime.lib
ucrt.lib
legacy_stdio_definitions.lib
legacy_stdio_wide_specifiers.lib
/subsystem:windows
/entry:WinMainCRTStartup

いずれにせよ、実行ファイル「sample2.exe」が生成されます。

実行します。

実行はされましたが、「printf」や「scanf」のコンソール入出力関数が無視されました。サブシステムが「console」の場合はコンソールが自動的に作成されますが、サブシステムが「windows」の場合は作成されないためです。かといってウィンドウが自動的に作成されるわけでもないので、ウィンドウを表示したい場合は「CreateWindowA」関数などを呼び出して独自にウィンドウを作る必要があります。
また、コマンドプロンプトから実行した「windows」プログラムの終了コードを取得するには、「start /wait」を利用してプログラムの終了を待ち合わせる必要があります。

C:\tmp> start /wait sample2.exe

C:\tmp> echo %errorlevel%
200

足し算をするだけのスタティックライブラリ (.lib)

次はスタティックライブラリ(静的ライブラリ)のサンプルです。
2 つの 64 ビット引数を受け取り加算した結果を返すだけの「MyAdd」関数を作ります。C 言語のライブラリや Windows API は呼び出しません。

.code

; __int64 MyAdd(__int64, __int64) 関数の定義
; デフォルトは PUBLIC
; 外部から隠すときは PRIVATE
MyAdd PROC PUBLIC
    ; 関数内に call 文があったり、
    ; ローカル変数の領域を確保したりする場合は、
    ; スタックの確保と整列が必要だが、
    ; 今回はないので何もしない。

    ; 第 1 引数は rcx に、
    ; 第 2 引数は rdx に格納される。
    ; 加算した結果を rax に格納する。
    ; 最適化すると「lea rax, QWORD PTR [rcx+rdx]」と書ける。
    add     rcx, rdx
    mov     rax, rcx

    ret
MyAdd ENDP

END

ポイントは次の通りです。

  • 公開する関数は「PUBLIC」で定義する(ただし、デフォルトで PUBLIC なので明記不要)。
  • 関数の呼び出され方は参考文献 [1] に従う。

アセンブルします。「/c」は「アセンブルのみ。リンクしない。」の意味です。

C:\tmp> ml64 /c sample3.asm

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample3.asm

「sample3.obj」というオブジェクトファイルが生成されました。
シンボルを見ると、「MyAdd」関数の存在が確認できます。

C:\tmp> dumpbin /symbols sample3.obj
......
Dump of file sample3.obj
......
008 00000000 SECT1  notype ()    External     | MyAdd
......

オブジェクトファイルのままでも使えますが、ライブラリファイルに変換します。

C:\tmp> lib sample3.obj /out:sample3.lib
Microsoft (R) Library Manager Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

「sample3.lib」が生成されました。
シンボルを見ると、同じく「MyAdd」関数の存在が確認できます。

C:\tmp> dumpbin /symbols sample3.lib
......
Dump of file sample3.lib
......
008 00000000 SECT1  notype ()    External     | MyAdd
......

ライブラリを呼び出す側の C 言語プログラムを作ります。

#include <stdio.h>

// アセンブリ言語で実装した MyAdd 関数の宣言
extern __int64 MyAdd(__int64 n1, __int64 n2);

int main()
{
    __int64 sum;

    // アセンブリ言語で実装した MyAdd 関数の呼び出し
    sum = MyAdd(0x111122229999FFFF, 0x1234ABCD00000002);
    printf("%016llx\n", sum);

    return 0;
}

ビルドします。「/link」以降で、さきほど作ったライブラリ「sample3.lib」とのリンクを指示しています。「/MD」は C 言語ランタイムライブラリの動的リンク指定です(「/MT」(デフォルト)を指定して静的にリンクしても構いませんが、実行ファイルのサイズが大きくなります。)

C:\tmp> cl /MD call_sample3.c /link sample3.lib

Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

call_sample3.c
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:call_sample3.exe
sample3.lib
call_sample3.obj

「call_sample3.exe」が生成されました。

実行します。

スタティックライブラリ「sample3.lib」内の「MyAdd」関数が呼び出され、2 つの 64 ビット整数を加算した結果が返ってきました。

C 言語のライブラリや Windows の API を呼び出すスタティックライブラリ (.lib)

今度は、スタティックライブラリから C 言語のライブラリ関数と Windows API の関数を呼び出してみます。
ひとつの整数とひとつの文字列を受け取り、その値をコンソール画面とメッセージボックスに表示する「MyMsg」 関数を作ります。

; 外部関数の宣言
; Windows の関数
EXTERN MessageBoxA: PROC
; C ランタイムライブラリの関数
EXTERN sprintf_s: PROC
EXTERN puts: PROC

; 定数の定義
MB_OK               EQU     0
MB_ICONINFORMATION  EQU     40h
MSGBUFLEN           EQU     64

; 初期化したデータのセグメント
; BYTE は DB(Define BYTE の意)という書き方もあり。
.data
PRINTFORMAT     BYTE    "number=%lld, str=%s", 0Dh, 0Ah, 0
MSGTITLE        BYTE    "Sample4", 0

; 初期化しないデータのセグメント
.data?
MSGBUF          BYTE    MSGBUFLEN dup (?)

; コードセグメント
.code

; __int64 MyMsg(__int64, char *) 関数の定義
; デフォルトは PUBLIC
; 外部から隠すときは PRIVATE
MyMsg PROC PUBLIC
    ;------ スタックの確保と整列
    ;       関数内に call がある場合は、
    ;       「8 バイト x 引数の個数の最大値」分のスタックが必要。
    ;       ローカル変数を使う場合は、その分のスタックも必要。
    ;       スタックを 10h バイト単位に整列する必要もあり
    ;       (関数に入った直後のスタックのアドレスの末尾は 8h)。
    sub     rsp, 28h

    ;       関数内で RBX, RBP, RDI, RSI, 
    ;       R12-R15, XMM6-XMM15 を書き換える場合は
    ;       退避・復帰が必要。

    ;------ sprintf_s(&MSGBUF, MSGBUFLEN, "number=%lld, str=%s\r\n", rcx, rdx);
    ;       引数が 5 個なので、28h バイトのスタックが必要(確保済み)。
    ;       最初の 4 つまでの引数は、
    ;       左から順に rcx, rdx, r8, r9 レジスタにセットする。
    ;       (小数のときや型が不明なときは別。)
    ;       5 個目およびそれ以降の引数は、スタックの次の位置に格納する。
    ;       [rsp + 20h], [rsp + 28h], [rsp + 30h], ......
    mov     [rsp + 20h], rdx
    mov     r9, rcx
    lea     r8, PRINTFORMAT
    mov     edx, MSGBUFLEN ; 上位 32bit はゼロ拡張される
    lea     rcx, MSGBUF
    call    sprintf_s

    ;------ puts(&MSGBUF);
    lea     rcx, MSGBUF
    call    puts

    ;------ MessageBoxA(0, &MSGBUF, &MSGTITLE, MB_ICONINFORMATION);
    mov     r9d, MB_ICONINFORMATION
    lea     r8, MSGTITLE
    lea     rdx, MSGBUF
    xor     ecx, ecx
    call    MessageBoxA

    ;------ 戻り値の設定
    mov     rax, 876543210987654321

    ;------ スタックの解放
    add     rsp, 28h
    ret
MyMsg ENDP

END

ポイントは次の通りです。

  • 公開する関数は「PUBLIC」で定義する(ただし、デフォルトで PUBLIC なので明記不要)。
  • 呼び出す関数は「EXTERN <関数名>: PROC」のように宣言する。
  • 関数の呼び出し方・呼び出され方は参考文献 [1] に従う。

先に書いた「C 言語のライブラリや Windows API を呼び出すプログラム」と「足し算をするだけのスタティックライブラリ」の合わせ技です。

アセンブルします。「/c」は「アセンブルのみ。リンクしない。」の意味です。

C:\tmp> ml64 /c sample4.asm

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample4.asm

「sample4.obj」というオブジェクトファイルが生成されました。
シンボルを見ると、「MyMsg」関数の存在が確認できます。

C:\tmp> dumpbin /symbols sample4.obj
.....
Dump of file sample4.obj
.....
010 00000000 SECT1  notype ()    External     | MyMsg
......

オブジェクトファイルのままでも使えますが、ライブラリファイルに変換します。

C:\tmp> lib sample4.obj /out:sample4.lib
Microsoft (R) Library Manager Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

「sample4.lib」が生成されました。
シンボルを見ると、同じく「MyMsg」関数の存在が確認できます。

C:\tmp> dumpbin /symbols sample4.lib
......
Dump of file sample4.lib
......
010 00000000 SECT1  notype ()    External     | MyMsg
......

ライブラリを呼び出す側の C 言語プログラムを作ります。

#include <stdio.h>

// アセンブリ言語で実装した MyMsg 関数の宣言
extern __int64 MyMsg(__int64 n, char *p);

int main()
{
    __int64 ret;

    // アセンブリ言語で実装した MyMsg 関数の呼び出し
    ret = MyMsg(1234567890123456789, "Hello, Japan!");
    printf("%lld\n", ret);

    return 0;
}

ビルドします。「user32.lib」をリンクしているのは、ライブラリ内で「MessageBoxA」関数を呼び出しているためです。

C:\tmp> cl /MD call_sample4.c /link sample4.lib user32.lib

Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

call_sample4.c
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:call_sample4.exe
sample4.lib
user32.lib
call_sample4.obj

オプションをすべて明示すると次のようになるでしょうか。

C:\tmp> cl /MD call_sample4.c /link /nodefaultlib kernel32.lib user32.lib msvcrt.lib vcruntime.lib ucrt.lib sample4.lib /subsystem:console /entry:mainCRTStartup

Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

call_sample4.c
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:call_sample4.exe
/nodefaultlib
kernel32.lib
user32.lib
msvcrt.lib
vcruntime.lib
ucrt.lib
sample4.lib
/subsystem:console
/entry:mainCRTStartup
call_sample4.obj

いずれにせよ、実行ファイル「call_sample4.exe」が生成されます。

実行します。

C 言語のプログラムからアセンブリ言語で実装した「MyMsg」関数が呼び出され、「MyMsg」関数から C 言語ランタイムライブラリの「sprintf_s」関数や「puts」関数、また、Windows API の「MessageBoxA」関数が呼び出されました。

[OK]ボタンを押します。


mov 命令で設定した終了コード「876543210987654321」が返ってきました。

足し算をするだけの DLL (.dll)

先に、足し算をするだけの「MyAdd」関数をスタティックライブラリとして作りましたが、今度は同じ関数をダイナミックリンクライブラリとして作成します。C 言語のライブラリや Windows API は呼び出しません。

.code

; DLL エントリーポイントの定義。
; この DLL では C 言語の関数を呼び出さないため、
; C 言語ランタイムライブラリの初期化については考慮不要。
MyDllEntryPoint PROC PUBLIC
    mov eax, 1 ; true (Success)
    ret
MyDllEntryPoint ENDP

; __int64 MyAdd(__int64, __int64) 関数の定義
; EXPORT でエクスポートする関数を指定
MyAdd PROC EXPORT
    ; 関数内に call 文があったり、
    ; ローカル変数の領域を確保したりする場合は、
    ; スタックの確保と整列が必要だが、
    ; 今回はないので何もしない。

    ; 第 1 引数は rcx に、
    ; 第 2 引数は rdx に格納される。
    ; 加算した結果を rax に格納する。
    ; 最適化すると「lea rax, QWORD PTR [rcx+rdx]」と書ける。
    add     rcx, rdx
    mov     rax, rcx

    ret
MyAdd ENDP

END

ポイントは次の通りです。

  • 任意の名前の DLL エントリーポイントを定義し、1(成功)を返すようにする。
  • DLL として公開する関数の定義に、EXPORT と付加する。

ビルドします。「/dll」オプションで DLL を生成することを示し、「/out」オプションで生成する DLL のファイル名を示し、「/entry」オプションで DLL エントリーポイントの関数を示しています。

C:\tmp> ml64 sample5.asm /link /dll /out:sample5.dll /entry:MyDllEntryPoint

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample5.asm
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:sample5.exe
sample5.obj
/dll
/out:sample5.dll
/entry:MyDllEntryPoint
   ライブラリ sample5.lib とオブジェクト sample5.exp を作成中

ライブラリ本体「sample5.dll」やインポートライブラリ「sample5.lib」が生成されました。
dumpbin コマンドにより、DLL が「MyAdd」関数をエクスポートしていることが確認できます。

C:\tmp> dumpbin /exports sample5.dll
......
Dump of file sample5.dll
......
          1    0 00001006 MyAdd
......

ちなみにビルド時にエントリーポイントを明示しないと、「_DllMainCRTStartup は未解決です」のエラーになります。

C:\tmp> ml64 sample5.asm /link /dll /out:sample5.dll

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample5.asm
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:sample5.exe
sample5.obj
/dll
/out:sample5.dll
   ライブラリ sample5.lib とオブジェクト sample5.exp を作成中
LINK : error LNK2001: 外部シンボル _DllMainCRTStartup は未解決です
sample5.dll : fatal error LNK1120: 1 件の未解決の外部参照

これは、次の理由によります。

  • DLL ビルド時、リンカーはデフォルトで「_DllMainCRTStartup」をエントリーポイントにしようとする。
  • 「_DllMainCRTStartup」は C 言語ランタイムライブラリの DLL 用初期化ルーチンである。
  • しかし今回は C 言語ランタイムライブラリは使わない(リンクしない)ため、当該関数が見つからない。

このエラーを避けるために、独自にエントリーポイント(MyDllEntryPoint)を作成し、ビルド時に指定(/entry:MyDllEntryPoint)したのでした。

さて、DLL を呼び出す側の C 言語プログラムを作ります。

#include <stdio.h>

// アセンブリ言語で実装した MyAdd 関数の宣言
extern __int64 MyAdd(__int64 n1, __int64 n2);

int main()
{
    __int64 sum;

    // アセンブリ言語で実装した MyAdd 関数の呼び出し
    sum = MyAdd(0x111122229999FFFF, 0x1234ABCD00000002);
    printf("%016llx\n", sum);

    return 0;
}

これは、スタティックライブラリの「MyAdd」関数を呼び出すプログラムとまったく同じです。
ビルドします。「/link」以降で、インポートライブラリ「sample5.lib」の参照を指示しています。「/MD」は C 言語ランタイムライブラリの動的リンク指定です(「/MT」(デフォルト)を指定して静的にリンクしても構いませんが、実行ファイルのサイズが大きくなります。)

C:\tmp> cl /MD call_sample5.c /link sample5.lib

Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

call_sample5.c
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:call_sample5.exe
sample5.lib
call_sample5.obj

「call_sample5.exe」が生成されました。

実行します。

ダイナミックリンクライブラリ「sample5.dll」内の「MyAdd」関数が呼び出され、2 つの 64 ビット整数を加算した結果が返ってきました。

試しに DLL ファイルを削除して実行してみましょう。

C:\tmp> del sample5.dll

C:\tmp> call_sample5.exe

DLL のロードに失敗し、「sample5.dll が見つからないため、コードの実行を続行できません。」というエラーメッセージが表示されました。

C 言語のライブラリや Windows の API を呼び出す DLL (.dll)

最後に、C 言語のライブラリや Windows の API を呼び出す DLL を作ります。
ひとつの整数とひとつの文字列を受け取り、その値をコンソール画面とメッセージボックスに表示する「MyMsg」関数です。

; 外部関数の宣言
; Windows の関数
EXTERN MessageBoxA: PROC
; C ランタイムライブラリの関数
EXTERN sprintf_s: PROC
EXTERN puts: PROC

; 定数の定義
MB_OK               EQU     0
MB_ICONINFORMATION  EQU     40h
MSGBUFLEN           EQU     64

; 初期化したデータのセグメント
; BYTE は DB(Define BYTE の意)という書き方もあり。
.data
PRINTFORMAT     BYTE    "number=%lld, str=%s", 0Dh, 0Ah, 0
MSGTITLE        BYTE    "Sample6", 0

; 初期化しないデータのセグメント
.data?
MSGBUF          BYTE    MSGBUFLEN dup (?)

; コードセグメント
.code

; 必要に応じて DllMain を定義する。
;DllMain PROC PUBLIC
;    mov eax, 1 ; true (Success)
;    ret
;DllMain ENDP

; __int64 MyMsg(__int64, char *) 関数の定義
; EXPORT でエクスポートする関数を指定
MyMsg PROC EXPORT
    ;------ スタックの確保と整列
    ;       関数内に call がある場合は、
    ;       「8 バイト x 引数の個数の最大値」分のスタックが必要。
    ;       ローカル変数を使う場合は、その分のスタックも必要。
    ;       スタックを 10h バイト単位に整列する必要もあり
    ;       (関数に入った直後のスタックのアドレスの末尾は 8h)。
    sub     rsp, 28h

    ;       関数内で RBX, RBP, RDI, RSI, 
    ;       R12-R15, XMM6-XMM15 を書き換える場合は
    ;       退避・復帰が必要。

    ;------ sprintf_s(&MSGBUF, MSGBUFLEN, "number=%lld, str=%s\r\n", rcx, rdx);
    ;       引数が 5 個なので、28h バイトのスタックが必要(確保済み)。
    ;       最初の 4 つまでの引数は、
    ;       左から順に rcx, rdx, r8, r9 レジスタにセットする。
    ;       (小数のときや型が不明なときは別。)
    ;       5 個目およびそれ以降の引数は、スタックの次の位置に格納する。
    ;       [rsp + 20h], [rsp + 28h], [rsp + 30h], ......
    mov     [rsp + 20h], rdx
    mov     r9, rcx
    lea     r8, PRINTFORMAT
    mov     edx, MSGBUFLEN ; 上位 32bit はゼロ拡張される
    lea     rcx, MSGBUF
    call    sprintf_s

    ;------ puts(&MSGBUF);
    lea     rcx, MSGBUF
    call    puts

    ;------ MessageBoxA(0, &MSGBUF, &MSGTITLE, MB_ICONINFORMATION);
    mov     r9d, MB_ICONINFORMATION
    lea     r8, MSGTITLE
    lea     rdx, MSGBUF
    xor     ecx, ecx
    call    MessageBoxA

    ;------ 戻り値の設定
    mov     rax, 876543210987654321

    ;------ スタックの解放
    add     rsp, 28h
    ret
MyMsg ENDP

END

ポイントは次の通りです。

  • 呼び出す関数は「EXTERN <関数名>: PROC」のように宣言する。
  • 関数の呼び出し方・呼び出され方は参考文献 [1] に従う。
  • DLL として公開する関数の定義に、EXPORT と付加する。
  • 必要であれば「DllMain」関数を定義する。

ビルドします。「/dll」オプションで DLL を生成することを示し、「/out」オプションで生成する DLL のファイル名を示しています。

C:\tmp> ml64 sample6.asm /link /dll user32.lib msvcrt.lib /out:sample6.dll

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample6.asm
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:sample6.exe
sample6.obj
/dll
user32.lib
msvcrt.lib
/out:sample6.dll
   ライブラリ sample6.lib とオブジェクト sample6.exp を作成中

リンカーは、DLL ビルド時、プログラムのエントリーポイントを C 言語ランタイムライブラリの DLL 用初期化ルーチンである「_DllMainCRTStartup」に自動的に設定します(「_DllMainCRTStartup」は、もしあれば、ユーザー定義の「DllMain」を呼び出します)。
また、リンカーには既定のライブラリを自動的にリンクする機能があります。
そういったリンカーのお任せ機能に頼らずにオプションをすべて明示すると、次のように書けます。

C:\tmp> ml64 sample6.asm /link /dll /nodefaultlib kernel32.lib user32.lib msvcrt.lib vcruntime.lib ucrt.lib /out:sample6.dll /entry:_DllMainCRTStartup

Microsoft (R) Macro Assembler (x64) Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: sample6.asm
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:sample6.exe
sample6.obj
/dll
/nodefaultlib
kernel32.lib
user32.lib
msvcrt.lib
vcruntime.lib
ucrt.lib
/out:sample6.dll
/entry:_DllMainCRTStartup
   ライブラリ sample6.lib とオブジェクト sample6.exp を作成中

いずれにせよ、ライブラリ本体「sample6.dll」や、インポートライブラリ「sample6.lib」が生成されます。
dumpbin コマンドにより、DLL が「MyMsg」関数をエクスポートしていることが確認できます。

C:\tmp> dumpbin /exports sample6.dll
......
Dump of file sample6.dll
......
    ordinal hint RVA      name

          1    0 00001000 MyMsg
......

ライブラリを呼び出す側の C 言語プログラムを作ります。

#include <stdio.h>

// アセンブリ言語で実装した MyMsg 関数の宣言
extern __int64 MyMsg(__int64 n, char *p);

int main()
{
    __int64 ret;

    // アセンブリ言語で実装した MyMsg 関数の呼び出し
    ret = MyMsg(1234567890123456789, "Hello, Japan!");
    printf("%lld\n", ret);

    return 0;
}

これは、スタティックライブラリの「MyMsg」関数を呼び出すプログラムとまったく同じです。

ビルドします。「/link」以降で、インポートライブラリ「sample6.lib」の参照を指示しています。「/MD」は C 言語ランタイムライブラリの動的リンク指定です(「/MT」(デフォルト)を指定して静的にリンクしても構いませんが、実行ファイルのサイズが大きくなります。)

C:\tmp> cl /MD call_sample6.c /link sample6.lib

Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

call_sample6.c
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:call_sample6.exe
sample6.lib
call_sample6.obj

「call_sample6.exe」が生成されました。

実行します。

「MyMsg」関数から C 言語ランタイムライブラリの「sprintf_s」関数や「puts」関数、また、Windows API の「MessageBoxA」関数が呼び出されました。

[OK]ボタンを押します。


mov 命令で設定した終了コード「876543210987654321」が返ってきました。

試しに DLL ファイルを削除して実行してみましょう。

DLL のロードに失敗し、「sample6.dll が見つからないため、コードの実行を続行できません。」というエラーメッセージが表示されました。

おわりに

x64 のアセンブリ言語で、Windows の実行ファイル (.exe)、スタティックライブラリ (.lib)、ダイナミックリンクライブラリ (.dll) を作ってみました。プログラム開発時のひな型になればと思います。

参考文献

[1] マイクロソフト「x64 での呼び出し規則」
https://learn.microsoft.com/ja-jp/cpp/build/x64-calling-convention?view=msvc-170

[2] マイクロソフト「ディレクティブ リファレンス」
https://learn.microsoft.com/ja-jp/cpp/assembler/masm/directives-reference?view=msvc-170

[3] @Stosstruppe「MASM(x64) で dll を作る」
https://qiita.com/Stosstruppe/items/d869dc2615a202689f14

[4] やや低レイヤー研究所「x64 呼び出し規約をわかりやすく説明してみる (Windows)」
https://yaya.lsv.jp/x64-calling-convention/