VBA P-codeと仮想マシンから実行系を理解する

LWP | VBA P-codeと仮想マシンから実行系を理解する

LWP TECHNICAL ARTICLE | 171

VBA P-codeと仮想マシンから実行系を理解する

xlsm内部、VBA Runtime、COM、Excel Object Modelの境界を追う

Copyright © 2026 LWP 山中 一弘

本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。

記事要約

VBAはソース文字列をCPUが直接実行する仕組みではありません。VBAプロジェクトにはソースと実行用の内部表現が保存され、VBA Runtimeがプロシージャを実行し、必要に応じてCOM経由でExcel Object Modelを呼び出します。

本記事では、xlsm内のvbaProject.bin、MS-OVBAが定めるストリーム、PerformanceCache、疑似P-code、呼び出しフレーム、Variant、Excel境界のコストを一つの流れで整理します。VBAのオペコードは公開仕様ではないため、公式仕様・一般モデル・逆解析上の推定を明確に分けます。

本記事の対象とゴール

想定読者

  • VBAのコンパイルと実行の違いを知りたい方

  • xlsm内部のVBAプロジェクト構造を理解したい方

  • 性能やセキュリティを実行系の境界から考えたい方

本記事で得られること

  1. ソース、内部表現、VBA Runtime、Excel本体を区別できます。

  2. 疑似P-codeで式や分岐を説明できます。

  3. 公式仕様と逆解析上の推定を分けて扱えます。

目次

  • はじめに P-codeは内部構造を考えるためのモデルです

  • 第1章 ソース、P-code、ネイティブコードを分ける

  • 第2章 xlsmの中でVBAはどこにあるのか

  • 第3章 コンパイルでは何が解決されるのか

  • 第4章 疑似P-codeで制御構造をほどく

  • 第5章 呼び出しフレームと値の寿命を考える

  • 第6章 VBA RuntimeとExcel Object Modelの境界

  • 第7章 解析とセキュリティで注意すること

  • 第8章 VBAプログラムの一生を一枚で捉える

  • まとめ P-codeは境界を見つけるために役立ちます

はじめに P-codeは内部構造を考えるためのモデルです

VBAのコードは、CPUがそのまま実行する機械語としてブックに保存されるわけではありません。ExcelはVBAプロジェクトを読み込み、VBAの実行環境を通して処理します。その途中に、一般にP-codeと呼ばれるコンパイル済み表現があります。

ただし、VBAのP-codeにはJVMのような公開命令仕様がありません。本記事で示す命令名やスタック図は、処理を理解するための疑似表現です。Microsoftが公開した公式オペコード一覧として扱ってはいけません。

第1章 ソース、P-code、ネイティブコードを分ける

1.1 CPUが直接実行するのは機械語です

CPUが直接理解するのは、そのCPU向けの機械語です。VBAのソースにあるIf、For、Rangeという文字列をCPUが直接読むわけではありません。実行系は、高水準の構文を解析し、実行しやすい内部表現へ変換します。

VBAソース
    ↓ 構文解析・名前解決・型確認
コンパイル済みの内部表現
    ↓ VBA Runtimeが解釈・実行
COM Automation
    ↓
Excel Object Model

ここで重要なのは、VBAの内部表現とExcel本体のネイティブコードを同一視しないことです。セルを消去する実処理はExcel側が担当し、VBA Runtimeは呼び出しを組み立てます。

1.2 JVMとの比較は共通点と境界を示します

Javaでは、Java Virtual Machine Specificationが命令セットやクラスファイルを定義しています。VBAにも「ソースを内部表現へ変換し、実行環境で動かす」という共通点はありますが、P-codeの命令仕様や検証器が同じように公開されているわけではありません。

したがって「VBAはJVMと同じスタックマシンだ」と断定するのではなく、プログラムカウンター、呼び出しフレーム、一時値という一般的なVMモデルを説明の補助線として使います。

