アセンブリ言語に入門したときのメモ
あらまし
なんとなくアセンブリ言語に入門したのでそのとき詰まったこと等を記します。入門の成果である以下のコードを参照したりします。
my first (ARMv6) assembly program - gist.github.com
まずはじめに、基礎知識を学ぶにあたって以下の本をまず通して読みました。
アーキテクチャとして普段つかっているマシンのx86ではなくARMを選んだのは、x86こわいのでまずARMで慣れようという理由からです。あと手持ちのラズパイを活用したかったというのも。この本を譲ってもらっていて読みがかったというのも。
この本では、いきなり単品のアセンブラファイルを書かずに、まずはgccのインラインアセンブラ機能を利用してプログラムの一部をアセンブリ言語で書いていくのを薦めています。標準出力とかABIとかGNU/Linuxのシステムコールとかの説明で煩雑になるし、覚えることがたくさんあるからのようです。
しかし本に書いてあることはここには書きません。本でカバーされてない詰まったポイントとか追加情報とかを記していくつもりです。具体的には…
といったかんじのことを書きます。
Cコードなしのアセンブリコードの記述ルール
本を一通り読んでインラインアセンブラを試したあとはCコード内ではなく直にアセンブリ言語を書いてみたくなりますが、それはどのような形をしていればよいのでしょうか。
ぼくは最初、gcc -s
の吐くアセンブラコードを参考にして(削って)つくろうとしましたが、かなりたくさんのアセンブラへの指示が書かれていてわからなかったので諦めました(どうもエントリポイントもgcc経由と直接asで異なるようでした。C言語の制約ぽい)。
というわけでいろいろ調べたところ、ARMでは以下の形が最小のアセンブリコードのようでした。
.text .global _start _start: mov r0, #0 // return 0; mov r7, #1 // 1: sys_exit() svc #0 // call sys_exit()
.text
はELFの.textセクション、つまり機械語の命令列を置くセクションを意味します。これを書くことで以降の行の内容はオブジェクトファイルやメモリの.textに置かれます。セクションによって「プログラムとして実行可能」だったり「書き込み許可」など可能な操作が異なるようです。
つぎに.global _start
ですが、これはアセンブル結果に含まれるシンボルを外部からも見れるようにする疑似命令です。モジュール外への関数のエクスポートのようなものと思っておけばよさそうです。ちなみに_start
はラベル(後述)です。
_start:
は、アセンブルしたときのこの位置(アドレス)に名前をつけておくためのラベルです。前行の.global
疑似命令で公開されているのでリンカでリンクする他のコードからも_start
を参照することができます。GNU/Linuxにおいてはこれがプログラムのエントリポイントであるようです(がソース不明)。
最後の三行はGNU/Linuxのシステムコールexitを叩いてプログラムを終了しています。このやりかたはアーキテクチャとABI毎に異なっており、man syscallに記載されています。ぼくのラズパイはarm/eabiなのでその方法に従っています。すなわち、
ということです。システムコールがどの番号なのかはアーキテクチャによって異なるようで、これはヘッダファイルを調べるとよいようです。以下のようにgrepすると定義が見つかります(定義方法もアーキテクチャによって異なるので注意)。ちなみに命令のオペランドに、即値としての数値を記述する場合は、数値の前に#
を付けるのがGNU asの記法です。
pi@raspberrypi:~ $ grep '__NR_exit' -R /usr/include/ # ココ! 「1」!!! /usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_exit (__NR_SYSCALL_BASE+ 1) /usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_exit_group (__NR_SYSCALL_BASE+248) /usr/include/arm-linux-gnueabihf/bits/syscall.h:#define SYS_exit __NR_exit /usr/include/arm-linux-gnueabihf/bits/syscall.h:#define SYS_exit_group __NR_exit_group /usr/include/arm-linux-gnueabihf/bits/syscall.h:#define SYS_exit __NR_exit /usr/include/arm-linux-gnueabihf/bits/syscall.h:#define SYS_exit_group __NR_exit_group /usr/include/asm-generic/unistd.h:#define __NR_exit 93 /usr/include/asm-generic/unistd.h:__SYSCALL(__NR_exit, sys_exit) /usr/include/asm-generic/unistd.h:#define __NR_exit_group 94 /usr/include/asm-generic/unistd.h:__SYSCALL(__NR_exit_group, sys_exit_group)
GNU asのディレクティブや記法のこと
つぎは文字列の表示がしたいです。
で、表示をするための処理のうちシステムコールはwriteを呼べばよさそうなので、あとはメモリにデータを置くのみです。ldr
命令とかでレジスタ-メモリ間はデータを移動できるのですが、メモリに直接データをおくにはどうするか。それはアセンブラやリンカに、起動時にデータを置くよう指示をします。これに用いるのが疑似命令(ディレクティブ)です。ここまでで.text
や.global
が登場しています。ディレクティブの一覧は以下のマニュアルを参照します。
Using as - Assembler Directives
ここでは.dataセクション(読み書き可能なデータを置くセクション)を用意して、そこに文字列を置き、それをsys_writeで表示してみます。プログラムはこんな感じです。
.data hello: .byte 0x68 // h .byte 0x6f // o .byte 0x67 // g .byte 0x65 // e .byte 0x0a // newline hello_len = . - hello .text .global _start _start: ldr r0, =hello mov r1, #42 strb r1, [r0, #1] mov r0, #1 // file discripter ldr r1, =hello mov r2, #hello_len // character count mov r7, #4 // sys_write svc #0 mov r0, #0 mov r7, #1 svc #0 // sys_exit
そういえば、コメントは上の本(のインラインアセンブラ章)には#
が使えるとあったのですが、アセンブリコードにはつかえなさそうでした。//
がコメントとして使えるようなので利用しています。
.data
領域にhello:
というラベルをつくり、それ以降に文字を1バイトずつ置いています。hello_len = . - hello
はシンボルhello_len
に、記述位置.
からhello:
の位置を減算した数値(つまり文字列のバイト数)を設定(代入?)しています。より親しみやすい記法で文字列を書ける.string
というディレクティブもありますが、今回はメモリに置いてるっぽさを重視して使いませんでした。
_start:
すぐ後の三行で、hello:
のアドレスをldr
命令でレジスタにロードしています。ラベル名に=
がついていますが、これは=
以降にラベル等の評価後数値になるシンボルがきたときそれを即値として記述できる、というGNU asの記法のようです("ARM Opcodes (Using as)"参照)。
ちなみについでにstrb
命令でメモリの書き換えも実験しています。第2オペランド[r0, #1]
はメモリアドレスの記法です。「r0レジスタのアドレスに1足したアドレス」という意味です。このへんはARMの命令セットチートシートがさくっと見れてよかったです。
アセンブリコードの実行方法
上記のhogeを出力するコード (hoge.s) を実行してみます。
まずはラズパイ上で
GNU asの使いかたがよくわからなかったのですが、以下のようにすれば(ちょっと長いけど)アセンブルとリンクができます。
# アセンブル $ as hoge.s -o hoge.o # リンク $ ld hoge.o -o hoge # 実行 $ ./hoge h*oge
GNU asにas hoge.s
とするとa.out
ができたりするのですが、リンカがエラーを吐くので上のようにしました。
Ubuntu (x86)上で
ラズパイだとスペックが低いためGNU Emacsやlemが重いので、x86上のUbuntuでもエミュレーションとかで動かせないかを試してみました。
QEMUのユーザーモードエミュレーションを使います。QEMUにはマシン全体をシミュレーションするモードとGNU/Linuxのユーザー環境をエミュレーションするモードがあるようで、こちらを利用すれば、HDDイメージにUbuntuをインストールしておく手間を省いて開発できます。
利用するには、まずQEMUとARM向けのgccをインストールします。
$ sudo apt install qemu qemu-user-static gcc-arm-linux-gnueabi
インストールが終わったら、ARM用のas/ldを用いてリンクまで行い、qemu上で実行します。
# アセンブル・リンク $ arm-linux-gnueabi-as hoge.s -o hoge.o && arm-linux-gnueabi-ld hoge.o -o hoge # 実行 $ qemu-arm-static -L /usr/arm-linux-gnueabi ./hoge h*ge
まとめ
アセンブラたのしい!!
マシン語は広大だわ…
なにをつくろうかしら…