Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14487] ファイルの概要

本コミットは、Go言語プロジェクト内のC言語で書かれた各種ツール(libmach, cmd/cc, cmd/cov, cmd/ld, cmd/prof)において、メモリ確保関数 malloc の戻り値をチェックする処理を追加するものです。これにより、メモリ確保が失敗した場合(メモリ不足など)に適切にエラーを検出し、プログラムの異常終了や未定義動作を防ぐ堅牢性が向上します。

コミット

  • コミットハッシュ: af375cde206ff131acf3e9afd126b36a8ff7b39e
  • 作者: Shenghou Ma minux.ma@gmail.com
  • コミット日時: 2012年11月27日 火曜日 03:05:46 +0800
  • コミットメッセージ:
    libmach, cmd/cc, cmd/cov, cmd/ld, cmd/prof: check malloc return value
    
    Update #4415.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6856080
    

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/af375cde206ff131acf3e9afd126b36a8ff7b39e

元コミット内容

libmach, cmd/cc, cmd/cov, cmd/ld, cmd/prof: check malloc return value

このコミットは、libmachcmd/cccmd/covcmd/ldcmd/prof といったGo言語のツール群において、malloc 関数によるメモリ確保の戻り値をチェックする処理を追加するものです。これは、Go言語のIssue #4415 に関連する更新です。

変更の背景

C言語において、malloc 関数は指定されたサイズのメモリブロックをヒープから確保し、そのブロックへのポインタを返します。しかし、システムに利用可能なメモリが不足している場合、malloc はメモリ確保に失敗し、NULL (またはGo言語のCコードでは nil) を返します。

malloc の戻り値をチェックせずに、返されたポインタが NULL であるにもかかわらずそのポインタを逆参照しようとすると、セグメンテーション違反(Segmentation Fault)などの実行時エラーが発生し、プログラムがクラッシュする原因となります。これは、プログラムの安定性や信頼性を著しく損なう深刻な問題です。

このコミットは、Go言語のツールチェインを構成するC言語コードにおいて、このようなメモリ不足によるクラッシュを防ぎ、より堅牢な動作を保証するために導入されました。特に、コンパイラ (cmd/cc)、リンカ (cmd/ld)、プロファイラ (cmd/prof)、カバレッジツール (cmd/cov)、および機械語関連のライブラリ (libmach) といった、システムリソースを多く消費する可能性のあるツール群において、メモリ確保の失敗に対する適切なハンドリングが不可欠です。

コミットメッセージにある "Update #4415" は、この変更が特定のGo言語のIssue(バグ報告や機能改善提案)に対応するものであることを示唆しています。残念ながら、Go言語のIssueトラッカーで直接「#4415」を検索しても関連する情報が見つかりませんでしたが、これは古いIssue番号であるか、内部的なトラッキング番号である可能性があります。しかし、変更内容から、メモリ管理の堅牢性向上を目的としたものであることは明らかです。

前提知識の解説

malloc 関数 (C言語)

malloc はC標準ライブラリの関数で、動的にメモリを確保するために使用されます。 void *malloc(size_t size);

  • size: 確保したいメモリのバイト数。
  • 戻り値: 確保されたメモリブロックの先頭への void ポインタ。メモリ確保に失敗した場合は NULL ポインタを返します。

NULL ポインタ / nil (Go言語のCコードにおける慣習)

C言語では、無効なポインタや何も指さないポインタを示すために NULL マクロが使われます。Go言語のCコードでは、慣習的に nilNULL と同義で使われることがあります。mallocNULL (または nil) を返すのは、メモリが不足しているか、要求されたサイズが大きすぎる場合などです。

メモリ確保の失敗とエラーハンドリング

プログラムが実行中に必要なメモリを確保できない状況は、特に長時間稼働するサーバーアプリケーションや、大量のデータを処理するツールで発生し得ます。このような場合、malloc の戻り値をチェックし、NULL であった場合には適切なエラー処理を行う必要があります。

本コミットでは、以下のようなエラー処理関数が使用されています。

  • print: 標準エラー出力にメッセージを出力する関数。
  • erroeexit(): プログラムをエラー終了させる関数。通常、エラーメッセージを出力した後に呼び出されます。
  • sysfatal(): システムエラーとして致命的なエラーメッセージを出力し、プログラムを終了させる関数。通常、回復不可能なエラーに対して使用されます。
  • diag(): 診断メッセージを出力する関数。erroeexit() と組み合わせて使用されることが多いです。

