+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 2012年 ==================== * Thu Jan 19 01:01:11 JST 2012 Naoyuki Sawa - pceFontPrintf()のバグ P/ECE APIのpceFontPrintf()には、バグがあります。 長い文字列を表示しようとすると、ハングアップすることがある、というバグです。 具体的には、「引数を展開中に35文字の境界を超えると、ハングアップする」というものです。 以下に、再現方法と、原因と、回避方法を説明します。 ■再現方法 □引数36文字⇒ハングアップ 以下のコードを実行すると、ハングアップします。 pceFontPrintf("%s", "012345678901234567890123456789012345"); □書式36文字⇒大丈夫 以下のコードを実行しても、ハングアップしません。 pceFontPrintf("012345678901234567890123456789012345"); □書式30文字の末尾に引数6文字を展開⇒ハングアップ 以下のコードを実行すると、ハングアップします。 pceFontPrintf("012345678901234567890123456789%s", "012345"); □書式30文字の先頭に引数6文字を展開⇒大丈夫 以下のコードを実行しても、ハングアップしません。 pceFontPrintf("%s012345678901234567890123456789", "012345"); ■原因 バグの原因は、P/ECEカーネルソース内のマクロ定義が、一箇所誤っているためです。 C:\usr\PIECE\sysdev\pcekn\doprnt.c 88行目 #define PUTC(ch) *pob->outp++ = (ch); if (pob->outp >= pob->endp) pob->flush(pob); PUTC()は、pceFontPrintf()に指定した書式と引数を、一時的なバッファに展開する際、一文字毎にバッファの終端に到達していないかチェックし、 バッファの終端に到達していたら、一旦そこまでを表示して、展開先を再びバッファの先頭に戻すために利用されているマクロです。 以下のコードは、書式の中の文字をそのまま展開しているところです。 これは、問題ありません。 C:\usr\PIECE\sysdev\pcekn\doprnt.c 122行目〜 while ( (ch = *fmt) && ch != '%' ) { PUTC( ch ); fmt++; cnt++; } 以下のコードは、書式に従って、引数を展開しているところです。(他にも何箇所かあります) ここに、問題があります。 C:\usr\PIECE\sysdev\pcekn\doprnt.c 402行目〜 /* the string or number proper */ n = size; while (--n >= 0) PUTC(*t++); 本来、上記のコードは、以下のようにコンパイルされなければなりません。 /* the string or number proper */ n = size; while (--n >= 0) { *pob->outp++ = *t++; if (pob->outp >= pob->endp) pob->flush(pob); } ところが、PUTC()の定義が { } で囲むのを忘れているため、上記のコードは以下のようにコンパイルされてしまいます。 /* the string or number proper */ n = size; while (--n >= 0) { *pob->outp++ = *t++; } if (pob->outp >= pob->endp) pob->flush(pob); 一文字展開する度にバッファの終端に到達しているかをチェックするのでなく、全部展開してからバッファの終端に到達しているかをチェックしてしまいます。 展開の途中でバッファの終端を超えた場合、メモリ破壊が発生してしまうというわけです。 □原因:補足 「引数を展開中に35文字の境界を超えると、ハングアップする」というバグ挙動の、なぜ"35文字"なのかを補足説明します。 C:\usr\PIECE\sysdev\pcekn\doprnt.c 834行目〜 int pceFontPrintf( const char *fmt, ... ) { OUTBF b; char buff[31+1]; int c; b.topp = b.outp = buff; b.endp = buff+sizeof(buff)-1; b.flush = OutFont; c = doprnt( &b, fmt, (va_list)((&fmt)+1) ); OutFont( &b ); return c; } 上記コードの"buff[31+1]"が、展開バッファです。 前述のとおり、引数の展開中に一文字毎にバッファの終端に到達(=31文字を超えた)していないかチェックする処理にバグがあるため、 引数の展開中にバッファの31文字目を超えて32文字目に到達すると、ヌル文字を含めて33文字以上になり、メモリ破壊が発生します。 ところがたまたま、"buff[31+1]"の直後には、変数"c"が4バイト分配置されています。 doprnt()の中で、"buff[31+1]"を突き抜けて変数"c"のメモリを破壊しても、doprnt()が処理を返すまで変数"c"は使用しないので影響がない、というわけです。 35文字を超えると、変数"c"の後ろに格納されているリターンアドレスを破壊してしまい、pceFontPrintf()からのリターン時にハングアップします。 ■回避方法1:簡単な回避方法 簡単な回避方法は、pceFontPrintf()で35文字を超える文字列を表示しないように注意する、という方法です。 厳密には、「引数を展開中に35文字の境界を超えなければ」良いのですが、この条件を事前にチェックするのは難しいです。 最終的な出力が35文字以下になるように注意して使うのが、安全だと思います。 ■回避方法2:確実な回避方法 確実な回避方法は、バグを修正したpceFontPrintf()を、アプリケーションに含めてしまう方法です。 このファイル(http://www.piece-me.org/piece-lab/pceFontPrintf/doprnt.c)を、アプリケーションに含めてコンパイルすればokです。 このファイルは、C:\usr\PIECE\sysdev\pcekn\doprnt.c をコピーして、少しだけ変更したものです。 変更点は、以下の二点です。 ・一点目:88行目 #define PUTC(ch) { *pob->outp++ = (ch); if (pob->outp >= pob->endp) pob->flush(pob); } //{{修正}} 上で説明した、PUTC()のバグを修正しました。この変更は必須です。 ・二点目:787行目〜 //int pcevsprintf( char *outp, const char *fmt0, va_list argp ) //{{削除}} //{ //{{削除}} // OUTBF b; //{{削除}} // b.outp = outp; //{{削除}} // b.endp = (char *)-1; //{{削除}} // return doprnt( &b, fmt0, argp ); //{{削除}} //} //{{削除}} //int pcesprintf( char *outp, const char *fmt, ... ) //{{削除}} //{ //{{削除}} // OUTBF b; //{{削除}} // b.outp = outp; //{{削除}} // b.endp = (char *)-1; //{{削除}} // return doprnt( &b, fmt, (va_list)((&fmt)+1) ); //{{削除}} //} //{{削除}} pcevsprintf()とpcesprintf()を削除しました。この変更は必須ではありませんが、メモリ節約のために削除しました。 pcevsprintf()とpcesprintf()は、P/ECEカーネル内のpcevsprintf()とpcesprintf()を、そのまま使っても大丈夫です。 pcevsprintf()とpcesprintf()は元々、バッファの終端をチェックしていないので、PUTC()のバグがあっても動作結果に違いは無いからです。 回避方法2の欠点は、アプリケーションのコードサイズが増えてしまうことで、3キロバイト近く増えてしまいます。 PUTC()の一行を修正するためだけに、P/ECEカーネル内のpceFontPrintf()を捨てて、アプリケーション内にpceFontPrintf()を抱えてしまうからです。 回避方法2の方が確実で安心ではあるのですが、pceFontPrintf()の使用箇所が少なく充分注意できる場合は、回避方法1の方が良いかも知れません。 □回避方法2:補足 P/ECE APIのバグを修正する場合、厳密には、修正した関数をアプリケーションにリンクするだけでなく、カーネルサービスベクタを登録しなければいけません。 カーネルサービスベクタを登録するには、以下のように行います。 PCEKSENT old_pceFontPrintf; void pceAppInit() { … //カーネルサービスベクタに、修正したpceFontPrintf()を登録する old_pceFontPrintf = pceVectorSetKs(29/*KSNO_FontPrintf*/, fix_pceFontPrintf); … } void pceAppExit() { … //カーネルサービスベクタに、元のpceFontPrintf()を戻す pceVectorSetKs(29/*KSNO_FontPrintf*/, old_pceFontPrintf); … } こうすることによって、アプリケーションだけでなく、カーネル内でAPIを使用している場合も、修正した関数が利用されるようになります─── ───本来はそうであるべきなのですが、残念ながら、pceFontPrintf()については、そのようになりません。 カーネル内からのpceFontPrintf()の呼び出しが、カーネルサービスベクタ経由でなく、直接呼び出しになっているからです。 そんなわけで、カーネル内からのpceFontPrintf()呼び出しは、どうしても、バグ有りのpceFontPrintf()が呼び出されてしまいます。 さいわい、カーネル内でpceFontPrintf()を使っている箇所は、システムメニューやエラー表示など、短い文字列の表示用途ばかりです。 これらの用途で、35文字を超える表示になることは無さそうなので、ひとまず安心です。 ■最後に 今回説明したバグは、P/ECE本体付属の一番古いカーネル(Ver 1.00)から有ったもので、現時点で最新のカーネル(Ver 1.20)でも修正されていませんでした。 pceFontPrintf()に、長い書式文字列を指定することはあっても、長い引数文字列を指定することはあまりないため、顕在化しなかったのかも知れませんね。 今回使用したサンプルプログラム一式は、こちら: http://www.piece-me.org/archive/pceFontPrintf_BugReport-20120119.zip