10進数を扱う

戻る



 ご存知の通り、最近のコンピュータはほとんどのものがバイナリ、すなわち2進数で動いています。SHも当然その仲間に入ります。しかし日ごろ10進数ばかり扱っている我々人間は2進数や、2進数を4桁づつまとめた16進数の数値を見せられてもピンときません。人間に役立つ計算機にするためには10進数を扱うことができなくてはなりません。今回は10進⇔2進変換を行うルーチン群を見ていきます。

アスキーコードで書かれた10進文字列をバイナリにする

・数字文字か否かの判断

QDECCH:	MOV	#H'30,R13
	CMP/HS	R13,R0		; '0' より小さければ
	BF	CLRCY		; 数字ではない
	MOV	#H'3A,R13
	CMP/HS	R13,R0		; '9' より大きければ
	BT	CLRCY		; 数字ではない
	ADD	#-H'30,R0	; アスキーコードをバイナリに変換
	RTS			; 数字だったら
	SETT			; Tビットを立てる(遅延スロット)

CLRCY:	RTS			; 数字でなければ
	CLRT			; Tビットをクリヤ(遅延スロット)

 まず、1文字のアスキーコードが数字を表しているかどうか調べるルーチンがこの QDECCH です。Q で始まるのは「道しるべ」にも書いたようにそれが目指すものであるかどうかを判別して結果をSRのTビットに返すルーチンであることを意味します。DEC は decimal、CH は character を意味します。

 このルーチンはR0に調べたい文字のアスキーコードを入れて呼びだします。R0の値は呼びだす前に EXTU.B で符号なし化する必要があります。
 呼ばれると、R0の値の範囲が H'30 〜 H'39 であるかどうか調べます。R13の使い方に注目してください。SHはレジスタと即値の比較ができる命令はきわめて少ないので、いったんレジスタにロードしてから比較します。CMBではR13はもっぱら即値ロード用とし、長期にわたって同じ値を保持するような目的には使いません。サブルーチンなど呼んだらほぼ間違いなく破壊されるつもりでプログラムを組みます。

 ところで、ご存知とは思いますが一応説明すると、 H'30 はアスキーコードにすると '0' で、以後順番に H'39 まで数字が割り当てられています。その範囲内でなければそれは数字ではありませんので、ラベル CLRCY に分岐します(いかん、このラベルZ80版のままだ・・・)。CLRCY では Tビットをクリヤしてリターンします。
 一方、範囲内だった場合は、H'30 を引きます。これにより一桁のアスキーコードが0〜9に該当するバイナリ値に変換されます。なお、SHには即値の減算命令はないので、負の値を加算します。さらにTビットをセットしてリターンします。

 結局のところ QDECCH の働きはR0のアスキーコードが数字かどうか調べ、数字であればR0にバイナリ変換した値を返す、ということになります。

・10進文字列をバイナリに変換

 次は複数桁の10進文字列をバイナリに変換するルーチン ATOI を見ていきます。名前の由来は、C言語の atoi() そのままです。
ATOI:	STS.L	PR,@-R15
	MOV	#0,R2

 お約束としてPRを保存します。また結果が累積されるR2をクリヤします。

ATOIL:	MOV.B	@R3+,R0		; メモリから1文字取り出す
	BSR	QDECCH		; 数字か?
	EXTU.B	R0,R0		; 符号なし化(遅延スロット)

	BF	ATOIE		; 数字でなければ終了
	MOV	R2,R1		; R2を10倍する処理
	SHLL2	R2		;  (左2ビットシフトで4倍)
	ADD	R1,R2		;  (元の数を足して5倍)
	SHLL	R2		;  (さらに左1ビットシフトして10倍)
	BRA	ATOIL		; ループ先頭に戻る
	ADD	R0,R2		; 獲得した一桁分のバイナリをR2に加算(遅延スロット)

 ループ部分です。R3が指すメモリから1バイト読みこんで、QDECCH で数字かどうか調べます。数字でなければループを抜けます。
 数字ならR0にはアスキーコードから0〜9のバイナリ値に変換された値が入っているので、R2を10倍してからR2にR0を加算します。SHですからMACを使っても良いのですが、10倍するくらいたいした手間ではないので汎用レジスタ上で処理しています。一応説明すると、4倍してから元の数を足し(これで5倍)、さらに2倍することによって10倍にします。

 CMBは全体的にオーバーフローをチェックしない方針なので、ここでもオーバーフローのチェックはしていません。なぜオーバーフローのチェックをしないかと言うと、CMBは数値を符号付きとみなすか符号無しとみなすかをプログラマーに任せているからです(ようするにアセンブラと同じ)。H'7FFFFFFF + 1 は符号付き数値であればオーバーフローとみなすべきですが、符号無し数値だとするとなんら問題の無い演算でであり、これをオーバーフローとみなされてエラー停止されるようではかえって困ります。

 CCMBでは符号無し数値の扱いをやめたのですが、オーバーフローチェックはインタプリタの動作にとってそれほど本質的なこととは思えなかったので、追加しませんでした。



