今回は、表題の通り x86/x86-64 の FreeBSD でアセンブラからシステムコールを呼んでみる。 システムコールは、OS (ディストリビューション) のコアとなるカーネルがユーザ空間のプログラムに向けて提供しているインターフェースのこと。 ファイルの入出力など、ユーザープログラムは大抵のことはシステムコールを通じてカーネルにお願いしてやってもらうことになる。 ただ、普段は色々な API がラップして実体が見えにくいので、今回はアセンブラから直接呼んでみることにした。
使った環境は次の通り。
$ uname -sr FreeBSD 12.0-RELEASE $ nasm -v NASM version 2.14.02 compiled on Aug 22 2019 $ ld -v LLD 6.0.1 (FreeBSD 335540-1200005) (compatible with GNU linkers)
TL; DR
分かったことは次の通り。
- 商用 UNIX の流れをくむ Unix 系 OS のシステムコールは ABI (Application Binary Interface) が x86 と x86-64 で異なる
- x86 (32bit) ではスタックを使って引数を渡す
- x86-64 (64bit) ではレジスタを使って引数を渡す
ちなみに Linux では、どちらもレジスタ経由でシステムコールの引数を渡していた点が異なる。
もくじ
- TL; DR
- もくじ
- 下準備
- NASM / x86 で exit(2) を呼び出すだけのプログラム
- NASM / x86-64 で exit(2) を呼び出すだけのプログラム
- write(2) を追加したハローワールドでも比較してみる
- FreeBSD の Linux バイナリ互換機能
下準備
あらかじめ、下準備として NASM (Netwide Assembler) をインストールしておく。
$ sudo pkg install nasm
NASM / x86 で exit(2) を呼び出すだけのプログラム
まずは、ハローワールドですらなく「終了するだけ」のプログラムを書いてみる。
FreeBSD には、プログラムを終了するためのシステムコールとして exit(2)
がある。
最初は x86 (32bit) アーキテクチャのアセンブラを用いる。 FreeBSD で x86 のシステムコールに関するドキュメントとしては以下があった。
以下が、上記のドキュメントにもとづいて x86 で exit(2)
を呼び出すだけのアセンブラのソースコードとなる。
FreeBSD の x86 アセンブラでは、割り込み番号 0x80
で int
命令を発行するだけの関数経由でシステムコールを呼ぶことを想定しているらしい。
また、Linux の x86 のシステムコール呼び出しとは異なり、引数はシステムコールの識別子を除いてスタック経由で受け渡しされる。
global _start section .text _syscall: ; FreeBSD expects to call system call via int 0x80 only function int 0x80 ret _start: ; exit system call (FreeBSD / x86) push dword 0 ; return code mov eax, 1 ; system call id (exit) call _syscall ; call system call add esp, byte 4 ; restore ESP (DWORD x1) ret ; don't reach here
x86 (32bit) 向けにビルドする
それでは、上記を実際にビルドして実行してみよう。
まずは NASM を使って 32bit の ELF (Executable and Linkable Format) としてアセンブルする。
$ nasm -f elf32 nop.asm $ file nop.o nop.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
上記をリンカを使って実行可能オブジェクトファイルにする。
$ ld -m elf_i386_fbsd -o nop nop.o $ file nop nop: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), statically linked, not stripped
できたファイルを実行してみよう。
$ ./nop
何も表示されないけど、エラーにならずに実行できていることがうまくいっていることを示している。
プログラムの返り値についてもスタック経由で指定したゼロになっている。
$ echo $? 0
x86-64 (64bit) 向けにビルドする
では、次に先ほどのサンプルコードを x86-64 (64bit) 向けのアプリケーションとしてビルドしてみよう。 x86-64 は機械語のレベルでは x86 と後方互換性があるけど、どういった結果になるだろうか。
まずは 64bit の ELF としてアセンブルする。
$ nasm -f elf64 nop.asm
実行可能オブジェクトファイルとしてリンクする。
$ ld -m elf_amd64_fbsd -o nop nop.o
実行してみよう。
$ ./nop
特にエラーにならず実行できているので、うまくいっていそうだけど…?
返り値を見ると非ゼロの値が入っている。
$ echo $? 144
どうやら返り値の受け渡しの部分がうまくいっていないようだ。
NASM / x86-64 で exit(2) を呼び出すだけのプログラム
実は FreeBSD というか商用 UNIX の流れをくんだ Unix 系 OS は x86 と x86-64 でシステムコールの ABI が異なっている。 具体的には、x86 ではスタック経由で引数を渡していたのが x86-64 ではレジスタ経由で渡すことになった。 この問題については以下が分かりやすい。
また、詳しくは以下のリポジトリでメンテナンスされている PDF についても参照のこと。
上記にもとづいて 64 ビットのアプリケーションとしてビルドできるサンプルコードを以下に示す。 これはシステムコールの識別子は異なるものの、前述した GNU/Linux の記事に出てきたサンプルコードとほとんど同じもの。 x86-64 においては Linux と商用 UNIX の子孫たちでシステムコールの呼び出し方は同じになっている。
global _start section .text _start: ; exit system call (FreeBSD / x86-64) mov rax, 1 ; system call id mov rdi, 0 ; return value syscall ; call system call
実際に上記を 64 bit の実行可能オブジェクトファイルとしてビルドしてみよう。
$ nasm -f elf64 nop.asm $ file nop.o nop.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped $ ld -m elf_amd64_fbsd -o nop nop.o $ file nop nop: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, not stripped
できたファイルを実行する。
$ ./nop $ echo $? 0
今度は返り値についてもゼロが設定されている。
ちなみに Linux では同じシステムコールでも x86 t x86-64 で識別子が異なったけど、FreeBSD の場合は変わらないらしい。
write(2) を追加したハローワールドでも比較してみる
呼び出すシステムコールが exit(2)
だけだと味気ないので write(2)
も追加してハローワールドも見ておこう。
x86 版 ABI のハローワールド
まずは x86 版 ABI を使ったハローワールドが以下の通り。
global _start section .data msg db 'Hello, World!', 0x0A msg_len equ $ - msg section .text _syscall: int 0x80 ret _start: ; write system call (FreeBSD / x86) push dword msg_len push dword msg push dword 1 mov eax, 4 call _syscall add esp, byte 12 ; restore ESP (DWORD x3) ; exit system call (FreeBSD / x86) push dword 0 mov eax, 1 call _syscall add esp, byte 4 ; restore ESP (DWORD x1) ret ; don't reach here
x86 (32bit) 向けの実行可能オブジェクトファイルとしてビルドする。
$ nasm -f elf32 greet.asm $ ld -m elf_i386_fbsd -o greet greet.o
できたファイルを実行する。
$ ./greet Hello, World! $ echo $? 0
ちゃんと動作しているようだ。
x86-64 版 ABI のハローワールド
続いて以下が x86-64 版 ABI のハローワールドになる。
global _start section .data msg db 'Hello, World!', 0x0A msg_len equ $ - msg section .text _start: ; write system call (FreeBSD / x86-64) mov rax, 4 mov rdi, 1 mov rsi, msg mov rdx, msg_len syscall ; exit system call (FreeBSD / x86-64) mov rax, 1 mov rdi, 0 syscall
x86-64 (64bit) 向けの実行可能オブジェクトファイルとしてビルドする。
$ nasm -f elf64 greet.asm $ ld -m elf_amd64_fbsd -o greet greet.o
できたファイルを実行する。
$ ./greet Hello, World! $ echo $? 0
うまくいっている。
FreeBSD の Linux バイナリ互換機能
ちなみに FreeBSD には Linux の ABI を使ったアプリケーションを実行するためのエミュレーション機能があるらしい。 実際に試してみよう。
エミュレーション機能はカーネルモジュールとして実行されているため linux.ko
を読み込む。
$ sudo kldload linux.ko $ kldstat | grep linux 7 1 0xffffffff8284c000 39960 linux.ko 8 1 0xffffffff82886000 2e28 linux_common.ko
Linux で動作する x86 版 ABI のサンプルコードを以下のように用意する。 レジスタで引数を渡しているので System V の x86 版 ABI とは異なる。
global _start section .text _start: mov eax, 1 mov ebx, 0 int 0x80
上記を 32 版の実行可能オブジェクトファイルとしてビルドする。
$ nasm -f elf32 nop.asm $ ld -m elf_i386 -o nop nop.o
このままでは実行できない。
$ ./nop ELF binary type "0" not known. -bash: ./nop: cannot execute binary file: Exec format error
そこで、brandelf
コマンドを使って Linux 互換のバイナリとしてマークする。
$ brandelf -t Linux nop
実行してみる。
$ ./nop $ echo $? 0
今度はちゃんと動作した。
- 作者:もみじあめ
- 発売日: 2020/02/29
- メディア: Kindle版