アセンブラで JIS から SJIS に変換するとき DAA が使える?
ずっと以前、私がまだ学生のころに、ちらと聞いたのだと思う。2進化10進数補正はほぼ使いようもないが、JIS コードの変換に使うと便利であるというような話だった。そのことについて「それはちょっと…」みたいに否定的なことを私は言って、その話が終わったように記憶している。
先日、シフトJIS(Shift-JIS)の話をたまたま読んで、シフトがビットシフトの意味でないと書いているのを見て、いや、しかし、アセンブラ特有の変な感じでシフトを使えることがこのコードの由来だったはずだぞ…という想いが浮かんだ。でも、Perl などでの実装も、ネットにあるいくつかのアセンブラのソースを見ても、そんな風には書かれていない。
つまり、それはかつての「2進化10進補正」を使うという議論に関係していたのかもしれない。そう私は推理し、自分なりにその方法を模索した。
本稿では Z80 のアセンブラを用いる。実験では、Perl 上のアセンブラ CPU::Z80::Assembler とエミュレータ CPU::Emulator::Z80 のモジュールを使った。
■ |
文字コードに関して
|
英アルファベットは 26 文字、大文字と区別しても 52 文字、ここにいくつかの記号を加えても、6 ビット以内で表せる。ここに改行などの制御文字を加えて、7 ビットしか使わないコードとして、標凖的なアスキーコードがある。
1 バイトは 8 ビットで、残り 1 ビットは、当初はテキストファイルの伝送時のエラーを簡易的に識別するのに使ったのかもしれない。通信には 1 バイトにつき 7 ビットしか使えないことがあった。
日本語は、漢字も含めれば当然、8 ビットでも済まない。そこで 2 バイト以上で 1 文字を表さねばならないのだが、ちょうど、アスキーコードは 7 bit しか使わないのだから、8 ビット目があるものは 2 バイトで一文字を表すことにしようという考え方が自然に浮かぶ。
一方で、通信にはあくまで 7 ビットでなければならないというなら、制御文字を使って、「次からは n バイトで 1 文字ですよ」と宣言するようなことをするしかない。そうすると、そこまでするなら、制御文字「列」をいろいろにできてもよいわけだから、いろいろなコードを混在もできよう。「8 ビット目があれば 2 バイト」というのに比べてより汎用的と言える。
そこで、制御文字列を使って 7 ビットで表示するのを基本としてコード体系を作り、それを「8 ビット目があれば 2 バイト」となるようなコード体系に変換すればいい……となるのは自然な流れだろう。
前者の 7 ビットコードの一つが JIS コードで、それを 8 ビットに変換したものの一つがシフト JIS コードである。
まぁ、こういう理解の仕方もできると思う。
■ |
JIS から シフト JIS へ変換
|
アスキーコードでは、0x00 から 0x1F, 0x7F は制御文字である。これにスペースの 0x20 を加えて、パソコンではここは 2 バイトコードも含むどんな文字コードでも空けておくべきだとされている。そこで JIS コードは、0x21 から 0x7E までのバイトを 2 バイト使って表現される。
日本語にはカナがあり、これだけならば 7 ビットで表現することも不可能ではない。そこで、アスキーコードとかぶらない 0xA1 から 0xDF までの間にカナのコードが割り当てられることがあった。
シフト JIS コードは、このカナのコードと互換性を持ちながら、8 ビットを使った 2 バイトコードを実現するために、2 バイトコードの最初の 1 バイトは 0x81 から 0x9F、そして 0xE0 以降だけしか使わないことにした。その替わり、2 バイト目は 8 ビット目を使っていない 7 ビットコードでいいとした。よって、あるバイトを取っただけでは、そのバイトが 2 バイトコードの 2 バイト目なのか、1 バイトのコードなのかわからなくなった。
具体的には、JIS コードの 1 バイト目の 1 ビット目は、シフト JIS コードの 2 バイト目の情報として使い、1 バイト目の残り 6 ビットで表される数字を、0x81 から順に並べ 0xA0 になったところで 0xE0 に飛び、そこからまた順に並べるようにする。
そして、JIS コードの 2 バイト目は、1 バイト目の 1 ビット目が 0 なら、 0x7F に重ならないよう、0x21 から 0x5F のコードを 0x40 から 0x7E に、 0x60 から 0x7E のコードを 0x80 から 0x9D に並べ直し、1 バイト目の 1 ビット目が 1 なら、0x21 から 0x7E のコードを 0x9E から 0xFC に並べ直す。
|
Wikipedia によると、このように複雑に「ずらす」のが「シフト」JIS の名の由来だそうだ。
ちなみに、2 バイト目のコードを 0x40 からはじめるのは、0x21 から 0x3F までの記号や数値がディレクトリの区切りを示す記号などの「制御文字」として扱えるようにするつもりだったのかもしれない。しかし、MS-DOS や Windows ではまさにディレクトリの記号として 0x5C (¥ or \) が使われたため、大きな混乱が置きた。これは日本のソフトウェア業界では有名な話である。
(もう一つマメ知識を述べておくと、2004 年にシフト JIS コードは拡張され、必ずしも上の計算式によらないコードが足されている。参:《JIS X 0213 (JIS2004) の代表的な符号化方式》)
■ |
Z80 アセンブラによる素直な実装
|
私のプログラム遍歴は、中学のころポケコンの BASIC にはじまり、PC-8801 FA で Z-80 互換アセンブラを覚え、大学に入って 8086 系アセンブラ、C 言語を経て TeX、Perl へ、そして時代は WWW といった感じである。オブジェクト指向には、理論的な面で高い関心があるが、逆にそこでの理想が強すぎて現存のオブジェクト指向になじめないといったところである。(オレがオブジェクト指向を使えないんじゃなくて、今のオブジェクト指向が使えないんだよ!…ってね。(^^;)
Z80 は私の青春の一つである。…というのはさすがに言い過ぎかもしれない。今回、久々に触ってみて、まったく覚えていないことにショックを受けた。
ただ、技術は私を置いて確実に進歩していて、Perl 上でゼッパチのエミュレータが動くらしい。そこまで手軽ならということで、今回の実験につながった。
上の JIS からシフト JIS への変換を素直に書けば次のようになるだろうか。
;; HL に JIS コードを入れ、HL に SJIS コードを返す。 ;; 31 bytes。 JIS2SJIS: LD A, H SUB 0x21 ;; SCF と RRA でビットシフトと +0x80 を行う。 SCF RRA LD H, A ;; L の処理。 LD A, 0x1F JR NC, L1 LD A, 0x7D L1: ADD A, L CP 0x7F JR C, L2 INC A L2: LD L, A ;; H の後処理。 LD A, H INC A CP 0xA0 JR C, L3 ADD A, 0x40 L3: LD H, A ;; STOP はエミューレタの特殊命令。ここで Perl に処理を戻す。 STOP |
H レジスタの 1 ビット目で JR NC するので、H の前処理→ L の処理 → H の後処理というようになっている。
■ |
DAA を用いた数値文字の変換
|
|
以上には、2進化10進補正の説明もあるので、私がその説明をするのを省くが、結局のところ、その「特殊処理」によって、条件分岐を使わなくて済むのが大事なようだ。
でも、クセのあるのが 6 を足すということ。1 つのビットが足されるというわけでもない。0x66 か 0x60 か 0x06 か、そのうちの何が足されるかがうまくはまる方法があればよいということだが、難しい。
確かに JIS からシフト JIS への変換には不連続な部分があるが、それが上と同じように不連続なところに DAA を対応させられるだろうか。
上の変換では、'CP 0xA0' で 0xA0 かどうかを判定している。ここには部分的に DAA を使えるかもしれない。H と 0xF0 の論理積に DAA をし、そのキャリーを 0x40 の位置にシフトしてくれば、確かに条件分岐が一つ減るだろう。
現代では、アセンブラにおいてはパイプライン処理との絡みなどで、条件分岐のコストが高いこともあり、一般にそれを減らすメリットはある。
しかし、条件分岐を減らす方法というのは DAA に限らない。ループがあるわけではないので、if 文を和積の式に直すのは簡単だ。
if (a > 0) { b = x; } else { b = y; } |
このような C 言語のルーチンは、b = (a > 0) * x + (a <= 0) * y といった式に直せる。このあたりは、Lisp や関数型言語とか、電子回路のカルノー図とかでも使う知識である。
アセンブラは掛け算が使えなかったり、コストが高かったりするので工夫が必要だが、今回のものを条件分岐を使わずに書けば、次のようにも書ける。
;; HL に JIS コードを入れ、HL に SJIS コードを返す。 ;; 36 bytes。 LD A, H SUB 0x21 SCF RRA LD H, A ;; L の処理。LD A,0 と RLA と DEC A で ;; キャリーフラグを A に 0 か 0xFF として保存。 LD A, 0 CCF RLA DEC A AND 0x5E ADD A, 0x1F ADD A, L CP 0x7F CCF ADC A, 0 LD L, A ;; H の後処理。 INC H LD A, H CP 0xA0 LD A, 0 CCF RRA RRCA ADD A, H LD H, A STOP |
ここに DAA を使うとすれば、H の後処理だけ換えて次のようにもできる。
;; HL に JIS コードを入れ、HL に SJIS コードを返す。 ;; 36 bytes。 LD A, H SUB 0x21 SCF RRA LD H, A ;; L の処理。 LD A, 0 CCF RLA DEC A AND 0x5E ADD A, 0x1F ADD A, L CP 0x7F CCF ADC A, 0 LD L, A ;; H の後処理。 INC H LD A, H AND 0xF0 DAA RRA RRCA AND 0x40 ADD A, H LD H, A STOP |
しかし、バイト数で特に得をしているわけでなく、見透しが悪くなっている。もしかすると、実行速度の違いがあるかもしれないが、大きい違いがあるほどには見えない。
■ |
もっとスマートなコード
|
;; HL に JIS コードを入れ、HL に SJIS コードを返す。 ;; 21 bytes。 LD BC, 0x217E ADD HL, BC LD A, H XOR 0x40 SCF RRA LD H, A JR C, DONE LD A, L SUB 0xDE SBC A, 0x80 LD L, A DONE: STOP |
0x40 は XOR でいいのぉ!?というのが正直な感想である。確かに動いている。あえて、ここから条件分岐を抜くなら、私の拙いコードがビューティフルなコードに混じって申し訳ない気がするが以下のようになる。
;; HL に JIS コードを入れ、HL に SJIS コードを返す。 ;; 27 bytes。 LD A, H ADD A, 0x21 XOR 0x40 SCF RRA LD H, A LD A, 0 CCF RLA DEC A AND 0x5E ADD A, 0x1F ADD A, L CP 0x7F CCF ADC A, 0 LD L, A STOP |
■ |
テストコード
|
実験は Perl で行った。
エミュレータのインストールは perl のモジュールを足す標準的な手法で良い。 `perl -MCPAN -e shell' をして、'install CPU::Z80::Assembler' と 'install CPU::Emulator::Z80' をすれば使えるようになる。
実験したままのコードは、えてして汚いものだが、アセンブラに関して素人な私には、恥知らずぐらいしか取り柄がないだろうから、以下のリンクに公開してしまおう。もちろん、パブリックドメインのつもりである。
使ったソフトのバージョンは次の通り。
|
■ |
結論
|
DAA は JIS から SJIS への変換には使えない。使う意味がない。釈然としないが、そういうことになる。
久々にアセンブラを使うのはパズルのようで楽しかったが、思うような結果は出なかった。
シフトについても、4 ビット境界とローテートをうまく使ったプログラムを逆アセンブル時か何かで見たことがあるよな気がするのだが、あれば別のプログラムだったのか。
私のような者が実験した限りでは何も言えない。そこに他者のチャレンジを期待したい。
■ |
参考
|
すでに挙げたものも再録する。
|
更新: | 2010-05-25 |
初公開: | 2010年05月25日 20:27:53 |
最新版: | 2010年05月25日 20:27:53 |
2010-05-25 20:27:52 (JST) in ハード | 固定リンク | コメント (0) | トラックバック (0)
トラックバック
他サイトなどからこの記事に自薦された関連記事(トラックバック)はまだありません。
» JRF のソフトウェア Tips:アセンブラで JIS から SJIS に変換するとき DAA が使える? (この記事)
コメント