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

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

このコミットは、Go言語のリンカ(cmd/ld)におけるアドレスのラップアラウンドチェックと、Windows環境での32ビットリロケーションの符号に関するバグ修正を目的としています。特に、PC相対(Program Counter relative)リロケーションとその他のリロケーションタイプで必要とされるオフセットの符号付き/符号なしの扱いを明確にし、WindowsのPE(Portable Executable)形式におけるリロケーション処理の正確性を向上させています。

コミット

commit 700a126c64948f91822dcc0380f32438dad6fc71
Author: Rob Pike <r@golang.org>
Date:   Tue Apr 30 09:10:10 2013 -0700

    cmd/ld: fix check for address wrap in relocation
    PC-relative needs a signed offset; others need unsigned.
    Also fix signedness of 32-bit relocation on Windows.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/9039045

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

https://github.com/golang/go/commit/700a126c64948f91822dcc0380f32438dad6fc71

元コミット内容

diff --git a/src/cmd/ld/data.c b/src/cmd/ld/data.c
index c57c0c69b6..263dd25828 100644
--- a/src/cmd/ld/data.c
+++ b/src/cmd/ld/data.c
@@ -259,8 +259,7 @@ relocsym(Sym *s)
 			cursym = s;
 			diag("bad reloc size %#ux for %s", siz, r->sym->name);
 		case 4:
-			// TODO: Test is causing breakages on ARM and Windows. Disable for now.
-			if(0 && o != (int32)o) {
+			if((r->type == D_PCREL && o != (int32)o) || (r->type != D_PCREL && o != (uint32)o)) {
 				cursym = S;
 				diag("relocation address is too big: %#llx", o);
 			}
diff --git a/src/cmd/ld/ldpe.c b/src/cmd/ld/ldpe.c
index 7a5bc0c608..033e522f27 100644
--- a/src/cmd/ld/ldpe.c
+++ b/src/cmd/ld/ldpe.c
@@ -291,7 +291,7 @@ ldpe(Biobuf *f, char *pkg, int64 len, char *pn)
 				case IMAGE_REL_AMD64_ADDR32: // R_X86_64_PC32
 				case IMAGE_REL_AMD64_ADDR32NB:
 					rp->type = D_PCREL;
-					rp->add = le32(rsect->base+rp->off);
+					rp->add = (int32)le32(rsect->base+rp->off);
 					break;
 				case IMAGE_REL_I386_DIR32NB:
 				case IMAGE_REL_I386_DIR32:

変更の背景

このコミットが行われた背景には、Go言語のリンカが生成する実行ファイルにおけるリロケーション処理の正確性に関する問題がありました。具体的には、以下の2つの主要な課題に対処しています。

  1. アドレスのラップアラウンドチェックの不備: リンカがリロケーションを行う際、計算されたオフセットがターゲットのアドレス空間に収まるかどうかのチェックが不適切でした。特に、PC相対リロケーションと絶対アドレスリロケーションで、オフセットの符号付き/符号なしの解釈が混同されており、これがアドレスのラップアラウンド(オーバーフローやアンダーフロー)を引き起こす可能性がありました。以前のコードでは、このチェックが一時的に無効化されていた(if(0 && ...))ため、潜在的なバグが表面化していませんでした。
  2. Windowsにおける32ビットリロケーションの符号問題: WindowsのPE(Portable Executable)形式では、特定のリロケーションタイプ(例: IMAGE_REL_AMD64_ADDR32)において、32ビットのアドレスオフセットが符号付きとして扱われるべき場面で、リンカがこれを適切に処理できていない可能性がありました。これにより、特に大きなアドレス空間を持つシステムや、特定のメモリアドレスに配置されるコードにおいて、不正なアドレス解決が発生するリスクがありました。

これらの問題は、生成される実行ファイルの安定性や移植性に影響を与えるため、リンカの正確な動作を保証するために修正が必要でした。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  • リンカ (Linker): コンパイラによって生成された複数のオブジェクトファイル(機械語コードとデータを含む)を結合し、実行可能なプログラムやライブラリを生成するソフトウェアツールです。リンカの主要な役割の一つに「リロケーション」があります。
  • リロケーション (Relocation): オブジェクトファイル内のコードやデータが、最終的にメモリ上のどこに配置されるか分からないため、リンカがこれらのアドレス参照を修正するプロセスです。例えば、関数呼び出しやグローバル変数へのアクセスは、コンパイル時には相対的なオフセットで表現され、リンカが最終的な絶対アドレスに変換します。
  • オフセット (Offset): ある基準点からの相対的な距離を示す値です。リロケーションにおいては、命令やデータが参照するターゲットのアドレスが、現在の命令ポインタ(PC)やセクションの先頭などからの相対的な距離として表現されます。
  • PC相対リロケーション (PC-relative Relocation): プログラムカウンタ(PC、現在の命令のアドレス)からの相対的なオフセットを用いてアドレスを計算するリロケーションです。この方式は、コードがメモリ上のどこにロードされても正しく動作するように、位置独立コード(PIC: Position-Independent Code)を生成する際によく用いられます。PC相対オフセットは、ターゲットが現在のPCより前にも後にも存在しうるため、通常は符号付き整数で表現されます。
  • 絶対アドレスリロケーション (Absolute Address Relocation): ターゲットの絶対アドレスを直接参照するリロケーションです。これは通常、データセクション内のグローバル変数への参照や、特定の固定アドレスへのジャンプなどに使用されます。絶対アドレスオフセットは、通常符号なし整数で表現されます。
  • アドレスのラップアラウンド (Address Wrap-around): 整数型で表現できる最大値を超えたり、最小値を下回ったりすることで、アドレスが予期せぬ値になる現象です。例えば、32ビット符号なし整数で0xFFFFFFFFに1を加えると0x00000000に戻る(ラップアラウンドする)ことがあります。これは、計算されたアドレスが実際にアクセス可能なメモリ範囲外になることを意味し、プログラムのクラッシュやセキュリティ脆弱性につながります。
  • 符号付き整数 (Signed Integer) と符号なし整数 (Unsigned Integer):
    • 符号付き整数: 正の値と負の値を表現できます。例えば、32ビット符号付き整数(int32)は、約-20億から+20億までの範囲の値を表現します。最上位ビットが符号ビットとして使われます。
    • 符号なし整数: 負の値を表現できず、0以上の値のみを表現できます。例えば、32ビット符号なし整数(uint32)は、0から約40億までの範囲の値を表現します。すべてのビットが数値の大きさを表すために使われます。 オフセットの計算において、符号付きか符号なしかの選択は、表現できる範囲と、負のオフセット(基準点より前のアドレス)を扱えるかどうかに直接影響します。
  • PE (Portable Executable) 形式: Microsoft Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLL(Dynamic Link Library)などのファイル形式です。PEファイルは、ヘッダ、セクション、リロケーション情報など、プログラムの実行に必要な様々な情報を含んでいます。
  • IMAGE_REL_AMD64_ADDR32: WindowsのPE形式において、AMD64(x86-64)アーキテクチャで使用されるリロケーションタイプの一つです。これは、32ビットの絶対アドレスを指すリロケーションエントリを示します。

技術的詳細

このコミットは、Goリンカのsrc/cmd/ld/data.csrc/cmd/ld/ldpe.cの2つのファイルに修正を加えています。

src/cmd/ld/data.cの変更点

このファイルでは、relocsym関数内のアドレスラップアラウンドチェックが修正されています。以前のコードでは、このチェックがif(0 && o != (int32)o)という形で実質的に無効化されていました。これは、ARMやWindows環境で問題を引き起こしていたための一時的な措置でした。

修正後のコードでは、リロケーションのタイプ(r->type)に応じて、オフセットoが適切に32ビットの範囲に収まるかをチェックするようになりました。

  • PC相対リロケーション (r->type == D_PCREL) の場合: o != (int32)oという条件でチェックされます。これは、計算されたオフセットoが32ビット符号付き整数(int32)の範囲に収まるかどうかを確認しています。PC相対オフセットは負の値も取りうるため、符号付き整数として扱われるのが正しい挙動です。もしoint32の範囲を超えていれば、ラップアラウンドが発生していると判断され、エラーが報告されます。
  • その他のリロケーション (r->type != D_PCREL) の場合: o != (uint32)oという条件でチェックされます。これは、計算されたオフセットoが32ビット符号なし整数(uint32)の範囲に収まるかどうかを確認しています。絶対アドレスなど、負のオフセットを考慮する必要がないリロケーションタイプでは、符号なし整数として扱われるのが適切です。

この変更により、リロケーションタイプに応じた厳密なアドレス範囲チェックが導入され、リンカが不正なアドレスを生成するのを防ぎます。

src/cmd/ld/ldpe.cの変更点

このファイルは、WindowsのPE形式に特化したリンカの処理を扱っています。ldpe関数内で、特定のリロケーションタイプに対するrp->add(リロケーションエントリのアドレス)の計算が修正されています。

具体的には、IMAGE_REL_AMD64_ADDR32およびIMAGE_REL_AMD64_ADDR32NBというAMD64アーキテクチャ用の32ビットアドレスリロケーションタイプにおいて、rp->addに値を代入する際に明示的に(int32)へのキャストが追加されました。

  • 変更前: rp->add = le32(rsect->base+rp->off);
  • 変更後: rp->add = (int32)le32(rsect->base+rp->off);

le32関数はリトルエンディアン形式の32ビット値を読み取るものですが、その結果が符号なしとして扱われる可能性がありました。WindowsのPE形式におけるこれらのリロケーションタイプは、32ビットのオフセットを符号付きとして解釈することが期待される場合があります。明示的にint32にキャストすることで、リンカが生成するリロケーションエントリがWindowsのローダーによって正しく解釈されるようになります。これにより、Windows環境での32ビットリロケーションにおける符号の不一致問題が解消されます。

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

src/cmd/ld/data.c

--- a/src/cmd/ld/data.c
+++ b/src/cmd/ld/data.c
@@ -259,8 +259,7 @@ relocsym(Sym *s)
 			cursym = s;
 			diag("bad reloc size %#ux for %s", siz, r->sym->name);
 		case 4:
-			// TODO: Test is causing breakages on ARM and Windows. Disable for now.
-			if(0 && o != (int32)o) {
+			if((r->type == D_PCREL && o != (int32)o) || (r->type != D_PCREL && o != (uint32)o)) {
 				cursym = S;
 				diag("relocation address is too big: %#llx", o);
 			}

src/cmd/ld/ldpe.c

--- a/src/cmd/ld/ldpe.c
+++ b/src/cmd/ld/ldpe.c
@@ -291,7 +291,7 @@ ldpe(Biobuf *f, char *pkg, int64 len, char *pn)
 				case IMAGE_REL_AMD64_ADDR32: // R_X86_64_PC32
 				case IMAGE_REL_AMD64_ADDR32NB:
 					rp->type = D_PCREL;
-					rp->add = le32(rsect->base+rp->off);
+					rp->add = (int32)le32(rsect->base+rp->off);
 					break;
 				case IMAGE_REL_I386_DIR32NB:
 				case IMAGE_REL_I386_DIR32:

コアとなるコードの解説

src/cmd/ld/data.cの変更解説

変更の中心は、relocsym関数内のcase 4:ブロック、すなわち4バイト(32ビット)のリロケーションを処理する部分です。

元のコードでは、if(0 && o != (int32)o)という条件がありました。0 &&があるため、この条件は常に偽となり、o != (int32)oというアドレスラップアラウンドチェックは実行されませんでした。これは、ARMやWindowsでのテストが壊れる原因となっていたため、一時的に無効化されていたことを示唆しています。

修正後のコードif((r->type == D_PCREL && o != (int32)o) || (r->type != D_PCREL && o != (uint32)o))は、このチェックをリロケーションのタイプに応じて再有効化し、より正確なものにしています。

  • r->type == D_PCREL: これは、現在処理しているリロケーションがPC相対リロケーションであることを意味します。PC相対オフセットは、現在の命令ポインタからの相対的な距離であり、前方向(負のオフセット)も後方向(正のオフセット)も取りうるため、符号付き整数として扱われるべきです。o != (int32)oは、oが32ビット符号付き整数の最大値または最小値を超えている場合に真となります。例えば、o0x80000000int32の最小値)より小さいか、0x7FFFFFFFint32の最大値)より大きい場合、この条件は真となり、アドレスが大きすぎるというエラーが報告されます。
  • r->type != D_PCREL: これは、PC相対ではないリロケーション、例えば絶対アドレスリロケーションなどを指します。これらのオフセットは通常、負の値を取る必要がなく、0以上の値のみを表現できれば十分です。そのため、符号なし整数として扱われるべきです。o != (uint32)oは、oが32ビット符号なし整数の最大値0xFFFFFFFFを超えている場合に真となります。