ATOIE:	LDS.L	@R15+,PR
	RTS
	ADD	#-1,R3

 終了部分です。PRを復帰します。R3をデクリメントしているのは、数字でない文字も最後に1文字読みこんでいるのからそのぶんのポインタを戻しているのです。
 その数字でない文字はひょっとすると '+' や '*' といった演算子かもしれません。それを取り込んだまま ATOI からリターンすると、数式処理ルーチンなどが処理すべき演算子が無くなってしまい正しく処理できません。

 まとめると、ATOI の働きはR3が指す10進文字列をバイナリに変換してR2に返します。R3は数字文字列の長さぶんだけ進みます。

・GETLIT、SGNLIT

 10進文字列をバイナリに変換するルーチンとしては ATOI のもう1レベル上位に GETLIT というルーチンがあります。 ATOI は数字でない文字を見つけた時点で終了しますので、必ず QDECCH によってTビットがクリヤされた状態でリターンしてきます。これでは、「正しく変換が行われた後に数字ではない文字を見つけて正常終了した」のか、あるいは「はじめっから数字などなく全く変換できなかった」か、の区別ができません。それを区別できるようにしたのが GETLIT です。get literal の意味です。
GETLIT:	MOV.B	@R3,R0		; R3が指す文字を読む
	MOV	#H'30,R13	; 数字かどうか判定(QDECCH とほぼ同じ処理)
	EXTU.B	R0,R0
	CMP/HS	R13,R0
	BF	CLRCY
	MOV	#H'3A,R13
	CMP/HS	R13,R0		; 数字でなければ
	BT	CLRCY		; Tビットをクリヤしてリターン
	STS.L	PR,@-R15	; 数字なら
	BSR	ATOI		; ATOI を呼び出し
	NOP

	LDS.L	@R15+,PR
	RTS			; Tビットをセットしてリターン
	SETT			; (遅延スロット)

 まずR3が指す1バイトを読みこんでそれが数字であるかどうか調べます。数字でなければ変換できる見込みはないので CLRCY に飛んでTビットをクリヤしてリターンします。数字であれば ATOI を呼びだします。ここで ATOI にジャンプせずに呼びだしているのは、最後にTビットをセットする必要があるからです。ジャンプしてしまうと ATOI は必ずTビットが0の状態でリターンしてしまいますので目的が果たせません。

 符号付き数値を獲得するルーチンとして SGNLIT があります。signed literal の意味です。これは最初の1文字をR0に入れてから呼びだします。
SGNLIT:	CMP/EQ	#H'2D,R0	; '-' か?
	BF	GETLIT		; '-' でなければそのまま GETLIT にジャンプ
	STS.L	PR,@-R15	; '-' なら
	BSR	GETLIT		; GETLIT を呼び出し
	ADD	#1,R3		; '-' のぶんR3を進める(遅延スロット)

	LDS.L	@R15+,PR
	RTS
	NEG	R2,R2		; GETLIT の結果を符号反転(遅延スロット)

 まずR0に入っている文字が '-' かどうか調べ、そうでなければそのまま GETLIT に飛びます。'-' なら GETLIT を呼びだし、返ってきた値を NEG で符号反転します。
 SGNLIT が最初の1文字をR0に入れてから呼ぶ仕様になっているのは、ほとんどの場合 SKIPSP の直後に呼ばれるからです。まだ説明していませんが SKIPSP は最後にR3が指しているメモリの内容がR0に入った状態でリターンします。

