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

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

このコミットは、Go言語のリンカ (cmd/ld) における移植性に関する修正です。具体的には、PE (Portable Executable) ファイルフォーマットを扱うコードが、暗黙的にリトルエンディアンのマシンを前提としていた問題を解決し、異なるエンディアンのマシンでも正しく動作するように改善しています。

コミット

commit d04ac4b0b74ff7fdb42d9578ddb3f25d15f5b477
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri Mar 22 04:00:54 2013 +0800

    cmd/ld: portability fixes
    fix code that implicitly assumes little-endian machines.

    R=golang-dev, bradfitz, rsc, alex.brainman
    CC=golang-dev
    https://golang.org/cl/6792043

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

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

元コミット内容

cmd/ld: portability fixes fix code that implicitly assumes little-endian machines.

このコミットは、Go言語のリンカ (cmd/ld) において、コードが暗黙的にリトルエンディアンのマシンを前提としていた問題を修正し、移植性を向上させるものです。

変更の背景

コンピュータのアーキテクチャには、データをメモリに格納する際のバイト順序に「エンディアン」という概念があります。主にリトルエンディアンとビッグエンディアンの2種類が存在します。x86やx64アーキテクチャはリトルエンディアンですが、ARMやPowerPCなど、一部のアーキテクチャはビッグエンディアンを採用しています。

Go言語はクロスプラットフォーム開発を強く意識しており、様々なアーキテクチャで動作するように設計されています。しかし、cmd/ld(Goリンカ)内のPEファイルフォーマットを処理するコードにおいて、バイト順序に関する暗黙の仮定が存在していました。具体的には、32ビット整数を読み込む際に、マシンのネイティブなエンディアンに依存してしまっており、これがビッグエンディアンのマシンで問題を引き起こす可能性がありました。

このコミットは、このようなエンディアン依存の問題を解消し、Goリンカがより多くのアーキテクチャで正しく動作するようにするための移植性向上を目的としています。

前提知識の解説

1. エンディアン (Endianness)

エンディアンとは、マルチバイトのデータをメモリに格納する際のバイト順序の規則です。

  • リトルエンディアン (Little-endian): データの最下位バイト (least significant byte) が、最も小さいアドレスに格納されます。Intel x86/x64プロセッサが採用しています。 例: 16進数 0x12345678 をメモリに格納する場合 (アドレスが小さい方から): 78 56 34 12
  • ビッグエンディアン (Big-endian): データの最上位バイト (most significant byte) が、最も小さいアドレスに格納されます。ネットワークバイトオーダーや、一部のRISCプロセッサ (PowerPC, SPARCなど) が採用しています。 例: 16進数 0x12345678 をメモリに格納する場合 (アドレスが小さい方から): 12 34 56 78

プログラムが異なるエンディアンのマシン間でバイナリデータをやり取りする場合や、バイナリファイルを読み書きする場合には、このエンディアンの違いを意識して適切にバイト順序を変換する必要があります。

2. PE (Portable Executable) ファイルフォーマット

PEファイルフォーマットは、Microsoft Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLL (Dynamic Link Library) などのバイナリファイルの標準フォーマットです。PEファイルは、ヘッダ、セクションテーブル、セクションデータなどから構成され、プログラムの実行に必要な情報(コード、データ、リソース、インポート/エクスポート情報など)を含んでいます。

Go言語のリンカは、Windows向けの実行ファイルを生成する際にこのPEフォーマットを扱います。PEファイル内の数値データは、通常リトルエンディアンで格納されることが期待されます。

3. Goリンカ (cmd/ld)

cmd/ld はGo言語のリンカです。Goコンパイラ (cmd/compile) が生成したオブジェクトファイル (.o ファイル) を結合し、実行可能なバイナリファイルを生成する役割を担っています。リンカは、外部ライブラリの解決、シンボルの配置、セクションの結合など、最終的な実行ファイルを構築するための様々な処理を行います。

技術的詳細

このコミットの技術的な核心は、src/cmd/ld/ldpe.c ファイルにおけるバイト順序の取り扱いを修正することにあります。

元のコードでは、PEファイル内の文字列テーブルのサイズを読み込む際に、Bread(f, &l, sizeof l) のように直接 int 型の変数 l に読み込んでいました。ここで sizeof l は通常4バイトですが、Bread 関数が読み込んだバイト列を l に格納する際、そのバイト順序は実行されているマシンのネイティブなエンディアンに依存します。

PEファイルフォーマットの仕様では、数値データはリトルエンディアンで格納されることが一般的です。したがって、もしリンカがビッグエンディアンのマシンで実行された場合、Bread が読み込んだバイト列はビッグエンディアンとして解釈され、PEファイル内のリトルエンディアンのデータとは異なる値になってしまいます。これにより、文字列テーブルのサイズが誤って解釈され、リンカの処理が失敗する可能性がありました。

修正では、以下の変更が行われています。

  1. l の型変更: int l から uint32 l に変更されました。これは、読み込むデータが符号なし32ビット整数であることを明確にします。
  2. 中間バッファ symbuf の導入: Bread(f, &l, sizeof l) の代わりに、Bread(f, symbuf, 4) を使用して、まず4バイトのデータを symbuf という中間バッファに読み込みます。
  3. 明示的なリトルエンディアン変換: l = le32(symbuf); という行が追加されました。le32 関数は、symbuf に読み込まれた4バイトのデータを、マシンのエンディアンに関わらず、リトルエンディアンとして解釈して32ビット符号なし整数に変換する役割を担います。これにより、常にPEファイルの仕様通りの正しい値が得られるようになります。
  4. オフセット計算の修正: 18*obj->fh.NumberOfSymbols というマジックナンバーが sizeof(symbuf)*obj->fh.NumberOfSymbols に変更されました。これは、シンボルテーブルのエントリサイズが symbuf のサイズ(おそらくシンボルエントリの構造体サイズ)に依存することを示唆しており、より堅牢な計算方法に修正されています。18 という値が何らかの理由で固定値として使われていたが、それがエンディアンやアーキテクチャに依存しない sizeof 演算子に置き換えられたと考えられます。

