flat assembler で helloworld を書いてみた

これは「[ドワンゴ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/dwango)」6日目の記事です。

# flat assembler とは

flat assembler (fasm) は Tomasz Grysztar さんが開発している x86 向けのアセンブラです。flat assembler はマクロ機能がとても充実しているアセンブラです。公式や非公式でさまざまなマクロが開発されていて独特な文化を形成しています。

- [flat assembler 公式サイト](https://flatassembler.net/index.php)
- [WikipediaのFASMの記事](https://en.wikipedia.org/wiki/FASM)

例を挙げると Fresh IDE とよばれる flat assembler で開発された IDE があります。Fresh IDE には FreshLib と呼ばれるマクロライブラリが付属しており、マルチプラットフォームのGUIライブラリや、オブジェクト指向プログラミングを実現しています。一般的なアセンブラのイメージであるプラットフォーム依存で低レイヤーという概念を覆しています。

- [Fresh IDE 公式サイト](https://fresh.flatassembler.net/)
- [Fresh IDE - Portable assembly language (YouTubeの紹介動画)](https://youtu.be/y8d_kteyf_8)

今回はこ flat assembler を利用して 64bit の windows プログラムの作成に挑戦します。

# flat assembler を動かしてみる

今回は 64bit の Window 10 上で flat assembler を動かします。[flat assembler のダウンロードサイト](https://flatassembler.net/download.php) から Windows版のzipファイルをダウンロードして、適当なディレクトリに展開します。 `fasm.exe` のある場所にパスを通して、`INCLUDE` ディレクトリのパスを環境変数 `INCLUDE` に設定します。PowerShell を開いて `fasm` コマンドを実行すると以下のようにバージョンとオプションのヘルプが表示されます。

```
> fasm
flat assembler  version 1.73.04
usage: fasm <source> [output]
optional settings:
 -m <limit>         set the limit in kilobytes for the available memory
 -p <limit>         set the maximum allowed number of passes
 -d <name>=<value>  define symbolic variable
 -s <file>          dump symbolic information for debugging
```

今回利用したバージョンは2018年8月18日に更新された、バージョン1.73.04です。

# hello, world.

まずは `hello, world.` とメッセージボックスに表示するプログラムを作ります。fasm には windows プログラミングのためのマクロが付属しており、まるで普通のプログラミング言語のようにコードを記述できます。以下は付属マクロを利用したコード例です。

```
include 'win64wx.inc'

.code
  start:
    invoke MessageBox, HWND_DESKTOP, 'hello, world.', 'Message', MB_OK
    invoke ExitProcess, 0 

.end start
```

上のファイルを `hello01.asm` という名前で保存します。以下のように `fasm` コマンドの引数に指定して実行するとビルドされ `hello01.exe` が作られます。

```
> fasm .\hello01.asm
flat assembler  version 1.73.04  (944650 kilobytes memory)
4 passes, 0.2 seconds, 1536 bytes.
```

`hello01.exe` を実行すると以下のようにメッセージボックスが表示されます。

```
> .\hello01.exe
```

![1.png](https://qiita-image-store.s3.amazonaws.com/0/70306/e6c55695-56d0-b96f-244c-2c2684251e5e.png)

# 日本語を表示する

Unicode版のWindowsでは文字列をUTF16で扱います。そのため以下のようにそのまま日本語の文字列を書き込んでUTF8で保存してビルドすると文字化けします。

```
include 'win64wx.inc'

.code
  start:
    invoke MessageBox, HWND_DESKTOP, 'こんにちは、世界。', 'メッセージ', MB_OK
    invoke ExitProcess, 0 

.end start
```

![2.png](https://qiita-image-store.s3.amazonaws.com/0/70306/e08c2071-563a-0f63-798a-1110acb978af.png)

文字列の内容を16進数の数値で書き込むこともできますが、fasmに用意されているマクロを利用すると、UTF8の文字列をUTF16に変換できます。マクロを利用するには以下のように `include 'encoding\utf8.inc'` を追加します。

```
include 'win64wx.inc'
include 'encoding\utf8.inc'

.code
  start:
    invoke MessageBox, HWND_DESKTOP, 'こんにちは、世界。', 'メッセージ', MB_OK
    invoke ExitProcess, 0 

.end start
```

ビルドすると以下のように正しく表示されます。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/70306/e6babbb3-4605-ef14-5558-7f2690b0adc4.png)

# マクロを使わずに書いてみる

前章までのコードで `invoke MessageBox` の引数に文字列を直接書いています。このように書くと、マクロの機能で文字列内容を適当な場所に置き、MessageBox には文字列のアドレスを渡すようになります。この機能を使わないように書くと、以下のようになります。

```
.code
  start:
    invoke MessageBox, HWND_DESKTOP, message, title, MB_OK
    invoke ExitProcess, 0
  
  message du 'こんにちは、世界。', 0
  title du 'メッセージ', 0
```

`du` は16bitのデータを配置する命令です。`'encoding\utf8.inc'` は `du` 以降の文字列をUTF8だと仮定してUTF16に変換します。

`.code` は以降のコードをテキストセクションに配置するマクロ、`.end` はコードの最後で import テーブルを生成し、指定したラベルをエントリポイントとして指定するマクロです。どうやら同じ構文が MASM (Microsoft Macro Assembler) に用意されているらしく、その構文を再現しているようです。これらは fasm のプリミティブな `section` 命令で書き換えることができます。

`invoke` の引数に文字列を渡す機能や、`.code` や `.end` は `win64wx.inc` で定義されています。そして、`win64wx.inc` はより低レイヤーな `win64w.inc` マクロで実現されています。以下のコードは `win64w.inc` の機能を利用してメッセージボックスを表示するコードを書き換えたものです。

```
format PE64 GUI 5.0
entry start

include 'win64w.inc'
include 'encoding\utf8.inc'

section '.text' code readable executable

  start:
    sub rsp, 8
    invoke MessageBox, HWND_DESKTOP, message, title, MB_OK
    invoke ExitProcess, 0

section '.data' data readable writeable
  message du 'こんにちは、世界。', 0
  title du 'メッセージ', 0

section '.idata' import data readable writeable
  library kernel32,'kernel32',\
    user32,'user32'

  include 'api\kernel32.inc'
  include 'api\user32.inc'
```

今回は文字列を書き換えることがないので `message` や `title` はデータセクションではなくテキストセクションに置くこともできます。`start:` の直後で `sub rsp, 8` と書いています。これは今回の記事で初めて出てきた機械語の命令です。`sub` は引き算の命令で、スタックのトップを指している `rsp` から 8 引いています。これは 64bit の windows の呼び出し規約で、API呼び出し時にスタックが16バイト境界に並んだ状態で呼び出す必要があるためです。`call` した直後に8バイトのリターンアドレスが積まれているので8引いて調整します。

# コンソール画面に出力する

プログラミングの教科書に出てくるようなコンソール画面に hello, world. と出力するコードを書いてみます。ここでは Windows API の `WriteFile` 関数を利用して出力します。

```
format PE64 console
entry start

include 'win64w.inc'

section '.text' code readable executable
  start:
  sub rsp, 8
  invoke GetStdHandle, STD_OUTPUT_HANDLE
  invoke WriteFile, rax, msg, msg.len, output, 0
  invoke ExitProcess, 0

section '.data' data readable writeable
  output dd ?
  msg db 'hello, world.', 0x0D, 0x0A
  .len = $ - msg

section '.idata' import data readable writeable
  library kernel32, 'kernel32'
  include 'api\kernel32.inc'
```

`$` は現在のアドレスを表します。`$ - msg` は現在のアドレスから `msg` のアドレスの差を求めることで文字列の長さを求めています。API呼び出しの戻り値は `rax` に格納されているので、`WriteFile` の呼び出しでは直前の `GetStdHandle` の戻り値を `rax` で渡しています。今回は `kernel32.dll` のAPIのみ利用しているので `user32.dll` は不要です。`db` や `dd` はデータを配置する命令です。`db` では後続の値をバイトサイズ(8bit)だと解釈して出力、`dw` はワードサイズ (16bit)、`dd` はダブルワード (32bit)、`dq` はクアッドワード (64bit) で出力します。以下は実行結果です。

```
> fasm .\hello05.asm
flat assembler  version 1.73.04  (1048576 kilobytes memory)
3 passes, 2048 bytes.
> ./hello05
hello, world.
```

# invokeを使わずに書いてみる

これまで利用していた `invoke` は fasm のマクロです。展開すると機械語の `call` に変換されます。以下のコードはメッセージボックスを表示するプログラムの `invoke` を `call` の呼び出しに書き換えたものです。

```
format PE64 GUI 5.0
entry start

include 'win64w.inc'
include 'encoding\utf8.inc'

HWND_DESKTOP = 0
MB_OK = 0

section '.text' code readable executable

  start:
    sub rsp, 0x28

    mov rcx, HWND_DESKTOP
    lea rdx, [message]
    lea r8, [title]
    mov r9, MB_OK
    call [MessageBoxW]

    mov rcx, 0
    call [ExitProcess]

section '.data' data readable writeable
  message du 'こんにちは、世界。', 0
  title du 'メッセージ', 0

section '.idata' import data readable writeable
  library kernel32, 'kernel32', \
    user32, 'user32'

  include 'api\kernel32.inc'
  include 'api\user32.inc'
```

これまでのコードと異なり、`start` 直後が `sub rsp, 0x28` になっています。これは Windows 64bit の ABI で引数分のスタック領域を開けておく必要があるためです。このあたりの話は以下の資料に書かれています。

- [x64 Software Conventions](https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/7kcdt6fy(v%3dvs.110))

# .idata セクションを分解する

`.idata` セクションにはインポートする関数の名前を並べます。Windows のローダはこのセクションを見てDLLを読み込み、必要なAPIのアドレスをメモリに書き込みます。こちらもマクロを利用せずに記述できます。具体的なフォーマットは以下の資料に書かれています。

- [PE Format](https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format)

```
format PE64 GUI 5.0
entry start

include 'encoding\utf8.inc'

HWND_DESKTOP = 0
MB_OK = 0

section '.text' code readable executable

  start:
    sub rsp, 0x28

    mov rcx, HWND_DESKTOP
    lea rdx, [message]
    lea r8, [title]
    mov r9, MB_OK
    call [MessageBoxW]

    mov rcx, 0
    call [ExitProcess]

section '.data' data readable writeable
  message du 'こんにちは、世界。', 0
  title du 'メッセージ', 0

section '.idata' import data readable writeable
  dd 0, 0, 0, rva kernel_name, rva kernel_table
  dd 0, 0, 0, rva user_name, rva user_table
  dd 0, 0, 0, 0, 0

  kernel_table:
    ExitProcess dq rva _ExitProcess
    dq 0
  user_table:
    MessageBoxW dq rva _MessageBoxW
    dq 0

  kernel_name db 'kernel32', 0
  user_name db 'user32', 0

  _ExitProcess dw 0
    db 'ExitProcess',0
  _MessageBoxW dw 0
    db 'MessageBoxW',0
```

`include 'win64w.inc'` が不要になり、fasm付属のマクロを利用しない形に到達しました。ここで `rva` はEXEファイルがメモリに展開されたアドレス(BaseImageAddress)からの相対位置を計算するfasmの演算子です。

# バイナリファイルを出力する

これまでファイルの先頭で `format PE64` と書いて、64bitのPEフォーマットのファイルを出力するように指示ていましたが、ここを `format binary` と書くとバイナリ形式で値を直接出力できます。以下はこの機能を使って `ABC` と書かれたテキストファイルを出力する例です。`as 'txt'` と書くと出力の拡張子が `.txt` になります。

```
format binary as 'txt'
db 65, 66, 67
```

65, 66, 67 は `ABC` それぞれの文字コードです。文字列リテラルで以下のように書くことも出来ます。

```
format binary as 'txt'
db 'ABC'
```

この方法を利用するとプログラムに限らず、テキストや画像を出力することもできます。

# EXEファイル全体を自力で書いてみる

バイナリファイルを出力する機能を利用すると、EXEファイルのすべてのバイトを指定して生成できそうです。ということで実際にやってみました。ここでは話を簡単にするために、以下のコードを生成します。

```
format PE64 console
entry start

include 'win64w.inc'

section '.text' code readable executable
  start:
  sub rsp, 28h
  mov rcx, 123
  call [ExitProcess]

section '.idata' import data readable writeable
  dd 0, 0, 0, rva kernel_name, rva kernel_table
  dd 0, 0, 0, 0, 0
  kernel_table:
    ExitProcess dq rva _ExitProcess
    dq 0
  kernel_name db 'kernel32',0
  _ExitProcess dw 0
    db 'ExitProcess',0
```

起動すると即終了するプログラムです。分かりやすくするために戻り値を `123` にしています。そしてこのコードをひたすら `db`, `dw`, `dd`, `dq` を使って書いたコードが以下になります。作成中のメモをコメントとして残しているので再現される方は参考にして下さい。

```
;
; 64bit EXE 生成プログラム
;

format binary as "exe"

;------------------------------
; DOS ヘッダ
;------------------------------

; 全部定数とみなして問題ない

dw 0x5A4D			; e_magic
dw 0				; e_cblp
dw 0				; e_cp
dw 0				; e_crlc
dw 0				; e_cparhdr
dw 0				; e_minalloc
dw 0				; e_maxalloc
dw 0				; e_ss
dw 0				; e_sp
dw 0				; e_csum
dw 0				; e_ip
dw 0				; e_csum
dw 0				; e_lfarlc
dw 0				; e_ovno
dw 0, 0, 0, 0		; e_res
dw 0				; e_oemid
dw 0				; e_oeminfo
dw 0, 0, 0, 0		; e_res2
dw 0, 0, 0, 0
dw 0, 0
dd 0x0080			; e_lfanew

dq 0, 0, 0, 0, 0, 0, 0, 0

;------------------------------
; EXE ヘッダ
;------------------------------

; NumberOfSections はセクション数。必要に応じて書き換え
; 他はそのままで良い

dw 0x4550, 0x0000	; Signature
dw 0x8664			; Machine
dw 0x0002			; NumberOfSections
dd 0				; TimeDateStamp
dd 0				; PointerToSymbolTable
dd 0				; NumberOfSymbols
dw 0x00F0			; SizeOfOptionalHeader
dw 0x002F			; Characteristics

;------------------------------
; オプションヘッダ
;------------------------------

; 重要なのは AddressOfEntryPoint　他は定数
; ImageBase + AddressOfEntryPoint がエントリーポイントの実アドレス

dw 0x020B			; Magic
db 0				; MajorLinkerVersion
db 0				; MinorLinkerVersion
dd 0				; SizeOfCode
dd 0				; SizeOfInitializedData
dd 0				; SizeOfUniinitializedData
dd 0x1000			; AddressOfEntryPoint
dd 0				; BaseOfCode

;------------------------------
; オプションヘッダ (Windows)
;------------------------------

; 以下は全部定数

dq 0x00400000		; ImageBase
dd 0x1000			; SectionAlignment
dd 0x0200			; FileAlignment
dw 0x0005			; MajorOperatingSystemVersion
dw 0x0000			; MinorOperatingSystemVersion
dw 0				; MajorImageVersion
dw 0				; MinorImageVersion
dw 0x0005			; MajorSubsystemVersion
dw 0x0000			; MinorSubsystemVersion
dd 0				; Win32VersionValue

; 以下を設定
; SizeOfImage　(ロードに必要なサイズ, SectionAlignment 揃え)
; SizeOfHeaders (ヘッダのサイズ, セクションテーブル含む, FileAlignment 揃え)
; スタック・ヒープサイズは適宜設定

dd 0x3000			; SizeOfImage
dd 0x0200			; SizeOfHeaders
dd 0				; CheckSum
dw 0x0003			; Subsystem
dw 0				; DllCharacteristics
dq 0x1000			; SizeOfStackReserve
dq 0x1000			; SizeOfStackCommit
dq 0x00010000		; SizeOfHeapReserve
dq 0x0000			; SizeOfHeapCommit

; 以下は定数

dd 0				; LoaderFlags
dd 0x0010 			; NumberOfRvaAndSizes

;------------------------------
; オプションヘッダ (Data)
;------------------------------
dd 0, 0				; ExportTable
dd 0x2000, 0x004F	; ImportTable
dd 0, 0				; ResourceTable
dd 0, 0				; ExceptionTable
dd 0, 0				; Certificate Table
dd 0, 0				; Base Relocation Table
dd 0, 0				; Debug
dd 0, 0				; Architecture
dd 0, 0				; Global Ptr
dd 0, 0				; TLS Table
dd 0, 0				; Load Config Table
dd 0, 0				; Bound Import
dd 0, 0				; IAT
dd 0, 0				; Delay Import Descriptor
dd 0, 0				; CLR Runtime Header
dd 0, 0				; Reserved

;------------------------------
; .text
;------------------------------
db ".text", 0, 0, 0	; Name
dd 0x0011			; VirtualSize
dd 0x1000			; VirtualAddress
dd 0x0200			; SizeOfRawData
dd 0x0200			; PointerToRawData
dd 0				; PointerToRelocations
dd 0				; PointerToLinenumbers
dw 0				; NumberOfRelocations
dw 0				; NumberOfLinenumbers
dd 0x60000020		; Characteristics

;------------------------------
; .idata
;------------------------------
db ".idata", 0, 0	; Name
dd 0x004F			; VirtualSize
dd 0x2000			; VirtualAddress
dd 0x0200			; SizeOfRawData
dd 0x0400			; PointerToRawData
dd 0				; PointerToRelocations
dd 0				; PointerToLinenumbers
dw 0				; NumberOfRelocations
dw 0				; NumberOfLinenumbers
dd 0xC0000040		; Characteristics

;------------------------------
; ゼロ埋め
;------------------------------

dq 0, 0, 0, 0, 0

;------------------------------
; text セクション
;------------------------------

; sub rsp, 28h
db 0x48			; REX.W PREFIX 0100WR0B W = 1, R = 0, B = 0
db 0x83			; SUB r/m64, imm8
db 0xEC			; RSP (Mod = 11, R/M = 100, Value = /5)
db 0x28			; VALUE

; mov rcx, 123
db 0x48			; REX.W
db 0xC7			; MOV r/m64, imm32
db 0xC1			; RCX (Mod = 11, R/M = 01, Value = /0)
db 0x7B, 0x00, 0x00, 0x00	; VALUE

; call [ExitProcess]
db 0xFF			; CALL m16:32
db 0x15			; mod = 00 reg = 10 r/m = 101
db 0x17, 0x10	; (EIP = 0x1011) + 0x1017 = 0x2028

db 0x00, 0x00

db 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0

;------------------------------
; idata セクション
;------------------------------
dd 0, 0, 0, 0x2038, 0x2028
dd 0, 0, 0, 0, 0
dq 0x2041, 0
db "kernel32", 0
db 0, 0, "ExitProcess", 0, 0

dq 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
dq 0, 0, 0, 0, 0, 0, 0, 0
```

コードを `hello10.asm` という名前で保存して以下のように PowerShell で実行すると 123 が返ってくることが確認できます。

```
> fasm .\hello10.asm
flat assembler  version 1.73.04  (1048576 kilobytes memory)
1 passes, 1536 bytes.
> .\hello10
> $LASTEXITCODE
123
```

書いてみた感想なのですが、これは人間が書くものではありません。特にRIP相対アドレッシングは凶悪です。想定ではこれまでの流れに合わせて hello, world. を出すはずだったのですが、時間切れで断念しました。

# おわりに

今回は flat assembler を触りいろいろなスタイルで hello, world. を書いてみました。flat assembler には他にもリソースを構築するマクロや、if や while といった制御構文を実現するマクロなどいろいろな機能がありとても面白いので、それはまた別の機会に紹介できればと考えています。

また flat assembler の次期バージョンである flat assembler g の開発も進んでいるようでこちらも注目しています。flat assembler g は、flat assembler の考え方をさらに推し進めたもので、x86 の機械語命令自体もマクロで表現してしまうようです。flat assembler 1 (初代 flat assembler の呼び方) では x86 の命令は組み込みで用意されていましたが、 flat assembler g では特定のプラットーフォーム向けの命令は搭載されておらず、すべてをマクロで表現されています。汎用アセンブラもしくはアセンブラを作るための言語が falt assembler g のようです。

- [flat assembler g](https://flatassembler.net/docs.php?article=fasmg)

私は日ごろはサーバサイドやフロントエンドで動く TypeScript や JavaScript を書いています。おそらく業務で flat assembler を使うような機会は来ないと思いますが、日頃自分が触っていたり耳にしたりする言語・環境・ツールがこの世の中のすべてではないということを、肝に銘じてコードを書いていこうと、今年も思いを新たにしました。