[インデックス 16241] ファイルの概要
このコミットは、Go言語のリンカ(cmd/ld
)における64ビット環境での問題を修正することを目的としています。具体的には、リンカが64ビット値を32ビットの「穴」を通して処理してしまうことや、リロケーション処理におけるオーバーフローの問題に対処しています。これにより、リンカの堅牢性が向上し、特に64ビットアーキテクチャ上でのGoプログラムのビルドにおける潜在的なバグが解消されます。
コミット
commit e4c4edf6819726886d05f33a01f98a117863bfb2
Author: Rob Pike <r@golang.org>
Date: Mon Apr 29 22:44:20 2013 -0700
cmd/ld: fix some 64-bit issues
A few places in the linker pushed 64-bit values through 32-bit holes,
including in relocation.
Clean them up, and check for a few other overflows as well.
Tests to follow.
R=dsymonds
CC=gobot, golang-dev
https://golang.org/cl/9032043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e4c4edf6819726886d05f33a01f98a117863bfb2
元コミット内容
cmd/ld: fix some 64-bit issues
A few places in the linker pushed 64-bit values through 32-bit holes,
including in relocation.
Clean them up, and check for a few other overflows as well.
Tests to follow.
変更の背景
この変更の背景には、Go言語のリンカ(cmd/ld
)が64ビットシステム上で正しく動作しない、あるいは予期せぬ動作を引き起こす可能性のあるバグが存在したことがあります。具体的には、リンカ内部で64ビットのメモリアドレスやオフセットを扱う際に、誤って32ビットの変数や型にキャストしてしまう「32ビットの穴(32-bit holes)」が存在していました。これにより、本来64ビットで表現されるべき情報が切り捨てられ、不正なアドレス計算やリロケーションが行われる可能性がありました。
特に、リロケーション(再配置)処理においてこの問題が発生すると、プログラムがメモリにロードされる際に、関数呼び出しやデータ参照が誤ったアドレスを指すことになり、クラッシュや未定義動作を引き起こす原因となります。また、データセグメントやBSSセグメントのサイズが32ビットの範囲を超えた場合に、リンカがそのサイズを正しく扱えず、オーバーフローが発生する可能性も指摘されていました。
このコミットは、これらの問題を解決し、Go言語が64ビットアーキテクチャ上でより安定して動作するようにするための重要な修正です。
前提知識の解説
32ビットと64ビットアーキテクチャ
コンピュータのアーキテクチャにおける32ビットと64ビットの主な違いは、CPUが一度に処理できるデータの量と、アドレス指定可能なメモリ空間のサイズにあります。
- 32ビットアーキテクチャ:
- CPUのレジスタが32ビット幅です。
- 最大で2^32バイト(約4GB)のメモリを直接アドレス指定できます。これは、32ビットOSやアプリケーションが利用できるRAMの上限となります。
- ポインタのサイズも32ビット(4バイト)です。
- 64ビットアーキテクチャ:
- CPUのレジスタが64ビット幅です。
- 理論上、2^64バイト(約18.4エクサバイト)という膨大なメモリをアドレス指定できます。これにより、4GBを超える大容量メモリを効率的に利用できます。
- ポインタのサイズは64ビット(8バイト)です。
- より大きなデータを一度に処理できるため、メモリを大量に消費するアプリケーションや、複雑な計算を行うアプリケーションで性能向上が期待できます。
リンカが64ビット環境で32ビットの「穴」を持つということは、64ビットのポインタやアドレスを扱うべき場所で、誤って32ビットの型に格納しようとし、上位32ビットの情報が失われることを意味します。
リンカ (Linker)
リンカは、コンパイラによって生成された複数のオブジェクトファイル(.o
ファイルなど)やライブラリを結合し、実行可能なプログラムや共有ライブラリを生成するソフトウェアツールです。リンカの主な役割は以下の通りです。
- シンボル解決: オブジェクトファイル間で未解決のシンボル(関数名や変数名など)を解決し、それらが定義されている実際のアドレスに紐付けます。
- 再配置 (Relocation): オブジェクトファイル内のコードやデータが、最終的な実行ファイル内でどのメモリ位置に配置されるかによって、内部の参照アドレスを調整します。
再配置 (Relocation)
再配置は、リンカの最も重要な機能の一つです。コンパイラがオブジェクトファイルを生成する際、コードやデータ内のアドレスは、そのオブジェクトファイル内での相対的な位置や、仮のアドレスで表現されています。しかし、複数のオブジェクトファイルが結合され、最終的な実行ファイルが生成されると、これらのコードやデータはメモリ上の具体的な絶対アドレスに配置されます。
再配置は、この絶対アドレスに基づいて、コード内の参照(例: 関数呼び出しのターゲットアドレス、グローバル変数へのアクセス)を修正するプロセスです。リンカはオブジェクトファイルに含まれる「再配置テーブル」を参照し、各エントリが示すアドレスを、最終的なメモリ配置に合わせて調整します。
64ビット環境での再配置の問題は、特に大きなプログラムやライブラリにおいて、参照元と参照先のアドレス間の距離が32ビットのオフセットで表現できる範囲を超えてしまう「リロケーションオーバーフロー」として現れることがあります。
Go言語における vlong
型
Go言語には、C言語の long long
に相当する vlong
という組み込み型は存在しません。Go言語では、64ビット整数を扱うために int64
(符号付き) や uint64
(符号なし) 型を使用します。このコミットのコード変更で vlong
が使われているのは、Go言語のリンカがC言語で書かれているためです。Go言語のリンカは、Go言語で書かれたプログラムをリンクしますが、リンカ自体はC言語で実装されています。
ELF (Executable and Linkable Format)
ELFは、Unix系OS(Linuxなど)で広く使用されている実行可能ファイル、オブジェクトファイル、共有ライブラリの標準ファイルフォーマットです。ELFファイルは、ヘッダ、プログラムヘッダテーブル、セクションヘッダテーブル、そして様々なセクション(コード、データ、シンボルテーブル、再配置情報など)で構成されます。リンカはELFフォーマットのファイルを読み書きし、再配置情報を処理します。
Mach-O (Mach object)
Mach-Oは、macOSやiOSなどのApple製OSで使われている実行可能ファイル、オブジェクトファイル、共有ライブラリのファイルフォーマットです。ELFと同様に、Mach-Oファイルもヘッダ、ロードコマンド、セグメント、セクションなどで構成され、リンカはMach-Oフォーマットのファイルを処理します。
技術的詳細
このコミットは、Go言語のリンカ(cmd/ld
)が64ビットアーキテクチャ上で、特にリロケーション処理において発生していた複数の問題を解決しています。
主な問題点は以下の通りです。
- 32ビットの「穴」: リンカ内部で64ビットのメモリアドレスやオフセットを扱うべき箇所で、誤って32ビットの変数(
int32
など)に格納しようとしていました。これにより、上位32ビットの情報が切り捨てられ、不正なアドレス計算やリロケーションが行われる可能性がありました。 - リロケーションオーバーフロー: 特に
relocsym
関数内で、リロケーションオフセットが32ビットの範囲を超えた場合に、そのオーバーフローを検出していませんでした。 - セグメントサイズのオーバーフロー: データセグメント(
.data
、.bss
など)やテキストセグメント(.text
)の合計サイズが32ビットの範囲を超えた場合に、リンカがこれを正しく扱えず、診断メッセージも出力していませんでした。
これらの問題に対処するため、コミットでは以下の技術的変更が加えられています。
vlong
型への変更: 多くの関数引数やローカル変数で、int32
がvlong
(C言語における64ビット整数型、Goリンカの文脈ではlong long
に相当)に変更されています。これにより、64ビットの値を正確に保持できるようになります。例えば、addaddrplus4
、setuintxx
、adduintxx
、addaddrplus
、addpcrelplus
、setaddrplus
などの関数で引数の型が変更されています。- サイズチェックの追加:
relocsym
関数内で、リロケーションオフセットo
がint32
の範囲に収まるかどうかのチェックが追加されました。もしo
がint32
の範囲を超えていれば、「relocation address is too big」という診断メッセージが出力されます。dodata
関数内で、データセグメントとテキストセグメントの最終的なサイズがuint32
の範囲に収まるかどうかのチェックが追加されました。もし範囲を超えていれば、「data or bss segment too large」または「text segment too large」という診断メッセージが出力されます。これは、6gリンカが4バイトのリロケーションオフセットを使用するため、セグメント全体が32ビットに収まる必要があるという制約に対応しています。
growdatsize
関数の導入:dodata
関数内で、各シンボルのサイズをdatsize
に加算する処理がgrowdatsize
という新しいヘルパー関数にまとめられました。この関数は、シンボルのサイズが負でないこと、およびdatsize
とs->size
の合計がオーバーフローしないことをチェックします。これにより、データセグメントのサイズ計算における堅牢性が向上しています。- Mach-Oリンカの修正:
ldmacho.c
において、rp->add
への代入時にe->e32(s->p+rp->off)
の結果を明示的にint32
にキャストする修正が加えられています。これは、Mach-OのPC相対リロケーションにおける加算値の計算が32ビットの範囲で行われることを保証するためです。
これらの変更により、Goリンカは64ビット環境でのアドレス計算やリロケーション処理をより正確に行えるようになり、大規模なGoプログラムのビルドにおける安定性が向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/cmd/ld/data.c
、src/cmd/ld/elf.c
、src/cmd/ld/ldmacho.c
、src/cmd/ld/lib.h
の4つのファイルにわたっています。
src/cmd/ld/data.c
addaddrplus4
関数の引数add
の型がint32
からvlong
に変更。-static vlong addaddrplus4(Sym *s, Sym *t, int32 add); +static vlong addaddrplus4(Sym *s, Sym *t, vlong add);
relocsym
関数内で、リロケーションオフセットo
がint32
の範囲に収まるかどうかのチェックを追加。@@ -259,6 +259,10 @@ relocsym(Sym *s) cursym = s; diag("bad reloc size %#ux for %s", siz, r->sym->name); case 4: + if(o != (int32)o) { + cursym = S; + diag("relocation address is too big: %#llx", o); + } fl = o; cast = (uchar*)&fl; for(i=0; i<4; i++)
setuintxx
関数の引数wid
の型がint
からvlong
に変更。-setuintxx(Sym *s, vlong off, uint64 v, int wid) +setuintxx(Sym *s, vlong off, uint64 v, vlong wid)
adduintxx
関数のローカル変数off
の型がint32
からvlong
に変更。- int32 off; + vlong off;
addaddrplus
,addpcrelplus
,setaddrplus
関数の引数add
の型がint32
からvlong
に変更。-addaddrplus(Sym *s, Sym *t, int32 add) +addaddrplus(Sym *s, Sym *t, vlong add) // ... -addpcrelplus(Sym *s, Sym *t, int32 add) +addpcrelplus(Sym *s, Sym *t, vlong add) // ... -setaddrplus(Sym *s, vlong off, Sym *t, int32 add) +setaddrplus(Sym *s, vlong off, Sym *t, vlong add)
aligndatsize
関数の引数datsize
と戻り値の型がint32
からvlong
に変更。-static int32 -aligndatsize(int32 datsize, Sym *s) +static vlong +aligndatsize(vlong datsize, Sym *s)
gcaddsym
関数の引数off
とローカル変数a
の型がint32
からvlong
に変更。-static void -gcaddsym(Sym *gc, Sym *s, int32 off) +static void +gcaddsym(Sym *gc, Sym *s, vlong off) // ... - int32 a; + vlong a;
growdatsize
関数の新規追加。void growdatsize(vlong *datsizep, Sym *s) { vlong datsize; datsize = *datsizep; if(s->size < 0) diag("negative size (datsize = %lld, s->size = %lld)", datsize, s->size); if(datsize + s->size < datsize) diag("symbol too large (datsize = %lld, s->size = %lld)", datsize, s->size); *datsizep = datsize + s->size; }
dodata
関数内で、datsize
の型がint32
からvlong
に変更され、datsize += s->size;
の代わりにgrowdatsize(&datsize, s);
が使用されるように変更。- int32 n, datsize; + int32 n; + vlong datsize; // ... - datsize += s->size; + growdatsize(&datsize, s);
dodata
関数内で、データセグメントとテキストセグメントのサイズがuint32
の範囲に収まるかどうかのチェックを追加。@@ -1201,10 +1219,15 @@ dodata(void) sect->len = datsize - sect->vaddr; lookup("end", 0)->sect = sect; + + // 6g uses 4-byte relocation offsets, so the entire segment must fit in 32 bits. + if(datsize != (uint32)datsize) { + diag("data or bss segment too large"); + } if(iself && linkmode == LinkExternal && s != nil && s->type == STLSBSS && HEADTYPE != Hopenbsd) { sect = addsection(&segdata, ".tbss", 06); // ... @@ -1301,9 +1324,14 @@ dodata(void) sect->len = datsize - sect->vaddr; } + + // 6g uses 4-byte relocation offsets, so the entire segment must fit in 32 bits. + if(datsize != (uint32)datsize) { + diag("text segment too large"); + } /* number the sections */ n = 1;
src/cmd/ld/elf.c
asmbelf
関数のローカル変数a
,o
の型がint
からvlong
に変更。- int a, o; + vlong a, o;
src/cmd/ld/ldmacho.c
ldmacho
関数内で、rp->add
への代入時に明示的なint32
キャストを追加。@@ -804,9 +804,9 @@ ldmacho(Biobuf *f, char *pkg, int64 len, char *pn) // // [For future reference, see Darwin's /usr/include/mach-o/x86_64/reloc.h] secaddr = c->seg.sect[rel->symnum-1].addr; - rp->add = e->e32(s->p+rp->off) + rp->off + 4 - secaddr; + rp->add = (int32)e->e32(s->p+rp->off) + rp->off + 4 - secaddr; } else - rp->add = e->e32(s->p+rp->off); + rp->add = (int32)e->e32(s->p+rp->off);
src/cmd/ld/lib.h
- リンカ内部で使用される関数のプロトタイプで、引数の型が
int32
からvlong
に変更。-vlong addaddrplus(Sym*, Sym*, int32); -vlong addpcrelplus(Sym*, Sym*, int32); +vlong addaddrplus(Sym*, Sym*, vlong); +vlong addpcrelplus(Sym*, Sym*, vlong); vlong addsize(Sym*, Sym*); -vlong setaddrplus(Sym*, vlong, Sym*, int32); +vlong setaddrplus(Sym*, vlong, Sym*, vlong);
コアとなるコードの解説
このコミットの核心は、Goリンカが64ビット環境でメモリアドレスやオフセットを扱う際の精度と堅牢性を向上させることにあります。
-
vlong
型への移行:src/cmd/ld/data.c
とsrc/cmd/ld/lib.h
におけるaddaddrplus4
,setuintxx
,adduintxx
,addaddrplus
,addpcrelplus
,setaddrplus
,aligndatsize
,gcaddsym
などの関数で、int32
型で定義されていた引数やローカル変数がvlong
型に変更されています。- これは、これらの関数がメモリアドレス、オフセット、サイズなどの値を扱う際に、32ビットの範囲を超えた64ビットの値を正確に保持できるようにするためです。以前は、64ビットの値が32ビットの変数に格納されることで、上位ビットが切り捨てられ、不正なアドレス計算やリロケーションが発生する可能性がありました。
vlong
(C言語のlong long
に相当)を使用することで、この「32ビットの穴」が塞がれます。 src/cmd/ld/elf.c
のasmbelf
関数でも、ローカル変数a
,o
がint
からvlong
に変更されており、ELFファイルのセクションやオフセットを扱う際の精度が向上しています。
-
リロケーションアドレスのサイズチェック:
src/cmd/ld/data.c
のrelocsym
関数に、リロケーションオフセットo
がint32
の範囲に収まるかどうかのチェックが追加されました。if(o != (int32)o)
という条件は、o
がint32
にキャストされた後に元の値と異なる場合、つまりo
がint32
の表現範囲を超えていることを検出します。この場合、「relocation address is too big」という診断メッセージが出力され、リンカが不正なリロケーションを試みる前に問題を特定できるようになります。これは、特にPC相対リロケーションなどで、参照元と参照先の距離が32ビットオフセットの範囲を超える場合に重要です。
-
セグメントサイズオーバーフローの検出と
growdatsize
の導入:src/cmd/ld/data.c
のdodata
関数では、データセグメントとテキストセグメントの合計サイズdatsize
がint32
からvlong
に変更されました。これにより、非常に大きなプログラムのセグメントサイズも正確に計算できるようになります。- さらに、
dodata
関数内でdatsize += s->size;
という直接的な加算の代わりに、新しく導入されたgrowdatsize(&datsize, s);
というヘルパー関数が使用されるようになりました。 growdatsize
関数は、シンボルのサイズs->size
が負でないこと、およびdatsize
とs->size
の合計がオーバーフローしないこと(datsize + s->size < datsize
)をチェックします。これにより、セグメントサイズの計算における潜在的なオーバーフローを早期に検出し、診断メッセージを出力できるようになります。dodata
関数の最後には、データ/BSSセグメントとテキストセグメントの最終的なサイズがuint32
の範囲に収まるかどうかのチェックが追加されました。これは、Goのリンカ(特に6g)が4バイトのリロケーションオフセットを使用するため、セグメント全体が32ビットの範囲に収まる必要があるという制約に対応するためのものです。このチェックにより、リンカが生成するバイナリがこの制約を満たしていることを保証します。
-
Mach-Oリロケーションの修正:
src/cmd/ld/ldmacho.c
では、Mach-O形式のバイナリを扱う際に、rp->add
への代入時にe->e32(s->p+rp->off)
の結果を明示的にint32
にキャストする修正が加えられました。- これは、Mach-OのPC相対リロケーションにおける加算値の計算が32ビットの範囲で行われることを保証するためです。
e->e32
は32ビットの値を読み取る関数ですが、その結果がvlong
型のrp->add
に代入される際に、意図しない型変換や値の解釈を防ぐために明示的なキャストが導入されました。
これらの変更は、Goリンカが64ビット環境でより正確かつ堅牢に動作するための基盤を強化し、大規模なGoアプリケーションのビルドにおける安定性と信頼性を向上させるものです。
関連リンク
- Go言語のリンカにおける64ビット問題とリロケーションオーバーフローに関する議論: https://github.com/golang/go/issues/11937 (これは一般的な問題に関するもので、直接このコミットのCLではないが、背景理解に役立つ)
- Go言語のリンカのソースコード: https://github.com/golang/go/tree/master/src/cmd/link (現在のリンカは
cmd/link
に統合されている)
参考にした情報源リンク
- Go linker 64-bit issues relocation: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEi1axKSt7bEan2BMy_EpYNCpu6p1QsMMLmeUJd9mZYdTr-kLzUUm0RM8W3GI5oW6MpFqTo9OY3wv4kRNbidZiT4rOw2EDw6xn2nkVJ5jKSjAbxmF-IsXONISLOSZehCrIVYAD1o4qh3RgAJWeax0=
- Linker relocation process explained: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEB_Rlt9AM8uTzCOqO_fvRcX4-KOvZhMSBcrD__Am_VShx_99cD2HZBOsjxyTvh1WYjNySk75Hu2aGdsK6qwagHm1AKNILZUP5dYfE2zq9slqTe7vND8FZqiPWwdccizR8yx3JxCA7gioR2qxJjaANLNvwpkc2oMwOPIY3lHkzwRdfinuyOSG5Hsa1LFsZ5VlCZJXFy4216Yd7m0Znfjzp3hOwQi_gwbuzMpES8
- 32-bit vs 64-bit architecture differences programming: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEB0KU8b2dhxviIGMhQYpefQ_dOM1cTbF9CeVvyMzvexrerWZ1W_QsgaWxIiG0trejjTFTbWUHzCsadBz4lNJgC6sfSNMr0X-DNmvGitmLR7XhWj_22diKFSaAIPY8AbCrrEku7ysM_
- Go vlong type: (検索結果からGoには
vlong
型がないことが確認されたため、特定のリンクはなし) - ELF file format relocation: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHnWHU7EHp_mcHuz8kBpGTF5bN_II06pcUm-o7uXU5Od2Lo9plJC9Pm9K9QUa53chP0JwdBon9CneyMfxhUdopnzgkwfUoMAJIzGrTeMD69klY5G10Zzx28ws4T-6B_RktPe3PYJK6-6Bj3SAgCJ3PaQoFf
- Mach-O file format relocation: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEoZzkod-20y0AgNHu_lPqU5KjaerN004rpPJn5Mk_N8QayPn0C4EGvGygUnFKfqTizOSUgc2ejxpAbJKnrhmhkGFFvJN2szRUOcCxIpHJDq6D4huOPfKZpDDhzaZ7UPw==