コンソール表示・入力ルーチン後編

戻る



 今回は、前回説明した低レベルルーチンを呼び出す、上位のルーチン群を解説します。

・CRLF/SPACE

 それぞれ改行/空白を表示するルーチンです。

CRLF:	STS.L	PR,@-R15	; PR保存
	BSR	PUTCH		;   ↓を1文字表示
	MOV	#H'0D,R0	; CRコード(遅延スロット)

	LDS.L	@R15+,PR	; PR復帰
	BRA	PUTCH		;   ↓を1文字表示
	MOV	#H'0A,R0	; LFコード(遅延スロット)

SPACE:	BRA	PUTCH		;   ↓を1文字表示
	MOV	#H'20,R0	; 空白(遅延スロット)

 説明の必要も無いほど簡単なルーチンですが、使用頻度は結構高く、ありがたいルーチンです。

・PUTSTR

 C言語で言うところの PUTS() に該当するルーチンです。0で終わる、C言語風文字列を表示します。文字列の先頭番地をR2に入れて呼びだします。
(ちなみに先頭に長さデータが付いているのが PASCAL風文字列です。なんだかんだ言って、C言語風文字列の方が扱いは楽なんですよね。PASCAL風だと、こういうとき長さをカウントするためにもう一つ汎用レジスタが必要になってしまいます。)

PUTSTR:	MOV.B	@R2+,R0		; 最初の文字を読む
	STS.L	PR,@-R15	; PR保存

PUTSTRL:CMP/EQ	#0,R0		; 0(文字列の終わり)か?
	BT	PUTSTE		; 終わりなら終了処理へ
	BSR	PUTCH		; その文字を出力
	NOP			; (遅延スロット)

	BRA	PUTSTRL		; ループ繰り返し
	MOV.B	@R2+,R0		; 次の文字を読む(遅延スロット)

PUTSTE:	LDS.L	@R15+,PR	; PR復帰
	RTS			; リターン
	NOP			; (遅延スロット)

 まずR2が指す最初の文字を読んでからPRを保存しています。これは次の CMP/EQ でのパイプラインストールを少しでも減らすためです。
 読んだ値が0なら文字列の終わりなのでループを抜けます。そうでなければその文字を表示します。BSR の遅延スロットに何も入れられないのがつくづく残念です。
 次の BRA の遅延スロットにはメモリを読む MOV 命令を入れています。パイプラインストールの軽減にはならないのですが、NOP を入れるよりましでしょう。
 最後はお約束的にPRを復帰してリターンします。

・DOT/UDOT

 符号付き/符号無し10進左詰表示ルーチンです。R2の値を表示します。名前の由来ですが、ずっと昔に作ったMSX用FORTHのルーチン名を引きずっています。FORTHでは数値表示するワードは "." です。だから「ドット」なんですね。それがMSX用超ミニBASICを作ったときにそのサブルーチンが使いまわしされ、他のCPUに移植した際にもルーチン名だけはそのまま残ってます。

DOT:	MOV.L	R3,@-R15	; R3保存
	MOV.L	ITOABF1,R3	; 変換用バッファの番地
	STS.L	PR,@-R15	; PR保存
	BSR	ITOA		; ITOA 呼びだし
	NOP			; (遅延スロット)

 開始部分です。10進変換するために第2回で説明した ITOA を使っています。R3は ITOA でも壊されるので保存します。また、このルーチンは末端ルーチンではないので当然ですがPRも保存します。
 ITOABF というのは、10進変換後のアスキーコード文字列を格納するワークエリアです。ITOA を呼びだすと、R2の値が10進変換され、R3が指す ITOABF に格納されます。

	MOV.L	ITOABF1,R2	; バッファの番地(変換済みデータが格納されている)
	BSR	PUTCH		; 最初の1文字(符号)を表示
	MOV.B	@R2+,R0		; 変換後の最初の1文字を読む(遅延スロット)

	BRA	UDOT1		; UDOT1 へ飛ぶ
	NOP			; (遅延スロット)

 ITOA によって変換された最初の番地には数値の符号が入っています。正の数なら空白、負の数なら '-' です。それを読みだして出力します。その後、DOT と UDOT の共通処理 UDOT1 へ飛びます。

UDOT:	MOV.L	R3,@-R15	; R3保存
	MOV.L	ITOABF1,R3	; 変換用バッファの番地
	STS.L	PR,@-R15	; PR保存
	BSR	UITOA		; UITOA 呼びだし
	NOP			; (遅延スロット)

	MOV.L	ITOABF1,R2	; バッファの番地(変換済みデータが格納されている)

 UDOT の開始部分です。こちらは符号無し変換ルーチン UITOA を使います。また最初の符号出力はしません。

