2025-10-26 03:50:40
諏訪子
lowlevel
assembly

【アセンブリ言語】関数とインクルード

此れまで、アセンブリ言語でテキストを出力する方法について詳しく説明してきましたが、入力については未だ触れていませんでした。
然し、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 ビット(esieax)で、一部は 64 ビット(rsirax)である事にも気づくでしょう。
何故でしょうか?
簡単な答えは、アセンブラが小さな値についてはビット長を短くする程賢いからです。
此れにより、リソースを無駄に消費しません!

以上