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

さて,これからアセンブリ言語によってプログラムを作成していきますが, これまでのように文字の出力の度に write システムコールを使っていては, 毎回長いコードが必要で面倒です.基本的な入出力のサブルーチンを作って おきましょう. ファイルの入出力は後回しで,まず標準入出力に関して用意 します. ちょっと長くなりますが我慢して付き合ってください.

Exit, OutString のような基本的なサブルーチンをいくつか含むファイルを用意して, たとえば %include "stdio.inc" としてアセンブリプログラムに取り込めば 簡単にプログラムが作成できるようになります.

まずは標準出力からの表示ができれば結構遊ぶことができるでしょう.

以下のコードは %include "syscall.inc" を宣言しているものとします.

最初にどんなプログラムにも必須のコード,プログラムを終了するための サブルーチンを作成します.

;------------------------------------
Exit:
               mov    eax, 1         ; sys_exit
               mov    ebx, 0         ; exit with code 0
               int    0x80

これで終了する場合は常に 0 (正常終了) となります. 異常終了など何らかの値を呼び出し元に伝えるために次のサブルーチンも用意しておきましょう. ebx に 終了コードを設定して call します.

;------------------------------------
; exit with ebx
ExitN:
               mov    ebx, eax       ; exit with code ebx
               mov    eax, 1         ; sys_exit
               int    0x80

write システムコールを用いて標準出力に文字列を表示するサブルーチン です.EAX に文字列が格納されている先頭アドレス,EDX に文字列の バイト数を渡してコールします.

;------------------------------------
; print string to stdout
; eax : top address
; edx : no of put char
OutString:
               pusha
               mov    ecx, eax
               mov    eax, SYS_write
               mov    ebx, 1         ; to stdout
               int    0x80
               popa
               ret

ここで気になるのは,文字列を表示するためだけに以下のように5行もコードを 書く必要があることです.

  msg            db   'Here it is.', 0x0A
  msglen         equ  $ - msg
  :
                 mov    eax, msg
                 mov    edx, msglen
                 call   OutString

つまり,文字列の先頭アドレスと文字列の長さを指定して文字列出力用のサブ ルーチンを呼んでいます.このアプローチは Pascal言語で採用されている 文字列の表現と似ています. つまり文字列の長さを文字列の内容と共に保持 する (アセンブラが計算する) 必要があります.

例えば文字列の終わりの印として 0 を使うようにすればサブルーチン中で文字 列の長さを求めるようにすることが可能となります.

まず EAX が示すアドレスに格納されている0で終わる文字列の長さを EAX に 求めます.ここでは 65Kバイト以上の文字列はないもの (切り捨て) とします.

;------------------------------------
; get length of asciiz string
; eax : top address
; eax : return length
StrLen:
               push   ecx
               push   edi
               mov    edi, eax
               push   eax
               xor    eax, eax
               mov    ecx, 0xFFFF  ; no more than 65k chars.
         repne scasb
               pop    ecx
               sub    edi, ecx
               mov    eax, edi
               pop    edi
               pop    ecx
               ret

StrLen と OutString を使って 0 で終わる文字列 (ASCIIZ文字列) を標準出力 に出力するサブルーチンを作成します.

;------------------------------------
; print asciiz string
; eax : pointer to string 
OutAsciiZ:
               push   edx
               push   eax
               call   StrLen
               mov    edx, eax
               pop    eax
               call   OutString
               pop    edx
               ret

OutString に文字数を数える前処理を加えています.文字列を表示するため に必要なプログラムは次のように3行で済みます.

                 mov    eax, msg
                 call   OutASCIIZ
  :
  msg            db     "Filename required.", 10, 0

パスカルタイプの文字列を表示するサブルーチンも用意しておきます. 文字列の先頭に 1 バイトを文字数として持ち,その後ろに文字列が続きます. 文字数を 1 バイトで持つため最大 255 文字に制限されます.

;------------------------------------
; print pascal string to stdout
; ebx : top address
OutPString
                push   eax
                push   ebx
                push   edx
                xor    edx, edx
                mov    dl, [ebx]
                inc    ebx
                mov    eax, ebx
                call   OutString
                pop    edx
                pop    ebx
                pop    eax
                ret

次に1文字(1バイト)を標準出力に書き出すサブルーチンです.write システム は文字列の格納されたバッファアドレスを渡す必要があるため,スタック上に バッファを確保(4バイト)しています.実行前後でレジスタは保存されます.