UDOT1:	MOV.B	@R2+,R0		; 1文字読む
	CMP/EQ	#0,R0		; 文字列の終わりか?
	BT	UDOTE		; 文字列の終わりなら終了
	CMP/EQ	#H'20,R0	; 空白か?
	BT	UDOT1		; 空白ならループ繰り返し
	BSR	PUTSTR		; PUTSTR 呼びだし
	ADD	#-1,R2		; 1文字進みすぎているので戻す(遅延スロット)

 共通処理です。ITOA/UITOA は右詰出力なので、頭の余分な空白を無視する必要があります。残念ながら文字列を指してるポインタがR2なので SKIPSP は使えません。また、このくらいの小さなループだとサブルーチン呼ぶよりその場に書いてしまったほうが簡単だったりするのでここでループしています。
 空白を飛ばした後、 PUTSTR を呼びだします。ITOA/UITOA は文字列の末尾に0を付加するので PUTSTR で出力できます。ITOABF を指すのにR3ではなくR2を使った理由は、PUTSTR のパラメータはR2で渡すからです。

UDOTE:	LDS.L	@R15+,PR	; PR復帰
	RTS			; リターン
	MOV.L	@R15+,R3	; R3復帰(遅延スロット)

 終了処理です。PRとR3を復帰してリターンします。

・UDOTR

 R2の値を符号無し右詰め表示するルーチンです。LISTコマンドの行番号表示で使われます。

UDOTR:	MOV.L	R3,@-R15	; R3保存
	MOV.L	ITOABF1,R3	; 変換用バッファの番地
	STS.L	PR,@-R15	; PR保存
	BSR	UITOA		; UITOA 呼びだし
	NOP			; (遅延スロット)

	MOV.L	ITOABF1,R2	; 変換後の文字列を
	BSR	PUTSTR		; そのまま PUTSTR で出力
	NOP			; (遅延スロット)

	LDS.L	@R15+,PR	; PR復帰
	RTS			; リターン
	MOV.L	@R15+,R3	; R3復帰(遅延スロット)

 DOT/UDOT と違い、空白除去がありません。UITOA の右詰出力結果をそのまま表示します。特に説明はいらないでしょう。

・GETLN

 行入力ルーチンです。コマンド、プログラムの入力や、INPUT文で使われます。一行ぶんの入力を行い、ワークエリア LINBUF にその内容を返します。ENTERキーが押されて正常終了した場合はTビット=1でリターンします。CTRL−Cで入力が中断された場合はTビット=0でリターンします。

 INPUT文がダイレクトモードで使えない仕様になっているのは、ここに理由があります。ダイレクトモードでは LINBUF に入力された文字列を解釈・実行しているわけですが、 LINBUF は1つしかないので、INPUT文の入力で LINBUF が上書きされると、今まさに実行している文字列が消えてしまいます。わざわざINPUT文のダイレクト実行のためだけに256バイト近いサイズのバッファをもう1つ確保するのもバカバカしいので(そもそも変数に数値を入れたいなら代入すればよい)、ダイレクトモードではINPUT文は使えない仕様にしました。



GETLN:	MOV.L	R4,@-R15		; R4保存
	MOV.L	R5,@-R15		; R5保存
	STS.L	PR,@-R15		; PR保存
	MOV	#0,R3			; 長さの初期値=0
	MOV	R3,R0			; ついでにその0で
	MOV.B	R0,@(LATKEY-WRKTOP,GBR)	; LATKEYもクリヤする
	MOV.L	LINBUF1,R4		; 入力用バッファの先頭番地
	MOV	#MAXLL,R5		; 最大入力可能文字数=240

 開始部分です。レジスタの保存を行い、LATKEY をクリヤします。このルーチンを実行中は、R4は文字の格納先の番地を指しており、またR3は入力された文字数を表しています。最初は何も入力されていないので、R3=0、R4は文字列格納領域の先頭番地 LINBUF です。R5には入力可能な最大文字数を入れておきます。

 って、R5を EXTU.B しなきゃダメじゃん・・・H8じゃないんだから。あ、本編CMBも EXTU やってない・・・orz ああ、自己嫌悪。

GETLNL:	BSR	GETCH		; 1文字入力する
	NOP			; (遅延スロット)

	CMP/EQ	#H'03,R0	; CTRL−Cなら
	BT	GETLNN		;   行入力キャンセル
	CMP/EQ	#H'0D,R0	; ENTERキーなら
	BT	GETLNE		;   行入力正常終了
	CMP/EQ	#H'08,R0	; BSキーなら
	BT	GETLBS		;   BS処理へ
	CMP/EQ	#H'09,R0	; TABキー以外の文字なら
	BF	GETLN1		;   GETLN1 へ
	MOV	#H'20,R0	; TABは空白に置きかえる
