x86 で asm 書く機会ってほんとなくて、むしろ ARM の命令セットを学んだほうがいろいろモチベーションがでてきそう、ということで、とりあえず ARM Linux 上で簡単なのを書いてみます。
ARM
ARM は伝統的に OABI ("arm"), EABI ("armeb")という2つのインターフェイスがあるらしいです。OABI が古いほうで汎用 (Old)。EABI は組込み (Embeded) 用。ABI の違いということはシステムコールの呼びかたとか、サブルーチンの呼びかたが違いということです…… すなわちアセンブリで書くときはどっちを書いてるか明確にする必要があります。
ここでは EABI だけでやってみます。EABI の場合システムコールは、r7 にシステムコール番号を指定して、SVC 命令を発行 (オペランドは 0) すれば良いようです。
環境は GCC でやります。
その前に
アセンブリ書くとき、そもそも命令セット (instruction set) がわからないとどうにもならない。リストはARM のサイトから探す。
x86 とかも、とにかく命令セットがわからないと何もしようがない。x86 instruction set とかでググったらよさそう。x86 は命令いっぱいありすぎて pdf が4つぐらいにわかれてる。
ただ、GCC で書く場合 gas (gnu as) のフォーマットで書かないといけないので、データシートと形式がちがうのが罠い。
GCC as (gas) での書きかたは、公式のドキュメントを参照する必要がある。x86 と arm でも書きかたが違う。
exit するだけのコード
まず exit するだけのコードを書いてみます。
/*#!as -o sketch.o sketch.s && ld -o sketch -e _start sketch.o && ./sketch */ .global _start _start: mov r0, $0x00 /* set exit status to 0 */ mov r7, $0x01 /* set system call number to 1 (exit) */ svc $0x00 /* supervisor call */
$ as -o sketch.o sketch.s $ ld -o sketch -e _start sketch.o $ ./a.out
簡単!!!
r7 にシステムコール番号を入れ、r0 には exit の唯一の引数である終了ステータスを入れ、svc 命令でシステムコールが実行されます。当然 exit なので返ってきません。
obj-dump するとそのままのコードが出てきます
Disassembly of section .text: 00008054 <_start>: 8054: e3a00000 mov r0, #0 8058: e3a07001 mov r7, #1 805c: ef000000 svc 0x00000000
Hello, World
こっちは微妙に面倒です。普通に同じようにシステムコールを呼ぶだけですが、文字列をどこに置くか、どうやってアドレスを参照するかがいろいろあります。
.text に一緒に置く
たぶん一番シンプルな例
/* text section */ .section .text .global _start _start: mov r0, $0x01 /* set file descripter to 1 (stdout) */ adr r1, msg /* adr is GNU as's special instruction. This is translated to add */ mov r2, $0x0d /* set msg length to 0x0d (13) */ mov r7, $0x04 /* set system call number to 4 (write) */ svc $0x00 /* supervisor call */ mov r0, $0x00 /* set exit status to 0 */ mov r7, $0x01 /* set system call number to 1 (exit) */ svc $0x00 /* supervisor call */ msg: .asciz "Hello, World\n" .align 2
1行ごとにコメントつけてますが、この例では "Hello, World\n\0" という文字列が text セクションに置かれ、それを adr 命令で参照して r1 に書きこんでいます。adr 命令は GNU as が解釈して、指定したラベルを示すように add 命令に書きかえられます。試しにこれをアセンブルしたものを objdump すると
Disassembly of section .text: 00008054 <_start>: 8054: e3a00000 mov r0, #0 8058: e28f1014 add r1, pc, #20 805c: e3a0200d mov r2, #13 8060: e3a07004 mov r7, #4 8064: ef000000 svc 0x00000000 8068: e3a00000 mov r0, #0 806c: e3a07001 mov r7, #1 8070: ef000000 svc 0x00000000 00008074 <msg>: 8074: 6c6c6548 .word 0x6c6c6548 8078: 57202c6f .word 0x57202c6f 807c: 646c726f .word 0x646c726f 8080: 000a .short 0x000a ...
とかなってます。_start の中の adr があったところは add 命令に書きかわってます。.text はプログラム用のセクションですが、普通にデータも置けます。
.data セクションに置く
もう少しまともな例です。
/* text section */ .section .text .global _start _start: mov r0, $0x01 /* set file descripter to 1 (stdout) */ ldr r1, msg /* load memory address of msg to register */ ldr r2, msg_len /* load memory address of msg_len to register */ mov r7, $0x04 /* set system call number to 4 (write) */ svc $0x00 /* supervisor call */ mov r0, $0x00 /* set exit status to 0 */ mov r7, $0x01 /* set system call number to 1 (exit) */ svc $0x00 /* supervisor call */ msg: .word data_msg /* write data_msg address to here */ msg_len: .word data_msg_len /* data section */ .section .data data_msg: .asciz "Hello, World\n" data_msg_len = . - data_msg .align 2
テキストデータ本体は data セクション (初期化される・書き換え可能・Cで書いた場合グローバル変数の領域) において、text セクションにはポインタのアドレスを置いています。ldr 命令でそのアドレスをレジスタの読みこみ、write を呼んでいます。
ついでに文字列の長さもアセンブラで計算させています。. は現在のアドレスを示していて、. から data_msg のアドレスを引くことで文字列の長さを求められます。
これを objdump -d -j .text -j .data sketch すると
Disassembly of section .text: 00008074 <_start>: 8074: e3a00000 mov r0, #0 8078: e59f1014 ldr r1, [pc, #20] ; 8094 <msg> 807c: e59f2014 ldr r2, [pc, #20] ; 8098 <msg_len> 8080: e3a07004 mov r7, #4 8084: ef000000 svc 0x00000000 8088: e3a00000 mov r0, #0 808c: e3a07001 mov r7, #1 8090: ef000000 svc 0x00000000 00008094 <msg>: 8094: 0001009c .word 0x0001009c 00008098 <msg_len>: 8098: 0000000e .word 0x0000000e Disassembly of section .data: 0001009c <data_msg>: 1009c: 6c6c6548 .word 0x6c6c6548 100a0: 57202c6f .word 0x57202c6f 100a4: 646c726f .word 0x646c726f 100a8: 000a .short 0x000a ...
みたいな感じになります。
ちなみに、ld に --verbose オプションをつけるとリンカスクリプトが表示されます。これはコードやデータを実際にメモリ上にどのように配置するかを定義したもので、ld はこの定義に従って、シンボルを解決 (アドレスを決めるということ) オブジェクトを配置します。