これらの関数は、Go言語のツールチェイン内で定義されているユーティリティ関数であり、標準Cライブラリの fprintf(stderr, ...)exit(EXIT_FAILURE) に相当する機能を提供します。

技術的詳細

このコミットの技術的詳細は、C言語におけるメモリ管理のベストプラクティス、特に動的メモリ確保の安全性に焦点を当てています。

  1. malloc の戻り値チェックの重要性: malloc は、要求されたメモリを割り当てることができない場合、NULL ポインタを返します。この NULL ポインタをチェックせずに使用しようとすると、プログラムは未定義の動作を引き起こし、多くの場合クラッシュします。これは、プログラムがアクセス権を持たないメモリ領域にアクセスしようとするため、オペレーティングシステムによってセグメンテーション違反として検出されます。

  2. 堅牢性の向上: if (ptr == nil) (または if (ptr == NULL)) の形式で戻り値をチェックし、メモリ確保が失敗した場合には、エラーメッセージを出力してプログラムを安全に終了させることで、ツールの堅牢性が大幅に向上します。これにより、メモリ不足の状況下でも、ユーザーに対して明確なエラーメッセージを提供し、デバッグを容易にします。

  3. 影響範囲: 変更は、Go言語のコンパイラ (cmd/cc)、カバレッジツール (cmd/cov)、リンカ (cmd/ld)、プロファイラ (cmd/prof)、および機械語関連のライブラリ (libmach) のC言語ソースコードにわたります。これらのツールは、Goプログラムのビルド、分析、実行において中心的な役割を果たすため、その安定性はGo開発エコシステム全体にとって非常に重要です。

  4. 具体的なエラーハンドリングパターン: コミットで追加されたコードは、以下の一般的なパターンに従っています。

    ptr = malloc(size);
    if (ptr == nil) { // または if (!ptr)
        // エラーメッセージの出力
        // プログラムの終了
    }
    // メモリが正常に確保された場合の処理
    

    エラーメッセージの出力には printdiag が使われ、プログラムの終了には erroeexit()sysfatal() が使われています。これらの関数は、Go言語のツールチェインのCコードで共通して使用されるエラー報告メカニズムです。

  5. 既存コードへの影響: この変更は、既存のコードのロジックを変更するものではなく、メモリ確保の安全性を高めるための防御的なプログラミングプラクティスを追加するものです。したがって、機能的な副作用はほとんどなく、むしろ潜在的なバグやクラッシュのリスクを低減します。

コアとなるコードの変更箇所

このコミットによって変更されたファイルと、それぞれのファイルにおける主要な変更点は以下の通りです。

  • src/cmd/cc/dcl.c: malloc の戻り値が nil の場合にエラーメッセージを出力し、erroeexit() で終了する処理を追加。

    if((n&15) == 0){
        na = malloc((n+16)*sizeof(Type*));
        if(na == nil) {
            print("%s: out of memory", argv0);
            erroeexit();
        }
        memmove(na, a, n*sizeof(Type*));
        free(a);
        a = tt->a = na;
    }
    
  • src/cmd/cov/main.c: malloc の戻り値が nil の場合に sysfatal("out of memory") で終了する処理を追加。

    n = malloc(sizeof *n);
    if(n == nil)
        sysfatal("out of memory");
    
    r = malloc(sizeof *r);
    if(r == nil)
        sysfatal("out of memory");
    
  • src/cmd/cov/tree.c: malloc の戻り値が nil の場合に sysfatal("out of memory") で終了する処理を追加。

    if(p == nil)
        p = malloc(sizeof *p);
    if(p == nil) // 2回目のチェックは、最初のmallocが失敗した場合に備える
        sysfatal("out of memory");
    
  • src/cmd/ld/dwarf.c: malloc の戻り値が nil の場合に diag("out of memory") を呼び出し、erroeexit() で終了する処理を追加。

    r = malloc(len + 1);
    if(r == nil) {
        diag("out of memory");
        erroeexit();
    }
    
    Linehist* lh = malloc(sizeof *lh);
    if(lh == nil) {
        diag("out of memory");
        erroeexit();
    }
    
  • src/cmd/prof/main.c: malloc の戻り値が nil の場合に sysfatal("out of memory") で終了する処理を追加。

    x = malloc(sizeof(PC));
    if(x == nil)
        sysfatal("out of memory");
    
    f = malloc(sizeof *f);
    if(f == nil)
        sysfatal("out of memory");
    
    ff = malloc(nfunc*sizeof ff[0]);
    if(ff == nil)
        sysfatal("out of memory");
    
    trace = malloc(ntrace * sizeof(Trace));
    if(trace == nil)
        sysfatal("out of memory");
    
  • src/libmach/executable.c: malloc の戻り値が NULL の場合に werrstr("out of memory") を呼び出し、return 0; でエラーを返す処理を追加。

    cmdbuf = malloc(mp->sizeofcmds);
    if(!cmdbuf) { // !cmdbuf は cmdbuf == NULL と同義
        werrstr("out of memory");
        return 0;
    }
    
    cmd = malloc(mp->ncmds * sizeof(MachCmd*));
    if(!cmd) {
        free(cmdbuf); // 既に確保したメモリを解放
        werrstr("out of memory");
        return 0;
    }
    
  • src/libmach/obj.c: malloc の戻り値が nil の場合に sysfatal("out of memory") で終了する処理を追加。

    sp = malloc(sizeof(Symtab));
    if(sp == nil)
        sysfatal("out of memory");
    