GETLN1:	MOV	#H'20,R13	; 文字コードが
	CMP/HS	R13,R0		;   H'20未満なら
	BF	GETLNL		;     その文字は無視
	CMP/HS	R5,R3		; 最大入力可能文字数に達していたら
	BT	GETLNL		;   それ以上入力しない
	BSR	PUTCH		; 入力された文字を表示
	MOV.B	R0,@R4		; その文字をバッファに置く(遅延スロット)

	ADD	#1,R3		; 文字数をインクリメント
	BRA	GETLNL		; ループ繰り返し
	ADD	#1,R4		; バッファの書きこみポインタをインクリメント

 ループ部分です。1文字入力し、そのキーコードによって処理を振り分けます。入力可能な文字ならバッファに格納し、ENTER、BSなどの機能キーならそれぞれの処理に分岐します。入力できない文字は無視します。
 最大文字数以上入力できないようにしていたつもりですが、R5を EXTU していないのでこのチェックは引っかかりません。まさかこんなところに今トレンディーな「バッファオーバーフロー脆弱性」が潜んでいようとは・・・orz

 まあ、不幸中の幸いというか、LINBUF はRAM領域のほぼ末尾に位置しており、その後ろには有効なワークエリアはありません。どんどん書き進んでいってもアドレスが0になってROM領域に突入すると、もう当分は何も書きこめません。もしこのバッファオーバーフロー脆弱性を利用して悪さをするつもりならI/O領域やRAM領域にたどりつくまで約4Gバイトも文字を送り続ける必要があります



;(正常終了処理)
GETLNE:	BSR	CRLF		; 改行する
	NOP			; (遅延スロット)
	BRA	GETLNEE		; 共通終了処理へ
	CLRT			; Tビットをセット(遅延スロット)

;(キャンセル終了処理)
GETLNN:	BSR	CRLF		; 改行する
	NOP			; (遅延スロット)

	SETT			; Tビットをクリヤ

GETLNEE:MOV	#0,R0		; 行末に0を置く
	MOV.B	R0,@R4		; 
	MOV.B	R0,@(LATKEY-WRKTOP,GBR)	; ついでにLATKEY もクリヤ
	LDS.L	@R15+,PR	; PR復帰
	MOV.L	@R15+,R5	; R5復帰
	MOV.L	@R15+,R4	; R4復帰
	RTS			; リターン
	MOV.B	R0,@(LATKTM-WRKTOP,GBR)	; LATKTM もクリヤ(遅延スロット)

 終了処理です。ENTERとCTRL−Cの違いはSRのTビットのセット/リセットだけで、CTRL−Cの場合でも行末に0を置きます。呼びだし元のルーチンは、GETLN 呼びだし後に LINBUF の先頭から、0が見つかるまでの文字列を処理すれば良いのです。

;(BSキー処理)
GETLBS:	TST	R3,R3		; 現在入力されている文字が無ければ
	BT	GETLNL		;   ループに戻る
	BSR	PUTCH		; BSを出力してカーソルを戻す
	MOV	#H'08,R0	; BSコード(遅延スロット)

	BSR	PUTCH		; 空白を出力して前の1文字を消す
	MOV	#H'20,R0	; 空白のコード(遅延スロット)

	BSR	PUTCH		; もう一度BSを出力してカーソルを戻す
	MOV	#H'08,R0	; BSコード(遅延スロット)

	ADD	#-1,R3		; 文字数を1減らす
	BRA	GETLNL		; ループに戻る
	ADD	#-1,R4		; 書きこみポインタを1つ戻す(遅延スロット)

 ここはBSキーが押されたときの処理です。ほとんどのターミナルソフトはBSコードを送ってもカーソルが戻るだけで文字を消してはくれないのでさらに空白を送って再度BSコードを送ります。文字数カウントや書きこみポインタも1文字ぶん戻します。

番外編 PUTXグループ

 16進表示するルーチン群です。これらはCCMBには含まれていない、CMBのルーチンなのですが、「表示ルーチンのソースを載せるならこれを載せないでどうする」という重要なルーチンです。実際オールアセンブラのプログラムをデバッグする際には10進より16進数の表示のほうが圧倒的にニーズが高いです。さほど難しくは無いので詳細な説明はしませんが、各自読解してみてください。

