著作一覧 |
王様がリンクしているので、まじめに書いてみる(意図を誤読しているかも知れないけどまあいいや)。
なんとなく16K境界で発現しそうに思ったら、思いのほか伸びたので途中から別の話になってたし。
問題は指定された長さのメモリーブロック内の文字走査の書き方だ。
int foobar(unsigned char* ptr, int len) { unsigned char* p = ptr; // 元のポインタ値を保持するためにインクリメンタル用ポインタを別に用意する。 for (int i = 0; *p != '\0' && i < len; i++, p++); // 0が現れるか指定長に到達するまでpを後ろへ移動する。 return p - ptr; // 元のポインタとの差=ターゲット文字までか、または元のメモリーブロックの長さを返す }
バグは、for文の条件式にある。
今、foobarに長さ2バイトのメモリーブロックを与えるとする。ターゲット文字(この例では0)は含まない。
unsigned char a[] = "\xff\xff"; printf("len=%d\n, foobar(a, 2));
ここで、aがポイントするアドレスを0とする。
すると、unsigned char* p = ptr; で、pには0が入る。
forの最初の時点でiは0、pは0、lenは2となっている。
条件式を実行すると、pは0(ということはa[0]なので'\xff')、iは0なので条件は成立せず、継続式に制御が移り、pは1、iは1となる。
条件式を実行すると、pは1(ということはa[1]なので'\xff')、iは1なので条件は成立せず、継続式に制御が移り、pは2、iは2となる。
条件式を実行すると、pは2(ということはa[2] --- バグ ----)仮にa[2]相当のメモリー位置の内容が0ならforステートメントを抜ける。そうでなければiは2で、lenの2と等しいためforステートメントを抜ける。
現在のpの値2から、ptrの値0を引いた値2が戻り値となる。
ここで、このバグがほとんど(見た目上)発現しないのは、条件式の左項が成立してもしなくても、右項のi < lenにより最終的にforステートメントを抜けることができるからだ。
しかし、メモリーアロケーションの結果、ptr + lenの位置がページ境界で、かつ以降のページが未割当ならば、i == lenの時点での条件式の左項は、未割当メモリーに対する参照となり、バグが(見えるかたちで)発現する。
正しいプログラムであれば、条件式は、i < len && *p != '\0'のように、最初に長さの比較をし、次に(左項により*pが安全なアクセスであることを確認できた後に)メモリーの参照を行う。
この形式で記述できるのは、Cが、条件式をショートカットするからだ。ショートカットにより、左項の条件が満たせなかった場合、つまり、iがlenと等しい=pが与えられたメモリーブロックの範囲を超えた位置を指している場合は右項の実行を回避できるからだ。
(ショートカットしない言語、具体的にはVBで、つい、i < len and a(i) <> 0 みたいな条件式を書いてIndexOutBoundExceptionみたいな例外で死んだことがあるので、言語仕様を読むのは重要ですなぁと)
あと、JavaとかC#とかインクリメンタル操作とかが可能なポインタが無い言語では、同じようなことをするには配列を使うしかないので、当然、IndexOutOfBoundExceptionみたいな例外をさっさと食らえるので(ユニットテストとかまじめにしていれば)この手のバグが入り込む余地はない。というわけで、ああいう言語は楽ですなぁと思う。
忘れてたけど、上のバグは慣れればすぐわかるのだが、しかし犯しやすい。
なぜなら、*p != '\0' && i < lenのほうが自然な記述だからだ。
というのは、ここでやりたいこと、主要な機能は、iがlenの長さになるまで、ではない。*pに'\0'が見つかるまで、だ。
関数コメントを1行で書けば、「与えられた文字列内の'\0'までの長さか、見つからなければ(と条件つきで)その長さを返す」になるだろう。
つまり、重要な機能を最初に書くのは、むしろ良い習慣だから、或る程度プログラムが書ければ逆に書いてしまうし、プログラムの機能を読めば見過ごしてしまいかねない。
というわけで、String#indexOfだとか、Array#find(あるかどうか知らないけど)とかのメソッドが用意された言語のほうが良い言語だし、自前でループ書くより用意された(名前がついた)メソッド使え、というような話になるのだ、と思う(けど他にも理由はあるから言い切れない)。
そのあたりが、Cのだめっぷりということになるんだけど、逆にその生々しさがCの魅力でもあってとっても悩ましい。というか、C好きだし。
ジェズイットを見習え |
strnlen を使うというのはどうでしょう (自分で使ったことはないのですが) 。
この場合は、そういう方法もあるのですね。でも、10年以上前のマシンで閉じた処理系しかないから、きっとその関数はないと思います。
for文のところのp++が抜けているようです
直しました。どうもありがとう。