Code Reading/2章/2.1~2.9

Last-modified: 2010-04-30 (金) 20:55:53

2.1

問題

C,C++,Javaコンパイラを使って未初期化変数の扱われ方を調べる。
未初期化変数を発見する方法を考える。

解答

  • C言語
    global変数は初期化される。ポインターはNULL、他は0(double, float だと0.0)で初期化される。
    local変数は初期化されない。
    私の環境ではheapも初期化されたが、一般適には初期化される保証はないので、自分で初期化するべき。
    警告option -Wall (or -W)をつけることで警告を出すことができる。ただし、global変数/localなポインターに対しては警告は出さない.
    あとheapを確保するときにmemsetもセットで使っておくと初期化し忘れを防ぐことができる。(mallocの次の行に書くなり、マクロでセットにしておくなりお好みで:))
    fileunused.c
  • Java
    メッソドのローカル変数は初期化されない。初期化せずに使うとコンパイルエラー。
    クラスのメンバ変数は初期化される。具体的な値は以下の通り。
    boolean false
    char '\u0000'
    byte 0
    short 0
    int 0
    long 0L
    float 0.0f
    double 0.0d
    参照型 null
    fileUnused.java

2.2

問題

echoでgetopt()を使えない理由

解答

論より証拠。

p.23記載のgetoptを使用した簡易echo

$ ./echo_getopt -test
./echo_getopt: invalid option -- t
./echo_getopt: invalid option -- e
./echo_getopt: invalid option -- s
./echo_getopt: invalid option -- t

GNU echo(本に載っているバイナリとは異なる)

$ echo -test
-test

つまり、p.23監訳者注2にあるように、-ではじまるオプションでない文字の処理形式が違うということなのだろう。

2.3

問題

STREQのようなマクロを定義する利点と欠点について。
又Cコンパイラでのstrcmpコールの最適化の可能性を論じなさい。