;(R2を16進8桁出力)
PUTX8:	STS.L	PR,@-R15	; PR保存
	MOV.L	R2,@-R15	; R2保存
	BSR	PUTX4		;   ↓で移動した上位16ビットを出力
	SHLR16	R2		; 上位16ビットを下位へ移動(遅延スロット)

	BSR	PUTX4		;   ↓で復帰したR2の下位16ビットを出力
	MOV.L	@R15+,R2	; R2復帰(遅延スロット)

	LDS.L	@R15+,PR	; PR復帰
	RTS			; リターン
	NOP			; (遅延スロット)

;(R2の下位16ビットを16進4桁出力)
PUTX4:	STS.L	PR,@-R15	; PR復帰
	MOV	R2,R0		; R2をR0にコピー
	BSR	PUTX2		;   ↓で移動したビット15〜8を出力
	SHLR8	R0		; ビット15〜8をビット7〜0に移動(遅延スロット)

	BSR	PUTX2		;   ↓でコピーしたビット7〜0を出力
	MOV	R2,R0		; もう一度R2をR0にコピー(遅延スロット)

	LDS.L	@R15+,PR	; 以下、お約束
	RTS
	NOP 

;(R0の下位8ビットを16進2桁出力)
PUTX2:	STS.L	PR,@-R15	; PR保存
	MOV.L	R0,@-R15	; R0保存
	SHLR2	R0		; ビット7〜4をビット5〜2に移動
	BSR	PUTX1		;   ↓で移動した最初のビット7〜4を表示
	SHLR2	R0		; 最初のビット7〜4をビット3〜0に移動(遅延スロット)

	BSR	PUTX1		;   ↓で復帰したR0のビット3〜0を表示
	MOV.L	@R15+,R0	; R0復帰(遅延スロット)

	LDS.L	@R15+,PR	; 以下お約束
	RTS 
	NOP

;(R0のビット3〜0を16進1桁出力)
PUTX1:	AND	#H'0F,R0	; ビット3〜0のみ取りだす
	MOV	#H'0A,R13	; 10より
	CMP/HS	R13,R0		;   小さく
	BF	PUTX11		;     なければ
	ADD	#H'07,R0	;       7を足す
PUTX11:	BRA	PUTCH		; 1文字出力(BRAなのでPR保存不用)
	ADD	#H'30,R0	; H'30を足す(遅延スロット)


超番外編 H8版 PUTXグループ

 ソースの見た目のインパクトはSH版より上です。PRを保存しなくてよいH8は、SHよりトリッキーなコードが書けます。

;(8桁出力)
PUTX8	PUSH.W	R2		; ER2の下位16ビット保存
	MOV.W	E2,R2		; 上位16ビットを下位へ移動
	BSR	PUTX4:8		; 移動した上位16ビットを出力
	POP.W	R2		; 下位16ビットを復帰し、
				; そのままPUTX4 になだれこんで下位16ビットを表示
;(4桁出力)
PUTX4	MOV.B	R2H,R0L		; R2のビット15〜8をR0Lに移動
	BSR	PUTX2:8		; 移動したビット15〜8を出力
	MOV.B	R2L,R0L		; R2のビット7〜0をR0Lに移動し、
				; そのままPUTX2 になだれこんでビット7〜0を表示
;(2桁出力)
PUTX2	PUSH.W	R0		; R0保存
	SHLR.B	R0L		; この4行でビット7〜4をビット3〜0に移動
	SHLR.B	R0L		; 〃
	SHLR.B	R0L		; 〃
	SHLR.B	R0L		; 〃
	BSR	PUTX1:8		; 移動したビット7〜4を表示
	POP.W	R0		; R0復帰し、
				; そのままPUTX1 になだれこんでビット3〜0を表示
;(1桁出力)
PUTX1	AND.B	#H'0F,R0L	; 下位4ビットのみ取りだす
	CMP.B	#H'0A,R0L	; 10より
	BCS	PUTX11:8	;   小さくなければ
	ADD.B	#H'07,R0L	;     7を足す
PUTX11	ADD.B	#H'30,R0L	; H'30 を足す
	BRA	PUTCH:8		; 1文字出力

 SH版の PUTXグループは、8桁出力ルーチンの中から4桁出力ルーチンを2回呼び、4桁出力ルーチンの中から2桁出力ルーチンを2回呼んで・・・というフラクタル的構造になっているのですが、H8版は8桁出力ルーチンの中から4桁出力ルーチンを1回呼んだ後、2回目の呼びだしはせずに直後に隣接する4桁出力ルーチンにそのままなだれ込みます。
 SH版が「フラクタル的」というならH8版は「レフレックス検波的」とでもいった感じでしょうか。最近はマイコンのプログラムも高級言語で組むのが普通になり、こういうトリッキーなアセンブラソースを書くことも無くなって寂しいかぎりです。

 今回はここまでとします。