第2章 xlsmの中でVBAはどこにあるのか

2.1 vbaProject.binは複合ファイルです

.xlsmはOffice Open XMLのZIPパッケージです。VBAプロジェクトは通常、xl/vbaProject.binというバイナリ部品に格納されます。この部品自体はCompound File Binary Formatで、階層的なStorageとStreamを持ちます。

workbook.xlsm
└─ xl
   └─ vbaProject.bin
      ├─ PROJECT
      ├─ dir
      ├─ _VBA_PROJECT
      └─ VBA
         ├─ Module1
         ├─ ThisWorkbook
         └─ Sheet1

MS-OVBAでは、PROJECT、dir、モジュールストリームなどの構造が規定されています。モジュールストリームには圧縮されたソースコードが含まれます。

2.2 PerformanceCacheは正本として読まない

_VBA_PROJECTに関連するPerformanceCacheは、実装依存かつバージョン依存の情報です。MS-OVBAは、読み取り時に無視し、書き込み時に保持しない扱いを規定しています。

この性質から、キャッシュを別環境へ移植可能な正本と考えるのは不適切です。解析や復旧の基準はソース、宣言、参照情報であり、キャッシュは再構築され得る派生物です。

第3章 コンパイルでは何が解決されるのか

3.1 構文と名前を確定します

VBEの「デバッグ」からプロジェクトをコンパイルすると、少なくとも構文、宣言、名前、参照可能な型やプロシージャの整合が確認されます。Option Explicitは、変数名の誤記をこの段階で検出しやすくします。

Option Explicit
 
Public Function AddTax(ByVal amount As Currency) As Currency
    AddTax = amount * 1.1
End Function

amountがローカルな引数であること、Currency型であること、戻り値がAddTaxへ代入されることは、実行前に解析できます。一方、開いたブックに特定のシートが存在するか、セルの値が適切かは実行時でなければ分かりません。

3.2 コンパイルエラーと実行時エラーを分けます

未宣言変数や構文の不整合はコンパイル時の問題です。存在しないシートへのアクセス、0除算、ファイル権限の不足などは実行時の問題です。この区別を持つと、調査の入口をVBEのコンパイル、参照設定、実データのどこに置くべきか判断できます。

第4章 疑似P-codeで制御構造をほどく

4.1 高水準の式は小さな操作へ分かれます

次のVBAを考えます。

Dim total As Long
total = 2 + 3

説明用の疑似P-codeなら、次のように表せます。

LOAD_CONST 2
LOAD_CONST 3
ADD
STORE_LOCAL total

これは実在する公式ニーモニックの提示ではありません。「定数を用意し、加算し、結果をローカル変数へ格納する」という状態変化を分解したものです。

4.2 IfとForも比較と分岐へ分かれます

If total > 0 Thenは、概念上は値の読み出し、比較、条件分岐になります。Forも初期化、終了条件の比較、本体、加算、先頭への分岐へ分解できます。高水準言語の構造が消え、次にどの位置を実行するかという制御フローになる点が要所です。

初期化 → 条件比較 → 条件が偽なら終了
                  ↓ 真
                 本体
                  ↓
                 加算 ──→ 条件比較へ戻る

第5章 呼び出しフレームと値の寿命を考える

5.1 プロシージャごとに実行文脈があります

関数を呼び出すと、引数、ローカル変数、戻り先、一時値を管理する文脈が必要です。一般的なVM用語では呼び出しフレームと呼ばれます。プロシージャが終了すると通常のローカル変数は寿命を終え、呼び出し元へ戻ります。

ByValは値を受け取る契約、ByRefは呼び出し元の変数を参照して変更できる契約です。オブジェクト変数の場合は、オブジェクト本体ではなく参照値がやり取りされる点に注意が必要です。

5.2 Variantは型情報を伴います