バイナリを10進文字列にする

 今度はバイナリを10新文字列にする ITOA を見ていきましょう。これもC言語の itoa() が名前の由来です(ただし、C言語の itoa() とは違って右詰出力です)。やはり最初には符号無し数値を扱う UITOA から見ていきましょう。

・UITOA

UITOA:	BRA	ITOA11
	MOV.L	R3,@-R15	; R3を保存(遅延スロット)

 ここが入り口です。R3を保存してから本処理に飛びます。

ITOA11:	STS.L	PR,@-R15	; PRを保存
	MOV.L	ITOAE9,R1	; 1000000000
	BSR	ITOASB		; R2を 1000000000 で割って10の9乗の桁を求める
	MOV	#H'30,R0	; 初期値、'0' のアスキーコード(遅延スロット)

	MOV.L	ITOAE8,R1	; 100000000
	BSR	ITOASB		; R2を 100000000 で割って10の8乗の桁を求める
	MOV	#H'30,R0	; 初期値、'0' のアスキーコード(遅延スロット)

	(中略)

	MOV.W	ITOAE2,R1	; 100
	BSR	ITOASB		; R2を 100 で割って10の2乗の桁を求める
	MOV	#H'30,R0	; 初期値、'0' のアスキーコード(遅延スロット)

	MOV	#H'0A,R1	; 10
	BSR	ITOASB		; R2を 10 で割って10の1乗の桁を求める
	MOV	#H'30,R0	; 初期値、'0' のアスキーコード(遅延スロット)

	LDS.L	@R15+,PR	; PR復帰

 本処理前半部分です。途中一部省略しています。全部を見たい人は
