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

[インデックス 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ファイルなど)やライブラリを結合し、実行可能なプログラムや共有ライブラリを生成するソフトウェアツールです。リンカの主な役割は以下の通りです。

  1. シンボル解決: オブジェクトファイル間で未解決のシンボル(関数名や変数名など)を解決し、それらが定義されている実際のアドレスに紐付けます。
  2. 再配置 (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ビットアーキテクチャ上で、特にリロケーション処理において発生していた複数の問題を解決しています。

主な問題点は以下の通りです。

  1. 32ビットの「穴」: リンカ内部で64ビットのメモリアドレスやオフセットを扱うべき箇所で、誤って32ビットの変数(int32など)に格納しようとしていました。これにより、上位32ビットの情報が切り捨てられ、不正なアドレス計算やリロケーションが行われる可能性がありました。
  2. リロケーションオーバーフロー: 特にrelocsym関数内で、リロケーションオフセットが32ビットの範囲を超えた場合に、そのオーバーフローを検出していませんでした。
  3. セグメントサイズのオーバーフロー: データセグメント(.data.bssなど)やテキストセグメント(.text)の合計サイズが32ビットの範囲を超えた場合に、リンカがこれを正しく扱えず、診断メッセージも出力していませんでした。

これらの問題に対処するため、コミットでは以下の技術的変更が加えられています。

  • vlong 型への変更: 多くの関数引数やローカル変数で、int32vlong(C言語における64ビット整数型、Goリンカの文脈ではlong longに相当)に変更されています。これにより、64ビットの値を正確に保持できるようになります。例えば、addaddrplus4setuintxxadduintxxaddaddrplusaddpcrelplussetaddrplusなどの関数で引数の型が変更されています。
  • サイズチェックの追加:
    • relocsym関数内で、リロケーションオフセットoint32の範囲に収まるかどうかのチェックが追加されました。もしoint32の範囲を超えていれば、「relocation address is too big」という診断メッセージが出力されます。
    • dodata関数内で、データセグメントとテキストセグメントの最終的なサイズがuint32の範囲に収まるかどうかのチェックが追加されました。もし範囲を超えていれば、「data or bss segment too large」または「text segment too large」という診断メッセージが出力されます。これは、6gリンカが4バイトのリロケーションオフセットを使用するため、セグメント全体が32ビットに収まる必要があるという制約に対応しています。
  • growdatsize関数の導入: dodata関数内で、各シンボルのサイズをdatsizeに加算する処理がgrowdatsizeという新しいヘルパー関数にまとめられました。この関数は、シンボルのサイズが負でないこと、およびdatsizes->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.csrc/cmd/ld/elf.csrc/cmd/ld/ldmacho.csrc/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関数内で、リロケーションオフセットoint32の範囲に収まるかどうかのチェックを追加。
    @@ -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ビット環境でメモリアドレスやオフセットを扱う際の精度と堅牢性を向上させることにあります。

  1. vlong 型への移行:

    • src/cmd/ld/data.csrc/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.casmbelf関数でも、ローカル変数a, ointからvlongに変更されており、ELFファイルのセクションやオフセットを扱う際の精度が向上しています。
  2. リロケーションアドレスのサイズチェック:

    • src/cmd/ld/data.crelocsym関数に、リロケーションオフセットoint32の範囲に収まるかどうかのチェックが追加されました。
    • if(o != (int32)o)という条件は、oint32にキャストされた後に元の値と異なる場合、つまりoint32の表現範囲を超えていることを検出します。この場合、「relocation address is too big」という診断メッセージが出力され、リンカが不正なリロケーションを試みる前に問題を特定できるようになります。これは、特にPC相対リロケーションなどで、参照元と参照先の距離が32ビットオフセットの範囲を超える場合に重要です。
  3. セグメントサイズオーバーフローの検出とgrowdatsizeの導入:

    • src/cmd/ld/data.cdodata関数では、データセグメントとテキストセグメントの合計サイズdatsizeint32からvlongに変更されました。これにより、非常に大きなプログラムのセグメントサイズも正確に計算できるようになります。
    • さらに、dodata関数内でdatsize += s->size;という直接的な加算の代わりに、新しく導入されたgrowdatsize(&datsize, s);というヘルパー関数が使用されるようになりました。
    • growdatsize関数は、シンボルのサイズs->sizeが負でないこと、およびdatsizes->sizeの合計がオーバーフローしないこと(datsize + s->size < datsize)をチェックします。これにより、セグメントサイズの計算における潜在的なオーバーフローを早期に検出し、診断メッセージを出力できるようになります。
    • dodata関数の最後には、データ/BSSセグメントとテキストセグメントの最終的なサイズがuint32の範囲に収まるかどうかのチェックが追加されました。これは、Goのリンカ(特に6g)が4バイトのリロケーションオフセットを使用するため、セグメント全体が32ビットの範囲に収まる必要があるという制約に対応するためのものです。このチェックにより、リンカが生成するバイナリがこの制約を満たしていることを保証します。
  4. 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アプリケーションのビルドにおける安定性と信頼性を向上させるものです。

関連リンク

参考にした情報源リンク