Variantは、実行時の値だけでなく、その値をどう解釈するかという型情報を必要とします。数値、文字列、日付、オブジェクト参照では、加算や比較の処理が異なります。Variantを多用した処理が単純な整数演算より複雑になる理由は、この動的な判定と変換にあります。

第6章 VBA RuntimeとExcel Object Modelの境界

6.1 Range操作はCOM呼び出しです

次のコードは一行ですが、VBA内部の加算とは性質が違います。

Worksheets("Sheet1").Range("A1").ClearContents

VBA Runtimeは参照を解決し、Excel Object Modelへメソッド呼び出しを渡します。Excel本体がセルの状態を変更し、結果やエラーがVBAへ返ります。

6.2 セルを一件ずつ触ると境界往復が増えます

100万セルをループで一件ずつ読み書きすると、VBA命令の回数だけでなく、VBAとExcelの境界を100万回近く往復します。Rangeを配列へ一括取得し、VBA側で処理して一括で戻すと速くなりやすいのは、この境界呼び出しを減らせるためです。

性能改善では、まず次を測ります。

  • Excel Object Modelへの呼び出し回数

  • ワークシート再計算や画面更新

  • ファイル、ネットワーク、外部アプリとのI/O

  • Variant変換や文字列処理

P-codeの細かな命令を推測する前に、より大きな境界コストを確認する方が実務的です。

第7章 解析とセキュリティで注意すること

7.1 ソースだけを見て安全とは断定できません

VBAプロジェクトにはソースとコンパイル済み表現が共存します。この二つが意図的に不一致になるVBA Stompingと呼ばれる攻撃手法が知られています。そのため、不審な文書の調査では、表示されたソースだけで安全性を断定しません。

不審なマクロ有効ブックは本番Excelで開かず、隔離環境、信頼できる静的解析、組織のセキュリティ手順を使います。P-code解析ツールの出力はOfficeバージョンやツールの実装に依存するため、公式仕様、ツールの推定、説明用疑似コードを区別して記録します。

7.2 decompileは万能な修復手順ではありません

/decompile起動やモジュールのエクスポート/再インポートで不具合が改善する場合がありますが、これはキャッシュや内部表現の再構築を促す経験的な手段です。実行前に元ファイルを保全し、署名、参照設定、フォーム、64bit対応、外部依存を含めて検証する必要があります。

第8章 VBAプログラムの一生を一枚で捉える

VBEでソースを書く
    ↓
構文・宣言・名前・型を解析する
    ↓
VBAプロジェクトとして保存する
    ↓
xlsm内のvbaProject.binへ格納する
    ↓
ExcelがVBA Runtimeへ読み込む
    ↓
プロシージャの内部表現を実行する
    ↓
必要に応じてCOM経由でExcelを呼ぶ
    ↓
Excelが処理し、結果をVBAへ返す

この全体像を持つと、コンパイルエラー、実行時エラー、Excel境界の性能問題、ファイル破損、セキュリティ調査を同じ言葉で混ぜずに扱えます。

まとめ P-codeは境界を見つけるために役立ちます

VBA P-codeを学ぶ目的は、未公開の命令名を暗記することではありません。ソース、内部表現、VBA Runtime、COM、Excel本体という層を分け、どこで何が起きるかを説明できるようにすることです。

公式に確認できるのはVBAプロジェクトのファイル構造やキャッシュの扱いです。実行命令の詳細は公開仕様ではないため、疑似P-codeや逆解析結果には根拠レベルを明記します。この慎重さが、性能改善にも障害調査にもセキュリティ判断にも必要です。

出典メモ

  • Microsoft Learn『[MS-OVBA]: Office VBA File Format Structure』 https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc

  • Microsoft Learn『[MS-CFB]: Compound File Binary File Format』 https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b

  • Oracle『The Java Virtual Machine Specification』 https://docs.oracle.com/javase/specs/jvms/se24/html/

  • 元ネタの4時間講義アウトラインは論点表として使用し、本文は公開仕様で確認できる範囲と説明用モデルを分けて再構成した。