CCMB全ソースのページで見てください(全部見ても同様な内容の繰り返しなので、ここに抜粋した部分だけで理解できるとは思いますが)。基本的なアルゴリズムは、変換したいバイナリを、32ビットで表現可能な10のn乗(n>0)の数の大きいほうから順に割っていって一桁ずつ求めていくやり方です。
 このアルゴリズムでバイナリを10進数に変換できる、ということは容易に理解できると思います。話を簡単にするために8ビットで説明すると、8ビットで表現可能な10のn乗の数は100と10です。まず大きいほうの100で割ると百の位の桁が定まり、余りを今度は10で割ると十の位が定まります。最後に余った数で1の位も定まります。32ビットになっても桁数が増えるだけでやっていることは本質的には同じです。

 ITOASB というのが一桁ぶん求めるための割り算ルーチンで、R2÷R1の商に H'30 を加算した結果の1バイトをR3が指すメモリに書きこみ、余りをR2に返します。見ての通り10の9乗から順々に割っていっているだけです。ループにできそうなのですが、元のZ80版や8086版は16ビットで、ループ回数もたったの4回だったのでループにせず、それを引きずる感じで32ビットのH8版やSH版もループになっていません。

 なお、 ITOASB 呼びだしのたびに遅延スロットでいちいち MOV #H'30,R0 を実行しています。「共通の処理なんだからサブルーチン側に入れればいいじゃないか」という声が聞こえてきそうですが、この遅延スロットに MOV #H'30,R0 を入れないとすると NOP を入れるしかなくなります。
 MOV.L ITOAE9,R1 のようなPC相対アドレッシングの命令は遅延スロットに入れられません(入れられたら相当楽になるのですが)。仮に入れたとすると、結局 MOV.L ITOAE9,R1 は BSR より後に実行されるため、相対アドレッシングのベースとなるPCは MOV.L ITOAE9,R1 を実行する時点ではすでにサブルーチンのアドレスになっており、とんでもない値を引っぱってくることになります(そこまで計算した上で MOV.L のオフセットを指定するようなプログラムを組めばそれは「神プログラム」になりますが、さすがにそこまでやる気力はありません)。
 結局、遅延スロットを無駄にしないためには本来はサブルーチン側で共通だった(実際H8版ではサブルーチン側に入ってます)MOV #H'30,R0 を呼びだし側に展開するしかないのです。



	MOV	R2,R0		; R2には1の桁が残っている
	ADD	#H'30,R0	; 1の桁をアスキーコードに変換
	MOV.B	R0,@R3		; メモリに書きこむ
	ADD	#1,R3		; R3を1文字進める
	MOV	#0,R0		; 文字列の終わり '\0' を 
	MOV.B	R0,@R3		; メモリに書きこむ

 最後に1の位の桁の値がR2に残っているのでそれに H'30 を足してアスキーコードに変換しメモリに書きます。さらに、文字列の終わりを表す、値が H'00 の1バイトを書き加えます。(CMBは基本的にC言語と同じく、文字列は H'00 で終わるものとしています)

	MOV.L	@R15+,R3	; R3復帰
	MOV	#H'20,R1	; 空白の文字コード
	MOV	#9,R4		; ループ回数(桁数)

ITOA2:	MOV.B	@R3,R0		; R3が指している文字は
	CMP/EQ	#H'30,R0	; '0' でないか?
	BF	ITOAE		; '0' でなければループ脱出
	MOV.B	R1,@R3		; '0' を空白で置きかえる
	DT	R4
	BF/S	ITOA2		; 規定回数繰り返し
	ADD	#1,R3		; R3を1バイト進める(遅延スロット)

ITOAE:	RTS			; 終了
	NOP

 本処理後半はゼロサプレスを行います。R3を保存したのはこのためで、もう一度先頭から文字列を見直します。先頭から見ていって '0' なら空白で置き換えます。途中、一つでも '0' でない文字が出てきたら終了します。また、1の位はゼロサプレスしないよう最初にループ回数を9回に設定しています。

・ITOA

 次は符号付きの ITOA を見ていきます。
ITOA:	CMP/PZ	R2		; 正の数か?
	BT/S	ITOA1		; 正の数ならR0に空白を入れて ITOA1 へ
	MOV	#H'20,R0	; 空白のアスキーコード(遅延スロット)
				; 負の数なら
	NEG	R2,R2		; R2を符号反転し
	MOV	#H'2D,R0	; R0に '-' のアスキーコードを入れる

ITOA1:	MOV.B	R0,@R3		; R0をメモリに書き込み
	ADD	#1,R3		; 書き込んだ符号文字ぶんR3を進める
	MOV.L	R3,@-R15	; R3を保存

 まずR2の符号を調べ、負であればR0に '-' のアスキーコードを入れて NEG でR2の符号を反転します。正であればR0に空白を入れR2はそのままです。
 そしてR0の符号文字をR3が指すメモリに書きこんでR3をインクリメントし、R3を保存します。このコード群のすぐあとはすでに見たラベル ITOA11 で、そのまま ITOA の本処理になだれこみます。

・ITOASB

 最後にITOA専用割り算ルーチン ITOASB を見ていきます。
ITOASB:	CMP/HS	R1,R2		; R2からR1を引くことができるか?
	BF	ITOASE		; できなければ終了
	SUB	R1,R2		; R2からR1を引く
	BRA	ITOASB		; ループ先頭に戻る
	ADD	#1,R0		; R0(引けた回数)をインクリメント(遅延スロット)

ITOASE:	MOV.B	R0,@R3		; 結果のアスキーコードをメモリに書き込む
	RTS			; R3をインクリメントしてリターン
	ADD	#1,R3		; (遅延スロット)

 やっていることは引き算ループです。ループ回数はたかだか9回なので割り算命令は使っていません。SHは特に割り算命令使うのはちょっと面倒くさいのでなおさらです。
 呼びだし側でR0には H'30、すなわち '0' のアスキーコードを入れています。R0はループを回るごとに、すなわちR1に入っている10のn乗の値が引かれるごとにインクリメントされるので、ループを抜ける時にはその桁の十進値に対応する数字のアスキーコードが入ることになります。もちろん、最初から引けなかった場合はR0は '0' のままループを抜けます。

 最後に結果をR3が指すメモリに書きこんで、R3をインクリメントしてリターンします。

 今回はこのへんまでにしようと思います。