8. 標準入出力用のサブルーチンの作成

アセンブリ言語でプログラムを作成する場合に文字の出力の度に write システムコールを使っていては、毎回長いコードが必要で面倒です。 ライブラリとして利用できるように、基本的な入出力用のサブルーチンを 作成します。 まず標準入出力に関して用意します。

文字列出力、数値出力などの基本的なサブルーチンを集めた stdio.s を 「.include "stdio.s"」としてアセンブリプログラムに取り込めば簡単に プログラムが作成できるようになります。


全体の構成

標準入出力用のサブルーチンを集めたファイルstdio.sを サブルーチンごとに見ていきますが、まずはサブルーチン以外の部分です。 最初の6行はコメントです。

eabi 版の標準入出力用のサブルーチンは こちら です。

@ ------------------------------------------------------------------------
@ 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...........

まずは標準出力からの表示ができれば比較的簡単にアセンブリ プログラミングを試すことができます。リダイレクトを使えばファイル を使う入出力も可能ですから、これだけでも色々遊ぶことができると思います。

このページの目次