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

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

src/cmd/6l/asm.c は、Go言語の初期のツールチェインにおける 6l リンカの一部です。6lamd64 (6シリーズ) アーキテクチャ向けのリンカであり、アセンブリコードの処理やオブジェクトファイルのリンクを担当していました。このファイルは、特に実行可能ファイルのセクション配置やファイルへの書き込みに関連するロジックを含んでいます。

コミット

Bug fix. Without rounding, .shstrtab is written on top of data segment

SVN=122567

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

https://github.com/golang/go/commit/67e141b637621d20cdb12b27cb63f0e74c9022cb

元コミット内容

このコミットは、Go言語のリンカ 6l におけるバグ修正です。具体的には、実行可能ファイルの .shstrtab (セクションヘッダ文字列テーブル) が、適切なアライメント処理なしに書き込まれると、データセグメントの上に上書きされてしまうという問題に対処しています。

変更の背景

このコミットが行われた2008年当時、Go言語はまだ開発の初期段階にありました。リンカは、コンパイルされたコードとデータを最終的な実行可能ファイルに結合する重要な役割を担っています。実行可能ファイルのフォーマット(LinuxではELF: Executable and Linkable Format)は、コード、データ、シンボルテーブル、セクションヘッダなど、様々な要素が特定の構造とアライメント規則に従って配置されることを要求します。

このバグは、リンカが .shstrtab をファイルに書き込む際の位置計算に誤りがあったために発生しました。具体的には、テキストセグメントとデータセグメントの合計サイズに、セクションヘッダ文字列テーブルを書き込むためのオフセットを加える際に、適切なメモリ境界への「丸め(rounding)」処理が欠けていました。この結果、.shstrtab がデータセグメントの末尾ではなく、その一部または全体を上書きしてしまう可能性がありました。

このような上書きが発生すると、実行可能ファイルが破損し、プログラムの実行時に予期せぬ動作、クラッシュ、またはセキュリティ上の脆弱性につながる可能性があります。リンカの役割は、これらの要素を正確に配置し、OSが正しくロードして実行できるような有効な実行可能ファイルを生成することであるため、このバグは非常に重要でした。

前提知識の解説

ELF (Executable and Linkable Format)

ELFは、Unix系オペレーティングシステム(Linuxを含む)で広く使用されている、実行可能ファイル、オブジェクトファイル、共有ライブラリの標準バイナリフォーマットです。ELFファイルは、以下の主要な部分で構成されます。

  • ELFヘッダ: ファイルのタイプ(実行可能、オブジェクトなど)、ターゲットアーキテクチャ、エントリポイントアドレスなどの基本的な情報を含みます。
  • プログラムヘッダテーブル (Program Header Table): 実行可能ファイルや共有ライブラリにのみ存在し、OSがプログラムをメモリにロードする方法を記述します。各エントリは「セグメント」を記述します。
  • セクションヘッダテーブル (Section Header Table): オブジェクトファイルや実行可能ファイルに存在し、ファイル内の各「セクション」に関する情報(名前、タイプ、サイズ、オフセットなど)を記述します。
  • セクション (Sections): 実際のコードやデータが格納される領域です。一般的なセクションには以下のようなものがあります。
    • .text: 実行可能なコード。
    • .data: 初期化されたグローバル変数や静的変数。
    • .rodata: 読み取り専用データ(文字列リテラルなど)。
    • .bss: 初期化されていないグローバル変数や静的変数(実行時にゼロ初期化される)。
    • .symtab: シンボルテーブル(関数名、変数名など)。
    • .strtab: シンボル名やセクション名などの文字列を格納する文字列テーブル。
    • .shstrtab: セクションヘッダ文字列テーブル。セクションヘッダテーブル内のセクション名が格納されている文字列テーブルへのオフセットを保持します。つまり、セクションヘッダテーブルの各エントリは、この .shstrtab 内の文字列へのインデックスを持ち、そのインデックスを使ってセクションの名前を取得します。

Go言語のリンカ (6l)