この論理的な分離により、リンカは各リロケーションタイプに適切なアドレス範囲チェックを適用し、アドレスのラップアラウンドによる潜在的な問題を未然に防ぐことができます。

src/cmd/ld/ldpe.cの変更解説

このファイルはWindowsのPE形式に特化しており、ldpe関数内でPEファイルのリロケーションエントリを処理しています。

変更は、IMAGE_REL_AMD64_ADDR32IMAGE_REL_AMD64_ADDR32NBというAMD64アーキテクチャ用のリロケーションタイプに対する処理です。これらのタイプは、32ビットの絶対アドレスを指すリロケーションです。

元のコードでは、rp->add = le32(rsect->base+rp->off);と記述されていました。ここでle32はリトルエンディアン形式の32ビット値を読み取る関数です。C言語では、特定の型キャストがない場合、le32の戻り値が符号なしとして扱われる可能性があります。

修正後のコードでは、rp->add = (int32)le32(rsect->base+rp->off);と明示的に(int32)へのキャストが追加されています。このキャストは、le32から返された32ビット値が、rp->addに代入される際に確実に符号付き32ビット整数として解釈されるようにします。

WindowsのPEローダーは、これらのリロケーションタイプに対して32ビットのオフセットを符号付きとして解釈することを期待する場合があります。例えば、ベースアドレスからの相対オフセットが負になる場合などです。この明示的なキャストにより、リンカが生成するPEファイルのリロケーション情報がWindowsの仕様と一致し、プログラムが正しくロードおよび実行されることが保証されます。これにより、Windows環境での32ビットリロケーションにおける符号の不一致に起因するバグが修正されます。

関連リンク

  • Go言語のリンカに関するドキュメント(公式ドキュメントやGoのソースコード内のコメントを参照)
  • PEファイル形式の仕様(Microsoftの公式ドキュメント)
  • リロケーション、PC相対アドレッシングに関するコンピュータアーキテクチャやオペレーティングシステムの教科書

参考にした情報源リンク