堅牢で省資源な文字列“ディスクリプタ”Symbian OS開発の勘所(5)(2/3 ページ)

» 2007年05月24日 00時00分 公開
[大久保 潤 管理工学研究所,@IT MONOist]

配列としての文字列

 さて文字列の問題にフォーカスしていきましょう。先の分類で挙げたLevel2までの組み込みシステムでは、以下のように文字列を扱うことが一般的です。


文字列とは文字の配列表現なり。汝適切に配列を操作し、望みのままに文字列を扱うべし。


 要するにC言語の教科書に出てくるスタイルです。char buff[256]のように記述し、配列サイズに収まるように文字列を格納、操作することになります。もちろんこんなナイーブな書き方をすることはまれで、

  TCHAR buff[MAX_LENGTH]; 

のように、文字のコード系を隠ぺいし(char→TCHAR)、サイズも仕様変更に追従できるように定数かマクロ間接で指定します(256→MAX_LENGTH)。明示的に長さを持っていないのは、文字列として有効な範囲の次にターミネータとして0を格納するというルールに依存しています。このようなスタイルの文字列表現を「配列としての文字列」と呼ぶことにします。

 われわれはずいぶんと長い間、この「配列としての文字列」を愛用してきました。多くのコンポーネントは、C言語における大原則「ポインタと配列は等価である」の下、char*を(ちょっと気が利いた人はconst char*と使い分けしつつ)インターフェイスとして提供してきました。

 しかしこの実現方法は、複雑で大規模なシステム(ということは多くの人が作業にかかわり、その部品も物理的、時間的に異なる場所で開発されるシステムであり、全体の信頼性はそれら部品の信頼性の積で表されるシステム)の要求を満たしません。例えば次のようなケースが「配列としての文字列」の典型的な問題です。

配列の境界を超えるアクセスを防げない −バッファオーバーラン

 char*で渡された領域をどこまでアクセスしてよいか、その答えは領域自身しか知りません。だから文字列は以下のサンプルコードのようにアクセスされます。

void    convU2L( char* pStr ){
    for ( char ch; ( ch = *( pStr ) ); pStr++ ) {
        if ( isupper( ch ) ) {
            *( pStr ) = tolower( ch );
        }
    }
}
大文字を小文字に変換する関数

 想像してみましょう。呼び出し側の不具合の結果により、0で表現されるターミネータがpStrで示される領域に適切に設定されていなかったとしたら?

 実行環境とそのときのメモリマップに依存しますが、よくて範囲を超えたメモリアクセスによるプログラムの即時強制終了、悪い場合には微妙なメモリ破壊を含む発見しづらい不具合の混入という結果を招きます(注3)。この種の不具合をバッファオーバーランと呼ぶのはご承知だと思います。ロバストであるべき組み込みのソフトウェアとして、バッファオーバーランは重大な問題です。そしてこれらは結局のところ、「配列としての文字列」が持っている以下の本質的な弱点によるものだと考えられます。

A) 配列自体の長さは確保した個所しか知らない
→ いったんポインタとして渡されると、配列としてアクセス可能な範囲の情報が失われてしまう

B) 文字の終端はターミネータで扱われている
→ ターミネータもデータとして操作可能なので、容易に破壊を行うことができる

※注3:
毎回確実に発生する不具合は良性で、その中でも即死するタイプの不具合は最も性質の良い部類に属します。再現テストも修正後の検証も定数コストで済むからです。逆に原因と結果の間に100万ステップもの距離がある不具合や、起きたり起きなかったりする不具合は極めて悪性であり、原因を特定して修正することが格段に困難です。


 これらの弱点を回避し、ロバストな文字列処理を行うには何をすればよいでしょうか。すべてのプログラマが適切に処理を行うように気をつける、またそれができるように管理体制を強化する。1980年代に隆盛を誇ったソフトウェア工場的アプローチを採ると、そのような結論になります。しかしその結果として21世紀における日本のソフトウェア産業は、依然として国際競争力を有するに至っていません。やはり技術的な問題は技術的なアプローチで解決されなければ。

 では必ず

  void convU2L( char* pStr, int size ){ 

のように配列領域のサイズを渡すようにして、不具合をなるべく早い時期に呼ばれた側で検知できるようにすればよいのでしょうか。ターミネータをナイーブに信用するアプローチよりはるかにマシだといえますが、それで助かる範囲は上記のA)に関する問題までです。どうせインターフェイスを変えると決断したのなら、もっとうまいやりようがあるはずです。

常識チェック−文字列Now

 文字列をロバストに処理するための弱点は明らかになりました。するとこの弱点を回避または軽減するように施策すれば、それが問題の解決策となります(転嫁や保有の案というのはこの場合有効ではありません。そして上述のソフトウェア工場的アプローチは、プログラムからプログラマへの転嫁と読むことができます)。

A') 配列自体の長さは確保した個所しか知らない
→ 配列の長さを持って回る

B') 文字の終端はターミネータで扱われている
→ ターミネータを直接操作させないようにする。さらにいえば、ターミネータという実現方法を隠ぺいできるようにする

 組み込み以外のソフトウェア領域においては、文字列はどのように扱われているのかを表1にまとめます。すべてクラスとして表現されていることに注意してください。

環境名 クラス名 言語 GC 特徴・備考
1 Java java.lang.String
java.lang.StringBuffer
(java.lang.StringBuilder)
Java 不変(immutable)、
可変(mutable)でクラスを分けている
2 .NET System.String
System.Text.StringBuilder
VB.NET
C#.NET
…etc
不変(immutable)、
可変(mutable)でクラスを分けている
3 MFC CString C++ × MFCはWindows上のC++用フレームワーク
4 STL std::string C++ × STLはC++の標準テンプレートライブラリ
表1 各環境における文字列

 A')、B')の方針を実現するために、これらの文字列クラスは結果として共通の設計を行っています。

  • 配列を直接見せない
  • ターミネータの有無を意識させない
  • 上記を実現するために、文字列に関する操作をAPIで隠ぺいしている

 → 値を格納、取り出し、複製する

 → 検索、置換、編集する

 実装を隠ぺいし、必要な機能をクライアントに提供するというアプローチは文字列においても「安定した概念」(用語の定義は第3回をご覧ください)であるようです。

 ロバストな文字列の仕組みは分かった。しかし、それは組み込みの環境で許されるコストなのか、too muchな仕掛けではないのか、という質問が出てきそうです。そのとおりです。ロバストな文字列を組み込みで使うためにはもうひとひねりする必要がある、Symbian OSのデザイナも同じことを考えたようです。

Copyright © ITmedia, Inc. All Rights Reserved.