CUBE SUGAR CONTAINER

技術系のこと書きます。

macOS (x86/x86-64) のシステムコールをアセンブラから呼んでみる

今回は、表題の通り x86/x86-64 の macOS でシステムコールをアセンブラから呼んでみる。 ただし、前回のエントリで FreeBSD についても同じようにシステムコールをアセンブラから呼んだ。 macOS は BSD を先祖に持つ XNU カーネルで動いている。 そのため、大筋は FreeBSD の場合と違いはない。 ようするに System V x86/x86-64 ABI の規約にもとづいて呼び出してやればいいだけだ。

blog.amedama.jp

とはいえ、FreeBSD と全く違いがないわけではない。 なので、それについて見ていくことにしよう。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ uname -sr
Darwin 18.7.0
$ nasm -v
NASM version 2.14.02 compiled on Dec 27 2018
$ ld -v
@(#)PROGRAM:ld  PROJECT:ld64-450.3
BUILD 18:16:53 Apr  5 2019
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em
LTO support using: LLVM version 10.0.1, (clang-1001.0.46.4) (static support for 22, runtime is 22)
TAPI support using: Apple TAPI version 10.0.1 (tapi-1001.0.4.1)

もくじ

下準備

最初に、アセンブラの実装として NASM (Netwide Assembler) を Homebrew でインストールしておく。

$ brew install nasm

GAS (GNU Assembler) を使っても問題ないけど、INTEL 記法と AT&T 記法でちょっと手直しが必要になる。

x86 のアセンブラで exit(2) を呼び出すだけのプログラム

先のエントリと同じように、まずは終了するだけのプログラムを書いてみよう。 macOS にも、他の Unix 系 OS と同じようにプログラムを終了するためのシステムコールとして exit(2) がある。 macOS のカーネルである XNU のソースコードは、リリースからやや遅れはあるもののオープンソースとして公開されている。 そのため、次のようにシステムコールの識別子を確認できる。

github.com

上記から、exit(1) に対応する識別子が 1 であることがわかる。 ただし、実は上記の値をそのまま使ってもエラーになってしまう。

実際には、上記の識別子に 0x2000000 を加えなきゃいけない。 その理由は以下のファイルを見るとわかる。

github.com

どうやら macOS のカーネルである XNU が Mach と BSD から生まれた影響で、何に由来するシステムコールなのか指定が必要らしい。 その指定が、BSD であれば 2 を 24 ビット左シフトした値、ということみたいだ。

前置きが長くなったので、そろそろサンプルコードを見ることにしよう。 以下が exit(2) を呼び出すだけのサンプルコードになる。 FreeBSD の x86 版と異なるのは、前述したマスク値がひとつ。 もう一つがエントリポイントのシンボルが _start ではなく _main になっている。 まあ、これはデフォルト値の話なので、リンカで別途上書きしてしまっても良いはず。

global _main

section .text

_syscall:
  int 0x80
  ret

_main:
  ; exit system call (macOS / x86)
  push dword 0
  mov eax, 0x2000001  ; 2 << 24 + 1
  call _syscall
  add esp, byte 4  ; restore ESP
  ret  ; don't reach here

x86 (32bit) 向けにビルドする

コードが用意できたので、x86 (32bit) 向けのバイナリとして Mach-O 32bit フォーマットにアセンブルする。

$ nasm -f macho32 nop.asm
$ file nop.o                       
nop.o: Mach-O object i386

動作に必要な最低バージョンの指定を入れつつ実行可能オブジェクトファイルにリンクする。

$ ld -macosx_version_min 10.14 -lsystem -o nop nop.o
$ file nop  
nop: Mach-O executable i386

できたファイルを実行してみよう。

$ ./nop
$ echo $?
0

ちゃんと返り値もゼロが設定されている。 うまく動いているようだ。

ちなみに、今回使っている macOS のバージョンが Mojave (10.14) だからこそ上記は動く。 なぜなら、次のバージョン Catalina (10.15) からは 32bit アプリケーションのサポートがなくなってしまった。 なので、おそらくこのマシンも OS をアップデートしたら上記のバイナリが動かなくなるはずだ。

x86-64 (64bit) 向けにビルドする

じゃあ 64bit 向けにビルドすれば良いのか、というとそうもいかない。 なぜなら、先のエントリで取り扱った通り System V ABI は x86 と x86-64 で引数の渡し方が異なっている。 そのため、先ほどのサンプルコードは x86-64 (64bit) 向けのアプリケーションとしては動作しない。

やろうと思えば、一応は Mach-O 64bit フォーマットとしてアセンブリはできる。

$ nasm -f macho64 nop.asm
$ file nop.o
nop.o: Mach-O 64-bit object x86_64
$ ld -macosx_version_min 10.14 -lsystem -o nop nop.o
$ file nop  
nop: Mach-O 64-bit executable x86_64

しかし、実行しようとするとエラーになってしまう。

$ ./nop 
zsh: illegal hardware instruction  ./nop

x86-64 のアセンブラで exit(2) を呼び出すだけのプログラム

64 bit 向けのアプリケーションを作るためには、以下のように System V v86-64 ABI に準拠した呼び出しが必要になる。

global _main

section .text

_main:
  ; exit system call (macOS / x86-64)
  mov rax, 0x2000001
  mov rdi, 0
  syscall

上記を Mach-O 64 bit フォーマットとしてアセンブリする。

$ nasm -f macho64 nop.asm
$ file nop.o
nop.o: Mach-O 64-bit object x86_64

そして、リンクする。

$ ld -macosx_version_min 10.14 -lsystem -o nop nop.o
$ file nop  
nop: Mach-O 64-bit executable x86_64

できたファイルを実行してみよう。 今度はエラーにならない。

$ ./nop
$ echo $?
0

返り値についても、ちゃんとゼロが設定されている。

x86-64 のアセンブラでハローワールド

一応、ハローワールドについても以下にサンプルコードを示す。

global _main

section .data
  msg db 'Hello, World!', 0x0A
  msg_len equ $ - msg

section .text

_main:
  ; write system call (macOS / x86-64)
  mov rax, 0x2000004
  mov rdi, 1
  mov rsi, msg
  mov rdx, msg_len
  syscall

  ; exit system call (macOS / x86-64)
  mov rax, 0x2000001
  mov rdi, 0
  syscall

ビルドする。

$ nasm -f macho64 greet.asm
$ ld -macosx_version_min 10.14 -lsystem -o greet greet.o

実行する。

$ ./greet
Hello, World!
$ echo $?
0

うまく動いているようだ。

いじょう。