Go言語の初期のツールチェインでは、各アーキテクチャ(例: amd64arm)に対応する専用のリンカが存在しました。6lamd64 アーキテクチャ向けのリンカであり、Goのソースコードから生成されたオブジェクトファイルを結合し、最終的な実行可能ファイルを生成する役割を担っていました。リンカは、オブジェクトファイル内のセクションを結合し、メモリレイアウトを決定し、シンボルを解決し、再配置を行い、最終的なバイナリを生成します。

メモリのアライメント (Memory Alignment)

メモリのアライメントとは、データがメモリ内で特定のアドレス境界に配置されることを指します。例えば、4バイトの整数は4の倍数のアドレスに配置される、といった具合です。

  • なぜ必要か:
    • パフォーマンス: 多くのCPUは、アライメントされたデータへのアクセスを、アライメントされていないデータへのアクセスよりも高速に処理できます。アライメントされていないアクセスは、追加のCPUサイクルを必要としたり、複数回のメモリアクセスを発生させたり、最悪の場合、ハードウェア例外(アライメント違反)を引き起こしたりすることがあります。
    • ハードウェア要件: 一部のハードウェアアーキテクチャでは、特定のアライメント要件を満たさないデータアクセスは許可されません。
    • データ構造の整合性: 構造体内のフィールドが適切にアライメントされることで、パディングが発生し、データ構造全体のサイズが最適化されたり、特定の命令セットが効率的に使用されたりします。
    • 実行可能ファイルのロード: オペレーティングシステムが実行可能ファイルをメモリにロードする際、セグメントやセクションは通常、ページ境界(例えば4KB)にアライメントされる必要があります。

テキストセグメントとデータセグメント

実行可能ファイルがメモリにロードされる際、通常、主要な2つのセグメントに分割されます。

  • テキストセグメント (Text Segment): プログラムの実行可能な機械語コードが含まれます。通常、読み取り専用で実行可能としてマークされます。
  • データセグメント (Data Segment): 初期化されたグローバル変数や静的変数が含まれます。通常、読み書き可能としてマークされます。

これらのセグメントは、メモリ内で連続しているとは限りませんが、ファイル内では特定の順序で配置され、リンカによってそのオフセットが決定されます。

技術的詳細

このコミットの核心は、リンカ 6l がELF実行可能ファイルの .shstrtab セクションをファイルに書き込む際の位置計算の誤りを修正することにあります。

ELFファイルでは、セクションはファイル内の特定のオフセットに配置されます。リンカは、各セクションのサイズとアライメント要件を考慮して、これらのオフセットを計算します。

問題のコードは src/cmd/6l/asm.ccase 7 ブロックにありました。このブロックは、おそらくELFファイルのセクションヘッダ文字列テーブル (.shstrtab) を書き込むための処理の一部です。

元のコードでは、seek 関数を使ってファイルポインタを移動させていました。seek(cout, HEADR+textsize+datsize, 0); は、ファイルヘッダのサイズ (HEADR)、テキストセグメントのサイズ (textsize)、データセグメントのサイズ (datsize) を単純に合計したオフセットにファイルポインタを移動させていました。

しかし、ここで問題が発生しました。テキストセグメントとデータセグメントの合計サイズが、次のセクション(この場合は .shstrtab)が配置されるべきメモリ境界に適切にアライメントされていなかったのです。特に、OSが実行可能ファイルをメモリにロードする際、セグメントは通常、ページ境界(例えば4KB)にアライメントされる必要があります。リンカは、ファイル内のセクションのオフセットを計算する際に、このアライメントを考慮する必要があります。

rnd 関数は、指定された値を特定のアライメント値 (INITRND) に丸める(切り上げる)ための関数です。INITRND は、おそらくページサイズやセグメントのアライメント要件を表す定数です。

修正後のコード seek(cout, rnd(HEADR+textsize, INITRND)+datsize, 0); は、以下の計算を行っています。

  1. HEADR+textsize: ファイルヘッダとテキストセグメントの合計サイズを計算します。
  2. rnd(HEADR+textsize, INITRND): この合計サイズを INITRND の倍数に切り上げます。これにより、テキストセグメントの直後に続く領域が、OSのロード要件を満たすように適切にアライメントされます。
  3. +datsize: アライメントされたオフセットにデータセグメントのサイズを加えます。