;------------------------------------
; print 1 character to stdout
; eax : put char
OutChar:
               pusha
               push   eax            ; work buffer on stack
               mov    eax, SYS_write
               mov    ebx, 1         ; to stdout
               mov    edx, 1         ; 1 char
               mov    ecx, esp
               int    0x80
               pop    eax
               popa
               ret

EAX の4バイトを文字列として出力します.下位が先に表示されます. また 8ビット目は常に0に設定します.メモリダンプ用です.

;------------------------------------
; print 4 characters in eax to stdout
;   destroyed : eax
OutChar4:
               push   ecx
               mov    ecx, 0x0408
.loop
               and    al, 0x7F       ; 7bit only
               cmp    al, 0x7F
               jz     .dot
               cmp    al, 0x20
               jae    .output
.dot           mov    al, '.'
.output        call   OutChar
               shr    eax, cl
               dec    ch
               jnz    .loop
               pop    ecx
               ret

OutChar で改行コード (0x0A) を出力すれば改行できますが,頻繁に使うため 改行を出力する専用のサブルーチンも用意しておきます. レジスタの値は変化しません.

;------------------------------------
; new line
;   all registers are preserved.
NewLine:
               push   eax
               mov    al, 0AH
               call   OutChar
               pop    eax
               ret

カーソル直前の文字を消す必要もあるでしょう.これも専用のサブルーチンを 用意します.バックスペースは1文字左に移動してスペースを書き出し, もう一度 1文字左に移動する必要があります.

;------------------------------------
; Backspace
;   destroyed : al
BackSpace:
               mov    al, 0x08
               call   OutChar
               mov    al, ' '
               call   OutChar
               mov    al, 0x08
               call   OutChar
               ret

次は数値の出力です.まず簡単な 16進数の出力です. EAX の内容を16進数で標準出力に書き出します.表示桁数は EAXの下位優先で PrintHex2 は 2桁,PrintHex4 は 4桁, PrintHex8 は 8桁で表示します.

;------------------------------------
; print 2 digit hex number (lower 8 bit of eax)
;   eax : number
;   destroyed : edx
PrintHex2:
               mov    edx, 2
               jmp    short PrintHex

;------------------------------------
; print 4 digit hex number (lower 16 bit of eax)
;   eax : number
;   destroyed : edx
;
PrintHex4:
               mov    edx, 4
               jmp    short PrintHex

;------------------------------------
; print 8 digit hex number (eax)
;   eax : number
;   destroyed : edx
;
PrintHex8:
               mov    edx, 8

;------------------------------------
; print hex number
;   eax : number     edx : digit
;
PrintHex:
               push   eax
               push   ecx
               push   ebx
               mov    ecx, edx
  .loop1:      mov    bl, al
               and    bl, 0x0F
               shr    eax, 4
               or     bl, 0x30
               cmp    bl, 0x3A
               jb     .skip
               add    bl, 0x41 - 0x3A   ; A-F
  .skip:
               push   ebx
               loop   .loop1
               mov    ecx, edx
  .loop2:      pop    eax
               call   OutChar
               loop   .loop2
               pop    ebx
               pop    ecx
               pop    eax
               ret

10進数の出力では,数値を数字に変換する必要があります.ここでは 上位の桁から順に表示します. PrintLeftは EAX の内容を符号付十進数値として左詰めで標準出力に書き出します. PrintLeftU は符号なし十進数値として左詰めで出力します.

;------------------------------------
; Output Unsigned Number to stdout
; eax : number
PrintLeftU:
                pusha
                xor     ecx, ecx            ; 桁数カウンタ
                xor     edi, edi            ; 正を仮定
                jmp     short PrintLeft.positive

;------------------------------------
; Output Number to stdout
; eax : number
PrintLeft:
                pusha
                xor     ecx, ecx            ; 桁数カウンタ
                xor     edi, edi            ; 正を仮定
                test    eax, eax
                jns     .positive
                inc     edi                 ; 負に設定
                neg     eax
    .positive:  mov     ebx, 10
    .PL1:       xor     edx, edx            ; 上位桁を 0 に
                div     ebx                 ; 10 で除算
                push    edx                 ; 剰余(下位桁)をPUSH
                inc     ecx                 ; 桁数更新
                test    eax, eax            ; 終了か?
                jnz     .PL1
    .PL2:       test    edi, edi
                je      .pos
                mov     al, '-'             ; 文字コードに変更
                call    OutChar             ; 出力
    .pos:       pop     eax                 ; 上位桁から POP
                add     al, '0'             ; 文字コードに変更
                call    OutChar             ; 出力
                loop    .pos
                popa
                ret

