+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 2016年 ==================== * Tue Oct 25 21:18:48 JST 2016 Naoyuki Sawa - sin()のバグ ■問題点 P/ECE開発環境に付属している、EPSON製Cライブラリの、「sin()関数」には、バグがあります。 『オーバーフローする演算が直前に在ると、sin()関数の結果が不正になる。』というバグです。 【バグを再現するプログラム】 │ #include │ #include │ #include │ #define NOPCESPRINTF │ #include │ unsigned char vbuff[128*88]; │ int errno, write; /* dummy */ │ void test(); │ void pceAppInit() { │ pceAppSetProcPeriod(255); │ pceLCDSetBuffer(vbuff); │ pceLCDDispStart(); │ pceFontSetPos(0, 0); │ test(); /* テストルーチンを呼び出す */ │ pceLCDTrans(); │ } │ void pceAppProc(int cnt) { /** no job **/ } │ void pceAppExit() { /** no job **/ } │ //↑↑↑↑↑ここまでは本題と関係の無い定型処理です。↑↑↑↑↑ │ //↓↓↓↓↓↓ここからが本題のテストルーチンです。↓↓↓↓↓↓ │ volatile int x = (1<<30); │ void test() { │ char buf[100]; │ double s; │ x += x; /* オーバーフローする演算が直前に在ると… */ │ s = sin(1.57); /* sin()関数の結果が不正↓になる場合が有る。 */ │ sprintf(buf, "%f", s); /* sin(π/2)=1のはずなのに「-1」と表示される。 */ │ pceFontPutStr(buf); │ } 上記のプログラムを実行すると、sin(π/2)=1のはずなのに、「-1」と表示されます。 ■原因 原因は、EPSON製Cライブラリの「sin()関数」の、バグでした。 バグの箇所は、以下の通りです。右側のコメントを参照して下さい。 【EPSONライブラリの「sin.s」の先頭部分を抜粋】 │ ;============================================================================== │ ; Function: double sin(double x) │ ; │ ; Input: %r12: Argument x in radian double (low). │ ; %r13: Argument x in radian double (high). │ ; │ ; Output: %r10: Return value, sine of x in double (low). │ ; %r11: Return value, sine of x in double (high). │ ; │ .code │ .align 1 │ .global sin │ │ sin: ┬ここから開始して… │ pushn %r3 ; Save registers %r3...%r0. │ │ xsub %sp,%sp,20 ; Presearve working area in five words. │ │ │ │ ;[%sp+16] = Taylor series's sign parameter (high). │ │ ;[%sp+12] = Taylor series's sign parameter (low). │ │ ;[%sp+08] = Quadrant of trigonometric function (word). │ │ ;[%sp+07] = 2nd argument of modf, pointer to its fraction part (high). │ │ ;[%sp+00] = 2nd argument of modf, pointer to its fraction part (low). │ │ │ │ ld.w %r2,%r12 ; Low word of argument x. │ │ ld.w %r3,%r13 ; High word of argument x. │ │ │ │ ;##### REMOVED 12/08/1999 ############# │ │ ;# ; │ │ ;# ; Clear the sign bit of argument x in %r3:%r2 by shifting │ │ ;# ; to make it a plus value. │ │ ;# ; │ │ ;# sll %r3,1 ; Shift the argument x to the left one bit. │ │ ;# srl %r3,1 ; Shift the argument x to the right one bit. │ │ ;##### REMOVE END ##################### │ │ │ │ xld.w %r4,0x0 ; Initialize the quadrant to 0. │ │ xld.w [%sp+8],%r4 ; Initialize the quadrant to 0. │…ここまでの間に、Vフラグを変化させる命令が存在しません。 │ ↓従ってこの時点で、Vフラグは関数が呼び出された時のままで、要するに「不定」です。 │ sra %r13,1 ; Check if x is minus? ←sra命令は、ZとNフラグを変化させますが、Vフラグは変化させません。従ってこの時点でも、Vフラグは「不定」のままです。 │ xjrge _ArgIsNotMinus ; Branch if plus or zero. ←xjrge命令の分岐条件は「!(N^V)」です。『不定なVフラグを参照して条件分岐を行っている』というバグです。 │ ; │ ; --- if the argument x is minus --- │ ; Clear the sign bit of argument x in %r3:%r2 by shifting │ ; to make it a plus value. │ ; │ sll %r3,1 ; Shift the argument x to the left one bit. │ srl %r3,1 ; Shift the argument x to the right one bit. │ │ xld.w %r4,0x2 ; Set the quadrant 2. │ xld.w [%sp+8],%r4 ; Set the quadrant 2. │ ; │ ; Mutiply the argument x by the constant 2*Pai │ ; │ _ArgIsNotMinus: │ ld.w %r12,%r2 ; Argument x (low). │ ld.w %r13,%r3 ; Argument x (high). │ xld.w %r14,TWO_PAI_L ; Multiplier, constant 2*Pai (low). │ xld.w %r15,TWO_PAI_H ; Multiplier, constant 2*Pai (high). │ ; │ xcall __muldf3 ; Calculate multiplication; argument x = x * (2 * Pai). │ ; The result product is %r11:%r10. │ ld.w %r2,%r10 ; Put result x in %r2 (low). │ ld.w %r3,%r11 ; Put result x in %r3 (high). │ │ (以下略) %r13レジスタの正負を判定するのに、 ×誤 │ sra %r13,1 ; %r13 >>= 1, %psr(Z) = !%r13, %psr(V) = %r13[31] │ xjrge _ArgIsNotMinus ; if(!(%psr(N)^%psr(V))) { goto _ArgIsNotMinus } としているのが間違いです。正しくは、 〇正 │ cmp %r13,0 ; %psr(C) = 0, %psr(V) = 0, %psr(Z) = !%r13, %psr(N) = %r13[31] │ xjrge _ArgIsNotMinus ; if(!(%psr(N)^%psr(V))) { goto _ArgIsNotMinus } とするか、又は、 〇正 │ add %r13,%r13 ; %psr(C) = %r13[31], %psr(V) = ?, %psr(Z) = ?, %psr(N) = ?, %r13 <<= 1 │ xjruge _ArgIsNotMinus ; if(!%psr(C)) { goto _ArgIsNotMinus } とするか、又は、 〇正 │ scan1 %r13,%r13 ; %psr(C) = ?, %psr(V) = 0, %psr(Z) = %r13[31], %psr(N) = 0 │ xjrne _ArgIsNotMinus ; if(!%psr(Z)) { goto _ArgIsNotMinus } とすべきです。 どれを使っても効率は同なので、素直に「cmp %r13,0」を使うのが自然だと思います。 元ソースが、何故、「sra」を使って符号比較をしようとしていたのかが、謎です。(シフトした値をその後で使ってもいませんし…) ■対策 二種類の対策方法があります。 □対策� ,箸蠅△┐魂麋鬚垢訛从�方法 「sin()関数」をあまり頻繁に使わない場合は、その都度対策するのが手っ取り早くて良いと思います。 「sin(x) == cos(x-π/2)」ですから、アプリケーションプログラムのソースの中で、「sin()関数」を以下のように定義して下さい。 アプリケーションプログラムのソースの中で「sin()関数」を定義しておけば、EPSONライブラリの「sin()関数」はリンクされません。 【アプリケーションプログラムの中で定義する】 │ double sin(double x) { return cos(x - 1.5707963267948966192313216916398); } □対策�◆〆�本的に修正する対策方法 「sin()関数」を頻繁に使う場合は、その都度上記の対策を行うのは面倒だし、見落とすおそれがあるので、 EPSON製Cライブラリの「sin()関数」を修正して、置き換える方が安全でしょう。 修正した「sin.s」を、アプリケーションと一緒にビルドすれば、 「\usr\PIECE\lib\math.lib」の中にある元の「sin()関数」よりも優先されて、修正した「sin()関数」がリンクされます。 修正した「sin.s」はこちら: http://www.piece-me.org/piece-lab/sinbug/sin.s ■ダウンロード バグ再現プログラムと、対策プログラムのサンプルは、こちらです。 ダウンロードはこちら: http://www.piece-me.org/piece-lab/sinbug/sinbug-20161025-src.zip * Sat Aug 06 00:37:42 JST 2016 Naoyuki Sawa - intをlong longに符号拡張する、色々な方法 アセンブラプログラムで、int(32ビット整数)とlong long(64ビット整数)を混在して計算する場合、intをlong longに符号拡張する必要が有ります。 PCのx86 CPUだと、intをlong longに符号拡張する専用のCDQ命令が有って、あまり工夫の余地は有りません。 P/ECEのS1C33 CPUには、intをlong longに符号拡張する専用の命令は無いので、色々な方法が考えられて面白いです。 今回は、S1C33で、intをlong longに符号拡張する、色々な方法を紹介します。 例として、%r12レジスタのint値の最上位ビットを、%r13レジスタの全ビットに符号拡張して、%r13:%r12レジスタのlong long値とするケースを考えます。 �〜把召癖�法ver.1 │ ld.w %r13, %r12 │ xsra %r13, 31 一見、良さそうなのですが、実は、コードサイズが大きくて遅い、という問題が有ります。 S1C33は一度に8ビットしかシフト出来ないため、上記のアセンブラプログラムは、実際には、以下の命令に展開されるからです。 │ ld.w %r13, %r12 ;//2バイト、1サイクル │ sra %r13, 8 ;//2バイト、1サイクル │ sra %r13, 8 ;//2バイト、1サイクル │ sra %r13, 8 ;//2バイト、1サイクル │ sra %r13, 7 ;//2バイト、1サイクル コードサイズは10バイトで、実行時間は5サイクルです。 まあ、これでも構わないのですが、もっと小さく、もっと早い方法が有るので、上記の方法は却下です。 �∩把召癖�法ver.2 │ cmp %r12, 0 ;//2バイト、1サイクル │ jrge.d 3 ;//2バイト、1サイクル │ ld.w %r13, 0 ;//2バイト、1サイクル │ ld.w %r13, -1 ;//2バイト、1サイクル 分岐した場合は実行しない コードサイズは8バイトで、実行時間は「%r12≧0」の場合3サイクル,「%r12<0」の場合4サイクルです。 S1C33は条件分岐が早いので、これはかなり良い方法です。 「x = (条件) ? (値A) : (値B)」の処理をアセンブラで書く場合、遅延分岐の直後に値Aのロードを置いて、さらにその直後に値Bのロードを置く、というのが最適化の定番です。 ��キャリーフラグを使った方法 │ ld.w %r13, %r12 ;//2バイト、1サイクル │ add %r13, %r13 ;//2バイト、1サイクル │ ld.w %r13, 0 ;//2バイト、1サイクル │ sbc %r13, %r13 ;//2バイト、1サイクル コードサイズは8バイトで、実行時間は4サイクルです。 �△諒�法よりも、平均0.5サイクル遅いので、敢えてこの方法を使う必要は有りません。(利点が無いのに紹介した理由は、この後�Г如ΑΑ�) 「ld.w %r13, %r12」「add %r13, %r13」で、元の値の最上位ビットを%psr(C)に送り、「ld.w %r13, 0」「sbc %r13, %r13」で、「%r13 = 0 - 0 - %psr(C)」に相当する処理を行っています。 元の値の最上位ビットが0だったら「%r13 = 0 - 0 - 0 = 0」、元の値の最上位ビットが1だったら「%r13 = 0 - 0 - 1 = -1」となります。 �ど箙羈板イ鯣爾μ仁瓩鰺�用する方法ver.1 │ ld.w %r13, 1 ;//2バイト、1サイクル │ mlt.w %r12, %r13 ;//2バイト、5サイクル │ ld.w %r13, %ahr ;//2バイト、1サイクル コードサイズは6バイトで、実行時間は7サイクルです。 コードサイズはここまでの中で最短ですが、実行時間は遅いので、いまいちな方法です。 mlt.w命令は、int値とint値を掛けてlong long値にするので、「%r12×1」を行えば、%r12の最上位ビットが、乗算結果の%ahr:%alrの上位ワード(%ahr)に拡張される仕組みです。 �ド箙羈板イ鯣爾μ仁瓩鰺�用する方法ver.2 │ ld.w %alr, %r12 ;//2バイト、1サイクル │ div0s %r12 ;//2バイト、1サイクル │ ld.w %r13, %ahr ;//2バイト、1サイクル コードサイズは6バイトで、実行時間は3サイクルです。 div0s命令は本来は除算の前準備のための命令なのですが、その時に%alrの最上位ビットが%ahrに符号拡張される事を利用した方法です。 最初は、これが最良の方法かと思っていたのですが、致命的な問題が有る事に気付きました。 %r12が0の場合に、「div0s %r12」がゼロ除算例外を投入してしまう問題です。 この用途では、div0s命令のオペランドは関係無いので、%r12以外を指定しても構わないのですが、確実に0以外であるレジスタは有りません。 〜「ld.w %r13, 1」「div0s %r13」〜なんてしてしまうと、コードサイズと実行時間が増えて本末転倒です。 という訳で、この方法は使えません。 �α把召癖�法ver.1(改善版) │ swap %r13, %r12 ;//2バイト、1サイクル │ ld.b %r13, %r13 ;//2バイト、1サイクル │ sra %r13, 7 ;//2バイト、1サイクル コードサイズは6バイトで、実行時間は3サイクルです。 これが、最良の方法の一つです。 �,能劼戮芯未�P/ECEはシフト命令が弱いのが欠点なのですが、その欠点を回避するために、swap命令とld.b命令を組み合わせてロードと24ビットシフトを一気に行ったのが、この方法です。 swap命令の他にも、mirror命令やnot命令はロードとビット処理を同時に実行出来るので、色々工夫が効いて便利な命令です。 �Д�ャリーフラグを使った方法(改善版) │ ld.w %r13, %r12 ;//2バイト、1サイクル │ add %r13, %r13 ;//2バイト、1サイクル │ sbc %r13, %r13 ;//2バイト、1サイクル コードサイズは6バイトで、実行時間は3サイクルです。 これも、最良の方法の一つです。 ��のプログラムをよく見ると、「ld.w %r13, 0」は不要である事に気付きました。 「%r13 = 0 - 0 - %psr(C)」も、「%r13 = 不定値 - 不定値 - %psr(C)」も、結果は同じだからです。('不定値'は、両方とも同じ値とする) というわけで、��のプログラムから一命令削除したのが、この方法です。 ■まとめ P/ECEのS1C33 CPUで、intをlong longに符号拡張する方法は、上記の�λ瑤廊Г諒�法が最良だと思います。 * Fri May 27 23:10:38 JST 2016 Naoyuki Sawa - 命令エクスタンダ「ext33.exe」のバグ P/ECE開発環境の、命令エクスタンダ「ext33.exe」には、バグが有ります。 文字列の末尾に有る、エスケープされたバックスラッシュを正しく認識出来ない、というバグです。 ■バグを再現する方法 まず、正常なケースを確認します。 下記のプログラムを、"sample.c"という名前で保存して下さい。 │const char str[]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm表現"; 下記のバッチファイルを、"compile.bat"という名前で保存して下さい。 │@REM compile.bat │pcc33.exe -b -c sample.c │@IF ERRORLEVEL 1 (ECHO エラーが発生しました。) ELSE (ECHO 正常終了しました。) コマンドラインで、compile.batを実行して下さい。 正常にコンパイルできて、以下のメッセージが表示されます。 │正常終了しました。 次に、エラーが発生するケースを確認します。 さきほど保存したsample.cを開いて、以下のように変更して、保存して下さい。 変更点は、"表"の前に"n"を一文字追加しただけです。 │const char str[]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn表現"; コマンドラインで、compile.batを実行して下さい。 エラーが発生して、以下のメッセージが表示されます。 │ ←空行です。 │ ←空行です。 │ ←空行です。 │エラーが発生しました。 ■原因調査 文字列に一文字追加しただけで、C言語のプログラムとしては全く正しいのに、なぜ後者ではエラーが発生するのでしょうか。 調査したところ、P/ECE開発環境のext33.exeのバグが原因である事が判りました。 以下、詳細です。 pcc33.exeはコンパイラドライバであり、以下のように複数のツールを呼び出してコンパイルを行っています。 �� gcc33.exe C言語ソースをコンパイルして、アセンブラソース(拡張命令有り)に変換する。(sample.c ⇒ sample.ps) �� ext33.exe アセンブラソース(拡張命令有り)の中の、拡張命令を展開して、アセンブラソース(拡張命令無し)に変換する。(sample.ps ⇒ sample.ms) �� as33.exe アセンブラソース(拡張命令無し)をコンパイルして、オブジェクトファイルに変換する。(sample.ms ⇒ sample.o) �� lk33.exe オブジェクトファイルをリンクする。(sample.o ⇒ sample.srf) ※今回の件には関係無いので、pcc33.exeに「-c」フラグを指定して、この処理は実行しません。 �△瞭�力ファイルであるsample.psを、エラーが発生しないケースと、エラーが発生したケースとで、比較してみました。 □エラーが発生しないケース │str: │ .ascii "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm\225\\\214" │ .ascii "\273\000" □エラーが発生したケース │str: │ .ascii "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn\225\\" │ .ascii "\214\273\000" 本来はどちらも、アセンブラソースとして正しい内容です。 後者は「表」の2バイト目の'\'が、文字列の一番最後になっています。きちんと'\\'でエスケープされているので、本来は正しいのですが、 どうやらext33.exeは、文字列を閉じる'"'を判定する時に、一文字前が'\'かどうかだけを見てエスケープされた'"'かどうかを判定しているようです。 ←←←【要点】これがバグです。 そして、文字列が閉じられていないと判定して、エラー終了してしまうようです。 試しに、上記の「□エラーが発生したケース」のsample.psを少し手で書き換えると、エラーが出なくなりました。 □エラーが発生したケース(変更後) │str: │ .ascii "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn\225" │ .ascii "\\\214\273\000" ←元々上の行の最後に有った'\\'を、次の行の先頭に移した。データの内容は全く同じで、ソース上の違いだけです。 そして、ext33.exeを直接実行してみると… │ext33.exe sample.ps │@IF ERRORLEVEL 1 (ECHO エラーが発生しました。) ELSE (ECHO 正常終了しました。) 結果は「正常終了しました。」になりました。 やはり、文字列の末尾に有る'\\'が原因です。 ちなみに、文字列が""で囲まれているかどうかを判定するのは、ext33.exeの機能の一つなのですが、その機能が正しく動いていないようですね。 □参照資料 │「S5U1C33000C Manual (S1C33 Family Cコンパイラパッケージ) (Ver.4) (P/ECE開発環境の「C:\usr\PIECE\docs\datasheet\EPSON」に入っています) │p.147「10. 命令エクステンダ - 10.8.3 文法チェック」 │>ext33は拡張命令と以下のアセンブラ疑似命令の文法チェックのみを行います。 │>〜 │>.ascii疑似命令 │> 文字列が" "で囲まれているかチェックします。 誤って、正しい文字列をエラーと見なしてしまうだけでなく、エラーメッセージを表示せずに意味不明な空行が三行出力されたり、 エラーコードを返しているにもかかわらず画面上には「Extend Completed」というメッセージを表示してしまうというバグも有ったりするようです。 ■バグの影響 一見、かなり致命的なバグのように見えますが、実際には、このバグが顕在化する事はかなり稀です。 なぜなら、C言語文字列の場合、最後にヌル文字が付くので、アセンブラソースになった時に文字列の最後に'\'が来る事は稀だからです。 │const char str1[]="明示的な\\"; │const char str2[]="暗黙の…表"; //「表」の2バイト目は'\'です。 上記のC言語文字列はどちらも'\'で終わっていますが、アセンブラソース上ではヌル文字が付くので… │str1: │ .ascii "\226\276\216\246\223I\202\310\\\000" │str2: │ .ascii "\210\303\226\331\202\314\201c\225\\\000" こんなふうに、最後が'\'にはなりません。 では、どんな時に問題が顕在化するかというと、折り返した位置が偶然'\'の直後で折り返された場合でです。 gcc33.exeは、長い文字列から.ascii疑似命令を生成する時に、ある程度の長さで折り返して複数行に分割するようです。 その時たまたま、折り返した位置が'\'の直後だと、.ascii疑似命令の文字列の最後が'\'になって、前述のext33.exeのバグに引っかかってしまうのです。 │const char str[]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn表現"; は、まさにその一例でした。 ちなみに、もっと単純にバグを再現しようと思えば、たとえばこんなC言語文字列でも再現します。 │const char str[]="\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"; コンパイルすると… │str: │ .ascii "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" ←ext33.exeが誤ってこの行をエラーと見なしてしまいます。 │ .ascii "\\\\\\\\\\\\\\\000" ■バグを回避する方法 簡単にバグを回避する方法は、思い付きませんでした。 ext33.exeの前段に、.psファイルをチェックして.ascii疑似命令の文字列の末尾に'\'が無いか調べるようなフィルタを作ろうかな、と思っています。 でも実際には、そこまでしなくても良いと思います。 これまで10年以上P/ECEで遊んでいて、今にして思えばこれが原因だったのかなあと思うコンパイルエラーに遭遇した事が、せいぜい1〜2回だけだからです。 実際にこのext33.exeバグが問題になる事は、ごく稀だと思います。 とりあえずこういうバグが有る事を覚えておいて、P/ECEのプログラムをビルドした時に謎の空行が表示されてコンパイルエラーになった場合は、この点を疑ってみるのが良さそうです。 そして、文字列を変更する事が可能ならば、文字列を少し変更して、.ascii疑似命令の文字列の末尾に'\'が来ないようにして回避すれば良いと思います。 ちなみに、文字列を変更できない場合は、文字列を配列で定義して回避するという手も有ります。 C言語で配列として定義すれば、アセンブラソースとしては.asciiではなく.byteで出力されるようだからです。 │const char str[]={'\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\'}; │str: │ .byte 92 │ .byte 92 │ 〜 │ .byte 92 │ .byte 92 しかしまあ、実際にこの対策が必要になる事は無いかな、と思っています。 * Sun May 15 21:25:46 JST 2016 Naoyuki Sawa - ispunct()のバグ P/ECE開発環境のispunct()にはバグが有ります。 ispunct(' ')は正しくはfalseですが、P/ECEではtrueになります。 EPSONライブラリのバグみたいです。 ■再現プログラム #include #include #include #include #include unsigned char vbuff[DISP_X * DISP_Y]; void pceAppInit() { pceLCDSetBuffer(vbuff); pceLCDDispStart(); } void pceAppProc(int cnt) { memset(vbuff, 0, sizeof vbuff); pceFontSetPos(0, 0); pceFontPrintf("ispunct(' ') = %s", ispunct(' ') ? "true" : "false"); pceLCDTrans(); } void pceAppExit() { } ■結果 ┌──────────┐ │ispunct(' ') = true │ └──────────┘ http://www.piece-me.org/piece-lab/ispunct/ispunct1.gif 修正するには、アプリケーションプログラム内で正しいispunct()を定義して、EPSONライブラリの関数を置き換えればokです。 ■修正プログラム #include #include #include #include #include unsigned char vbuff[DISP_X * DISP_Y]; void pceAppInit() { pceLCDSetBuffer(vbuff); pceLCDDispStart(); } void pceAppProc(int cnt) { memset(vbuff, 0, sizeof vbuff); pceFontSetPos(0, 0); pceFontPrintf("ispunct(' ') = %s", ispunct(' ') ? "true" : "false"); pceLCDTrans(); } void pceAppExit() { } //--- 正しいispunct()でEPSONライブラリの関数を置き換える --- int ispunct(int c) { return ((c >= 0x21) && (c <= 0x2F)) || //EPSONライブラリは多分ここが((c >= 0x20) && (c <= 0x2F))になっている。 ((c >= 0x3A) && (c <= 0x40)) || ((c >= 0x5B) && (c <= 0x60)) || ((c >= 0x7B) && (c <= 0x7E)); } ■結果 ┌──────────┐ │ispunct(' ') = false│ └──────────┘ http://www.piece-me.org/piece-lab/ispunct/ispunct2.gif 今回使用した検証プログラム一式の、ダウンロードはこちらです: http://www.piece-me.org/piece-lab/ispunct/ispunct-src.zip