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

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

このコミットは、Go言語のデバッグツールであるcmd/addr2lineと、Plan 9 a.out形式のオブジェクトファイルを扱うdebug/plan9objパッケージにおける修正です。具体的には、Plan 9環境において実行ファイルのテキストセグメントの開始アドレスを正確に特定するためのロジックが改善されました。これまではリンカが追加するtextシンボルに依存していましたが、これがamd64アーキテクチャで誤った値を示す問題があったため、a.outヘッダのサイズとロードアドレスを直接利用するように変更されました。

コミット

commit 0a2083edd7a31f2248da1cdaca6e39466a9fb05b
Author: Aram Hăvărneanu <aram@mgk.ro>
Date:   Wed Jul 9 12:33:13 2014 +0200

    debug/plan9obj, cmd/addr2line: on Plan 9 use a.out header
    size instead of abusing text symbol
    
    cmd/addr2line needs to know the virtual address of the start
    of the text segment (load address plus header size). For
    this, it used the text symbol added by the linker. This is
    wrong on amd64. Header size is 40 bytes, not 32 like on 386
    and arm. Function alignment is 16 bytes causing text to be
    at 0x200030.
    
    debug/plan9obj now exports both the load address and the
    header size; cmd/addr2line uses this new information and
    doesn't rely on text anymore.
    
    LGTM=0intro
    R=0intro, gobot, ality
    CC=ality, golang-codereviews, jas, mischief
    https://golang.org/cl/106460044

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

https://github.com/golang/go/commit/0a2083edd7a31f2248da1cdaca6e39466a9fb05b

元コミット内容

debug/plan9obj, cmd/addr2line: on Plan 9 use a.out header
size instead of abusing text symbol

cmd/addr2line needs to know the virtual address of the start
of the text segment (load address plus header size). For
this, it used the text symbol added by the linker. This is
wrong on amd64. Header size is 40 bytes, not 32 like on 386
and arm. Function alignment is 16 bytes causing text to be
at 0x200030.

debug/plan9obj now exports both the load address and the
header size; cmd/addr2line uses this new information and
doesn't rely on text anymore.

LGTM=0intro
R=0intro, gobot, ality
CC=ality, golang-codereviews, jas, mischief
https://golang.org/cl/106460044

変更の背景

cmd/addr2lineは、実行ファイル内のアドレスから対応するソースコードのファイル名と行番号を特定するためのツールです。このツールが正しく機能するためには、実行ファイルのテキストセグメント(実行可能コードが格納されている領域)の仮想アドレス上の開始位置を正確に知る必要があります。

従来のcmd/addr2lineは、このテキストセグメントの開始位置を特定するために、リンカによって追加されるtextという特殊なシンボルの値に依存していました。しかし、このアプローチには以下の問題がありました。

  1. amd64アーキテクチャでの不正確さ: amd64アーキテクチャにおけるPlan 9 a.out形式のヘッダサイズは40バイトですが、386armアーキテクチャでは32バイトでした。textシンボルは、このヘッダサイズの差異を適切に考慮していなかったため、amd64環境ではテキストセグメントの実際の開始位置とtextシンボルの値がずれてしまうことがありました。
  2. 関数アライメントの影響: amd64では関数のアライメントが16バイトであるため、テキストセグメントが0x200030のような特定のアドレスから始まることがあり、これもtextシンボルが示す値との乖離を生じさせていました。

これらの問題により、cmd/addr2lineamd64環境のPlan 9 a.outファイルに対して正確な情報を提供できない可能性がありました。このコミットは、この不正確さを解消し、より堅牢な方法でテキストセグメントの開始位置を特定することを目的としています。

前提知識の解説

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

  • a.out形式: Unix系システムで古くから使われている実行ファイル形式の一つです。Plan 9オペレーティングシステムでも独自のa.out形式が採用されており、Go言語のツールチェインもPlan 9環境をサポートしています。a.outファイルは通常、ヘッダ、テキストセグメント(コード)、データセグメント(初期化済みデータ)、BSSセグメント(初期化されていないデータ)、シンボルテーブルなどから構成されます。
  • テキストセグメント (Text Segment): 実行可能コードが格納されるメモリ領域です。通常、読み取り専用としてマークされ、複数のプロセスで共有されることがあります。
  • ロードアドレス (Load Address): プログラムがメモリにロードされる際の仮想アドレス上の開始位置です。オペレーティングシステムや実行ファイル形式によって、デフォルトのロードアドレスが定められています。
  • ヘッダサイズ (Header Size): 実行ファイルのヘッダ部分のサイズです。ヘッダには、ファイルの種類、セグメントのサイズ、エントリポイントなどのメタデータが含まれます。テキストセグメントは通常、ヘッダの直後から始まります。
  • シンボル (Symbol): プログラム内の関数、変数、セクションなどの名前と、それに対応するアドレスやサイズなどの情報です。リンカによって生成され、デバッグや動的リンクに利用されます。textシンボルは、伝統的にテキストセグメントの開始位置を示すために使われることがあります。
  • cmd/addr2line: Go言語の標準ツールの一つで、実行ファイル内のアドレス(プログラムカウンタの値など)から、そのアドレスに対応するソースコードのファイル名と行番号を逆引きするデバッグツールです。クラッシュレポートの解析などで利用されます。
  • debug/plan9objパッケージ: Go言語の標準ライブラリの一部で、Plan 9 a.out形式のオブジェクトファイルを解析するための機能を提供します。ファイルヘッダやセクション情報、シンボルテーブルなどを読み取るためのAPIが含まれています。
  • 仮想アドレス (Virtual Address): プロセスが利用するメモリ空間のアドレスです。物理メモリのアドレスとは異なり、メモリ管理ユニット (MMU) によって物理アドレスに変換されます。
  • アライメント (Alignment): データやコードがメモリ上で特定のバイト境界に配置されることを保証する要件です。例えば、16バイトアライメントの場合、データのアドレスは16の倍数でなければなりません。これは、プロセッサが効率的にメモリにアクセスするために重要です。

