現在のブログ
ゲーム開発ブログ (2025年~) Gamedev Blog (2025~)
レガシーブログ
テクノロジーブログ (2018~2024年) リリースノート (2023~2025年) MeatBSD (2024年)
【アセンブリ言語】関数とインクルード
此れまで、アセンブリ言語でテキストを出力する方法について詳しく説明してきましたが、入力については未だ触れていませんでした。
然し、scanf() をアセンブリで再現する方法についての記事を丸ごと書くのは退屈でしょう。
其処で代わりに、関数の作り方と他のアセンブリファイルのインクルード方法についての記事を作成します。
入力処理も其の一部として扱います。
前提条件
此の記事を理解するには、以下の記事を先に読んで下さい:
関数
此れまでのアセンブリ記事を読んでいれば、既に関数について触れている事に気づいているでしょう。
関数を持つ為には、少し異なる扱いが必要です。
_start にコードを書くのとは異なり、関数にはプロローグとエピローグが必要です。
プロローグとエピローグはアセンブリ言語特有の部分で、高級言語では全て抽象化されています。
プロローグは、前のベースポインタ(rbp)をスタックに保存し、其れを現在のスタックポインタ(rsp)に移動します。
エピローグは、スタックポインタとベースポインタを復元します。
此れを実現する為に、以下の様にします:
foo:
; 関数プロローグ
push rbp ; 前のベースポインタをスタックに保存
mov rbp, rsp ; ベースポインタを現在のスタックポインタに設定
; 何かやりますねぇ
; 関数エピローグ
mov rsp, rbp ; スタックポインタを復元
pop rbp ; ベースポインタを復元
ret ; 関数から戻る
此れにより、_start 内で call foo を使用出来、メインファンクションがすっきりと保たれます。
インクルード
インクルードの方法は、使用するアセンブラによって異なります。
GCC では #include "io.s" ですが、NASM では %include "io.s" です。
此処では NASM を使用しているので、其の構文を使用します。
例
では、main.s を作成する前に、io.s を作成しましょう。
; io.s
section .text
printf: ; void printf(char *buf, size_t len)
; 関数プロローグ
push rbp ; 前のベースポインタをスタックに保存
mov rbp, rsp ; ベースポインタを現在のスタックポインタに設定
push rsi ; len を保存
push rdi ; バッファ(buf)を保存
; 書き出し
mov rax, 4 ; FreeBSDでの書込のシステムコール番号
mov rdi, 1 ; ファイルディスクリプター1(標準出力)
mov rsi, [rsp] ; バッファー(保存した buf)
mov rdx, [rsp + 8] ; 長さ(保存した len)
syscall ; システムコール実行
; 関数エピローグ
add rsp, 16 ; スタックをクリーンアップ(rdi, rsi をポップ)
;mov rsp, rbp ; スタックポインタを復元
;pop rbp ; ベースポインタを復元
leave ; = move rsp, rbp ; pop rbp
ret ; 関数から戻る
scanf: ; size_t scanf(char *buf, size_t max_len)
; 関数プロローグ
push rbp ; 前のベースポインタをスタックに保存
mov rbp, rsp ; ベースポインタを現在のスタックポインタに設定
push rsi ; max_len を保存
push rdi ; バッファ(buf)を保存
; 読み込み
mov rax, 3 ; FreeBSDでの読込のシステムコール番号
mov rdi, 0 ; ファイルディスクリプター0(標準入力)
mov rsi, [rsp] ; バッファー(保存した buf)
mov rdx, [rsp + 8] ; 最大長さ(保存した max_len)
syscall ; システムコール実行
; 関数エピローグ
add rsp, 16 ; スタックをクリーンアップ(rdi, rsi をポップ)
;mov rsp, rbp ; スタックポインタを復元
;pop rbp ; ベースポインタを復元
leave ; = move rsp, rbp ; pop rbp
ret ; 関数から戻る
次に、main.s では以下の様にします:
; main.s
section .rodata
prompt db "お名前は? ", 0 ; プロンプト(ヌル終端文字列)
promptLen equ $-prompt-1 ; プロンプトの長さ(ヌル文字を除く)
greet db "こんにちは、" ; 挨拶の前半
greetLen equ $-greet ; 前半の長さ
grEnd db "!", 10 ; 挨拶の後半(!+改行)
grEndLen equ $-grEnd ; 後半の長さ
section .bss
buffer resb 65 ; 入力用バッファ(64バイト確保)+NULL用1バイト
section .text
%include "io.s" ; io.s をインクルード
global _start
_start:
; プロンプト表示
mov rdi, prompt ; バッファー(プロンプト)
mov rsi, promptLen ; 長さ
call printf ; 出力
; 名前入力
mov rdi, buffer ; バッファー(入力先)
mov rsi, 64 ; 最大長さ
call scanf ; 読み込み
; rax = 読み込んだバイト
mov rbx, rax ; 読み込んだバイト数を rbx に保存
; クラバリングを防ぐため、rax の値を rbx に退避しました。
; 入力が 0 バイトなら終了
test rbx, rbx ; rax == 0かを確認
jz .done ; ゼロの場合、.doneにジャンプ
; バッファをヌル終端
mov byte [buffer + rbx], 0 ; [読み込み位置] に 0 を書き込み
cmp byte [buffer + rbx - 1], 10 ; 最後の文字が改行(\n)か?
jne .print ; そうではない場合、 .printにジャンプ
dec rbx ; 改行があったら出力長を 1減らす
.print:
mov r12, rbx ; 出力する名前の長さを r12 に保存
; 「こんにちは、」を出力
mov rdi, greet
mov rsi, greetLen
call printf
; 名前を出力
mov rdi, buffer
mov rsi, r12
call printf
; 「!\n」を出力
mov rdi, grEnd
mov rsi, grEndLen
call printf
.done:
; プログラム終了
mov rax, 1 ; FreeBSDでの終了のシステムコール番号
mov rdi, 0 ; 終了ステータス 0 (成功) = return 0;
syscall ; カーネルを呼び出し
注意:コード内の全てのシステムコール番号は FreeBSD 専用です。
Linux ではシステムコール番号を変更する必要があります。
Linux では exit は 60 ですが、FreeBSD では 1 です。
Linux では write は 1 ですが、FreeBSD では 4 です。
Linux では read は 0 ですが、FreeBSD では 3 です。
其れ以外の部分は問題なく動作するはずです。
注意:io.s をアセンブラに渡す必要はありません。
既に main.s にインクルードしている為、アセンブラが自動的に含めてくれます。
$ nasm -f elf64 main.s -o main.o
$ ld main.o -o io
$ file io
io: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, stripped
$ ls -thal io
-rwxr-xr-x 1 suwako suwako 1.1K 10月 26 01:58 io
$ ./io
お名前は?
お名前は? 諏訪子
こんにちは、諏訪子!
パラメータの渡し方
パラメータの渡し方は ABI によって異なります。
System V(Linux、BSD、Illumos 等)では以下の通りです:
| パラメートル | レジスター |
|---|---|
| 第1目 | rdi |
| 第2目 | rsi |
| 第3目 | rdx |
| 第4目 | rcx |
| 第5目 | r8 |
| 第6目 | r9 |
| 第7目以上 | スタック |
戻り値は rax になります。
バイナリの確認
objdump を使ってバイナリを確認してみましょう。
アセンブリで作成した為、libc からの物は何も含んでいないので、今回は結果を less にパイプする必要はありません。
出力全体が 4K 画面にぴったり収まります!
$ objdump -d -Mintel ./io
./io: file format elf64-x86-64
Disassembly of section .text:
0000000000201180 <.text>:
201180: 55 push rbp
201181: 48 89 e5 mov rbp, rsp
201184: 56 push rsi
201185: 57 push rdi
201186: b8 04 00 00 00 mov eax, 0x4
20118b: bf 01 00 00 00 mov edi, 0x1
201190: 48 8b 34 24 mov rsi, qword ptr [rsp]
201194: 48 8b 54 24 08 mov rdx, qword ptr [rsp + 0x8]
201199: 0f 05 syscall
20119b: 48 83 c4 10 add rsp, 0x10
20119f: 48 89 ec mov rsp, rbp
2011a2: 5d pop rbp
2011a3: c3 ret
2011a4: 55 push rbp
2011a5: 48 89 e5 mov rbp, rsp
2011a8: 56 push rsi
2011a9: 57 push rdi
2011aa: b8 03 00 00 00 mov eax, 0x3
2011af: bf 00 00 00 00 mov edi, 0x0
2011b4: 48 8b 34 24 mov rsi, qword ptr [rsp]
2011b8: 48 8b 54 24 08 mov rdx, qword ptr [rsp + 0x8]
2011bd: 0f 05 syscall
2011bf: 48 83 c4 10 add rsp, 0x10
2011c3: 48 89 ec mov rsp, rbp
2011c6: 5d pop rbp
2011c7: c3 ret
2011c8: 48 bf 58 01 20 00 00 00 00 00 movabs rdi, 0x200158
2011d2: be 10 00 00 00 mov esi, 0x10
2011d7: e8 a4 ff ff ff call 0x201180 <.text>
2011dc: 48 bf 54 22 20 00 00 00 00 00 movabs rdi, 0x202254
2011e6: be 40 00 00 00 mov esi, 0x40
2011eb: e8 b4 ff ff ff call 0x2011a4 <.text+0x24>
2011f0: 48 89 c3 mov rbx, rax
2011f3: 48 85 db test rbx, rbx
2011f6: 74 50 je 0x201248 <.text+0xc8>
2011f8: c6 83 54 22 20 00 00 mov byte ptr [rbx + 0x202254], 0x0
2011ff: 80 bb 53 22 20 00 0a cmp byte ptr [rbx + 0x202253], 0xa
201206: 75 03 jne 0x20120b <.text+0x8b>
201208: 48 ff cb dec rbx
20120b: 49 89 dc mov r12, rbx
20120e: 48 bf 69 01 20 00 00 00 00 00 movabs rdi, 0x200169
201218: be 12 00 00 00 mov esi, 0x12
20121d: e8 5e ff ff ff call 0x201180 <.text>
201222: 48 bf 54 22 20 00 00 00 00 00 movabs rdi, 0x202254
20122c: 4c 89 e6 mov rsi, r12
20122f: e8 4c ff ff ff call 0x201180 <.text>
201234: 48 bf 7b 01 20 00 00 00 00 00 movabs rdi, 0x20017b
20123e: be 04 00 00 00 mov esi, 0x4
201243: e8 38 ff ff ff call 0x201180 <.text>
201248: b8 01 00 00 00 mov eax, 0x1
20124d: bf 00 00 00 00 mov edi, 0x0
201252: 0f 05 syscall
アセンブリコードとどれだけ密接に対応しているかが分かります。
又、一部の命令は 32 ビット(esi、eax)で、一部は 64 ビット(rsi、rax)である事にも気づくでしょう。
何故でしょうか?
簡単な答えは、アセンブラが小さな値についてはビット長を短くする程賢いからです。
此れにより、リソースを無駄に消費しません!
以上