PrintRight は EAX の内容を符号つき十進数値として空白を補って右詰めで標準出力に 書き出します.ECX に桁を指定します.PrintRightU は符号なし数値を出力, PrintRight0 は符号なしで前に0を補って数値を出力します.

;------------------------------------
; Output Number to stdout
; ecx:column
; eax:number
PrintRight0:
                pusha
                mov     ebp, '0'
                jmp     short PrintRightU.pr0

;------------------------------------
; Output Unsigned Number to stdout
; ecx:column
; eax:number
PrintRightU:
                pusha
                mov     ebp, ' '
    .pr0:       mov     esi, ecx            ; 表示桁数を esi に
                xor     ecx, ecx            ; 桁数カウンタ
                xor     edi, edi            ; 正を仮定
                jmp     short PrintRight.positive

;------------------------------------
; Output Number to stdout
; ecx:column
; eax:number
PrintRight:
                pusha
                mov     ebp, ' '
    .pr0:       mov     esi, ecx            ; 表示桁数を esi に
                xor     ecx, ecx            ; 桁数カウンタ
                xor     edi, edi            ; 正を仮定
                test    eax, eax
                jns     .positive
                dec     esi
                inc     edi                 ; 負を設定
                neg     eax
    .positive:
                mov     ebx, 10
    .pr1:       xor     edx, edx            ; 上位桁を 0 に
                div     ebx                 ; 10 で除算
                push    edx                 ; 剰余(下位桁)をPUSH
                inc     ecx                 ; 桁数更新
                test    eax, eax            ; 終了か?
                jnz     .pr1
                sub     esi, ecx            ; esi にスペース数
                jbe     .done               ; 表示桁数を超える
                xchg    esi, ecx            ; ecx にスペース数
    .space:     mov     eax, ebp            ; スペースか 0
                call    OutChar             ; スペース出力
                loop    .space
                xchg    esi, ecx            ; ecx に表示桁数
    .done:
                jmp     short PrintLeft.PL2

1文字を標準入力から読みこみます.読んだ文字は EAX レジスタに格納されます. 入力バッファはスタック上に4バイト確保 (push eax) して, 結果を EAX に格納 (pop eax) しています.当然 AL の1バイトのみが有効です.

;------------------------------------
; input 1 character from stdin
; eax : get char
InChar:
               push   ebx
               push   ecx
               push   edx
               push   eax            ; work buffer on stack
               mov    eax, SYS_read
               mov    ebx, 0         ; from stdin
               mov    ecx, esp       ; into Input Buffer
               mov    edx, 1         ; 1 char
               int    0x80           ; call kernel
               pop    eax
               pop    edx
               pop    ecx
               pop    ebx
               ret

EAX に指定した文字数(バイト数)を標準入力から読みこみます. キーボードからの入力も標準入力となり,編集機能が全く無いと 実用的ではありません.ここでは1文字消去 (バックスペース) の の機能のみを実装しておきます.任意の位置への挿入などは後で 作成することにします.

;------------------------------------
; Input Line
; eax : BufferSize
; ebx : Buffer Address
; return       eax : no. of char
;
InputLine0:
               push   edi
               push   ecx
               push   edx
               mov    edx, eax
               mov    edi, ebx       ; Input Buffer
               xor    ecx, ecx
   .in_char:
               call   InChar
               cmp    al, 0x08       ; BS ?
               jnz    .in_char2
               test   ecx, ecx
               jz     .in_char2
               call   BackSpace      ; backspace
               dec    ecx
               jmp    short .in_char
   .in_char2:
               cmp    al, 0x0A       ; enter ?
               jz     .in_exit

   .in_printable:
               call   OutChar
               mov    [edi + ecx], al
               inc    ecx
               cmp    ecx, edx       ;
               jae    .in_toolong
               jmp    short .in_char
   .in_toolong:
               dec    ecx
               call   BackSpace
               jmp    short .in_char

   .in_exit:
               mov    dword [edi + ecx], 0
               inc    ecx
               call   NewLine
               mov    eax, ecx
               pop    ecx
               pop    edi
               ret

以上のサブルーチンを stdio.inc にまとめます. hello.asm を syscall.inc と stdio.inc を使って書きなおします.

  ;------------------------------------
  ; hello2.asm
  ;------------------------------------

  %include "syscall.inc"
  %include "stdio.inc"

  section .text
  global _start

  msg             db   'hello, world', 0x0A, 0

  _start:
                 mov    eax, msg
                 call   OutAsciiZ
                 call   Exit

少し簡単になりました.

リダイレクトを使えばファイルを使う入出力も可能ですから,これだけ でも色々遊ぶことができると思います.