14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

flat assembler で helloworld を書いてみた

Last updated at Posted at 2018-12-05

これは「ドワンゴ Advent Calendar 2018」6日目の記事です。

flat assembler とは

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

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

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

flat assembler を動かしてみる

今回は 64bit の Window 10 上で flat assembler を動かします。flat assembler のダウンロードサイト から 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

日本語を表示する

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

include 'win64wx.inc'

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

.end start

2.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

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

前章までのコードで 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.endwin64wx.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'

今回は文字列を書き換えることがないので messagetitle はデータセクションではなくテキストセクションに置くこともできます。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 は不要です。dbdd はデータを配置する命令です。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 に変換されます。以下のコードはメッセージボックスを表示するプログラムの invokecall の呼び出しに書き換えたものです。

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 で引数分のスタック領域を開けておく必要があるためです。このあたりの話は以下の資料に書かれています。

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

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

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 のようです。

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

14
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?