この文書は、ShiftJISで書いたC/C++のソースコードを-gオプションを付けてgccでコンパイルして出来たバイナリプログラムをgdbに読み込ませてデバッグする際にリスト表示が文字化けする現象を直す方法を書きます。
不具合の原因を探るためにgdbのソースコードを読みました。
gdbのソースは巨大で非常に複雑です。ビルドにも物凄く時間が掛かります。configureですら大変な時間がかかります。
マクロスイッチ次第では、printf すら #define で別のものに変更される場合があり、そのマクロスイッチの起源となる autoconf (configure) の複雑さも絡んで、不具合の原因を探るにはコードを読むだけでは無理が有り、現実的には実際にコードをいじって「printf デバッグ」するしかないと思いますね。
gdbの文字化けという大変重要な問題、何十年間(?)も放置されてきたことが何よりの証拠です。
何かというと
「ShiftJISは古い、utf8にすれば解決する」
という声が大きいですが、そうは思いません。
utf8はサイズが大きいので無駄ですから。
最近エコやSDGsといいますが、UTF8にするのはエコに反すると思います。
個人的には、UTF8は、所詮、アメリカ人などが日本人が困ることを無視して無理やり広めた問題コードだと思っています。
普通、文字化けしたらプログラムに問題があると思いますよね?
今回のケースでは実はそうではなく、printf() 自体が文字化けを起こしていたのでした。え、cmd.exe がSJISモードになってなかっただけじゃないかって? これがそうでもないのです。ちゃんと、cmd.exe の chcp は、932 の SJIS モードだったのでした。? printf() に渡す文字列が SJIS になってなかったのじゃないかって? それも違います。ちゃんと SJIS になっていたのです。
実際の gdb.exe のソースの中で、printf() 関数に渡す直前の文字列のバイナリコードを16進数でバイナリダンプする関数を作って、調査し、5分くらい色々と調べましたがちゃんと SJISコードのままでした。
つまり、printf()に渡していたのも正真正銘の正しい SJIS コードで、それを受け取る cmd.exe も完全に SJIS モードで動いているにもかかわらず、文字化けしていたのでした。
(これが数十年間もgdbの文字化けが直らなかった原因だったんでしょうね。)
これはプログラマの制御の限界を超えました!! printf が文字化けしたのでは、手も足も出ませんね。はい。---> そうでもないわけでしたが。
こんな風になっていれば、普通、printf が、#define されていたり、gdb のソース内部で同名で関数定義されていたり、または、printf() が内部的に呼び出しているであろう vfprintf() が同名で「ローカル関数定義」されているのではないかと考えますよね。--> ちなみに、Linuxやcygwin(などのUnix的環境)では「ローカルに定義した同名シンボルがあると、リンク段階で二重定義にならずにそっちが使われてしまうことがある法則」があります。
でも、そうでもないのでした。それも Unix 系の *.o を objdump して、printf などのシンボルが undef になってるかどうかなども調べましたが、特に問題有りませんでした。
ややこしいことに、gdb のソースコードでは、 #if の条件次第では、printf が不可思議な何かに #define されて、C++ の namespace を使ってなにやら複雑な処理すらされる場合があるようになっていたのでした。
しかし、それも、#error 的なものでその #if ブロックに来ているかどうか調べましたが、来てませんでした。しかし、実験的には大丈夫そうでも、現実に不可思議現象は起きており、理解不能な混乱状態に陥りました。
とても困りました。
というわけで、main() 関数冒頭から、printf("SJISコード\n");
というコードを入れまくって、どこかで文字化けが始まるのではないかと追跡したところ、その通りの結果となったのでした。
といいますか、文字化けしている関数では、SJISの文字列リテラル("文字列定数")を書いた printf が異常を来たしているのに、main() 関数冒頭では、同じprintf文が全く文字化けしてなかったのが決定的な証拠となりました。
これはさすがに調べなくては寝られない状態になり、調べましたよ、そりゃ、もう。--> 実際は早寝早起きなので、朝の8時くらいから調べ始めて午後二時くらいには原因が分かったと思いますが。
Androidでマシン語用のデバッガーのgdbで、SJISで書かれたソースコードを (gdb) list(l) コマンドで表示する際に文字化けする原因が分かった。結論から言えば、環境変数 LC_ALLを定義してC にするだけで直る。gdbのソースコードを読んだところ、ユーザーのソースコードに関する限り、文字コード変換は全く行ってないように見えた。つまり、gdb.exeは、ソースコードを無加工で内部バッファに読み取り、それを無加工で printf() などで表示している。gdbのソースコードをいじくって、実験してみた。起動直後のmain()関数冒頭で、SJISコードでprintf("こんにちは。\n");とするとちゃんとcmd.exe端末に、日本語がそのまま文字化けせずに表示される。ところが、しばらくすると、ある関数内(main.c の captured_main_1() 関数)で setlocale(LC_CTYPE,""); が実行され、その直後からは、同じコードを実行しても、文字化けが始まる。これは、setlocale()の前後に上記の様なprintf()を書いてみて実験して確かめた。setlocale()の前のprintf()では文字化けしないのに、後のprintf()では文字化けした。printf()自体が文字化けしてしまうので、gdb自身のコードによる論理的な文字変換は全く関係なかった。
setlocale()の仕様を調査したところ、glibcでは、このような引数を渡した場合、環境変数 LC_ALL, LC_C, LANG などを読み取って、その設定で以後の文字列処理系の関数の動作を変える。
これらの環境変数が全く設定されてないと、少なくともSJISの文字コードを printf() に渡すと、勝手に変換して、cmd.exe には、(SJISではない)「へんてこなバイト列」に変えてしまった文字コード列が渡されてくる。だから、文字化けする。なお、cmd.exe は、日本語のWindowsの場合、デフォルトでは、chcp 932 (CP932) で SJISである。
- おさらい
androidでは、NDK(など)を使ってC/C++などを(Armの)マシン語に直したプログラムを動かすことが出来る。マシン語(バイナリコード)は *.so の shared libraryの形式に収められており、それを Java の MainActivity の最初の方でロードして利用する。
Javaとマシン語の行き来や変数のやり取りは、JNI(Java Native Interface) という仕組みを使って行う。
デバッグするにはパソコン側で gdb でユーザーとのやり取り(CUI)を担当し、Android側に gdbserverを常駐させて、USBケーブルで通信し合って行う。
setlocale()の第二引数は locale と呼ばれる。
locale を "C" とすると、仕様的には文字を1バイトだと見なすようになるとされているが、実質的には、printf などでは、文字列から渡されたバイト列を何も加工せず、そのまま stdout などに渡すようになる。この場合、単独でN バイトの1文字でも N 個の1バイト文字の連続だと見なして何もせずにそのまま stdout などに渡してくれるので、プログラマ目線では分かり易い。
だから、自分のプログラムコードで文字変換の責任を持つ場合、locale は、"C" にするのが楽である。他の設定にすると、不可解な現象に悩まされるだろう。個人的見解では、C言語では文字コードの変換は全て自分のコードか自分が仕様を深く理解しているライブラリが責任を持つようにして、setlocale() などには頼らないほうが良いようだ。setlocale() は、環境依存の仕様が含まれており、どうなるかは不明な点があるから。
該当箇所:
static void
captured_main_1 (struct captured_main_args *context)
{
・・・
printf( "こんにちは\n" ); // (1) SJISコード、文字化けせず、正常。
#if defined (HAVE_SETLOCALE) && defined (HAVE_LC_MESSAGES)
setlocale (LC_MESSAGES, ""); // (2)
#endif
#if defined (HAVE_SETLOCALE)
setlocale (LC_CTYPE, ""); // (3)
#endif
printf( "こんにちは\n" ); // (4) SJISコード、文字化け発生!!。
・・・
}
実際にgdbのソースを変更して「printf デバッグ」を行ってみたところ、今回の confgiure 設定では、(3)は実行されるが (2) は実行されなかった。なので、(2)は無視して (3) だけに着目すると良い。
ネットでは文字コードに関して色々なことを書く人がおり、有名な会社でも現実と合わない(間違った)ことを書いていることがあるようだから大変である。とにかく、上記の場合、環境変数のLC_ALL=Cとすると、文字コードに関してプログラムコードが完全に制御を持つことが出来るようになる。
今回の話とは関係の無い一般論として、setlocale() の動作は非常に複雑なので、LC_ALL に、"ja_JP.SJIS" などと指定することは決してお勧めしない。
また、今回の実験では、LC_ALL, LC_CTYPE, LANG などを全て完全に未定義にすると文字化けした。
なので、これらどれかの内の少なくとも一つを必ず C に設定することをお勧めする、というより、LC_ALL を C にするのが恐らく一番安全。念のため書いておくと、
cmd.exe だと
$ set LC_ALL=C
とする。
なお、gdb は、版によっては、
(gdb) set charset CP932
(gdb) set target-charset CP932
も設定できる場合があるが、それが設定できる場合でも、ソースコードを list コマンドで表示する際に文字化けする現象には効果が無い。これは、gdb を自分でソースからビルドして上記のset charset系コマンドが認識できるようにして実験した体験から言っている。
なお、GoogleのAndroid SDKのNDKに付属している gdb では、なぜか、上記の set charset系コマンドで、CP932 が設定出来ないようになっている。