12. 浮動小数点演算 その2
前回は一般的な浮動小数点の取り扱いに関する解説でしたが,今回は実際に 浮動小数点演算を利用する実用的なプログラムを作成します.
ちょっと安易な発想ですが「電卓」にします.
12345.678 * 12345 / 3.14 =
と入力した場合に
4.8537426e7
というように結果が返ることを考えます. この場合1行入力された式を 解析 (parse) する必要があり,コンパイラやインタプリタのような 言語処理系を作成する基礎となります.演算子に優先順位をつけ,カッコ を含む式を扱えるようにすると,サンプルとしては長すぎるプログラムに なってしまうので:
- 演算子に優先順位はない.(乗算や除算は, 加算や減算に優先しない)
- カッコを含む式は不可.
- 入力する式の行編集を可能にする.
という仕様で電卓を作ってみます.それでも昔の Tiny BASIC のようなインタプリタを作成する上で重要な多くの部分を含みます.
さて,行編集のために 10. コンソール入力の編集 で作成した READ_LINE サブルーチンや termios 制御のサブルーチンを利用します. また,前回の文字列と浮動小数点数の相互変換のサブルーチン, 5. 標準入出力用のサブルーチンの作成 も利用できます. この辺までくるとアセンブラのプログラムもずいぶんと楽になってきます.
プログラムの流れは次の様になります.
- 1行入力
- 入力行を最初から順に解析して実行,1.に戻る
- 演算子なら登録
- 数値なら演算子が記録されていれば計算,演算子がなければ数値を登録
- コマンドなら実行
;---------------------------------------------------------------------
; Floating Point
; 2000/11/04 Jun Mizutani
; calc.asm
;---------------------------------------------------------------------
%include "stdio.inc"
%include "float.inc"
%include "readline.inc"
%assign MAXLINE 256
%assign MAXWORD 256
%assign OP_NULL 0
%assign OP_ADD 1
%assign OP_SUB 2
%assign OP_MUL 3
%assign OP_DIV 4
%assign OP_ATAN 5
;==============================================================
section .text
global _start
_start:
finit ; FPU を初期化
call GET_TERMIOS ; termios の保存
call SET_TERMIOS ; 端末のローカルエコーをOFF
fldz ; 0 をFPUにロード
mov byte[level], 0 ; レジスタスタックレベル
ReadLine: ; 1行入力
mov eax, prompt ; プロンプト表示
call OutAsciiZ
mov eax, MAXLINE ; 1 行入力
mov ebx, input
call READ_LINE
mov ecx, MAXLINE ; 行前処理
mov esi, input
call LineCleaner
mov eax, esi ; CHECK!!
mov byte[operation], OP_NULL
xor ebx, ebx ; 行内オフセット = 0
Parse: ; 行解析
mov eax, 0x20 ; 空白スキップ
mov ecx, MAXLINE ; reserved
mov esi, input
call SkipChar
mov eax, 0x20 ; 一語取得
mov edi, wordbuf
call Word1
jb ReadLine
cmp edx, 0
jz ReadLine ; empty
mov al, [edi]
cmp al, "0" ; 数字か?
jb .command
cmp al, "9"
ja .command
jmp .num ; 0 - 9 なら数値入力
.command ; 1文字コマンド
cmp edx, 1
jne near .num ; 1文字以上は数値
cmp al, 'Q' ; Q, q ならば終了
jne .com1
call RESTORE_TERMIOS ; termios の復帰
call Exit
.com1
cmp al, '.' ; 数値の可能性有り
jne .com2
dec ebx ; 1文字戻す(ungetc)
jmp .num
.com2
cmp al, '+' ; 加算
jne .com3
mov byte[operation], OP_ADD
jmp Parse
.com3
cmp al, '-' ; 減算
jne .com4
mov byte[operation], OP_SUB
jmp Parse
.com4
cmp al, '*' ; 乗算
jne .com5
mov byte[operation], OP_MUL
jmp Parse
.com5
cmp al, '/' ; 除算
jne .com6
mov byte[operation], OP_DIV
jmp Parse
.com6
cmp al, '=' ; 数値の表示
jne .com7
fld st0 ; 複製 (DUP)
call print_float ; 表示
jmp Parse
.com7
cmp al, 'D' ; スタック位置表示
jne .com8
mov eax, '<'
call OutChar
fstsw ax
shr eax, 11
and eax, 0x07
call PrintLeft ; 表示
mov eax, '>'
call OutChar
call NewLine
jmp Parse
.com8
cmp al, 'R' ; 平方根
jne .com9
fsqrt
jmp Parse
.com9
cmp al, 'S' ; サイン
jne .com10
fsin
jmp Parse
.com10
cmp al, 'C' ; コサイン
jne .com11
fcos
jmp Parse
.com11
cmp al, 'T' ; タンジェント
jne .com12
fptan
jmp Parse
.com12
cmp al, '$' ; アークタンジェント
jne .com13
mov byte[operation], OP_ATAN
jmp Parse
.com13
cmp al, 'P' ; π
jne .com14
cmp byte[operation], OP_NULL
jne .com13_1
ffree st0 ;
fincstp
.com13_1: fldpi
jmp .op_add
.com14
cmp al, 'H' ; ヘルプ表示
jne .com15
mov eax, help1
call OutAsciiZ
mov eax, help2
call OutAsciiZ
mov eax, help3
call OutAsciiZ
mov eax, help4
call OutAsciiZ
mov eax, help5
call OutAsciiZ
jmp Parse
.com15
jmp Parse
.num ; 数値
.op_num: mov eax, wordbuf
call Ascii2Float ; 浮動小数点数の文字列をFPUにセット
jb near .num_err ; 数値変換エラー
cmp byte[operation], OP_NULL
jne .op_add
fxch ; スタックを調節
ffree st0
fincstp ; pop
.op_add: cmp byte[operation], OP_ADD
jne .op_sub
faddp st1,st0
mov byte[operation], OP_NULL
jmp Parse
.op_sub: cmp byte[operation], OP_SUB
jne .op_mul
fsubp st1,st0
mov byte[operation], OP_NULL
jmp Parse
.op_mul: cmp byte[operation], OP_MUL
jne .op_div
fmulp st1,st0
mov byte[operation], OP_NULL
jmp Parse
.op_div: cmp byte[operation], OP_DIV
jne .op_atan
fdivp st1,st0
mov byte[operation], OP_NULL
jmp Parse
.op_atan: cmp byte[operation], OP_ATAN
jne .op_end
fpatan ; atan(st1/st0)
mov byte[operation], OP_NULL
jmp Parse
.op_end:
jmp Parse
.num_err
mov eax, numerror ; 変換エラー表示
call OutAsciiZ
mov eax, wordbuf
call OutAsciiZ
call NewLine
jmp Parse
;------------------------------------
; FPU のスタックトップの浮動小数点レジスタを文字列に変換して表示
; スタックトップは POP (廃棄) される.必要なら先に DUP する.
print_float
mov edi, fstring
call Float2Ascii
mov eax, ' '
call OutChar
mov eax, fstring
call OutAsciiZ
call NewLine
ret
;--------------------------------------------------------------
; esi で指定され, 長さ ecx の文字列バッファ領域の文字を変換
; 英小文字は英大文字,TAB は SPACE, LF は 0 に変換される.
; ecx : buffer length (reserved)
; esi : buffer start address (reserved)
;--------------------------------------------------------------
LineCleaner:
jcxz .lc_end
push esi
push ecx
.next
mov al, [esi]
or al, al
jz .lc_end
cmp al, 0x09 ; TAB
jne .lc_lf
mov byte [esi], 0x20 ; SPACE
jmp short .skip
.lc_lf
cmp al, 0x0A ; Line feed
jne .lc_upper
mov byte [esi], 0 ; 0
jmp short .skip
.lc_upper
cmp al, 0x60
jb .skip
cmp al, 0x7B
jae .skip
sub al, 0x20
mov byte [esi], al ; uppercase
.skip
inc esi
loop .next
.lc_end
pop ecx
pop esi
ret
;--------------------------------------------------------------
; esi で指定される文字列バッファ領域中のオフセット ebx から
; al で指定される文字を読み飛ばして,al 以外の文字まで
; ebx を進める. バッファ領域を超えると何もしない.
; al : skip character (reserved)
; ebx : start offset within buffer, return new offset
; ecx : buffer length (reserved)
; esi : buffer start address (reserved)
;--------------------------------------------------------------
SkipChar:
.skip
cmp ebx, ecx
jns .skipend
cmp al, [esi + ebx]
jne .skipend
inc ebx
jmp .skip
.skipend
ret
;--------------------------------------------------------------
; 区切り文字か, 行末か, 行バッファ末までの文字列を
; edi からの語句バッファにコピー.
; 語句バッファの文字列末に 0 を設定
; 行バッファをすべて使ったらキャリーセット
; コピーした文字列長(0を含まない) を edx に返す.
; eax : delimiter
; ebx : start offset within buffer, return new offset
; ecx : line buffer length (reserved)
; edx : return word length
; esi : line buffer start address (reserved)
; edi : word buffer start address (reserved)
; Set CF=1, if line buffer contains no data.
;--------------------------------------------------------------
Word1:
xor edx, edx
.word
cmp ebx, ecx
jns .wend ; 範囲外 ?
mov ah, [ebx + esi]
or ah, ah ; 0 ?
je .wend
inc ebx
cmp ah, al ; 区切り文字
jne .copy
or edx, edx
jne .wend
jmp short .word
.copy
mov [edx + edi], ah
inc edx
jmp short .word
.wend
mov byte [edx + edi], 0
or edx, edx
je .empty
clc
ret
.empty
stc
ret
;--------------------------------------------------------------
; 現在の termios を保存
GET_TERMIOS:
pusha
mov ebx, old_termios
call tcgetattr
mov ecx, new_termios - old_termios
mov esi, old_termios
mov edi, new_termios
rep
movsb
popa
ret
;--------------------------------------------------------------
; 新しい termios を設定
; Rawモード, ECHO 無し, ECHONL 無し
; VTIME=0, VMIN=1 : 1バイト読み取られるまで待機
SET_TERMIOS:
pusha
mov eax, [new_termios.c_lflag]
and eax, ~ICANON & ~ECHO & ~ECHONL
or eax, ISIG
mov [new_termios.c_lflag], eax
mov eax, 1
mov [new_termios.c_cc + VMIN], al
mov eax, 0
mov [new_termios.c_cc + VTIME], al
mov ebx, new_termios
call tcsetattr
popa
ret
;--------------------------------------------------------------
; 保存されていた termios を復帰
RESTORE_TERMIOS:
pusha
mov ebx, old_termios
call tcsetattr
popa
ret
;--------------------------------------------------------------
; 標準入力の termios の取得と設定
; tcgetattr(&termios)
; tcsetattr(&termios)
; eax : destroyed
; ebx : termios buffer adress
; ecx, edx : destroyed
tcgetattr:
mov eax, TCGETS
jmp short IOCTL
tcsetattr:
mov eax, TCSETS
;--------------------------------------------------------------
; 標準入力の ioctl の実行
; sys_ioctl(unsigned int fd, unsigned int cmd,
; unsigned long arg)
; eax : cmd
; ebx : buffer adress
IOCTL:
mov ecx, eax ; set cmd
mov edx, ebx ; set arg
mov eax, 54 ; sys_ioctl
mov ebx, 0 ; to stdin
int 0x80 ; call kernel
ret
;==============================================================
section .bss
fstring resb 32
input resb MAXLINE
wordbuf resb MAXWORD
operation resb 1
old_termios istruc termios
.c_iflag UINT 1 ; input mode flags
.c_oflag UINT 1 ; output mode flags
.c_cflag UINT 1 ; control mode flags
.c_lflag UINT 1 ; local mode flags
.c_line UCHAR 1 ; line discipline
.c_cc UCHAR NCCS ; control characters
iend
new_termios istruc termios
.c_iflag UINT 1 ; input mode flags
.c_oflag UINT 1 ; output mode flags
.c_cflag UINT 1 ; control mode flags
.c_lflag UINT 1 ; local mode flags
.c_line UCHAR 1 ; line discipline
.c_cc UCHAR NCCS ; control characters
iend
new_termios_end
;==============================================================
section .data
level db 0
prompt db 'calc>',0
help1 db ' LineEdit ^H:BS ^D:Delete ^B:Back ^F:Forward', 0x0A, 0
help2 db ' Function R:SquareRoot S:Sine C:Cosine', 0x0A, 0
help3 db ' T:Tangent $:atan(a/b)= a $ b P:Pi', 0x0A, 0
help4 db ' Display =:Result D:StackLevel H:Help', 0x0A, 0
help5 db ' Quit Q', 0x0A, 0
numerror db ' Error>',0
入力行はタブがあればスペースに変換され,英小文字は大文字に変換された後に スペースを区切りとして単語に分離して解析,実行されます.演算子,コマンドは 1文字になっているため,2 文字以上の単語は数値と解釈され数値でなければ エラーとなり無視されます.
実行結果
jm:~/ex_asm$ asm calc
jm:~/ex_asm$ ./calc
calc>h
LineEdit ^H:BS ^D:Delete ^B:Back ^F:Forward
Function R:SquareRoot S:Sine C:Cosine
T:Tangent $:atan(a/b)= a $ b P:Pi
Display =:Result D:StackLevel H:Help
Quit Q
calc>1.0 + 2.0 * 3 =
9.00000000000000000
calc>/10.0
Error>/10.0
calc>/ 10.0 =
9.00000000000000000e-1
calc>p / 6 s =
5.00000000000000000e-1
calc>p / 6 c =
8.66025403784438647e-1
calc>3 r / 2 =
8.66025403784438647e-1
calc>d
<7>
calc>q
使用方法は H コマンドで表示されます.終了は Q です. コマンドは小文字でも 内部で大文字に変換されるため,どちらでもかまいません.D コマンドは FPU の スタックの位置を表示します.常に <7> が表示されますが,calc を拡張する 場合に FPU のデータレジスタスタックの状態を確認するために使ってください. 0 除算などの例外のチェックはしていません.0 除算をしてもエラーとはならず,不正な数値が 設定されます.
対数や指数の計算は実装していません.拡張してみてください.