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

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

このコミットは、Go言語のリンカ(cmd/ld)におけるMach-O形式の出力ファイル(特に6.outというGo特有の命名規則によるバイナリ)に関する修正です。具体的には、macOS上でGoバイナリをLLDBデバッガでデバッグする際に発生していた問題に対処しています。LLDBがDWARFデバッグ情報を正しく解釈できるよう、__DWARFセクションに仮想アドレスを割り当てる変更が加えられました。

コミット

commit a4a1fadfa2bdb4f6cfb1ed1dcf6b4bb0cd58060f
Author: Russ Cox <rsc@golang.org>
Date:   Tue May 20 11:35:20 2014 -0400

    cmd/ld: make lldb happy with Mach-O 6.out files

    Apparently all the __DWARF sections need addresses
    even though they are marked as "do not load from disk".
    Continue the address numbering from the data segment.

    With this change:

    g% lldb helloworld
    Current executable set to 'helloworld' (x86_64).
    (lldb) b main.main
    Breakpoint 1: where = helloworld`main.main + 25 at helloworld.go:12, address = 0x0000000000002019
    (lldb) r
    Process 68509 launched: '/Users/rsc/g/go/src/cmd/6l/helloworld' (x86_64)
    1 location added to breakpoint 1
    (lldb)
    Process 68509 stopped
    * thread #1: tid = 0x8b7a27, 0x0000000000002019 helloworld`main.main + 25 at helloworld.go:12, stop reason = breakpoint 1.2
        frame #0: 0x0000000000002019 helloworld`main.main + 25 at helloworld.go:12
       9    package main
       10
       11   func main() {
    -> 12           print("hello, world\\n")
       13   }
    (lldb) bt
    * thread #1: tid = 0x8b7a27, 0x0000000000002019 helloworld`main.main + 25 at helloworld.go:12, stop reason = breakpoint 1.2
      * frame #0: 0x0000000000002019 helloworld`main.main + 25 at helloworld.go:12
    (lldb) disas
    helloworld`main.main at helloworld.go:11:
       0x2000:  movq   %gs:0x8a0, %rcx
       0x2009:  cmpq   (%rcx), %rsp
       0x200c:  ja     0x2015                    ; main.main + 21 at helloworld.go:11
       0x200e:  callq  0x20da0                   ; runtime.morestack00_noctxt at atomic_amd64x.c:28
       0x2013:  jmp    0x2000                    ; main.main at helloworld.go:11
       0x2015:  subq   $0x10, %rsp
    -> 0x2019:  leaq   0x2c2e0, %rbx
       0x2021:  leaq   (%rsp), %rbp
       0x2025:  movq   %rbp, %rdi
       0x2028:  movq   %rbx, %rsi
       0x202b:  movsq
       0x202d:  movsq
       0x202f:  callq  0x10300                   ; runtime.printstring at compiler.go:1
       0x2034:  addq   $0x10, %rsp
       0x2038:  ret
       0x2039:  addb   %al, (%rax)
       0x203b:  addb   %al, (%rax)
       0x203d:  addb   %al, (%rax)
    (lldb) quit
    Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] y
    g%

    Fixes #7070.

    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/93510043

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

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

元コミット内容

cmd/ld: make lldb happy with Mach-O 6.out files

Apparently all the __DWARF sections need addresses
even though they are marked as "do not load from disk".
Continue the address numbering from the data segment.

With this change:

g% lldb helloworld
Current executable set to 'helloworld' (x86_64).
(lldb) b main.main
Breakpoint 1: where = helloworld`main.main + 25 at helloworld.go:12, address = 0x0000000000002019
(lldb) r
Process 68509 launched: '/Users/rsc/g/go/src/cmd/6l/helloworld' (x86_64)
1 location added to breakpoint 1
(lldb)
Process 68509 stopped
* thread #1: tid = 0x8b7a27, 0x0000000000002019 helloworld`main.main + 25 at helloworld.go:12, stop reason = breakpoint 1.2
    frame #0: 0x0000000000002019 helloworld`main.main + 25 at helloworld.go:12
   9    package main
   10
   11   func main() {
-> 12           print("hello, world\n")
   13   }
