NetBSD Advent Calendar 2017 1日目の記事です。今日はアセンブリ言語でのHello,World.プログラムについて書こうと思います。
アセンブリ言語でのHello,World.プログラム
NetBSDをインストールすると、 /usr/share/examples/asm
というディレクトリがあるということに気がつきます。
中身は何やらアセンブリ言語のプログラムのようです。
$ /usr/share/examples/asm
$ ls -R
Makefile.inc README hello/
./hello:
Makefile powerpc.s
READMEには「ハハッ(甲高い声) このディレクトリには様々なプラットフォーム向けのアセンブリ言語サンプルが入っているよ!」(意訳)と書かれているようですが、どうみてもPowerPCのサンプルしか存在していない状態です...。
$ cat README
$NetBSD: README,v 1.2 2011/11/27 09:07:11 skrll Exp $
This directory contains example programs written in assembly language
for a variety of platforms. They are intended to illustrate the
specific details of how to write assembly code on a given platform;
they are not supposed to teach assembly (although they might have this
side-effect).
If you want to build one of these example programs, you can "cp -rf"
the corresponding directory anywhere else where you have write
permissions and then issue a "make" within the directory.
hello/powerpc.s
の中身はこんな感じ。write(2)システムコールを直接呼び出す形で"Hello, world!"を表示するという動作になっています。
とはいえ、Hello,World.を様々なプラットフォーム向けに最小限のアセンブリ言語で書くという、まさにNetBSD向けの題材にも思えます。
.section ".note.netbsd.ident", "a"
# This ELF section is used by the kernel to determine, among other
# things, the system call interface used by the binary.
#
# See http://www.netbsd.org/docs/kernel/elf-notes.html for more
# details.
.int 7 # Length of the OS name field below.
.int 4 # Length of the description field below.
.int 0x01 # The type of the note: NetBSD OS Version.
.ascii "NetBSD\0\0" # The OS name, padded to 8 bytes.
.int 0x23b419a0 # The description value; 5.99.56.
# ------------------------------------------------------------------------
.section ".data"
message:
.ascii "Hello, world!\n"
.set MESSAGE_SIZE, . - message
# ------------------------------------------------------------------------
.section ".text"
.balign 4
.globl _start
.type _start, @function
_start:
# write(STDOUT_FILENO, message, MESSAGE_SIZE)
li %r0, 4 # r0: write(2) syscall number.
li %r3, 1 # r3: first argument.
addis %r4, %r0, message@h # r4: second argument.
ori %r4, %r4, message@l
li %r5, MESSAGE_SIZE # r5: third argument.
sc
# exit(EXIT_SUCCESS)
li %r0, 1 # r0: exit(2) syscall number.
li %r3, 0 # r3: first argument.
sc
.size _start, . - _start
i386アーキテクチャでのアセンブリ言語によるHello,World.
某所で「大熱血!アセンブラ入門 読書会」のお手伝いをさせていだいているという経緯もあり、自分の分かる範囲でサンプルを増やしたいところです。
と思っていたら、かなり前にi386アーキテクチャ向けに上記のプログラムを移植したのを忘れていました...。
i386アーキテクチャ向けのHello,World.は以下になります。やっていることは write(2)
で"Hello,World."を出力し、 exit(3)
で終了しているだけです。
.section ".note.netbsd.ident", "a"
# This ELF section is used by the kernel to determine, among other
# things, the system call interface used by the binary.
#
# See http://www.netbsd.org/docs/kernel/elf-notes.html for more
# details.
.int 7 # Length of the OS name field below.
.int 4 # Length of the description field below.
.int 0x01 # The type of the note: NetBSD OS Version.
.ascii "NetBSD\0\0" # The OS name, padded to 8 bytes.
.int 0x23b419a0 # The description value; 5.99.56.
# ------------------------------------------------------------------------
.section ".data"
message:
.ascii "Hello, world!\n"
.set MESSAGE_SIZE, . - message
# ------------------------------------------------------------------------
.section ".text"
.balign 4
.globl _start
.type _start, @function
_start:
# write
movl $MESSAGE_SIZE, 0xc(%esp)
movl $message, 0x8(%esp)
movl $0x1, 0x4(%esp)
mov $0x4, %eax
int $0x80
# exit(EXIT_SUCCESS)
movl $0x0,(%esp)
mov $0x1,%eax
int $0x80
ret
.size _start, . - _start
実行例は以下になります。
$ uname -a
NetBSD vmnbsd71 7.1 NetBSD 7.1 (GENERIC.201703111743Z) amd64
$ gcc -m32 -nostdlib i386.s
$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for NetBSD 5.99.56, not stripped
$ ./a.out
Hello, world!
amd64アーキテクチャでも動かしたい!
これで今日のAdvent Calendarに間にあわせることができましたぞ、と言いたいところですが、64bit環境(amd64)でも動かしたいのが人情というものです。
(なにも考えずに)そのままアセンブルして動かすと、もちろんクラッシュします。rip
レジスタの指しているアドレスは 0x400100
で _start
の開始アドレスなので、実行即クラッシュという状態ですね...。
なんでクラッシュするの→64bitでの引数渡しはスタックじゃなくてレジスタを使うから、この呼び出し方だと変なレジスタに変な値が入った状態でシステムコールが呼ばれているからだよね...という話なので、64bit環境向けにも別途Hello,World.プログラムを作成したいものです。
$ gcc -nostdlib i386.s
$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for NetBSD 5.99.56, not stripped
$ gdb a.out
GNU gdb (GDB) 7.7.1
...
(gdb) run
Starting program: /home/fpig/work/advcal/2017/1201/fpig_sample/hello/nbsd/a.out
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400100 in _start ()
(gdb) bt
#0 0x0000000000400100 in _start ()
(gdb) info register
rax 0x0 0
rbx 0x7f7fffffffe0 140187732541408
rcx 0x400100 4194560
rdx 0x0 0
rsi 0x0 0
rdi 0x0 0
rbp 0x0 0x0
rsp 0x7f7fffffdb88 0x7f7fffffdb88
r8 0x0 0
r9 0x0 0
r10 0x7f7ff6c83a0a 140187577891338
r11 0x202 514
r12 0x74c968 7653736
r13 0x74a008 7643144
r14 0x74d188 7655816
r15 0x74d208 7655944
rip 0x400100 0x400100 <_start>
eflags 0x10202 [ IF RF ]
cs 0x47 71
ss 0x3f 63
ds 0x3f 63
es 0x3f 63
fs 0x0 0
gs 0x0 0
(gdb)
フィーリングでシステムコールの呼び出し方を調べる
大熱血!アセンブラ入門にある熱血バイナリアン十訓の中に、「わからなくても気にせず読め!」という項目があります。
12月1日も終わりつつあるなか、NetBSD-amd64環境でアセンブリ言語からシステムコールを呼び出す方法を今から(!)調べるのはなかなか無茶がありますが、熱血バイナリアン十訓の教えに従い、がんばって調べてみます。
exit(3)で試してみる。
話を簡単にするため、引数を持たない exit(3)
を64bit環境で呼び出すという例で考えてみます。
まずはi386向けのアセンブリで exit(3)
を呼ぶだけのプログラムにしてみます。
.section ".text"
.balign 4
.globl _start
.type _start, @function
_start:
# exit(EXIT_SUCCESS)
movl $0x0,(%esp)
mov $0x1,%eax
int $0x80
ret
もちろんクラッシュします。
$ gcc -nostdlib i386_exit.s
$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for NetBSD 5.99.56, not stripped
$ gdb ./a.out
GNU gdb (GDB) 7.7.1
...
(gdb) rn
Target exec does not support this command.
(gdb) run
Starting program: /home/fpig/work/advcal/2017/1201/fpig_sample/hello/nbsd/a.out
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400100 in _start ()
ではどうやって exit(3)
を呼び出すのだろうという話ですが、ここは一つ、実際に exit(3)
を読んでいる箇所から調べてみましょう。
libcのライブラリであれば必ず exit(3)
を呼び出している箇所があるはずなので、それを手がかりにしてみます。
$ objdump -d /lib/libc.so
...
000000000010e6c0 <_Exit>:
10e6c0: b8 01 00 00 00 mov $0x1,%eax
10e6c5: 49 89 ca mov %rcx,%r10
10e6c8: 0f 05 syscall
10e6ca: c3 retq
10e6cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
000000000010e6d0 <_lwp_self>:
なにやらそれっぽい箇所があります。 eax
レジスタに入れるのはシステムコール種別のようなので、 esp
レジスタが指している(スタック領域として使っているメモリ)に入れていた値を rcx
r10
で渡せば良さそうです。
さっそく以下のように書き換えて試してみます。
_start:
# exit(EXIT_SUCCESS)
mov $0x1,%eax
mov $0x7,%rcx
mov %rcx,%r10
syscall
retq
今度はクラッシュせずに動作しました! ただ、確認のため終了ステータスを 7
としたつもりなのですが、 "exited normally"と言われているので、引数の渡し方がまだちょっと間違っているようです。
でもとりあえずは64bit環境でシステムコールが呼べるようになったので、引数の渡し方についてはおいおい調べて行こうと思います。
$ gcc -nostdlib i386_exit.s
$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for NetBSD 5.99.56, not stripped
$ gdb a.out
GNU gdb (GDB) 7.7.1
...
(gdb) run
Starting program: /home/fpig/work/advcal/2017/1201/fpig_sample/hello/nbsd/a.out
[Inferior 1 (process 25952) exited normally]
まとめ
NetBSDの /usr/share/examples/asm
ディレクトリに置かれている、アセンブリ言語でのHello,World.プログラムについて紹介しました。
様々なアーキテクチャ向けのHello,World.プログラムはNetBSDに向いている題材だと思うので、ぜひサンプルを増やして行きたいところです。