解答

  • 利点
    • 文字列が等しい場合に真を返す(p.23)
    • strcmpでは等しいときに偽を返すため、STREQマクロの方が直感的
    • 命令を自分好みにカスタマイズできる。
    • (今回の例では)動作が若干高速化される。
    • 関数で命令を最適化するより便利な場合がある
      • 関数呼び出しのオーバーヘッドがない
      • マクロで関数を定義するとファイルリンクの手間が省ける(#include hogehoge.h と書くだけでok!)
  • 欠点
    • なんでも渡せてしまう
      • パラメータが複数回出てくることで予期せぬ動作する可能性がある()(プログラミング作法 -p.38 -)
      • パラメータに代入式を渡した場合、プログラマが「その式が複数回判定される」ことがわからない
      • (今回の例では)引数として代入式を渡すと怪しげな動作をする(最初の1文字だけ判定して、それが等しかった場合のみ全体の文字列を判定するため)
    • マクロがどのような動作をするのかわかりづらい(自分だけがコーディング、保守を行うのならともかく、複数人で作業する場合は止めた方がいい)
      • 現場のメンバーにマクロの動作についての理解を徹底させることで解消できる
    • コードの入力ミスに弱い
    • 引数の評価回数が複数回になることで、処理速度が遅くなる可能性がある。
    • 何度も呼び出される場合にコードサイズが大きくなる
    • 副作用のある呼び出しをするとまずい

保留。

ライブラリ自体読めということ?

2.4

問題

ライブラリコールの結果をチェックしていないプログラムを探せ。

現実的な解決策を提案せよ。

解答

題材は以下の通り。

Linux-PAM-0.99.8.1/modules/pam_tally/pam_tally.c

static int
getopts( char **argv )
{
  const char *pname = *argv;
  for ( ; *argv ; (void)(*argv && ++argv) ) {
    if      ( !strcmp (*argv,"--file")    ) cline_filename=*++argv;
    else if ( !strncmp(*argv,"--file=",7) ) cline_filename=*argv+7;
    else if ( !strcmp (*argv,"--user")    ) cline_user=*++argv;
    else if ( !strncmp(*argv,"--user=",7) ) cline_user=*argv+7;
    else if ( !strcmp (*argv,"--reset")   ) cline_reset=0;
    else if ( !strncmp(*argv,"--reset=",8)) {
      if ( sscanf(*argv+8,TALLY_FMT,&cline_reset) != 1 )
        fprintf(stderr,_("%s: Bad number given to --reset=\n"),pname), exit(0);
    }
    else if ( !strcmp (*argv,"--quiet")   ) cline_quiet=1;
    else {
      fprintf(stderr,_("%s: Unrecognised option %s\n"),pname,*argv);
      return FALSE;
    }
  }
  return TRUE;
}

このgetopts()の中で、条件式の中で使われていない、すなわち結果をチェックしていない標準ライブラリ関数がfprintf()である。
K&Rによれば、fprintf()はint型の返り値を持つ関数で、返り値は書き出された文字の数。
エラーが起きた場合、負の数を返すとのこと。
つまり上記の関数ではfprintfが失敗した場合のリカバリ手段がないということだ。
もっとも標準エラー出力にも書けないようではシステムとしてかなり致命的な段階に達しているとは思うが…。
現実的な解決策としては、エラーが起きたら強制終了するぐらいだろうか。

if (fprintf(...) < 0) {
    MethodUnderFatalError();
    exit();
}

こんな感じで。毎回これでは面倒だろうけど…。

中規模~以上のプログラムではfrintfに皮をかぶせる。

void myfprintf (.....)
{
    if (fprintf(.....) < 0) {
        MethodUnderFatalError();
        exit();
    }
}

2.5

問題:標準出力への書き込みエラーについてのプログラムの挙動をチェックするテスト方法を考案せよ。
解答:
普通にわからない…。
まず標準出力への書き込みエラーを意図的に起こす方法がわからない。
ソースコードを読んでいいのならgdb使って該当箇所で無理矢理失敗させる方法もあるが…。

2.6

問題:以下のライブラリ関数を使用するために必要なヘッダファイルを示せ。

  • sscanf
  • qsort
  • strchr
  • setjmp
  • open
    以下は環境によっては存在しない
  • adjacent_find
  • FormatMessage
  • XtOwnSelection

解答:他の問題に比べて不気味なぐらい易しい。
せっかくなので何も知らないつもりで、/usr/includeファイル内を検索。

$ find ./ -name '*.h' -print | xargs grep 'sscanf'

という感じで。
結果:
まずC標準関数。

  • sscanf:stdio.h
  • qsort:stdlib.h
  • strchr:string.h
  • setjmp:setjmp.h

続いてPOSIX標準関数。

  • open:fcntl.h

次はSTLの関数……?

  • adjacent_find:c++/4.1.2/bits/stl_algo.h
  • FormatMessage:boost/iostreams/detail/system_failure.hpp
  • XtOwnSelection:発見できず。

setjmpは知らなかった…。

2.7

問題

適当なプログラムを選んで、不必要に公開しないようにできる関数や変数を探せ。

解答

hengband-1.7.0/src/generate.c

int dun_tun_rnd;
int dun_tun_chg;
int dun_tun_con;
int dun_tun_pen;
int dun_tun_jct;

これら5つの変数はグローバル変数として定義されているが、generate.cと、そのサブ的な位置づけの
grid.cにしか使われていない。他のソースコードファイルで使われているからなのかもしれないが、
static宣言しても何とかならないのだろうか?

2.8

問題

適当な関数・メソッドを選び、本文中の各戦略を駆使してそれらの役割を調べよ。
できる限り素早く調べ、作業に要した時間順に戦略を並べよ。

解答

本文p.28より、コードリーディングの戦略は次の5つ。

  1. 関数の名前から機能を推測
  2. 関数の冒頭に書かれているコメントを読む
  3. 関数の使われ方を調べる
  4. 関数本体のコードを読む
  5. 関連ドキュメントを調べてみる

3.は多分その関数を参照している箇所を探せということだろう。

題材は、device-mapper.1.02.22/lib/libdm-common.cから、
_default_log()関数を選択することにする。

1.関数の名前から機能を推測
文字通りデフォルトのログ出力をする関数ということだろう。
これだけではわからないのが、いつどこで何に出力するのか
ということである。
(所用時間2分)

2.関数の冒頭に書かれているコメントを読む

関数冒頭のコメントを引用すると以下の通り。

/*
 * Library users can provide their own logging
 * function.
 */

訳すと、「ライブラリのユーザはそのユーザ独自のロギング関数を提供することができる」
つまり_default_log()はユーザ毎に所有するログ関数のラッパーになれるということか。
(所用時間5分)

3.関数の使われ方を調べる
grepで検索してみよう。

device-mapper.1.02.22/lib/
$ grep _default_log *.*
libdm-common.c:static void _default_log(int level, const char *file __attribute((unused)),
libdm-common.c:dm_log_fn dm_log = _default_log;
libdm-common.c:         dm_log = _default_log;

2ヶ所でしか使われていない。
少ないので両方読むことにする。

dm_log_fn dm_log = _default_log;
void dm_log_init(dm_log_fn fn)
{
	if (fn)
		dm_log = fn;
	else
		dm_log = _default_log;
}

まあなんとなく、何か特定のログ用関数が指定された場合(fn≠0)その関数を使い、
そうでない場合(fn=0)はデフォルトのログ用関数_default_log()を使うという
ことなのだろうとはわかる。
(所要時間6分)

4.関数本体のコードを読む
短いので全部引用(冒頭コード部分は除く)

static void _default_log(int level, const char *file __attribute((unused)),
			 int line __attribute((unused)), const char *f, ...)
{
	va_list ap;
	int use_stderr = level & _LOG_STDERR;

まずuse_stderrがエラー出力を使うかどうかチェックしている。

	level &= ~_LOG_STDERR;
	if (level > _LOG_WARN && !_verbose)
		return;

ここではレベルがwarnより上で、冗長出力モードではない場合に何もせず戻っている。

	va_start(ap, f);
	if (level < _LOG_WARN)
		vfprintf(stderr, f, ap);
	else
		vfprintf(use_stderr ? stderr : stdout, f, ap);

つまり、レベルがwarn未満の場合はエラー出力に、warn以上の場合は
use_stderr次第でエラー出力か標準出力に出力先がわかれる。
ここで気になるのは、ログレベルがwarnのときだ。
このコードの場合、ログレベルwarnで冗長出力モードなしの場合でも、最後の行のおかげで
ログレベルwarnだけ標準出力にいく可能性はある。

	va_end(ap);
	if (level < _LOG_WARN)
		fprintf(stderr, "\n");
	else
 		fprintf(use_stderr ? stderr : stdout, "\n");
}

(所用時間16分)

5.関連ドキュメントを調べてみる
かなり下層レベル(と思われる)関数のためか、関連ドキュメントからは
大した情報が得られなかった。
(所用時間5分)

結果
4.関数本体のコードを読む(16分)
3.関数の使われ方を調べる(6分)
2.関数の冒頭に書かれているコメントを読む(5分)
5.関連ドキュメントを調べてみる(5分)
1.関数の名前から機能を推測(2分)
合計:34分

考察
1.~3.をこなしておくと、4.でコードを読むのが非常に楽だった。
今回は5.で時間を割くことがなかったが、巨大なプロジェクトの
コアな関数だったらより多くの時間を要するだろう。

2.9

問題

現在使用しているエディタの括弧認識機能を調べよ。

解答

Emacs-22.1-8.fc8で調査。
.emacsについては別項参照。
1.拡張子なしのファイル
小括弧のみ認識。対応する括弧が存在する場合はその両方が水色でハイライトされる。
対応する開き括弧がない状態で閉じ括弧を記入した場合は紫色でハイライトされる。
2.拡張子が.cのファイル
小括弧、中括弧、大括弧全てに対応。