(lldb) bt
* thread #1: tid = 0x8b7a27, 0x0000000000002019 helloworld`main.main + 25 at helloworld.go:12, stop reason = breakpoint 1.2
  * frame #0: 0x0000000000002019 helloworld`main.main + 25 at helloworld.go:12
(lldb) disas
helloworld`main.main at helloworld.go:11:
   0x2000:  movq   %gs:0x8a0, %rcx
   0x2009:  cmpq   (%rcx), %rsp
   0x200c:  ja     0x2015                    ; main.main + 21 at helloworld.go:11
   0x200e:  callq  0x20da0                   ; runtime.morestack00_noctxt at atomic_amd64x.c:28
   0x2013:  jmp    0x2000                    ; main.main at helloworld.go:11
   0x2015:  subq   $0x10, %rsp
-> 0x2019:  leaq   0x2c2e0, %rbx
   0x2021:  leaq   (%rsp), %rbp
   0x2025:  movq   %rbp, %rdi
   0x2028:  movq   %rbx, %rsi
   0x202b:  movsq
   0x202d:  movsq
   0x202f:  callq  0x10300                   ; runtime.printstring at compiler.go:1
   0x2034:  addq   $0x10, %rsp
   0x2038:  ret
   0x2039:  addb   %al, (%rax)
   0x203b:  addb   %al, (%rax)
   0x203d:  addb   %al, (%rax)
(lldb) quit
Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] y
g%

Fixes #7070.

LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/93510043

変更の背景

このコミットは、Go言語でビルドされた実行可能ファイル(特にmacOS向けのMach-O形式のバイナリ)をLLDBデバッガでデバッグする際に発生していた問題を解決するために導入されました。

Goのリンカ(cmd/ld)は、デバッグ情報(DWARF)を含むセクションを生成しますが、これらのセクションは実行時にはメモリにロードされないようにマークされていました。これは、デバッグ情報がプログラムの実行に直接必要ないため、バイナリサイズを最適化し、ロード時間を短縮するための一般的なプラクティスです。

しかし、LLDBデバッガは、たとえディスクからロードされないとマークされていても、__DWARFセクション内のすべてのデバッグ情報に有効な仮想アドレスが割り当てられていることを期待していました。Goのリンカがこれらのセクションにアドレスを割り当てていなかったため、LLDBはデバッグ情報を正しく解釈できず、ブレークポイントの設定やスタックトレースの表示など、基本的なデバッグ機能が動作しないという問題が発生していました。

この問題は、GoのIssue #7070として報告されており、このコミットはその修正を目的としています。

前提知識の解説

Mach-O (Mach object)

Mach-Oは、macOS、iOS、watchOS、tvOSなどのApple製オペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、共有ライブラリ、動的にロードされるコード、およびコアダンプのファイル形式です。Mach-Oファイルは、以下の主要なコンポーネントで構成されます。

  • ヘッダ (Header): ファイルの基本的な情報(CPUタイプ、ファイルタイプ、ロードコマンドの数とサイズなど)を識別します。
  • ロードコマンド (Load Commands): ファイルのレイアウトとリンケージ特性を定義する可変サイズのコマンド群です。これには、仮想メモリ内の初期レイアウト、シンボルテーブルの位置、共有ライブラリの名前などが含まれます。
  • セグメント (Segments): 1つ以上のセクションを含む論理的なブロックです。セグメントは通常、ページ境界で開始され、__TEXT(読み取り専用のコードと定数データ)や__DATA(読み書き可能なデータ)などの一般的なセグメントがあります。

コミットメッセージにある「6.out」は、Goのリンカが64ビットアーキテクチャ(amd64)向けに生成するバイナリの慣習的な命名であり、Mach-Oの標準的な拡張子ではありません。

DWARF (Debugging With Attributed Record Formats)

DWARFは、ソースレベルデバッガがプログラムの実行状態をソースコードのコンテキストで解釈するために使用する標準的なデバッグ情報形式です。コンパイルされたバイナリに埋め込まれ、以下の情報を提供します。

  • ソースファイルと行番号のマッピング: 実行中のマシンコードがどのソースファイルのどの行に対応するか。
  • 変数情報: 変数の名前、型、スコープ、メモリ上の位置。
  • 関数情報: 関数の名前、引数、戻り値、スタックフレームのレイアウト。
  • 型情報: 構造体、配列、ポインタなどの複雑なデータ型の定義。

DWARF情報は通常、実行可能ファイル内の特定のセクション(例: __debug_abbrev, __debug_line, __debug_info, __debug_frameなど)に格納されます。これらのセクションは、プログラムの実行には直接必要ないため、「ディスクからロードしない (do not load from disk)」というフラグが付けられることがあります。

LLDB

LLDBは、LLVMプロジェクトの一部として開発されたオープンソースのデバッガです。GDB(GNU Debugger)の代替として設計されており、モダンな言語やアーキテクチャに対応しています。LLDBは、Mach-O形式のバイナリのデバッグに広く使用されており、macOSのXcodeにバンドルされています。

デバッガは、実行可能ファイル内のデバッグ情報を読み取り、それを使用してソースコードレベルでのデバッグ機能(ブレークポイント、ステップ実行、変数検査、スタックトレースなど)を提供します。LLDBがDWARF情報を正しく利用するためには、デバッグ情報が格納されているセクションが、たとえメモリにロードされなくても、有効な仮想アドレスを持っていることを期待します。

技術的詳細

このコミットの核心は、GoのリンカがMach-O形式のバイナリを生成する際に、__DWARFセクションに仮想アドレス(vaddr)を割り当てるように変更した点です。

以前のGoリンカでは、__DWARFセクションは「ディスクからロードしない」とマークされていたため、これらのセクションには仮想アドレスが割り当てられていませんでした。これは、実行時にこれらのセクションがメモリにマッピングされる必要がないという論理に基づいています。しかし、LLDBのようなデバッガは、デバッグ情報を参照する際に、これらのセクションがメモリ上のどこかに存在するという仮定(たとえそれが仮想的なアドレス空間であっても)に基づいて動作します。有効なアドレスがないと、LLDBはデバッグ情報を正しく参照できず、デバッグセッションが失敗したり、期待通りに動作しなくなったりします。

この修正では、dwarfaddmachoheaders関数内で、各__DWARF関連セクション(__debug_abbrev, __debug_line, __debug_frame, __debug_info, __debug_pubnames, __debug_pubtypes, __debug_aranges, __debug_gdb_scripts)に対して、msect->addrフィールドを設定する行が追加されました。

具体的には、msect->addr = msect->off + segdata.vaddr - segdata.fileoff; という計算式が用いられています。

  • msect->off: セクションのファイルオフセット(ファイル内の物理的な位置)。
  • segdata.vaddr: データセグメントの仮想アドレス(メモリ上の開始アドレス)。
  • segdata.fileoff: データセグメントのファイルオフセット。

この計算は、__DWARFセクションの仮想アドレスを、データセグメントの仮想アドレス空間の続きとして割り当てることを意味します。これにより、__DWARFセクションは、実際にメモリにロードされなくても、LLDBが期待する有効な仮想アドレスを持つことになります。この変更によって、LLDBはGoバイナリのDWARF情報を正しく解析し、デバッグセッションを円滑に進めることができるようになりました。

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

変更は src/cmd/ld/dwarf.c ファイルの dwarfaddmachoheaders 関数内で行われています。

--- a/src/cmd/ld/dwarf.c
+++ b/src/cmd/ld/dwarf.c
@@ -2353,31 +2353,37 @@ dwarfaddmachoheaders(void)
 	ms = newMachoSeg("__DWARF", nsect);
 	ms->fileoffset = fakestart;
 	ms->filesize = abbrevo-fakestart;
+	ms->vaddr = ms->fileoffset + segdata.vaddr - segdata.fileoff;
 
 	msect = newMachoSect(ms, "__debug_abbrev", "__DWARF");
 	msect->off = abbrevo;
 	msect->size = abbrevsize;
+	msect->addr = msect->off + segdata.vaddr - segdata.fileoff;
 	ms->filesize += msect->size;
 
 	msect = newMachoSect(ms, "__debug_line", "__DWARF");
 	msect->off = lineo;
 	msect->size = linesize;
+	msect->addr = msect->off + segdata.vaddr - segdata.fileoff;
 	ms->filesize += msect->size;
 
 	msect = newMachoSect(ms, "__debug_frame", "__DWARF");
 	msect->off = frameo;
 	msect->size = framesize;
+	msect->addr = msect->off + segdata.vaddr - segdata.fileoff;
 	ms->filesize += msect->size;
 
 	msect = newMachoSect(ms, "__debug_info", "__DWARF");
 	msect->off = infoo;
 	msect->size = infosize;
+	msect->addr = msect->off + segdata.vaddr - segdata.fileoff;
 	ms->filesize += msect->size;
 
 	if (pubnamessize > 0) {
 		msect = newMachoSect(ms, "__debug_pubnames", "__DWARF");
 		msect->off = pubnameso;
 		msect->size = pubnamessize;
+		msect->addr = msect->off + segdata.vaddr - segdata.fileoff;
 		ms->filesize += msect->size;
 	}
 
@@ -2385,6 +2391,7 @@ dwarfaddmachoheaders(void)
 	msect = newMachoSect(ms, "__debug_pubtypes", "__DWARF");
 	msect->off = pubtypeso;
 	msect->size = pubtypessize;
+	msect->addr = msect->off + segdata.vaddr - segdata.fileoff;
 		ms->filesize += msect->size;
 	}
 
@@ -2392,6 +2399,7 @@ dwarfaddmachoheaders(void)
 	msect = newMachoSect(ms, "__debug_aranges", "__DWARF");
 	msect->off = arangeso;
 	msect->size = arangessize;
+	msect->addr = msect->off + segdata.vaddr - segdata.fileoff;
 		ms->filesize += msect->size;
 	}
 
@@ -2400,6 +2408,7 @@ dwarfaddmachoheaders(void)
 	msect = newMachoSect(ms, "__debug_gdb_scripts", "__DWARF");
 	msect->off = gdbscripto;
 	msect->size = gdbscriptsize;
+	msect->addr = msect->off + segdata.vaddr - segdata.fileoff;
 		ms->filesize += msect->size;
 	}
 }

コアとなるコードの解説

dwarfaddmachoheaders関数は、Mach-OバイナリにDWARFデバッグ情報セクションを追加する役割を担っています。この関数内で、各DWARFセクションを表すmachoSect構造体(msect)が作成されます。

変更前は、msect->addrフィールド(セクションの仮想アドレス)は設定されていませんでした。このコミットでは、以下の行が各DWARFセクションの初期化に追加されました。

msect->addr = msect->off + segdata.vaddr - segdata.fileoff;

この行は、各DWARFセクションの仮想アドレスを計算し、設定しています。

  • msect->off: ファイル内でのセクションの開始オフセット(物理的な位置)。
  • segdata.vaddr: データセグメントの仮想アドレス(メモリ上の開始アドレス)。
  • segdata.fileoff: データセグメントのファイルオフセット。

この計算により、DWARFセクションの仮想アドレスは、データセグメントの仮想アドレス空間の続きとして割り当てられます。これにより、LLDBはこれらのセクションを、あたかもメモリにロードされているかのように参照できるようになり、デバッグ情報の解析が可能になります。

また、__DWARFセグメント自体にも仮想アドレスが割り当てられています。

ms->vaddr = ms->fileoffset + segdata.vaddr - segdata.fileoff;

これは、__DWARFセグメント全体の仮想アドレスを、そのファイルオフセットとデータセグメントの仮想アドレス/ファイルオフセットの差分に基づいて設定するものです。これにより、__DWARFセグメントもLLDBが期待するアドレス空間内に配置され、デバッグ情報の整合性が保たれます。

この修正により、Goのリンカは、LLDBがMach-OバイナリのDWARFデバッグ情報を正しく解釈するために必要な仮想アドレスを、たとえセクションが「ディスクからロードしない」とマークされていても、適切に割り当てるようになりました。

関連リンク

参考にした情報源リンク