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 除算をしてもエラーとはならず,不正な数値が 設定されます.
対数や指数の計算は実装していません.拡張してみてください.