-内容のまとめ


書いてあった内容の概要です

  • echoプログラムのソースファイルを読む

  • getopt関数が便利でみんな使ってるが、必ずしも全てのオプションを処理できない。

  • echoのオプション処理はgetopt関数が使えない。

  • イディオム:#define STREQ(a, b) (*(a) == *(b) && strcmp*1 == 0)

    • 最初の文字を比較しておくと、効率が良い
  • 標準出力が失敗した時も考慮しよう

  • -練習問題


    (2.1) C, C++, Javaコンパイラを実際に使用して、未初期化変数がどのように扱われるかを調べなさい。その結果をまとめ、未初期化変数を見つけ出す方法を考えなさい。

    調査用コード

    // ex0201.c 
    #include
    int main(void)
    {
    int a, b, c;
    int ini = 4;
    return 0;
    }

    gcc -Sのアセンブリ出力コード

     // ex0201.s
    .file "ex0201_NoInitVal.c"
    .text
    .p2align 4,,15
    .globl main
    .type main, @function
    main:
    leal 4(%esp), %ecx
    andl $-16, %esp
    pushl -4(%ecx)
    pushl %ebp
    movl %esp, %ebp
    pushl %ecx
    // 4つの変数分スタック領域を確保(4つ*4byte=16)
    subl $16, %esp

    // 後ろから数えて1つめの変数(ini)に4を代入
    // -4はスタックフレームとかが入ってたはず

    movl $4, -8(%ebp)
    movl $0, %eax
    addl $16, %esp
    popl %ecx
    popl %ebp
    leal -4(%ecx), %esp
    ret
    .size main, .-main
    .ident "GCC: (GNU) 4.2.1 20070719 [FreeBSD]"
    以上のアセンブラコードから、未初期化変数はスタックの領域を確保されるが、初期値が代入されていない状態になっている。
    未初期化変数を見つけ出すには、スタック領域を確保したのちに値が代入されているかどうかを見れば良い。


    (2.2) (Dave Thomasから教えていただいた)echoプログラムでgetopt関数を使えないのはなぜか
     getopt関数の詳しい使い方はmanページを参照。どうも、この関数を使うと、与えられたオプションを解析するコードを自分で書かなくても良くなるし、どのコマンドでも、引数の指定方法が統一される、というメリットがあるみたい。だから、POSIX環境でなにかコマンドを作るとき、ふつうオプションの処理には、この関数を使うようだ(psコマンドとか、そうでないのもある)。
     では、どうしてechoコマンドでは、このライブラリ関数を使わないのだろう?  その理由は、ひとことで言うと、間違ったオプションを与えられた時のechoコマンドの動作が、getopt関数が指定する動作と異なるから、ということらしい。
     掘り下げてみる。echoコマンドは、ご存じのとおり、与えられた引数をそのまま標準出力に出力する、というコマンドである。このシンプルなコマンドにもオプションがあって、例えば、'-n'オプションは、出力の最後に改行を付加しない、という指定を行う。
     ではもしここに、間違ったオプションが指定されたとしたら?
     普通のコマンドの場合———例えば、TAB文字を^Iで表示する-Tオプションを指定しようとして、誤って-Yを指定してcatコマンドを呼び出すと———、こんな動作をする:


    $ cat -Y hoge.c
    cat: invalid option -- 'Y'
    詳しくは `cat --help' を実行してください。

     そりゃそうだって感じ。間違ったオプションを指定すると、そのまま処理を実行することもできるにはできるけれど、たいていのプログラムは、そこで処理をストップする。
     でも、これはechoコマンドである。間違ったオプションは、そもそもオプションじゃなくて、そういう文字列を画面に表示したいだけかもしれない(!)。 だから、指定されたオプションが間違っていたら、それをそのまま画面に出力するようにしたほうが無難だろう。
     実際、間違ったオプションを指定しても、echoはエラーを返さず、それを淡々と画面に表示するようにできている。こんな具合だ:


    $ echo -A
    -A

     ところが、getopt関数を使うと、この動作を実現することはできない。というのも、さっきのコマンド例で、エラーメッセージを出力しているのはgetopt関数そのものだからだ。コマンドの引数をgetoptに渡した時点で、間違ったオプション指定があると、エラーとして扱われてしまうのである。これはとても都合が悪い。
     仕方ない、自分で処理するしかないや。

     以上のような理由で、echoプログラムではgetopt関数を使用していないものと思われます。マル。



    (2.3) STREQのようなマクロを定義する利点と欠点を論じなさい。Cコンパイラでのstrcmpコールの最適化の可能性を検討しなさい。


    #define STREQ(a, b) (*(a) == *(b) && strcmp((a), (b)) == 0)
    とする。

    利点:
    strcmp関数は真の時'0'を返す。これは直感に反する。マクロを使えば、真の時に'1'を返すように設定を変更することができる。
    欠点:
    個人的には利点な気がするが、長所と短所は紙一重だ!(他に見つからなかった笑)STREQ(NULL, hoge)という記述ができない(コンパイルエラーとなる)。strcmp(NULL, hoge)という記述はコンパイルが通る(ただし、実行するとsegmentation fault)

    最適化の可能性:
    上記の様にSTREQを定義すると、まず最初の文字のみを比較して、最初の文字が一致しない場合はstrcmpを呼び出すまでもなく偽と判定できる。

    サンプル作ってみた


    #include
    #include

    #define STREQ(a, b) (*(a) == *(b) && strcmp((a), (b)) == 0)

    int main(void){

    char A[] = {"hello"};
    char B[] = {"kattun"};
    char C[] = {"kattun"};

    if(STREQ(A, B)){
    printf("true!\n");
    }
    else{
    printf("false!\n");
    }

    if(STREQ(B, C)){
    printf("true!\n");
    }
    else{
    printf("false!\n");
    }
    }

    実行結果はもちろん


    false!
    true!
    となる。




    (2.4) ライブラリコールの結果をチェックしていないプログラムを、現在の環境あるいは本書の付属CD-ROMから探しなさい。それに関して現実的な解決策を提案しなさい。
    Ubuntu10.10のcoreutilsパッケージに含まれるhostnameコマンド。debian系のOSであれば、以下のコマンドでダウンロードできる。

    # apt-get source coreutils
     このコマンドでは、53行目、uage()関数において、エラーを表示する目的でfprintf()関数を使用している。しかし、その際返り値が考慮されていない。fprintf()は成功するとは限らず、失敗した際には返り値の値が-1となる。返り値をチェックして、-1なら別の出力(ファイルなど)に出力しなおすようにif文を追加すればよい。


    (2.5) プログラムのある種の機能については、ソースコードを読むよりも現実にプログラムを実行した方が理解しやすい場合がある。標準出力の書き込みエラーに関してプログラムの挙動をチェックするテスト手続き(または枠組み)を考案しなさい。キャラクタベースのJavaプログラムまたはCプログラム(たとえば、コマンドライン版のコンパイラなど)をいくつか取り上げ、それを実際に試して結果を報告しなさい。

    ん???標準出力とエラー出力のテストを作れということでおk?
    その環境を試せばいいことにする。

    以下にwriteできないファイル(NoWrite)に書き込もうとするプログラムのソースコードを示す

    /* ex0205_STDOUT_2.c */
    #include
    #include // for err関数
    #include // for dup2関数

    int main(void){
    int fd;

    fd = open("NoWrite", 'w');
    if(dup2(fd, 1) < 0){
    err(1, "stdout");
    // #3では以下をコメントアウトして実行
    //printf("ファイルを開けないエラーに成功\n");
    }
    printf("hoge\n");
    }

    ※ただ単にopen関数が失敗した時のエラーを拾おうとすると、ファイル側のエラーで処理が終了してしまった。なので、dup2関数でファイル記述子を複製しようとして、失敗したところを拾うようにした。

    以下に実行手順を示す

    #1 writeできないファイルを作成する
    [~/codeReading$]touch NoWrite.txt
    [~/codeReading$]chmod -w NoWrite.txt
    [~/codeReading$]ls -l NoWrite.txt

    • r--r--r-- 1 ***** ***** 0 4月 8 10:09 NoWrite.txt

    #2 writeできないファイルにwriteしようとするプログラムをコンパイル&実行
    [~/codeReading$]cc ex0205_STDOUT_2.c
    [~/codeReading$]./a.out
    adout: Bad file descriptor

    #3 ソースコードの err 関数部分を printf関数に変えて実行
    [~/codeReading$]cc ex0205_STDOUT_2.c
    [~/codeReading$]./a.out
    開けないエラーに成功
    hoge
    [~/codeReading$]



    (2.6) ライブラリ関数sscanf、qsort、strchr、setjmp、open、adjacent_find、FormatMessage、XtOwnSelectionを使用するために必要なヘッダファイルを示しなさい(最後の3つはオペレーティング環境に固有な関数なので、現在の環境に存在しないかもしれない)。

    以下のとおり。最後の3つは、Mac OS X (10.6.6)、Ubuntu 10.10 のどちらにも見つけることができなかった。


    sscanf stdio.h
    qsort stdlib.h
    strchr string.h
    setjmp setjmp.h
    open fcntl.h (OSX10.6)

    *1:a), (b