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
少し簡単になりました.
リダイレクトを使えばファイルを使う入出力も可能ですから,これだけ でも色々遊ぶことができると思います.