種別[software] cocolog:63479911
セクションJRF のソフトウェア Tips
日時2010年05月25日 20:27:52
元URLhttp://jrf.cocolog-nifty.com/software/2010/05/post-3.html
タグ[ハード]

アセンブラで 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 に並べ直す。

言葉で書くよりプログラム的な記述を見たほうがわかりやすいかもしれない。 《JIS to SJIS transform》を引用する。

  >
漢字コードの第一バイト(上位バイト)をHIGH
                第二バイト(下位バイト)をLOW
とする。
HIGHが奇数なら      LOW=LOW+0x1F
それ以外なら        LOW=LOW+0x7D

LOWが0x7F以上なら   LOW=LOW+1

(無条件)           HIGH=(HIGH-0x21)/2 + 0x81

HIGHが0xA0以上なら  HIGH=HIGH+0x40

  <
  
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進補正と文字コードということに関して、《daa命令の技 : メカAG》という記事を見つけた。

  >
adda #$90
daa
adca #$40
daa

    この4行のプログラムが何をやっているかというと、16進数への変換。aレジスタに入っている0x00〜0x0fの値を'0'〜'9'ないし'A'〜'F'に変換する。 '9'は文字コードで0x39、'A'は文字コードで0x41だから、不連続部分があり、0x0a以上と以下で場合分けするのが普通だ。
    
    (…)
    
    daa命令がポイントで、これはもともとbcd演算(2進化10進数)のための補正命令。命令自体はこんな動作をする。
    
      <b>下位4bitの処理</b>0x0a以上か、ハーフキャリーフラグが立っていれば、0x06を加える。
      <b>上位4bitの処理</b>0xa0以上か、キャリーフラグが立っていれば、0x60を加える。
        
    キャリーフラグとハーフキャリーフラグは今回の処理では関係ないので考えなくていい。おおざっぱにいえば上位と下位のそれぞれ4bitの値が10以上なら桁上がりさせる。0x0aなら0x10に、0xa0なら0x100にする。4bitずつ見た時に10進数として10や100と読めるようにするわけだ(これが2進化 10進数)。
    
    (…)
    
    Cで書けば
    
if (a < 10) {
      a += '0';
} else {
      a += 'A' - 10;
}

    (…)冒頭のアセンブリプログラムは条件分岐なしにそれを実現している。
  <
  
以上には、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
    
しかし、バイト数で特に得をしているわけでなく、見透しが悪くなっている。もしかすると、実行速度の違いがあるかもしれないが、大きい違いがあるほどには見えない。

■もっとスマートなコード
  
巷にはもっとスマートなプログラムが公開されている。《JIS-SJIS conversion》には 8086 系のコードが載っている。それを Z80 系に直したものが以下である。

;; 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' をすれば使えるようになる。

実験したままのコードは、えてして汚いものだが、アセンブラに関して素人な私には、恥知らずぐらいしか取り柄がないだろうから、以下のリンクに公開してしまおう。もちろん、パブリックドメインのつもりである。

  ●z80jis2sjis-20100525.pl
    
使ったソフトのバージョンは次の通り。

  ●CPU-Z80-Assembler-2.09.tar.gz
  ●CPU-Emulator-Z80-1.0.tar.gz
  ●Perl 5.10.1
  ●Cygwin 1.7.5
  ●Windows XP SP3
    
■結論
  
DAA は JIS から SJIS への変換には使えない。使う意味がない。釈然としないが、そういうことになる。

久々にアセンブラを使うのはパズルのようで楽しかったが、思うような結果は出なかった。

私が聞いたのは、単に、《daa命令の技 : メカAG》にあった DAA を文字コードへの変換に使えるということでしかなかったのだろうか。

シフトについても、4 ビット境界とローテートをうまく使ったプログラムを逆アセンブル時か何かで見たことがあるよな気がするのだが、あれば別のプログラムだったのか。

私のような者が実験した限りでは何も言えない。そこに他者のチャレンジを期待したい。

■参考
  
すでに挙げたものも再録する。

  ●《Z80 命令表》。ここで命令を一つ一つ確かめながら作った。
  ●《JIS to SJIS transform》。プログラム的記述。
  ●《daa命令の技 : メカAG》。
  ●《JIS X 0213 (JIS2004) の代表的な符号化方式》。
  ●《JIS-SJIS conversion》。ビューティフル。
  ●《今までに見たソースコードで一番感動したのは - varusu.com》。逆変換の SJIStoJIS のビューティフルコードが載っている。
    
更新:2010-05-25
初公開:2010年05月25日 20:27:53
最新版:2010年05月25日 20:27:53

後方参照 (3 件)