MASMにおける関数の記述方法（C/C++とのクロス開発を踏まえた）

Microsoft Macro Assembler（以降、MASMと略）Version 6以降でアプリケーションを開発する際のメモ書きです。
この記事では、以下の内容について記します。

1. [MASMにおける、関数のプロトタイプ宣言(`PROTO`)](#proto)
2. [MASMにおける、関数の記述(`PROC` ~ `ENDP`)](#proc--endp)
3. [MASMにおける、関数の呼び出し(`INVOKE`)](#invoke)


# PROTO

## 書式

```Assembly
label PROTO (distance) (langtype) ([,parameter:tag]...)
```

## 説明

関数のプロトタイプ宣言及び外部宣言をする事ができる。

[INVOKEディレクティブ](#invoke)で関数を呼ぶ際、`PROTO`ディレクティブにより前もってプロトタイプ宣言（若しくは外部宣言）する事により、アセンブラに関数がある事を通知できる。
（[INVOKEディレクティブ](#invoke)の前に関数、又はプロトタイプ宣言が無い場合は、エラーとなる。）

## パラメータ

### label

関数名を指定する。

### distance

関数の場所を指定する。

| distance | 説明 |
|----------|------|
| near     | 同一セグメントから呼び出される関数。|
| far      | 他のセグメントから呼び出される関数。|
| near16   | 同一セグメントから呼び出される16bitコードの関数。16bit, 32bitモードが混在するプロジェクトにて、使用可。 |
| far16    | 他のセグメントから呼び出される16bitコードの関数。16bit, 32bitモードが混在するプロジェクトにて、使用可。 |
| near32   | 同一セグメントから呼び出される32bitコードの関数。同一セグメントから呼び出される関数。16bit, 32bitモードが混在するプロジェクトにて、使用可。 |
| far32    | 他のセグメントから呼び出される32bitコードの関数。16bit, 32bitモードが混在するプロジェクトにて、使用可。 |

### langtype
関数呼出規約を指定する。
省略した場合は、[.modelディレクティブ](https://qiita.com/sha_w/items/8c703fefa76741cfebaa#model)にて指定した[langtype](https://qiita.com/sha_w/items/8c703fefa76741cfebaa#langtype)とみなされる。

関数呼出規約についての詳細は、[MASM, Visual C++における関数呼出規約](https://qiita.com/sha_w/items/15c598c2bf28814f7929)を参照。

### parameter:tag

当該関数の引数とその型を指定する。
parameterは引数の名前、tagは引数の型を示す。
配列変数は不可。配列を渡す時は、ポインターで渡す事。

| tag     | 内容　 | 
|---------|--------|
| BYTE    | 1 Byte の符号なし整数 (0～255) |
| SBYTE   | 1 Byte の符号付き整数 (-128～127) |
| WORD    | 2 Bytesの符号なし整数 (0～65,535) |
| SWORD   | 2 Bytesの符号付き整数 (-32,768～327,67) |
| DWORD   | 4 Bytesの符号なし整数 (0 ～ 4,294,967,295) |
| SDWORD  | 4 Bytesの符号付き整数 (2,147,483,648 ～ +2,147,483,647) |
| FWORD   | 6 Bytesの整数 |
| QWORD   | 8 Bytesの符号なし整数 |
| SQWORD  | 8 Bytesの符号付き整数 |
| TBYTE   | 10 Bytesの整数 |
| REAL4   | 単精度 (4 Bytes) の浮動小数点数 |
| REAL8   | 倍精度 (8 Bytes) の浮動小数点数 |
| REAL10  | 10 Bytes の浮動小数点数 |
| XMMWORD | Visual C++ における`__m128`型を示す。 |
| YMMWORD | Visual C++ における`__m256`型を示す。 |
| *struc* | `struc`ディレクティブにて定義された構造体（※構造体のメンバー全てがスタックにコピーされ渡される）。
|      PTR *tag* | `tag`へのポインタ（構造体へのポインタも可） |
| NEAR PTR *tag* | `tag`へのnearポインタ（構造体へのポインタも可） |
| FAR  PTR *tag* | `tag`へのfarポインタ（構造体へのポインタも可） |
| VARREG         | 可変長引数 （記述する時は、引数の一番最後に記述する。） |


# PROC ~ ENDP

## 書式

```Assembly
label   PROC    (distance) (langtype) (visibility) (USES reglist) ([,parameter:tag]...)
        (LOCAL  variable([count]):tag)

        (statements)

label   ENDP
```

## 説明

プロシージャー(関数)ブロックの開始(`PROC`)と終了(`ENDP`)を宣言する。
このプロシージャーは、`CALL`命令の他、[INVOKEディレクティブ](#invoke)から呼び出すことが出来る。

## パラメータ

### label

プロシージャー名（関数名）を指定する。

### distance

関数の場所を指定する。
[PROTOディレクティブ](#proto)の項を参照。

### langtype

関数の呼出規約を指定する。
[PROTOディレクティブ](#proto)の項を参照。

### visibility

関数の属性を指定する。

| visibility | 属性 |
|------------|------|
| PRIVATE    | 外部から関数を隠す |
| PUBLIC     | 外部から関数が呼ばれる |
| EXPORT 　　|

### USES reglist

プロシージャー(関数)が呼び出されたときに、最初にスタックに退避するレジスタを指定する。

### parameter:tag

当該プロシージャー(関数)の引数と、その型を指定する。
parameterは引数の名前、tagは引数の型を示す。
配列を渡す時は、ポインタで渡す（配列の実体を渡す事は不可）。
引数がある場合、`BP`, `EBP`レジスタを自動で退避し、スタックフレームとして使用する。
[PROTOディレクティブ](#proto)の項を参照。

### LOCAL variable([count]):tag

関数内のみで使う変数を指定する。
variableは変数の名前、[count]は要素数がcountの配列変数である事を示し、tagは引数の型を示す（構造体も可）。
この変数はスタック領域に確保され、関数から戻る時に自動で破棄される。
ローカル変数がある場合、`BP`, `EBP`レジスタを自動で退避し、スタックフレームとして使用する。
スタックのサイズが許す限り、いくらでも定義できる。

### statements

ここにコードを記述する。

## 例

以下の関数を記述した場合、

```Assembly
.186
.model  small,stdcall
.dosseg
.code
func    proc    far     uses ax ds di,
                para1:word,
                para2:far ptr WORD
        local   var[2]:word

        lds     di, para2
        mov     ax, [di]
        mov     var[sizeof(word)*0], ax

        mov     ax, para1
        mov     var[sizeof(word)*1], ax

        ret
func	endp
        END
```

MASM 5.0以前のアセンブリ言語で記述すると、以下と同等の意味を持つ。

```Assembly
.186
_TEXT   segment	word public 'CODE'
_TEXT   ends
_DATA   segment	word public 'DATA'
_DATA   ends
STACK   segment	para stack 'STACK'
STACK   ends
_BSS    segment	word public 'BSS'
_BSS    ends

DGROUP  group   _DATA,STACK,_BSS

        public  func@6

_TEXT   segment	word public 'CODE'
        assume  cs:_TEXT, ds:DGROUP

_func@6:
        push    bp              ;
        mov     bp, sp          ;スタックフレームの作成
        add     sp, 0FFFCh      ;ローカル変数領域の作成

        push    ax
        push    ds
        push    di              ;USESで指定したレジスタの保存

        lds     di, [bp+8]      ;ds:di ← 引数"para2"
        mov     ax, [di]
        mov     [bp-4], ax      ;var[0] ← ax
        mov     ax, [bp+6]      ;ax ← 引数"para1"
        mov     [bp-2], ax      ;var[1] ← ax

        pop     di
        pop     ds
        pop     ax              ;USESで指定したレジスタの復帰

        leave                   ;ローカル変数の破棄
        retf    6               ;__stdcallなので、引数を破棄して関数から戻る。

        END
```

尚、上のコードを見て解るとおり、最適化はされないので、 パイプラインの効率を気にする、且つ、どうしてもアセンブリ言語で書きたいのであれば、 `PROC` ～ `ENDP`を使わず、自力で最適化して関数を書く方が良い。
又、C言語から呼ぶ場合は、`__fastcall`を使う手もある。例えば、Visual C++にて

```cpp
extern "C" __fastcall void func(int para1, int para2, int para3);
```

上記の様に宣言された関数は、アセンブリ言語にて
（extern "C"が無いと、Visual C++独自の名前修飾が行われてしまう。）

```Assembly
.386
.model	flat,c
.code
@func@12        proc    near    syscall,
        para3:SDWORD            ;この場合、スタックフレームがebpに作成される。
;       ecx     = para1:SDWORD
;       edx     = para2:SDWORD

;       statement

        ret     4		;スタックに入った分の引数は、破棄する。
@func@12        endp
        END
```

または、

```Assembly
.386
.model  flat,c
.code
@func@12        proc    near    syscall
;       ecx     = para1:SDWORD
;       edx     = para2:SDWORD
;       [esp+4] = para3:SDWORD  ;何かをpushした時は、オフセットに気をつける。

;       statement

        ret     4		;スタックに入った分の引数は、破棄する。
@func@12        endp
        END
```

と記述する事で、呼び出すことができる。
尚、エル・エス・アイ・ジャパン社のLSI-C86（Wonder Witch版を除く）における関数規約では、引数はレジスタで渡される。
従って、`__cdecl`等、MASMで対応しているlangtypeでは記述不可能なので、自力でLSI-C86の関数規約に基づいた関数を書く必要がある。

# INVOKE

## 書式

```Assembly
INVOKE expression ([, arguments]...)
```

## 説明

指定された関数規約に基づき、関数expressionを呼ぶ。

## パラメータ

### expression

関数名を指定する。
尚、関数規約に基づく名前修飾は自動で行われる。

### arguments

関数の引数を指定する。
引数には、実数、レジスタ、レジスタのペア（`dx::ax`等）、アドレス式（`ADDR`で始まる式）が指定可能。

## 例

```Assembly
func	PROTO	near c, var:far ptr word
```
と宣言された関数を呼ぶ

```Assembly
        invoke	func,addr label[si]
```

は、

```Assembly
        push    DGROUP          ;ローカル変数の場合、"push ss"となる。
        lea     ax,[label + si] ;ローカル変数の場合、bpレジスタ経由となるため、
        push    ax              ;配列要素はdiかsiレジスタのみが可となる。
        call    _func
        add     sp,+4
```

と変換される。
これにより、他言語で記述したプログラムを簡易的に呼び出すことが出来る。
（※LSI-C86は、引数はレジスタで渡される規約の為、`__cdecl`等の規約での呼出は不可。）
尚、最適化はしないので、パイプラインの効率を気にするのであれば、`invoke`を使わず、自力で関数規約に基づいたコードを書く方がよい。
例えば上記のコードの場合、`lea ax,[n]`の後に`push ax`があり、 初代ペンティアムでは1 clockのペナルティーが発生する。
又、連続して関数を呼ぶときに、連続した関数を全て終えてからではなく、 各関数毎にスタックポインタを元に戻すので、その文`add sp,+n`命令が増える事となる。
（これは、スタックの破棄を関数側で行う規約（`__stdcall`等）を使えば、`ret n`命令で戻ってくる為、関係ない。）