コアとなるコードの解説

このコミットのコアとなる変更は、C言語の malloc 関数呼び出しの直後に、その戻り値が NULL (または nil) であるかどうかをチェックする条件分岐を追加することです。これにより、メモリ確保が失敗した場合に、プログラムがクラッシュする代わりに、適切なエラーメッセージを出力して安全に終了するようになります。

例として、src/cmd/cc/dcl.c の変更を見てみましょう。

// 変更前:
// na = malloc((n+16)*sizeof(Type*));
// memmove(na, a, n*sizeof(Type*));

// 変更後:
if((n&15) == 0){
    na = malloc((n+16)*sizeof(Type*));
    if(na == nil) { // ここでmallocの戻り値をチェック
        print("%s: out of memory", argv0); // エラーメッセージを出力
        erroeexit(); // プログラムをエラー終了
    }
    memmove(na, a, n*sizeof(Type*));
    free(a);
    a = tt->a = na;
}

このコードスニペットでは、mallocna にメモリを割り当てようとします。もしメモリ確保が成功すれば na は有効なポインタとなり、その後の memmove などの処理が続行されます。しかし、もし malloc がメモリ確保に失敗して nil を返した場合、if(na == nil) の条件が真となり、以下のエラー処理が実行されます。

  1. print("%s: out of memory", argv0);: プログラム名 (argv0) と共に「out of memory」(メモリ不足)というエラーメッセージを標準エラー出力に表示します。
  2. erroeexit();: プログラムを直ちに終了させます。これにより、無効なポインタを逆参照しようとすることによるクラッシュを防ぎ、ユーザーにメモリ不足であることを明確に伝えます。

同様のパターンが、他のすべての変更箇所にも適用されています。

  • src/cmd/cov/main.csrc/cmd/prof/main.csrc/libmach/obj.c では、より致命的なエラーを示す sysfatal("out of memory"); が使用されています。これは、これらのツールがメモリ不足に陥った場合、それ以上処理を続行することが不可能であることを示唆しています。
  • src/cmd/ld/dwarf.c では diag("out of memory");erroeexit(); の組み合わせが使われています。
  • src/libmach/executable.c では、werrstr("out of memory"); でエラー文字列を設定し、return 0; で関数からエラーコードを返しています。これは、ライブラリ関数であるため、直接プログラムを終了させるのではなく、呼び出し元にエラーを通知する設計になっているためです。また、既に確保済みの cmdbuffree(cmdbuf); で解放している点も重要です。

これらの変更は、Go言語のツールチェインの信頼性と安定性を高める上で非常に重要です。

関連リンク

参考にした情報源リンク

  • C言語 malloc 関数に関する一般的なドキュメントやチュートリアル (例: C言語の標準ライブラリに関する書籍やオンラインリソース)
  • Go言語のIssueトラッカーの一般的な動作に関する知識
  • C言語におけるエラーハンドリングのベストプラクティスに関する知識
  • Go言語プロジェクトのC言語コードにおける慣習(nil の使用、sysfatal などのユーティリティ関数)