技術的詳細

このコミットの技術的な核心は、cmd/addr2lineがテキストセグメントの開始アドレスを計算する方法を、不正確なtextシンボルへの依存から、より信頼性の高いdebug/plan9objパッケージが提供する情報へと変更した点にあります。

具体的には、以下の変更が行われました。

  1. debug/plan9obj/file.goの変更:

    • FileHeader構造体にLoadAddress (uint64) と HdrSize (uint64) フィールドが追加されました。これにより、Plan 9 a.outファイルのロードアドレスとヘッダサイズが明示的に構造体の一部として保持されるようになります。
    • NewFile関数(a.outファイルを解析してFile構造体を生成する関数)内で、これらの新しいフィールドが適切に初期化されるようになりました。
      • デフォルトのLoadAddress0x1000HdrSize4 * 8 = 32バイト(386/armの場合)と設定されます。
      • Magic64フラグ(64ビットa.outを示す)がセットされている場合、PtrSizeが8バイトに更新されるとともに、LoadAddress0x200000に、HdrSize8バイト追加されて40バイト(amd64の場合)に更新されます。これにより、amd64アーキテクチャにおけるヘッダサイズの差異が正確に反映されるようになりました。
    • セクションのオフセット計算において、これまではローカル変数hdrSizeを使用していた箇所が、f.HdrSizeを使用するように変更されました。これにより、ファイルヘッダから直接取得した正確なヘッダサイズが利用されるようになります。
  2. src/cmd/addr2line/main.goの変更:

    • loadPlan9Table関数内で、これまでfindPlan9Symbol(f, "text")を呼び出してtextシンボルの値を取得していた部分が削除されました。
    • 代わりに、f.LoadAddressf.HdrSizeという、debug/plan9objパッケージが新しく提供するフィールドを利用して、テキストセグメントの開始アドレスを計算するようになりました。具体的には、f.LoadAddress + f.HdrSizeがテキストセグメントの仮想アドレス上の開始位置として使用されます。
    • これにより、data[ssym.Value-text.Value : esym.Value-text.Value]という計算が、data[ssym.Value-(f.LoadAddress+f.HdrSize) : esym.Value-(f.LoadAddress+f.HdrSize)]に変更されました。この計算は、シンボルの値からテキストセグメントの開始アドレスを引くことで、データセグメント内での相対オフセットを正確に求めるためのものです。

この変更により、cmd/addr2lineは、リンカが生成するtextシンボルの潜在的な不正確さに依存することなく、Plan 9 a.outファイルの構造から直接、テキストセグメントの正確な開始アドレスを導き出せるようになりました。特にamd64アーキテクチャにおけるヘッダサイズの差異や関数アライメントによるオフセットの問題が解消され、より信頼性の高いアドレス解決が可能になります。

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

src/cmd/addr2line/main.go

--- a/src/cmd/addr2line/main.go
+++ b/src/cmd/addr2line/main.go
@@ -237,10 +237,6 @@ func loadPlan9Table(f *plan9obj.File, sname, ename string) ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	text, err := findPlan9Symbol(f, "text")
-	if err != nil {
-		return nil, err
-	}
 	sect := f.Section("text")
 	if sect == nil {
 		return nil, err
@@ -249,5 +245,5 @@ func loadPlan9Table(f *plan9obj.File, sname, ename string) ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	return data[ssym.Value-text.Value : esym.Value-text.Value], nil
+	return data[ssym.Value-(f.LoadAddress+f.HdrSize) : esym.Value-(f.LoadAddress+f.HdrSize)], nil
 }

src/pkg/debug/plan9obj/file.go

