実務であまり役に立たないVBAの内部の話

# はじめに
この記事では、VBAが、どのようにExcelやWordに格納されているかを考えてみます。

なお、もし、VBAのソースコードをプログラムから取得したりしたいだけならば、この記事は訳に立ちません。そういうことをしたい場合は[ここでやっている](https://github.com/mima3/VbaDoxygen/blob/master/ExcelVBADoxygen.vbs)感じで[VBProject](https://docs.microsoft.com/en-us/office/vba/api/excel.workbook.vbproject)プロパティを操作するとできると思います。

## xlsm中のVBAはどう格納されているか
まず、拡張子がxlsmというファイルの場合、Excelファイルを[7-Zip](https://www.7-zip.org/download.html)などの解凍ソフトで開いてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/7e572460-4916-47e9-bd72-c70b3fcaba3e.png)

すると、圧縮されているファイルの内容が確認されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/cfc7d823-6d9e-7692-76f5-a41ac35d59d8.png)

そのファイル中の「xl」フォルダに「vbaProject.bin」というバイナリファイルが存在し、それがVBAの中身になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/41e1d626-6ce9-a6cd-0264-8af8253fefc8.png)

このvbaProject.binというバイナリファイルは[Compound File Binary Format](https://en.wikipedia.org/wiki/Compound_File_Binary_Format)というファイル形式で格納されています。
ルートのストレージオブジェクトの中に子ストレージオブジェクトとストリームオブジェクトのツリー構造で構成されており、その仕様は以下に公開されています。

**[MS-CFB]: Compound File Binary File Format**
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b

なお、7Zipを使用するとCFBの内容を閲覧可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/b0813310-5f1c-20cf-da85-39c8f9f51817.png)

7ZIPで確認できるのはストレージまでなのでストリームの内容に関しては後述する方法か、自分でバイナリエディタを開いて解析する必要があります。
VBA固有のストレージとストリームの仕様は以下に定義されています。

**[MS-OVBA]: Office VBA File Format Structure**
https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc

### 古い形式のXLSの場合
2003以前のExcelでVBAを確認する場合は、xlsファイルがCFBになっているので、7ZIPで直接開くと、＿VBA_PROJECT_DIRと言うストレージオブジェクトが確認できます。この中にVBAの情報が格納されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/b45c8b9a-53d0-74cd-f422-8cd28e25b390.png)

## バイナリからVBAのソースコードを取得する
[[MS-OVBA]: Office VBA File Format Structure](https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc)によると、ソースコードはModule Stream中のCompressedSourceCodeとして圧縮されたバイト配列で格納されています。

なお、このModule Streamからソースコードを取得するようなPythonのツールが存在しています。

**oletools**
https://github.com/decalage2/oletools/tree/master/oletools

