8. 標準入出力用のサブルーチンの作成
アセンブリ言語でプログラムを作成する場合に文字の出力の度に write システムコールを使っていては、毎回長いコードが必要で面倒です。 ライブラリとして利用できるように、基本的な入出力用のサブルーチンを 作成します。 まず標準入出力に関して用意します。
文字列出力、数値出力などの基本的なサブルーチンを集めた stdio.s を 「.include "stdio.s"」としてアセンブリプログラムに取り込めば簡単に プログラムが作成できるようになります。
- 全体の構成
- プログラムの終了
- 文字列の表示
- 1文字表示
- 改行の出力
- カーソル直前の文字消去
- 2進数の表示
- 8進数の表示
- 16進数の表示
- 10進数の表示用の除算
- 10進数の表示(左詰め)
- 10進数の表示(右詰め)
- 1文字入力
- 1行入力
- サンプルプログラム
全体の構成
標準入出力用のサブルーチンを集めたファイルstdio.sを サブルーチンごとに見ていきますが、まずはサブルーチン以外の部分です。 最初の6行はコメントです。
@ ------------------------------------------------------------------------ @ Standard I/O Subroutine for ARM @ 2003/09/22 @ Copyright (C) 2003 Jun Mizutani <[email protected]> @ stdio.s may be copied under the terms of the GNU General Public License. @ ------------------------------------------------------------------------ .ifndef __STDIO __STDIO = 1 .ifndef __SYSCALL .equ sys_exit, 0x900001 .equ sys_read, 0x900003 .equ sys_write, 0x900004 .endif .text
stdio.s はファイルにインクルードして使いますが、別のファイルですでに stdio.s を インクルードしている場合にはエラーになるため「.ifndef __STDIO」で stdio.s が 一度だけアセンブルされるようにしています。Cのヘッダファイルと同じです。 「.ifndef __SYSCALL」の部分でこのファイルで使用する3つのシステムコールに 名前をつけていますが、__SYSCALL が定義済み (すべてのシステムコールに名前をつけた ファイルsyscalls.sで定義) ならばスキップされます。
eabi 版の syscalls.s です。
SWI# | 型 | 名前 | r0 | r1 | r2 |
---|---|---|---|---|---|
900001 | long | sys_exit | int error_code | - | - |
900003 | ssize_t | sys_read | unsigned int fd | char * buf | size_t count |
900004 | ssize_t | sys_write | unsigned int fd | const char * buf | size_t count |
「.text」はセクション定義でこのファイルのプログラムはすべてtextセクションに 配置されます。
stdio.s の最後に「.ifndef __STDIO」に対応する「.endif」 をおきます。
.endif
プログラムの終了
最初にどんなプログラムにも必須のコード、プログラムを終了するためのサブルーチンです。 終了するときに常に 0 (正常終了) を親プロセスに返すExitと、異常終了など何らかの値を 呼び出し元の親プロセスに返す ExitN も用意しておきましょう。 r0 に 終了コードを設定して呼びます。プロセスが終了するためサブルーチンコール「 bl Exit」 でもジャンプ「 b Exit」でも同じ動作となります。
@------------------------------------ @ exit with 0 Exit: mov r0, #0 swi #sys_exit mov pc, lr @------------------------------------ @ exit with r0 ExitN: swi #sys_exit mov pc, lr
文字列の表示
write システムコールを用いて標準出力に文字列を表示するサブルーチン です。r0 に文字列が格納されている先頭アドレス、r1 に文字列の バイト数を渡してコールします。
@------------------------------------ @ print string to stdout @ r0 : address, r1 : length OutString: stmfd sp!, {r0-r2, lr} mov r2, r1 @ a2 length mov r1, r0 @ a1 string address mov r0, #1 @ a0 stdout swi #sys_write ldmfd sp!, {r0-r2, pc} @ return
OutStringでは文字列の長さを指定する必要があります。 使う文字列の長さを数えて文字列の長さを指定するのも面倒ですから、 文字列の終わりの印として 0 を使うようにします。サブルーチン中で文字 列の長さを求めさせることができます。
まず r0 が示すアドレスに格納されている0で終わる文字列の長さを r1 に返します。
@------------------------------------ @ input r0 : address @ output r1 : return length of strings StrLen: stmfd sp!, {r0, r2, lr} mov r1, #0 @ r1 : counter 1: ldrb r2, [r0], #1 @ r2 = *pointer++ (1byte) cmp r2, #0 addne r1, r1, #1 @ counter++ bne 1b ldmfd sp!, {r0, r2, pc} @ return
StrLen と OutString を使って 0 で終わる文字列 (ASCIIZ文字列) を標準出力 に出力するサブルーチンです。
@------------------------------------ @ print asciiz string @ r0 : pointer to string OutAsciiZ: stmfd sp!, {r1, lr} bl StrLen bl OutString ldmfd sp!, {r1, pc} @ return
文字列を表示するために必要なプログラムは次のように3行で済みます。
@ 例 adr r0, msg bl OutAsciiZ : msg .asciz "Filename required.\n"
パスカルタイプの文字列を表示するサブルーチンも用意しておきます。 文字列の先頭に 1 バイトを文字数として持ち、その後ろに文字列が続きます。 文字数を 1 バイトで持つため最大 255 文字に制限されます。
@------------------------------------ @ print pascal string to stdout @ r0 : top address OutPString: stmfd sp!, {r0-r1, lr} ldrb r1, [r0] add r0, r0, #1 bl OutString ldmfd sp!, {r0-r1, pc} @ return
1文字表示
1文字(1バイト)を標準出力に書き出すサブルーチンです。1文字表示は文字列表示の 特殊なパターンです。write システムコールには文字列の格納されたバッファアドレス を渡す必要がありますが、1文字表示するためにわざわざメモリ上にバッファを用意する もの面倒ですから、実行時にスタック上にバッファを確保(4バイト)しています。 実行前後でレジスタは保存されます。
@------------------------------------ @ print 1 character to stdout @ r0 : put char OutChar: stmfd sp!, {r0-r2, lr} mov r1, sp @ r1 address mov r0, #1 @ r0 stdout mov r2, r0 @ r2 length swi #sys_write ldmfd sp!, {r0-r2, pc} @ pop & return
r0 の4バイトを上位バイトから文字表示するサブルーチンで、メモリダンプに便利なように 表示可能文字以外はピリオドを表示します。
@------------------------------------ @ print 4 characters in r0 to stdout OutChar4: stmfd sp!, {r0-r2, lr} mov r1, r0 mov r2, #4 1: and r0, r1, #0x7F cmp r0, #0x20 movlt r0, #'.' bl OutChar mov r1, r1, LSR #8 subs r2, r2, #1 bne 1b ldmfd sp!, {r0-r2, pc} @ return
改行の出力
OutChar で改行コード (0x0A) を出力すれば改行できますが、頻繁に使うため 改行を出力する専用のサブルーチンも用意しておきます。 レジスタの値は変化しません。
@------------------------------------ @ new line NewLine: stmfd sp!, {r0, lr} mov r0, #10 bl OutChar ldmfd sp!, {r0, pc} @ return
カーソル直前の文字消去
カーソル直前の文字を消す必要もあるでしょう。これも専用のサブルーチンを 用意します。バックスペースは1文字左に移動してスペースを書き出し、 もう一度 1文字左に移動する必要があります。レジスタの値は変化しません。
@------------------------------------ @ Backspace BackSpace: stmfd sp!, {r0, lr} mov r0, #8 bl OutChar mov r0, #' ' bl OutChar mov r0, #8 bl OutChar ldmfd sp!, {r0, pc} @ return
2進数の表示
数値の表示の最初は r0の内容を2進数として表示するルーチンです。 r1に表示するビット数(下位優先)を設定して呼びます。
@------------------------------------ @ print binary number @ r0 : number @ r1 : bit PrintBinary: stmfd sp!, {r0-r3, lr} teq r1, #0 @ r1 > 0 ? beq 2f @ if r1=0 exit mov r2, r0 mov r3, #32 cmp r1, r3 movhi r1, r3 @ if r1>32 then r1=32 subs r3, r3, r1 mov r2, r2, LSL r3 1: mov r0, #'0' movs r2, r2, LSL #1 addcs r0, r0, #1 bl OutChar subs r1, r1, #1 bne 1b 2: ldmfd sp!, {r0-r3, pc} @ return
8進数の表示
次は、r0の内容をを8進数の数値として表示するルーチンです。 r1 に表示する桁数(下位優先)を設定して呼びます。
@------------------------------------ @ print ecx digit octal number @ r0 : number @ r1 : columns PrintOctal: stmfd sp!, {r0-r3, lr} teq r1, #0 @ r1 > 0 ? beq 3f @ if r1=0 exit mov r3, r1 @ column 1: and r2, r0, #7 mov r0, r0, LSR #3 stmfd sp!, {r2} @ 剰余(下位桁)をPUSH subs r3, r3, #1 bne 1b 2: ldmfd sp!, {r0} @ 上位桁から POP add r0, r0, #'0' @ 文字コードに変更 bl OutChar @ 出力 subs r1, r1, #1 @ column-- bne 2b 3: ldmfd sp!, {r0-r3, pc} @ return
16進数の表示
16進数の出力です。 r0 の内容を16進数で標準出力に書き出します。表示桁数は r0の下位優先で PrintHex2 は 2桁,PrintHex4 は 4桁, PrintHex8 は 8桁で表示します。 16進数の出力では r1 レジスタの値が破壊されることに注意してください。
@------------------------------------ @ print 2 digit hex number (lower 8 bit of r0) @ r0 : number PrintHex2: mov r1, #2 b PrintHex @------------------------------------ @ print 4 digit hex number (lower 16 bit of r0) @ r0 : number PrintHex4: mov r1, #4 b PrintHex @------------------------------------ @ print 8 digit hex number (r0) @ r0 : number PrintHex8: mov r1, #8 @------------------------------------ @ print hex number @ r0 : number r1 : digit PrintHex: stmfd sp!, {r0-r3,lr} @ push mov r3, r1 @ column 1: and r2, r0, #0x0F @ mov r0, r0, LSR #4 @ orr r2, r2, #0x30 cmp r2, #0x39 addgt r2, r2, #0x41-0x3A @ if (r2>'9') r2+='A'-'9' stmfd sp!, {r2} @ push digit subs r3, r3, #1 @ column-- bne 1b mov r3, r1 @ column 2: ldmfd sp!, {r0} @ pop digit bl OutChar subs r3, r3, #1 @ column-- bne 2b ldmfd sp!, {r0-r3,pc} @ restore & return
10進数の表示用の除算
10進数を出力する方法は10で割った余りを求めて下位の桁から順に上位の桁を 求めるのが簡単です。ARMは除算命令を持っていないため、除算ルーチンを用意する 必要があります。ここでは符号無しの除算ルーチンを用意します。 筆算と同じ方法を使っていますが、ループを1ビット毎に32回も繰り返すことの ないように CLZ 命令を使いました。r0に非除数(割られる数)、r0 に除数(割る数) を設定してから呼ぶと、r0に答え、r1に余りが返ります。0で除算しようとすると r0、r1 は変化せず、キャリーフラグをセットして戻ります。
@------------------------------------ @ Unsigned Number Division @ in : r0 : divident / r1:divisor @ out: r0 : quotient...r1:remainder @ carry=1 : divided by 0 udiv: @ r0 / r1 = r0 ... r1 stmfd sp!, {v1-v6, lr} rsbs v2, r1, #0 @ Trap div by zero bcs 4f @ if carry=1 Error mov v1, #0 @ Init result (v1) mov v2, #1 clz v4, r1 1: cmps r0, r1 @ A-b bcc 3f @ if A<b exit clz v3, r0 @ sub v6, v4, v3 mov v5, r1, LSL v6 @ b << v6 cmps r0, v5 @ A-b movcc v5, v5, LSR #1 @ if A<b b >> 1 subcc v6, v6, #1 @ if A<b v6=v6-1 sub r0, r0, v5 @ A=A-b add v1, v1, v2, LSL v6 @ v1=v1-(1<<v6) b 1b @ goto 1 3: mov r1, r0 mov r0, v1 4: ldmfd sp!, {v1-v6, pc} @ return
10進数の表示(左詰め)
10進数の出力では、数値を数字に変換する必要があります。10で割った余りを 順にスタックに積んだ後、スタックから取り出して上位の桁から順に表示します。 PrintLeftは r0 の内容を符号付10進数値として左詰めで標準出力に書き出します。 PrintLeftU は符号なし10進数値として左詰めで出力します。
@------------------------------------ @ Output Unsigned Number to stdout @ r0 : number PrintLeftU: stmfd sp!, {r0-r3, lr} @ push mov r2, #0 @ counter mov r3, #0 @ positive flag b 1f @------------------------------------ @ Output Number to stdout @ r0 : number PrintLeft: stmfd sp!, {r0-r3, lr} @ push mov r2, #0 @ counter mov r3, #0 @ positive flag cmp r0, #0 movmi r3, #1 @ set negative submi r0, r2, r0 @ r0 = 0-r0 1: mov r1, #10 @ r3 = 10 bl udiv @ division by 10 add r2, r2, #1 @ counter++ stmfd sp!, {r1} @ least digit (reminder) cmp r0, #0 bne 1b @ done ? cmp r3, #0 movne r0, #'-' @ if (r0<0) putchar("-") blne OutChar @ output '-' 2: ldmfd sp!, {r0} @ most digit add r0, r0, #'0' @ ASCII bl OutChar @ output a digit subs r2, r2, #1 @ counter-- bne 2b ldmfd sp!, {r0-r3, pc} @ pop & return
10進数の表示(右詰め)
PrintRight は r0 の内容を符号付10進数値として空白を補って右詰めで標準出力に 書き出します。r1 に桁を指定します。PrintRightU は符号なし数値を出力、 PrintRight0 は符号なしで前に0を補って数値を出力します。指定した桁数で 表示できない場合は桁数を無視して出力します。
@------------------------------------ @ Output Number to stdout @ r1:column @ r0:number PrintRight0: stmfd sp!, {r0-r3, v1-v2, lr} @ push mov v1, #'0' b 0f @------------------------------------ @ Output Unsigned Number to stdout @ r1:column @ r0:number PrintRightU: stmfd sp!, {r0-r3, v1-v2, lr} @ push mov v1, #' ' 0: mov v2, r1 mov r2, #0 @ counter mov r3, #0 @ positive flag b 1f @ PrintRight.1 @------------------------------------ @ Output Number to stdout @ r1:column @ r0:number PrintRight: stmfd sp!, {r0-r3, v1-v2, lr} @ push mov v1, #' ' mov v2, r1 mov r2, #0 @ counter mov r3, #0 @ positive flag cmp r0, #0 movlt r3, #1 @ set negative sublt r0, r2, r0 @ r0 = 0-r0 1: mov r1, #10 @ r3 = 10 bl udiv @ division by 10 add r2, r2, #1 @ counter++ stmfd sp!, {r1} @ least digit cmp r0, #0 bne 1b @ done ? subs v2, v2, r2 @ v2 = no. of space ble 3f @ dont write space cmp r3, #0 subne v2, v2, #1 @ reserve spase for - 2: mov r0, v1 @ output space or '0' bl OutChar subs v2, v2, #1 @ nspace-- bgt 2b 3: cmp r3, #0 movne r0, #'-' @ if (r0<0) putchar("-") blne OutChar @ output '-' 4: ldmfd sp!, {r0} @ most digit add r0, r0, #'0' @ ASCII bl OutChar @ output a digit subs r2, r2, #1 @ counter-- bne 4b ldmfd sp!, {r0-r3, v1-v2, pc} @ pop & return
1文字入力
1文字を標準入力から読みこみます。読んだ文字は r0 レジスタに格納されます。 入力バッファはスタック上に4バイト確保して, 結果を r0 に返します。r0 の上位 3バイトは0で返ります。
@------------------------------------ @ input 1 character from stdin @ r0 : get char InChar: mov r0, #0 @ clear upper bits stmfd sp!, {r0-r2, lr} mov r1, sp @ r1 address mov r0, #0 @ r0 stdin mov r2, #1 @ r2 length swi #sys_read ldmfd sp!, {r0-r2, pc} @ pop & return
1行入力
r0 に指定した文字数(バイト数)を標準入力から読みこみます。 キーボードからの入力も標準入力となり、編集機能が全く無いと 実用的ではありません。ここでは1文字消去 (バックスペース) の機能のみを実装しておきます。
@------------------------------------ @ Input Line @ r0 : BufferSize @ r1 : Buffer Address @ return r0 : no. of char InputLine0: stmfd sp!, {r1-r3, v1-v2, lr} mov v1, r0 @ BufferSize mov v2, r1 @ Input Buffer mov r3, #0 @ counter 1: bl InChar cmp r0, #0x08 @ BS ? bne 2f cmp r3, #0 beq 2f bl BackSpace @ backspace sub r3, r3, #1 b 1b 2: cmp r0, #0x0A @ enter ? beq 4f @ exit bl OutChar @ printable: strb r0, [v2, r3] @ store a char into buffer add r3, r3, #1 cmp r3, v1 bge 3f b 1b 3: sub r3, r3, #1 bl BackSpace b 1b 4: mov r0, #0 strb r0, [v2, r3] add r3, r3, #1 bl NewLine mov r0, r3 ldmfd sp!, {r1-r3, v1-v2, pc} @ pop & return
サンプルプログラム
stdio.s を使ったサンプルプログラムです。自身を含む512バイトのメモリの内容を表示します。 16進数8桁を表示する「PrintHex8」、1文字出力の「OurChar」、16進数8桁を表示する「PrintHex2」、 4バイトの値を上位バイトから表示する「OutChar4」、改行するための「NewLine」、 プログラムを終了させる「Exit」を使用しています。 短いプログラムですから解析してみて下さい。
@------------------------------------------------------------------------- @ file : dump.s @ 2003/09/22 @------------------------------------------------------------------------- .include "stdio.s" .text .global _start _start: mvn r0, #0xFF and r3, pc, r0 mov r4, #32 0: mov r0, r3 bl PrintHex8 mov r2, #0 1: mov r0, #' ' bl OutChar ldrb r0, [r3, r2] bl PrintHex2 add r2, r2, #1 cmp r2, #16 blo 1b mov r0, #' ' bl OutChar mov r2, #0 2: ldr r0, [r3, r2] bl OutChar4 add r2, r2, #4 cmp r2, #16 bls 2b bl NewLine add r3, r3, #16 subs r4, r4, #1 bne 0b b Exit
アセンブルして実行してみましょう。dump自身を含む512バイトのメモリダンプ が表示されます。dump の前には stdio.s がアセンブルされたコードがあります。 コード全体を逆アセンブルするには「objdump -D dump」を実行してみてください。 アセンブルとリンクには前回のasld スクリプトを使っています。
~/Documents/asm$ asld dump ~/Documents/asm$ ./dump 00008400 03 00 A0 E1 3E 80 BD E8 FF 00 E0 E3 00 30 0F E0 .. a>.=h.`c.0.` @ c 00008410 20 40 A0 E3 03 00 A0 E1 70 FF FF EB 00 20 A0 E3 @ c.. apk. c . c 00008420 20 00 A0 E3 2D FF FF EB 02 00 D3 E7 67 FF FF EB . c-k..Sggk. .b 00008430 01 20 82 E2 10 00 52 E3 F8 FF FF 3A 20 00 A0 E3 . .b..Rcx: . c&k 00008440 26 FF FF EB 00 20 A0 E3 02 00 93 E7 29 FF FF EB &k. c...g)k. .b 00008450 04 20 82 E2 10 00 52 E3 FA FF FF 9A 30 FF FF EB . .b..Rcz.0k.0.b 00008460 10 30 83 E2 01 40 54 E2 E9 FF FF 1A 00 FF FF EA [email protected] 00008470 00 2E 73 79 6D 74 61 62 00 2E 73 74 72 74 61 62 ..symtab..strtab..sh 00008480 00 2E 73 68 73 74 72 74 61 62 00 2E 74 65 78 74 ..shstrtab..text..da 00008490 00 2E 64 61 74 61 00 2E 73 62 73 73 00 2E 62 73 ..data..sbss..bss... 000084A0 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 s................... 000084B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .................... 000084C0 00 00 00 00 00 00 00 00 00 00 00 00 1B 00 00 00 .................... 000084D0 01 00 00 00 06 00 00 00 74 80 00 00 74 00 00 00 ........t...t...|... 000084E0 FC 03 00 00 00 00 00 00 00 00 00 00 04 00 00 00 |................... 000084F0 00 00 00 00 21 00 00 00 01 00 00 00 03 00 00 00 ....!...........p... 00008500 70 04 01 00 70 04 00 00 00 00 00 00 00 00 00 00 p...p............... 00008510 00 00 00 00 01 00 00 00 00 00 00 00 27 00 00 00 ............'....... 00008520 01 00 00 00 01 00 00 00 70 04 01 00 70 04 00 00 ........p...p....... 00008530 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 .................... 00008540 00 00 00 00 2D 00 00 00 08 00 00 00 03 00 00 00 ....-...........p... 00008550 70 04 01 00 70 04 00 00 00 00 00 00 00 00 00 00 p...p............... 00008560 00 00 00 00 01 00 00 00 00 00 00 00 11 00 00 00 .................... 00008570 03 00 00 00 00 00 00 00 00 00 00 00 70 04 00 00 ............p...2... 00008580 32 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 2................... 00008590 00 00 00 00 01 00 00 00 02 00 00 00 00 00 00 00 .................... 000085A0 00 00 00 00 E4 05 00 00 E0 02 00 00 07 00 00 00 ....d...`.......%... 000085B0 25 00 00 00 04 00 00 00 10 00 00 00 09 00 00 00 %................... 000085C0 03 00 00 00 00 00 00 00 00 00 00 00 C4 08 00 00 ............D...e... 000085D0 65 01 00 00 00 00 00 00 00 00 00 00 01 00 00 00 e................... 000085E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .................... 000085F0 00 00 00 00 00 00 00 00 74 80 00 00 00 00 00 00 ........t...........
まずは標準出力からの表示ができれば比較的簡単にアセンブリ プログラミングを試すことができます。リダイレクトを使えばファイル を使う入出力も可能ですから、これだけでも色々遊ぶことができると思います。