rundll32 はなぜ __cdecl の関数も __stdcall の関数も呼び出せるのか

通常、関数を呼び出す際は、呼び出す側と呼び出される側とで呼び出し規約 (calling convention) を合わせておく必要があります。また、__stdcall 呼び出し規約では、引数の個数も合わせておく必要があります。
しかし、rundll32 コマンドは、呼び出す関数の仕様を規定していません。なぜ、どんな関数も呼び出せるのでしょうか。

動作確認環境

  • Windows 10 Home 21H1, 64bit
    (ただし、今回は 32bit の C:\Windows\SysWOW64\rundll32.exe モジュールについて扱います)
  • Visual Studio Community 2019

呼び出し規約の不整合が発生した場合

__cdecl(C 呼び出し規約)の関数を __stdcall(Windows 標準呼び出し規約)で呼び出した場合の動作を確認します。引数の数は 4 個とします。(簡単のため、本記事では引数はすべて DWORD 型とします。)

  1. 下図の 1 が初期状態です。
  2. 関数の呼び出し元がスタックに 4 個の引数を積みます。
  3. 関数を呼び出します。このとき、戻りアドレスがスタックに積まれます。
  4. 呼び出された関数は、自身の終了時にスタックから戻りアドレスを取り出し、呼び出し元に戻ります。
    (※ __stdcall の関数であれば、戻りアドレスを取り出すとともに引数 4 個分スタックを巻き戻し、呼び出し元に戻ります。)

呼び出し規約の不整合が原因で、関数呼び出し前後でスタックポインタ (ESP) の位置が変わっています。具体的には、スタックの消費量が引数 4 個分増えています。
この関数呼び出しを for ループなどで何度も繰り返すと、スタックオーバーフロー例外が発生します。

引数の個数の不整合が発生した場合

4 個の引数を取る関数に対して 1 個の引数しか渡さなかった場合の動作を確認します。呼び出し規約は __stdcall とします。

  1. 下図の 1 が初期状態です。
  2. 関数の呼び出し元がスタックに 1 個の引数を積みます。
  3. 関数を呼び出します。このとき、戻りアドレスがスタックに積まれます。
  4. 呼び出された関数は、自身の終了時に、スタックから戻りアドレスを取り出すとともに、引数 4 個分スタックを巻き戻し、呼び出し元に戻ります。
    (※ 1 個の引数を取る関数であれば、スタックから戻りアドレスを取り出すとともに引数 1 個分スタックを巻き戻し、呼び出し元に戻ります。)

引数の個数の不整合が原因で、関数呼び出し前後でスタックポインタ (ESP) の位置が変わっています。具体的には、スタックが引数 3 個分余計に巻き戻されています。
この状態で処理を続けると、たとえばスタック上にあるローカル変数の値が想定外の値に書き換わってしまいます。

rundll32 の関数呼び出し動作

それでは rundll32 がどのように関数を呼び出すのか見てみましょう。

  1. 下図の 1 が初期状態です。
  2. 現在のスタックポインタ (ESP) の値を既定の場所に退避します。
  3. スタックに 16 個の 0x00000000 を積みます。これは、関数の 5~20 個目の引数(もしあれば)に対応します。
  4. スタックに 4 個の値を積みます。これは、関数の 1~4 個目の引数(もしあれば)に対応します。具体的な値については「rundll32 が関数に渡す引数は何なのか」を参照してください。


  5. 関数を呼び出します。このとき、戻りアドレスがスタックに積まれます。
  6. 呼び出された関数は、自身の終了時に、スタックから戻りアドレスを取り出すとともに、(関数が __stdcall であれば)引数の個数分スタックを巻き戻し、呼び出し元に戻ります。呼び出し元は、どこまで巻き戻されるかを知りません。
  7. 呼び出し元は、退避していたスタックポインタの値を復元します。これにより、呼び出された関数がスタックをどこまで巻き戻したかに関係なく、関数呼び出し前後のスタックポインタの位置が同じになります。


以上のように、rundll32 は独自の方式で関数を呼び出すことで、呼び出される関数が __cdecl でも __stdcall でも、引数の個数がいくつでも、例外などの異常が発生しないようになっています。