これらの変更により、リンカはPEファイル内のデータを読み込む際に、マシンのエンディアンに依存することなく、常にリトルエンディアンとして正しく解釈できるようになり、移植性が大幅に向上しました。

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

変更は src/cmd/ld/ldpe.c ファイルに集中しています。

--- a/src/cmd/ld/ldpe.c
+++ b/src/cmd/ld/ldpe.c
@@ -135,7 +135,8 @@ ldpe(Biobuf *f, char *pkg, int64 len, char *pn)
 {
  char *name;
  int32 base;
- int i, j, l, numaux;
+ uint32 l; // 変更点1: l の型を uint32 に変更
+ int i, j, numaux;
  PeObj *obj;
  PeSect *sect, *rsect;
  IMAGE_SECTION_HEADER sh;
@@ -170,11 +171,12 @@ ldpe(Biobuf *f, char *pkg, int64 len, char *pn)
  // TODO return error if found .cormeta
  }
  // load string table
- Bseek(f, base+obj->fh.PointerToSymbolTable+18*obj->fh.NumberOfSymbols, 0);\n- if(Bread(f, &l, sizeof l) != sizeof l)
+ Bseek(f, base+obj->fh.PointerToSymbolTable+sizeof(symbuf)*obj->fh.NumberOfSymbols, 0); // 変更点4: オフセット計算の修正
+ if(Bread(f, symbuf, 4) != 4) // 変更点2: 直接 l に読み込まず、symbuf に4バイト読み込む
  goto bad;
- l = le32(symbuf); // 変更点3: symbuf からリトルエンディアンとして l に変換
+ l = le32(symbuf);
  obj->snames = mal(l);
- Bseek(f, base+obj->fh.PointerToSymbolTable+18*obj->fh.NumberOfSymbols, 0);\n+ Bseek(f, base+obj->fh.PointerToSymbolTable+sizeof(symbuf)*obj->fh.NumberOfSymbols, 0); // 変更点4: オフセット計算の修正
  if(Bread(f, obj->snames, l) != l)
  goto bad;
  // read symbols

コアとなるコードの解説

変更点1: l の型変更

- int i, j, l, numaux;
+ uint32 l;
+ int i, j, numaux;

l は文字列テーブルのサイズを格納する変数であり、PEファイルフォーマットではこのサイズは符号なし32ビット整数として扱われます。元の int 型では、環境によっては32ビットより小さい場合や、符号付き整数として扱われることで意図しない挙動を引き起こす可能性がありました。uint32 に変更することで、PEファイルの仕様に厳密に準拠し、符号なし32ビット整数として確実に扱われるようになります。

変更点2 & 3: バイト読み込みとエンディアン変換

- Bseek(f, base+obj->fh.PointerToSymbolTable+18*obj->fh.NumberOfSymbols, 0);
- if(Bread(f, &l, sizeof l) != sizeof l)
+ Bseek(f, base+obj->fh.PointerToSymbolTable+sizeof(symbuf)*obj->fh.NumberOfSymbols, 0);
+ if(Bread(f, symbuf, 4) != 4)
  goto bad;
+ l = le32(symbuf);

元のコードでは、Bread(f, &l, sizeof l) でファイルから直接 l にデータを読み込んでいました。Bread はファイルから指定されたバイト数を読み込む関数ですが、そのバイト列をメモリ上の l にどのように配置するかは、実行環境のエンディアンに依存します。

修正後のコードでは、まず Bread(f, symbuf, 4) を使って、4バイトのデータを symbuf という一時的なバッファに読み込みます。symbuf はおそらく char symbuf[4] のような配列として定義されており、バイト列をそのまま保持します。

次に、l = le32(symbuf); という行で、symbuf に格納された4バイトのデータを、le32 関数を使って明示的にリトルエンディアンの32ビット整数として解釈し、その結果を l に代入します。le32 関数は、内部でバイトオーダーの変換ロジック(例えば、ビッグエンディアンのマシンであればバイト順を反転させるなど)を持っているため、これによりどのエンディアンのマシンで実行されても、PEファイル内のリトルエンディアンの数値が正しく読み込まれることが保証されます。

変更点4: オフセット計算の修正

- Bseek(f, base+obj->fh.PointerToSymbolTable+18*obj->fh.NumberOfSymbols, 0);
+ Bseek(f, base+obj->fh.PointerToSymbolTable+sizeof(symbuf)*obj->fh.NumberOfSymbols, 0);

この変更は、シンボルテーブルのオフセット計算に関するものです。元のコードでは 18 というマジックナンバーが使われていましたが、これはシンボルエントリのサイズを固定値として扱っていた可能性があります。しかし、シンボルエントリの実際のサイズは、PEフォーマットのバージョンや、あるいはコンパイラやリンカの実装によって異なる場合があります。

sizeof(symbuf) に変更することで、symbuf がシンボルエントリの構造体またはその一部を表す場合に、その実際のサイズに基づいてオフセットが計算されるようになります。これにより、コードの堅牢性が向上し、将来的なPEフォーマットの変更や異なるアーキテクチャへの対応が容易になります。

関連リンク

参考にした情報源リンク