--- a/src/pkg/debug/plan9obj/file.go
+++ b/src/pkg/debug/plan9obj/file.go
@@ -15,10 +15,12 @@ import (
 
 // A FileHeader represents a Plan 9 a.out file header.
 type FileHeader struct {
-	Magic   uint32
-	Bss     uint32
-	Entry   uint64
-	PtrSize int
+	Magic       uint32
+	Bss         uint32
+	Entry       uint64
+	PtrSize     int
+	LoadAddress uint64
+	HdrSize     uint64
 }
 
 // A File represents an open Plan 9 a.out file.
@@ -148,20 +150,21 @@ func NewFile(r io.ReaderAt) (*File, error) {
 	}
 
 	f := &File{FileHeader: FileHeader{
-		Magic:   ph.Magic,
-		Bss:     ph.Bss,
-		Entry:   uint64(ph.Entry),
-		PtrSize: 4,
+		Magic:       ph.Magic,
+		Bss:         ph.Bss,
+		Entry:       uint64(ph.Entry),
+		PtrSize:     4,
+		LoadAddress: 0x1000,
+		HdrSize:     4 * 8,
 	}}
 
-	hdrSize := 4 * 8
-
 	if ph.Magic&Magic64 != 0 {
 		if err := binary.Read(sr, binary.BigEndian, &f.Entry); err != nil {
 			return nil, err
 		}
 		f.PtrSize = 8
-		hdrSize += 8
+		f.LoadAddress = 0x200000
+		f.HdrSize += 8
 	}
 
 	var sects = []struct {
@@ -177,7 +180,7 @@ func NewFile(r io.ReaderAt) (*File, error) {
 
 	f.Sections = make([]*Section, 5)
 
-	off := uint32(hdrSize)
+	off := uint32(f.HdrSize)
 
 	for i, sect := range sects {
 		s := new(Section)

コアとなるコードの解説

src/pkg/debug/plan9obj/file.goの変更点

  • FileHeader構造体の拡張:
    • LoadAddress uint64: 実行ファイルがメモリにロードされる際の仮想アドレス上の開始位置を保持します。
    • HdrSize uint64: 実行ファイルのヘッダのサイズを保持します。 これらのフィールドが追加されたことで、debug/plan9objパッケージはa.outファイルの基本的なレイアウト情報をより正確に表現できるようになりました。
  • NewFile関数での初期化ロジックの変更:
    • f.LoadAddressf.HdrSizeが初期化されます。デフォルト値は32ビット環境(386/arm)を想定した0x100032バイト(4 * 8)です。
    • Magic64フラグが検出された場合(64ビット環境、amd64など)、f.LoadAddress0x200000に、f.HdrSize40バイト(32 + 8)に更新されます。これは、amd64におけるPlan 9 a.outのヘッダサイズとロードアドレスの慣習を反映しています。
    • セクションのオフセット計算に使用されていたローカル変数hdrSizeが削除され、代わりにf.HdrSizeが直接使用されるようになりました。これにより、ファイルヘッダから読み取られた正確なヘッダサイズが、セクションの配置計算に反映されます。

src/cmd/addr2line/main.goの変更点

  • textシンボルへの依存の排除:
    • loadPlan9Table関数内で、これまでfindPlan9Symbol(f, "text")を呼び出してtextシンボルの値を取得していた行が削除されました。これは、textシンボルがamd64環境で不正確な値を示す可能性があったためです。
  • LoadAddressHdrSizeの利用:
    • テキストセグメント内のデータオフセットを計算する際に、ssym.Value - text.Value(シンボルの値からtextシンボルの値を引く)という計算から、ssym.Value - (f.LoadAddress + f.HdrSize)(シンボルの値からロードアドレスとヘッダサイズの合計を引く)という計算に変更されました。
    • f.LoadAddress + f.HdrSizeは、Plan 9 a.out形式において、ヘッダの直後に続くテキストセグメントの仮想アドレス上の開始位置を正確に表します。これにより、cmd/addr2lineは、実行ファイルの物理的な構造に基づいた正確なオフセット計算を行うことができるようになり、amd64環境でのアドレス解決の精度が向上しました。

これらの変更により、debug/plan9objパッケージがPlan 9 a.outファイルの構造に関するより詳細で正確な情報を提供するようになり、cmd/addr2lineはその情報を利用して、より堅牢で正確なアドレス解決を実現しています。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴: https://github.com/golang/go/commit/0a2083edd7a31f2248da1cdaca6e39466a9fb05b
  • Go言語のコードレビューシステム (Gerrit): https://golang.org/cl/106460044
  • Plan 9 a.out format (Wikipediaなど、一般的な情報源)
  • Go言語のcmd/addr2linedebug/plan9objのソースコード
  • Go言語のamd64アーキテクチャに関する情報 (Goのドキュメントやソースコード)
  • 関数アライメントに関する一般的なコンピュータアーキテクチャの知識
  • 仮想アドレスと物理アドレスに関する一般的なオペレーティングシステムの知識
  • 実行ファイル形式に関する一般的な知識 (a.out, ELF, PEなど)
  • シンボルテーブルとリンカに関する一般的な知識