oletools中の[olevba](https://github.com/decalage2/oletools/wiki/olevba)コマンドを使用することで、xlsmファイル中のモジュールストリームからVBAのコードが抽出可能です。

```
C:\dev\python3>olevba -c C:\share\vba\test\test.xlsm
olevba 0.54.2 on Python 3.7.4 - http://decalage.info/python/oletools
===============================================================================
FILE: C:\share\vba\test\test.xlsm
Type: OpenXML
-------------------------------------------------------------------------------
VBA MACRO ThisWorkbook.cls
in file: xl/vbaProject.bin - OLE stream: 'VBA/ThisWorkbook'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit

-------------------------------------------------------------------------------
VBA MACRO Sheet1.cls
in file: xl/vbaProject.bin - OLE stream: 'VBA/Sheet1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit

-------------------------------------------------------------------------------
VBA MACRO Module1.bas
in file: xl/vbaProject.bin - OLE stream: 'VBA/Module1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit

Public Sub test()
    Dim x As String
    Dim y As String
    Dim z As String
    x = "TEST"
    y = "abe"
    z = x & " " & z & "neko"
    MsgBox z
End Sub
-------------------------------------------------------------------------------
VBA MACRO Class1.cls
in file: xl/vbaProject.bin - OLE stream: 'VBA/Class1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit

Public a As Long
Public Sub test()
    MsgBox "test"
End Sub

Public Function test2(a, b)
    test2 = a + b
End Function
-------------------------------------------------------------------------------
VBA MACRO UserForm1.frm
in file: xl/vbaProject.bin - OLE stream: 'VBA/UserForm1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit

Private Sub CommandButton1_Click()
    MsgBox "test"
End Sub
-------------------------------------------------------------------------------
VBA FORM STRING IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1/o'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
�Label1
-------------------------------------------------------------------------------
VBA FORM STRING IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1/o'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MS UI Gothic
-------------------------------------------------------------------------------
VBA FORM STRING IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1/o'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
�CommandButton1�
-------------------------------------------------------------------------------
VBA FORM STRING IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1/o'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MS UI Gothic
-------------------------------------------------------------------------------
VBA FORM Variable "b'Label1'" IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
None
-------------------------------------------------------------------------------
VBA FORM Variable "b'CommandButton1'" IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
None

```

## 中間コードを確認する
VBAにはソースコードの他に中間コードが存在しています。
ここ以降の内容は現時点でMicrosoftのホームページから確認できる内容でなく、非公式の話になります。

※2020.05.21追記
下記のVBAの中間コードやキャッシュを圧縮したり削除するツールのドキュメント中にMicrosoftのページを参考にしています。ただ、Link切れを起こしているのでアーカイブなどで当時の公式情報を探すことはできるかもしれませんが、**今**の情報がどうなっているかは不明です。
http://cpap.com.br/orlando/VBADecompilerMore.asp


Visual Basic Editorでソースコードを編集すると、その時点で[P-Code](https://en.wikipedia.org/wiki/Microsoft_P-Code)という中間コードが作成されて、Module Stream中のPerformanceCacheに記録されます。

P-Codeが一度でも実行されると、さらにトークン化された形式でSRP Streamsに格納されます。


### P-Codeを取得する
下記のツールを使用することでVBA中のp-codeを取得することが可能です。

**pcodedmp.py - A VBA p-code disassembler**
https://github.com/bontchev/pcodedmp

たとえば以下のようなVBAのコードがあるとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/692eddfe-89f3-ab33-67f8-cb2d4a77f92f.png)

これをpcodemp.pyでダンプを行うと以下のようになります。

```
C:\dev\python3\pcodedmp\pcodedmp>pcodedmp -d C:\share\vba\test\testdata\test_006.xlsm
Processing file: C:\share\vba\test\testdata\test_006.xlsm
===============================================================================
Module streams:
VBA/ThisWorkbook - 1067 bytes
Line #0:
        Option  (Explicit)
Line #1:
VBA/Sheet1 - 1059 bytes
Line #0:
        Option  (Explicit)
Line #1:
VBA/Module1 - 1588 bytes
Line #0:
        Option  (Explicit)
Line #1:
Line #2:
        FuncDefn (Public Sub test())
Line #3:
        Dim
        VarDefn z (As String)
Line #4:
        LitStr 0x000A "abcdあいう"
        St z
Line #5:
        Ld z
        ArgsCall MsgBox 0x0001
Line #6:
        EndSub
```

もし-bオプションを使用した場合は、詳細なアドレスも確認できます。

```
略
0583: Line #4:
00000000   B9 00 0A 00 61 62 63 64 82 A0 82 A2 82 A4 27 00    ....abcd......'.
00000010   34 02                                              4.

        00B9 LitStr 0x000A "abcdあいう"
        0027 St z
略
```

### P-Codeからソースコードを取得する
P-Codeからソースコードに変換するPythonのツールも存在します。

**pcode2code.py - A VBA p-code decompiler**
https://github.com/Big5-sec/pcode2code


下記のpcodedmpの出力結果からソースコードを取得してみます。

```code.txt
Processing file: C:\share\vba\test\testdata\test_006.xlsm
===============================================================================
Module streams:
VBA/ThisWorkbook - 1067 bytes
Line #0:
	Option  (Explicit)
Line #1:
VBA/Sheet1 - 1059 bytes
Line #0:
	Option  (Explicit)
Line #1:
VBA/Module1 - 1588 bytes
Line #0:
	Option  (Explicit)
Line #1:
Line #2:
	FuncDefn (Public Sub test())
Line #3:
	Dim 
	VarDefn z (As String)
Line #4:
	LitStr 0x000A "abcdあいう"
	St z 
Line #5:
	Ld z 
	ArgsCall MsgBox 0x0001 
Line #6:
	EndSub 
```

以下のようにpcode2codeコマンドの-pオプションにp-codeが記述されたテキストを与えることで、そのVisualBasicのソースコードが確認できます。


```
C:\dev\python3\pcodedmp\pcodedmp>pcode2code -p code.txt
stream : VBA/ThisWorkbook - 1067 bytes
########################################

Option Explicit

stream : VBA/Sheet1 - 1059 bytes
########################################

Option Explicit

stream : VBA/Module1 - 1588 bytes
########################################

Option Explicit

Public Sub test()
  Dim z As String
  z = "abcdあいう"
  MsgBox z
End Sub
```

## まとめ
OfficeのVBAは[Compound File Binary File Format](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b)の形式でファイル内に格納されています。
この内容は、[Office VBA File Format Structure](https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc
)に記載されています。ただし、ここには、中間コードについての詳細は記載されていません。

ファイルから圧縮されたVBAのソースコードを抜き出すには[olevba](https://github.com/decalage2/oletools/wiki/olevba)を使用すること抽出できます。
また、中間コードのp-codeを抜き出すには[pcodedmp.py - A VBA p-code disassembler](https://github.com/bontchev/pcodedmp)が使用できます。

p-codeが一度実行されたあとに作成されるキャッシュの内容を解析できる方法は現時点で確認できませんでした。

VBAはp-codeまたはキャッシュされた内容で動作するので、ソースコードで記載した通りに動作しない場合は、ここで詳細した方法でp-codeの内容を確認することで解決する可能性があるかもしれません。（未検証）

## 参考
**Compound File Binary Format**
https://en.wikipedia.org/wiki/Compound_File_Binary_Format

**[MS-CFB]: Compound File Binary File Format**
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b


**[MS-OVBA]: Office VBA File Format Structure**
https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc

**olevba**
https://github.com/decalage2/oletools/wiki/olevba


**pcodedmp.py - A VBA p-code disassembler**
https://github.com/bontchev/pcodedmp

**pcode2code.py - A VBA p-code decompiler**
https://github.com/Big5-sec/pcode2code