この修正により、.shstrtab が書き込まれるオフセットが、データセグメントの末尾に適切にアライメントされた位置に移動し、データセグメントとの重複が解消されました。これにより、生成される実行可能ファイルが破損することなく、OSによって正しくロードおよび実行されることが保証されます。

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

--- a/src/cmd/6l/asm.c
+++ b/src/cmd/6l/asm.c
@@ -226,7 +226,7 @@ debug['s'] = 1;
 			break;
 		case 7:
 debug['s'] = 1;
-			seek(cout, HEADR+textsize+datsize, 0);
+			seek(cout, rnd(HEADR+textsize, INITRND)+datsize, 0);
 			linuxstrtable();
 			break;
 		case 6:
@@ -498,7 +498,6 @@ w = bsssize;
 			8,			/* align */
 			0);			/* entsize */
 
-fo = HEADR+textsize+datsize;
 w = stroffset +
 	strlen(".shstrtab")+1;
 //	strlen(".gosymtab")+1;

コアとなるコードの解説

変更は src/cmd/6l/asm.ccase 7 ブロック内で行われています。

  • seek(cout, offset, 0);:

    • seek は、ファイルポインタを移動させるための関数です。
    • cout は、出力ファイル(この場合は生成される実行可能ファイル)のファイルディスクリプタです。
    • offset は、ファイルポインタを移動させるバイト数です。
    • 0 は、オフセットがファイルの先頭からの相対位置であることを示します。
  • HEADR:

    • これは、実行可能ファイルのヘッダ部分のサイズを表す定数です。ELFヘッダやプログラムヘッダテーブルなどが含まれます。
  • textsize:

    • これは、プログラムのテキストセグメント(実行可能なコード)のサイズを表す変数です。
  • datsize:

    • これは、プログラムのデータセグメント(初期化されたデータ)のサイズを表す変数です。
  • rnd(value, align):

    • この関数は、valuealign の最も近い倍数に切り上げる(丸める)ために使用されます。
    • 例えば、rnd(10, 4)12 を返します。これは、10を4の倍数に切り上げた値です。
    • この文脈では、rnd(HEADR+textsize, INITRND) は、ファイルヘッダとテキストセグメントの合計サイズを、INITRND で定義されたアライメント境界に丸めています。
  • INITRND:

    • これは、初期の丸め(アライメント)値を定義する定数です。通常、これはメモリページサイズ(例: 4096バイト)や、OSがセグメントをロードする際に要求するアライメント境界に対応します。

変更前: seek(cout, HEADR+textsize+datsize, 0); この行は、ファイルヘッダ、テキストセグメント、データセグメントの合計サイズにファイルポインタを移動させていました。しかし、この合計値が必ずしも適切なメモリ境界にアライメントされているとは限りませんでした。

変更後: seek(cout, rnd(HEADR+textsize, INITRND)+datsize, 0); この変更により、ファイルポインタの計算方法が修正されました。

  1. まず、HEADR+textsize でファイルヘッダとテキストセグメントの合計サイズを計算します。
  2. 次に、この合計サイズを rnd 関数と INITRND を使って、適切なアライメント境界に切り上げます。これにより、テキストセグメントの直後の領域が、OSがセグメントをロードする際に要求するアライメントを満たすようになります。
  3. 最後に、このアライメントされたオフセットに datsize(データセグメントのサイズ)を加えます。

この修正によって、.shstrtab が書き込まれるファイル内のオフセットが、データセグメントの末尾に適切にアライメントされた位置に設定されるようになり、データセグメントとの重複が回避されました。これにより、生成されるELF実行可能ファイルが、メモリレイアウトの観点から見て正しく、OSによって問題なくロードおよび実行されることが保証されます。

また、削除された fo = HEADR+textsize+datsize; の行は、おそらくデバッグ目的か、以前の計算ロジックの名残であり、今回の修正とは直接関係ないため削除されたと考えられます。

関連リンク

参考にした情報源リンク

  • ELFフォーマットに関する一般的な知識
  • メモリのアライメントに関する一般的な知識
  • Go言語の初期のツールチェインに関する情報(公開されているドキュメントやソースコードのコメントなど)
  • seek 関数の一般的な動作
  • rnd 関数の一般的な動作(丸め処理)
  • Go言語のコミット履歴と関連する議論(もしあれば)