[インデックス 174] ファイルの概要
このコミットは、Go言語のツールチェーンにおいて、MACH-Oバイナリ形式(主にmacOSで使用される実行可能ファイル形式)のデバッグサポートを追加するものです。具体的には、Goリンカ(6l
)がMACH-O形式の実行可能ファイルを生成する際にデバッグ情報を適切に埋め込むように変更され、libmach_amd64
ライブラリがMACH-Oバイナリからシンボル情報やセクション情報を正確に読み取れるように修正されています。これにより、db
(デバッガ)がmacOSおよびLinux上でGoプログラムのデバッグをより効果的に行えるようになりました。
コミット
add support for debugging in MACH binaries
fix up libmach_amd64 to handle MACH binaries and symbols
db now works on mac and linux
SVN=122807
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/073486c3ed10f4f737484253320f44804cac1447
元コミット内容
add support for debugging in MACH binaries
fix up libmach_amd64 to handle MACH binaries and symbols
db now works on mac and linux
SVN=122807
変更の背景
Go言語が初期段階にあった頃、異なるオペレーティングシステム(OS)やアーキテクチャで生成されたバイナリのデバッグは、そのOS固有の実行可能ファイル形式への対応が不十分なため、困難な場合がありました。特にmacOS(当時のMac OS X)は、他のUnix系システムで一般的なELF(Executable and Linkable Format)とは異なる独自のMACH-O(Mach Object)形式を採用していました。
このコミットが行われた2008年6月時点では、Go言語はまだオープンソース化されておらず、Google内部で開発が進められていました。Goのデバッグツール(db
)がmacOS上でGoプログラムをデバッグできるようにするためには、MACH-Oバイナリの構造を理解し、そこに含まれるデバッグシンボルやセクション情報を正しく解析できる機能が必要でした。
この変更の主な目的は以下の通りです。
- macOSでのデバッグサポートの実現: GoプログラムをmacOS上で開発・デバッグする際の障壁を取り除く。
- クロスプラットフォームデバッグの改善:
db
ツールがmacOSとLinuxの両方で機能するように、基盤となるライブラリ(libmach_amd64
)の汎用性を高める。 - MACH-O形式への対応: GoリンカがMACH-O形式の実行可能ファイルを生成する際に、デバッグツールが利用できる形式で情報を埋め込むようにする。
これにより、Go開発者はmacOS環境でも効率的にデバッグ作業を行えるようになり、Go言語の利用範囲が拡大しました。
前提知識の解説
MACH-O (Mach Object) フォーマット
MACH-Oは、AppleのmacOS(旧Mac OS X)、iOS、watchOS、tvOSで採用されている実行可能ファイル、オブジェクトファイル、共有ライブラリ、コアダンプなどのバイナリ形式です。Unix系システムで広く使われるELF(Executable and Linkable Format)やWindowsのPE(Portable Executable)とは異なる独自の構造を持っています。
MACH-Oバイナリの主要な構成要素は以下の通りです。
- Mach Header: ファイルの先頭に位置し、CPUタイプ、サブタイプ、ファイルタイプ(実行可能ファイル、ライブラリなど)、ロードコマンドの数とサイズ、フラグなどの基本的な情報を含みます。
- Load Commands: Mach Headerの直後に続く一連のコマンドで、バイナリの構造とOSがそれをどのようにメモリにロードするかを記述します。これには、セグメントの定義、シンボルテーブルの位置、ダイナミックリンカの情報などが含まれます。
- Segment Command (
LC_SEGMENT_64
for 64-bit): メモリ上の連続した領域(セグメント)を定義します。各セグメントは1つ以上のセクションを含みます。一般的なセグメント名には、コードを含む__TEXT
、初期化済みデータを含む__DATA
、ゼロ初期化データを含む__BSS
などがあります。 - Section: セグメント内の論理的なブロックで、特定の種類のコードやデータを格納します。例えば、
__TEXT
セグメント内には__text
セクション(実行可能コード)や__stubs
セクション(スタブコード)などがあります。 - Symbol Table Command (
LC_SYMTAB
): シンボルテーブルの位置とサイズを定義します。シンボルテーブルには、関数名、変数名などの情報が含まれ、デバッグやリンク時に使用されます。 - Link-edit Info Command (
LC_DYLD_INFO
): ダイナミックリンカが使用する情報(エクスポート、インポートなど)を定義します。
- Segment Command (
- Raw Segment Data: ロードコマンドで定義されたセグメントの実際のデータ(コード、データなど)が格納されます。
MACH-Oは、その柔軟性と拡張性から、macOSの高度な機能(例えば、ダイナミックリンキングやコード署名)をサポートするために設計されています。
Goリンカ (6l)
Go言語のコンパイラツールチェーンは、go build
コマンドを通じて利用されますが、その内部では複数のツールが連携して動作します。6l
(またはcmd/link
)は、Goのリンカであり、Goコンパイラ(6g
など)によって生成されたオブジェクトファイル(.o
ファイル)を結合し、最終的な実行可能バイナリを生成する役割を担います。
リンカの主な機能は以下の通りです。
- オブジェクトファイルの結合: 複数のオブジェクトファイルからコードとデータを集約します。
- シンボル解決: 未解決のシンボル(関数呼び出しや変数参照)を、他のオブジェクトファイルやライブラリで定義されたシンボルに解決します。
- 再配置: コード内のアドレス参照を、最終的なメモリ配置に合わせて調整します。
- 実行可能ファイルの生成: ターゲットOSの実行可能ファイル形式(ELF、PE、MACH-Oなど)に準拠したバイナリを生成します。この際、デバッグ情報やシンボルテーブルなども埋め込まれます。
このコミットでは、6l
がMACH-O形式のバイナリを生成する際の内部ロジックが変更され、デバッグツールが利用しやすいようにシンボル情報が適切に配置されるようになりました。
libmach_amd64
libmach_amd64
は、Goのデバッグツールやプロファイリングツールが、特定のアーキテクチャ(この場合はAMD64)の実行可能ファイル形式を解析するために使用するライブラリです。このライブラリは、バイナリファイルからヘッダ情報、セグメント、セクション、シンボルテーブルなどの低レベルな情報を読み取り、Goのデバッグツールがプログラムの実行状態を理解するための基盤を提供します。
デバッガは、プログラムの実行中にメモリ上のアドレスとソースコードの行番号、変数名などを関連付ける必要があります。この関連付けを行うためには、実行可能ファイルに埋め込まれたデバッグシンボル情報が必要です。libmach_amd64
のようなライブラリは、これらのデバッグシンボルを解析し、デバッガが利用できる形式に変換する役割を担います。
このコミットでは、libmach_amd64
がMACH-O形式のバイナリを正しく解析できるように拡張され、特にシンボルテーブルやPC-lineテーブル(プログラムカウンタと行番号のマッピング情報)の読み取り能力が向上しました。
デバッグシンボル
デバッグシンボルは、コンパイルされたプログラムの実行可能ファイルに埋め込まれる追加情報で、デバッガがソースコードレベルでプログラムを理解し、操作できるようにするために不可欠です。これには以下のような情報が含まれます。
- 関数名とアドレスのマッピング: どのメモリアドレスにどの関数が配置されているか。
- 変数名とアドレスのマッピング: どのメモリアドレスにどの変数が格納されているか。
- ソースファイル名と行番号のマッピング: 実行可能コードの特定のアドレスが、どのソースファイルのどの行に対応するか。
- 型情報: 変数や関数のデータ型。
これらの情報は、通常、実行可能ファイルの特定のセクション(例: DWARF形式のデバッグ情報)や、OS固有のシンボルテーブル(例: MACH-OのLC_SYMTAB
)に格納されます。デバッガはこれらのシンボル情報を読み取り、ユーザーがソースコードを見ながらブレークポイントを設定したり、変数の値を検査したり、ステップ実行したりすることを可能にします。
このコミットは、MACH-Oバイナリにこれらのデバッグシンボルが適切に埋め込まれ、libmach_amd64
がそれらを正しく読み取れるようにすることで、Goプログラムのデバッグ体験を向上させています。
技術的詳細
このコミットは、Goリンカ(src/cmd/6l
)とMACH-Oバイナリ解析ライブラリ(src/libmach_amd64
)の両方に変更を加えて、MACH-O形式の実行可能ファイルに対するデバッグサポートを統合しています。
src/cmd/6l/asm.c
の変更
このファイルは、Goリンカの主要なアセンブリ生成ロジックを含んでいます。MACH-Oバイナリの生成に関する重要な変更が加えられています。
-
64-bitアドレスの有効化:
@@ -179,6 +179,7 @@ asmb(void) seek(cout, HEADR+textsize, 0); break; case 6: + debug['8'] = 1; /* 64-bit addresses */ v = HEADR+textsize; myseek(cout, v); v = rnd(v, 4096) - v;
case 6
はMACH-O形式のバイナリ生成パスを示します。ここでdebug['8'] = 1;
が設定され、64-bitアドレスが有効になります。これは、AMD64アーキテクチャで64-bitバイナリを生成する際に必須です。 -
シンボルテーブルのオフセット調整:
@@ -232,7 +233,7 @@ asmb(void) \t\tseek(cout, HEADR+textsize+datsize, 0); \t\tbreak; \t\tcase 6: -\t\t\tdebug['s'] = 1; +\t\t\tseek(cout, rnd(HEADR+textsize, INITRND)+rnd(datsize, INITRND), 0); \t\tbreak; \t\tcase 7: \t\t\tseek(cout, rnd(HEADR+textsize, INITRND)+datsize+strtabsize, 0);
debug['s']
はシンボルテーブル関連のフラグを示唆します。以前は単にdebug['s'] = 1;
と設定されていましたが、変更後はseek
関数でシンボルテーブルの開始オフセットを計算しています。これは、MACH-Oバイナリにおけるシンボルテーブルの配置が、テキストセクションとデータセクションの後に続くことを考慮したものです。rnd
関数はアライメント調整に使用されます。 -
ロードコマンド数の動的な設定:
@@ -348,7 +349,10 @@ asmb(void) \t\tlputl((1<<24)|7);\t\t/* cputype - x86/ABI64 */ \t\tlputl(3);\t\t\t/* subtype - x86 */ \t\tlputl(2);\t\t\t/* file type - mach executable */ -\t\t\tlputl(4);\t\t\t/* number of loads */ +\t\t\tif (debug['s']) +\t\t\t\tlputl(4);\t\t\t/* number of loads */ +\t\telse +\t\t\tlputl(6);\t\t\t/* number of loads */ \t\tlputl(machheadr()-32);\t\t/* size of loads */ \t\tlputl(1);\t\t\t/* flags - no undefines */ \t\tlputl(0);\t\t\t/* reserved */
MACH-Oヘッダの
ncmds
(ロードコマンドの数)が、debug['s']
フラグの有無によって動的に設定されるようになりました。debug['s']
が設定されている場合(シンボル情報を含む場合)、ロードコマンドの数が6に増えます。これは、シンボルテーブルとPC-lineテーブルのための追加のロードコマンド(MACH_SYMSEG
)が発行されるためです。 -
シンボルセグメントの追加:
@@ -386,7 +390,15 @@ asmb(void) \t\t\tva+v+datsize,bsssize,\t/* addr size */ \t\t\t0,0,0,0,\t\t/* offset align reloc nreloc */ \t\t\t1);\t\t\t/* flag - zero fill */ +\n \t\tmachstack(va+HEADR);\n+\n+\t\tif (!debug['s']) {\n+\t\t\tv += rnd(datsize, INITRND);\n+\t\t\tmachsymseg(v,symsize);\t/* fileoffset,filesize */\n+\t\t\tv += symsize;\n+\t\t\tmachsymseg(v,lcsize);\t/* fileoffset,filesize */\n+\t\t}\n \tbreak;\n \tcase 7:\n \t\t/* elf amd-64 */
debug['s']
が設定されていない場合(つまり、デバッグシンボルを生成しない場合)に、machsymseg
関数が呼び出され、シンボルテーブルとPC-lineテーブルのセグメントが追加されるようになりました。これは、デバッグ情報がMACH-Oバイナリに適切に埋め込まれるようにするための重要な変更です。symsize
はシンボルテーブルのサイズ、lcsize
はPC-lineテーブルのサイズを示します。 -
machsymseg
関数の追加:@@ -737,7 +749,7 @@ void machseg(char *name, vlong vaddr, vlong vsize, vlong foff, vlong fsize,\n ulong prot1, ulong prot2, ulong nsect, ulong flag)\n {\n -\tlputl(25);\t// section\n +\tlputl(25);\t/* segment 64 */\n \tlputl(72 + 80*nsect);\n \tstrnput(name, 16);\n \tvputl(vaddr);\
machseg
関数のコメントが修正されていますが、より重要なのは新しいmachsymseg
関数の追加です。+void +machsymseg(ulong foffset, ulong fsize) +{ +\tlputl(3);\t/* obsolete gdb debug info */ +\tlputl(16);\t/* size of symseg command */ +\tlputl(foffset);\n+\tlputl(fsize);\n+}
この関数は、
MACH_SYMSEG
(タイプ3)というロードコマンドを書き込みます。このコマンドは、GDBの古いデバッグ情報形式を指しますが、GoではシンボルテーブルとPC-lineテーブルのオフセットとサイズをMACH-Oバイナリに埋め込むために再利用されています。 -
machheadr
の変更:@@ -799,6 +820,10 @@ machheadr(void) \ta += 20;\t/* data sect */ \ta += 20;\t/* bss sect */ \ta += 46;\t/* stack sect */ +\tif (!debug['s']) {\n+\t\ta += 4;\t/* symtab seg */\n+\t\ta += 4;\t/* lctab seg */\n+\t}\n \n \treturn a*4;\n }
machheadr
関数は、MACH-Oヘッダのサイズを計算します。debug['s']
が設定されていない場合(デバッグシンボルを生成しない場合)に、シンボルテーブルとPC-lineテーブルのセグメント(それぞれ4バイト)のサイズが追加されるようになりました。これは、これらのセグメントがヘッダの一部として考慮される必要があるためです。
src/cmd/6l/l.h
の変更
machsymseg
関数のプロトタイプ宣言が追加されました。
@@ -421,6 +421,7 @@ int zaddr(uchar*, Adr*, Sym*[]);
void zerosig(char*);
void machseg(char*, vlong, vlong, vlong, vlong, ulong, ulong, ulong, ulong);
+void machsymseg(ulong, ulong);
void machsect(char*, char*, vlong, vlong, ulong, ulong, ulong, ulong, ulong);
void machstack(vlong);
ulong machheadr(void);
src/cmd/6l/obj.c
の変更
リンカのコマンドラインオプションに関するコメントが追加されました。
@@ -43,6 +43,7 @@ char* paramspace = "FP";
* -H3 -T4128 -R4096 is plan9 32-bit format
* -H5 -T0x80110000 -R4096 is ELF32
* -H6 -Tx -Rx is apple MH-exec
+ * -H7 -Tx -Rx is linux elf-exec
*
* options used: 189BLQSWabcjlnpsvz
*/
-H7
がlinux elf-exec
としてコメントされていますが、このコミットの主要な目的はMACH-Oサポートであり、この行は既存のコメントの修正か、将来的なELFサポートの準備を示唆している可能性があります。
src/libmach_amd64/Makefile
の変更
macho.h
ヘッダファイルがビルドプロセスに追加されました。
@@ -70,7 +70,7 @@ OFILES=\
# qobj.$O\
# vcodas.$O\
-HFILES=$(GOROOT)/include/mach_amd64.h elf.h obj.h
+HFILES=$(GOROOT)/include/mach_amd64.h elf.h macho.h obj.h
install: $(LIB)
cp $(LIB) $(GOROOT)/lib
src/libmach_amd64/executable.c
の変更
このファイルは、様々な実行可能ファイル形式を解析するためのロジックを含んでいます。
-
macho.h
のインクルード:@@ -32,6 +32,7 @@ #include <bootexec.h> #include <mach_amd64.h> #include "elf.h" +#include "macho.h"
新しく定義されたMACH-O関連の構造体や定数を使用するために、
macho.h
がインクルードされました。 -
ExecHdr
構造体の拡張:@@ -50,6 +51,7 @@ typedef struct { struct mips4kexec mipsk4; /* bootexec.h */ struct sparcexec sparc; /* bootexec.h */ struct nextexec next; /* bootexec.h */ + Machhdr machhdr; /* macho.h */ } e; long dummy; /* padding to ensure extra long */ } ExecHdr;
ExecHdr
は、様々な実行可能ファイル形式のヘッダを統一的に扱うための共用体(union)です。ここにMachhdr machhdr;
が追加され、MACH-Oヘッダを解析できるようになりました。 -
machdotout
関数の登録:@@ -62,6 +64,7 @@ static int common(int, Fhdr*, ExecHdr*); static int commonllp64(int, Fhdr*, ExecHdr*); static int adotout(int, Fhdr*, ExecHdr*); static int elfdotout(int, Fhdr*, ExecHdr*); +static int machdotout(int, Fhdr*, ExecHdr*); static int armdotout(int, Fhdr*, ExecHdr*); static void setsym(Fhdr*, long, long, long, vlong); static void setdata(Fhdr*, uvlong, long, vlong, long);
machdotout
関数のプロトタイプ宣言が追加され、exectab
(実行可能ファイル形式のテーブル)に登録されました。@@ -256,6 +259,15 @@ ExecTable exectab[] =\ sizeof(Ehdr64),\ nil,\ elfdotout },\ + { MACH_MAG, /* 64-bit MACH (apple mac) */ + "mach executable",\ + nil,\ + FAMD64,\ + 0,\ + &mi386,\ + sizeof(Ehdr64),\ + nil,\ + machdotout },\ { E_MAGIC, /* Arm 5.out and boot image */ "arm plan 9 executable",\ "arm plan 9 dlm",\
MACH_MAG
はMACH-Oバイナリのマジックナンバーです。このエントリにより、libmach_amd64
はMACH-O形式のファイルを認識し、machdotout
関数を呼び出して解析するようになります。 -
machdotout
関数の実装: この関数は、MACH-Oバイナリの解析の中核を担います。+static int +machdotout(int fd, Fhdr *fp, ExecHdr *hp) +{ + uvlong (*swav)(uvlong); + ulong (*swal)(ulong); + ushort (*swab)(ushort); + Machhdr *mp; + MachCmd **cmd; + MachSeg64 *text; + MachSeg64 *data; + MachSymSeg *symtab; + MachSymSeg *pclntab; + MachSeg64 *seg; + MachSect64 *sect; + uvlong textsize, datasize, bsssize; + uchar *cmdbuf; + uchar *cmdp; + int i; + + /* bitswap the header according to the DATA format */ + mp = &hp->e.machhdr; + if (mp->cputype != leswal(MACH_CPU_TYPE_X86_64)) { + werrstr("bad MACH cpu type - not amd64"); + return 0; + } + swab = leswab; + swal = leswal; + swav = leswav; + + mp->magic = swal(mp->magic); + mp->cputype = swal(mp->cputype); + mp->cpusubtype = swal(mp->cpusubtype); + mp->filetype = swal(mp->filetype); + mp->ncmds = swal(mp->ncmds); + mp->sizeofcmds = swal(mp->sizeofcmds); + mp->flags = swal(mp->flags); + mp->reserved = swal(mp->reserved); + if (mp->cpusubtype != MACH_CPU_SUBTYPE_X86) { + werrstr("bad MACH cpu subtype - not amd64"); + return 0; + } + if (mp->filetype != MACH_EXECUTABLE_TYPE) { + werrstr("bad MACH cpu subtype - not amd64"); + return 0; + } + mach = &mamd64; + fp->type = FAMD64; + + cmdbuf = malloc(mp->sizeofcmds); + seek(fd, sizeof(Machhdr), 0); + if(read(fd, cmdbuf, mp->sizeofcmds) != mp->sizeofcmds) { + free(cmdbuf); + return 0; + } + cmd = malloc(mp->ncmds * sizeof(MachCmd*)); + cmdp = cmdbuf; + text = 0; + data = 0; + symtab = 0; + pclntab = 0; + textsize = datasize = bsssize = 0; + for (i = 0; i < mp->ncmds; i++) { + MachCmd *c; + + cmd[i] = (MachCmd*)cmdp; + c = cmd[i]; + c->type = swal(c->type); + c->size = swal(c->size); + switch(c->type) { + case MACH_SEGMENT_64: + seg = (MachSeg64*)c; + seg->vmaddr = swav(seg->vmaddr); + seg->vmsize = swav(seg->vmsize); + seg->fileoff = swav(seg->fileoff); + seg->filesize = swav(seg->filesize); + seg->maxprot = swal(seg->maxprot); + seg->initprot = swal(seg->initprot); + seg->nsects = swal(seg->nsects); + seg->flags = swal(seg->flags); + if (strcmp(seg->segname, "__TEXT") == 0) { + text = seg; + sect = (MachSect64*)(cmdp + sizeof(MachSeg64)); + if (strcmp(sect->sectname, "__text") == 0) { + textsize = swav(sect->size); + } else { + werrstr("no text section"); + goto bad; + } + } + if (strcmp(seg->segname, "__DATA") == 0) { + data = seg; + sect = (MachSect64*)(cmdp + sizeof(MachSeg64)); + if (strcmp(sect->sectname, "__data") == 0) { + datasize = swav(sect->size); + } else { + werrstr("no data section"); + goto bad; + } + sect++; + if (strcmp(sect->sectname, "__bss") == 0) { + bsssize = swav(sect->size); + } else { + werrstr("no bss section"); + goto bad; + } + } + break; + case MACH_UNIXTHREAD: + break; + case MACH_SYMSEG: + if (symtab == 0) + symtab = (MachSymSeg*)c; + else if (pclntab == 0) + pclntab = (MachSymSeg*)c; + break; + } + cmdp += c->size; + } + if (text == 0 || data == 0) { + free(cmd); + free(cmdbuf); + return 0; + } + /* compute entry by taking address after header - weird - BUG? */ + settext(fp, text->vmaddr+sizeof(Machhdr) + mp->sizeofcmds, text->vmaddr, textsize, text->fileoff); + setdata(fp, data->vmaddr, datasize, data->fileoff, bsssize); + if(symtab != 0) + setsym(fp, symtab->filesize, 0, pclntab? pclntab->filesize : 0, symtab->fileoff); + free(cmd); + free(cmdbuf); + return 1; +bad: + free(cmd); + free(cmdbuf); + return 0; +}
この関数は、以下のステップでMACH-Oバイナリを解析します。
- ヘッダのバイトスワップと検証:
Machhdr
構造体を読み込み、エンディアンを調整(バイトスワップ)し、CPUタイプ、サブタイプ、ファイルタイプが期待通り(AMD64の実行可能ファイル)であることを検証します。 - ロードコマンドの読み込みと解析: Mach Headerの
sizeofcmds
フィールドに基づいて、すべてのロードコマンドをメモリに読み込みます。 - ロードコマンドのループ処理: 各ロードコマンドを順に処理します。
MACH_SEGMENT_64
: 64-bitセグメントコマンドを解析します。__TEXT
セグメント(コード)と__DATA
セグメント(データ)を探し、それぞれのセグメント内の__text
、__data
、__bss
セクションのサイズを抽出します。MACH_SYMSEG
: このコマンドは、GoがシンボルテーブルとPC-lineテーブルのオフセットとサイズを格納するために再利用しています。最初に見つかったMACH_SYMSEG
をsymtab
(シンボルテーブル)、次に見つかったものをpclntab
(PC-lineテーブル)として記録します。
- ファイルヘッダ (
Fhdr
) の設定: 解析した情報(テキストセクションのアドレスとサイズ、データセクションのアドレスとサイズ、BSSセクションのサイズ、シンボルテーブルとPC-lineテーブルのオフセットとサイズ)を、Goの内部的なファイルヘッダ構造体Fhdr
に設定します。これは、デバッガがバイナリの構造を理解するために使用されます。 - メモリ解放: 割り当てたメモリを解放します。
- ヘッダのバイトスワップと検証:
src/libmach_amd64/macho.h
の追加
この新しいヘッダファイルは、MACH-Oバイナリ形式の主要な構造体と定数を定義しています。
Machhdr
: MACH-Oファイルのヘッダ構造体。マジックナンバー、CPUタイプ、サブタイプ、ファイルタイプ、ロードコマンドの数とサイズ、フラグなどが含まれます。MachCmd
: ロードコマンドの基本構造体。タイプとサイズが含まれます。MachSeg64
: 64-bitセグメントコマンドの構造体。セグメント名、仮想メモリアドレスとサイズ、ファイルオフセットとサイズ、保護フラグ、セクション数などが含まれます。MachSymSeg
: GoがシンボルテーブルとPC-lineテーブルの情報を格納するために再利用する構造体。ファイルオフセットとサイズが含まれます。MachSect64
: 64-bitセクションの構造体。セクション名、セグメント名、メモリアドレスとサイズ、ファイルオフセット、アライメント、再配置情報、フラグなどが含まれます。- 定数:
MACH_CPU_TYPE_X86_64
、MACH_CPU_SUBTYPE_X86
、MACH_EXECUTABLE_TYPE
、MACH_SEGMENT_64
、MACH_SYMSEG
、MACH_UNIXTHREAD
、MACH_MAG
など、MACH-O形式を識別し、解析するために必要なマジックナンバーやコマンドタイプが定義されています。
これらの定義により、libmach_amd64
はMACH-Oバイナリの構造をプログラム的に理解し、アクセスできるようになります。
src/libmach_amd64/sym.c
の変更
このファイルはシンボル関連の処理を含んでいます。
@@ -1050,7 +1050,6 @@ fileline(char *str, int n, uvlong dot)
bot = mid;
else {
line = pc2line(dot);
-\t\t\tprint("line %d\\n", line);
if(line > 0 && fline(str, n, line, f->hist, 0) >= 0)
return 1;
break;
fileline
関数からデバッグ用のprint("line %d\\n", line);
が削除されました。これは機能的な変更ではなく、開発中のデバッグ出力のクリーンアップと考えられます。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、以下の2つの部分に集約されます。
src/libmach_amd64/macho.h
の新規追加: MACH-Oバイナリ形式の構造をC言語で表現するためのヘッダファイルが導入されました。これにより、MACH-Oヘッダ、ロードコマンド、セグメント、セクションなどの各要素をプログラム的にアクセス・解析するための基盤が確立されました。src/libmach_amd64/executable.c
内のmachdotout
関数の実装: この関数は、MACH-O形式の実行可能ファイルを実際に読み込み、その内部構造(テキスト、データ、BSSセクションの場所とサイズ、シンボルテーブルとPC-lineテーブルのオフセットとサイズ)を解析し、Goのデバッグツールが利用できる形式に変換する役割を担います。
これらの変更により、GoのデバッグツールはMACH-Oバイナリを「理解」し、デバッグシンボルを利用してソースコードレベルでのデバッグを可能にしました。
コアとなるコードの解説
src/libmach_amd64/macho.h
の構造体定義
macho.h
で定義されている主要な構造体は、MACH-Oバイナリの論理的な構成要素を直接マッピングしています。
-
Machhdr
:typedef struct { ulong magic; /* mach magic number identifier */ ulong cputype; /* cpu specifier */ ulong cpusubtype; /* machine specifier */ ulong filetype; /* type of file */ ulong ncmds; /* number of load commands */ ulong sizeofcmds; /* the size of all the load commands */ ulong flags; /* flags */ ulong reserved; /* reserved */ } Machhdr;
これはMACH-Oファイルの先頭にあるヘッダで、ファイル全体の概要を提供します。
magic
はファイル形式を識別するマジックナンバー(MACH_MAG
)、cputype
とcpusubtype
はターゲットCPU(例:MACH_CPU_TYPE_X86_64
とMACH_CPU_SUBTYPE_X86
)、filetype
はファイルの種別(例:MACH_EXECUTABLE_TYPE
)、ncmds
とsizeofcmds
は後続のロードコマンドの数と合計サイズを示します。 -
MachCmd
:typedef struct { ulong type; /* type of load command */ ulong size; /* total size in bytes */ } MachCmd;
すべてのロードコマンドの基本構造です。
type
フィールドはコマンドの種類(例:MACH_SEGMENT_64
、MACH_SYMSEG
)を識別し、size
はそのコマンド全体のバイトサイズを示します。これにより、パーサーは各コマンドの終わりを判断し、次のコマンドへ進むことができます。 -
MachSeg64
:typedef struct { MachCmd cmd; char segname[16]; /* segment name */ uvlong vmaddr; /* memory address of this segment */ uvlong vmsize; /* memory size of this segment */ uvlong fileoff; /* file offset of this segment */ uvlong filesize; /* amount to map from the file */ ulong maxprot; /* maximum VM protection */ ulong initprot; /* initial VM protection */ ulong nsects; /* number of sections in segment */ ulong flags; /* flags */ } MachSeg64; /* for 64-bit architectures */
これは、実行可能ファイルのメモリセグメントを定義するロードコマンド(
LC_SEGMENT_64
)の構造体です。segname
(例:__TEXT
、__DATA
)、vmaddr
(仮想メモリ開始アドレス)、vmsize
(仮想メモリサイズ)、fileoff
(ファイル内オフセット)、filesize
(ファイル内サイズ)、nsects
(含まれるセクション数)などの重要な情報を含みます。デバッガはこれらの情報を使って、コードやデータがメモリ上のどこに配置されるかを把握します。 -
MachSymSeg
:typedef struct { MachCmd cmd; ulong fileoff; /* file offset of this segment */ ulong filesize; /* amount to map from the file */ } MachSymSeg;
この構造体は、Goが
MACH_SYMSEG
(タイプ3)ロードコマンドを再利用して、シンボルテーブルとPC-lineテーブルのファイル内オフセットとサイズを格納するために使用されます。fileoff
とfilesize
が、それぞれのテーブルの場所と大きさを指し示します。 -
MachSect64
:typedef struct { char sectname[16]; /* name of this section */ char segname[16]; /* segment this section goes in */ uvlong addr; /* memory address of this section */ uvlong size; /* size in bytes of this section */ ulong offset; /* file offset of this section */ ulong align; /* section alignment (power of 2) */ ulong reloff; /* file offset of relocation entries */ ulong nreloc; /* number of relocation entries */ ulong flags; /* flags (section type and attributes)*/ ulong reserved1; /* reserved (for offset or index) */ ulong reserved2; /* reserved (for count or sizeof) */ ulong reserved3; /* reserved */ } MachSect64; /* for 64-bit architectures */
セグメント内の個々のセクションを定義する構造体です。
sectname
(例:__text
、__data
、__bss
)、segname
(所属セグメント)、addr
(メモリ開始アドレス)、size
(サイズ)、offset
(ファイル内オフセット)などが含まれます。デバッガはこれらの情報を使って、特定のコードやデータがバイナリ内のどこにあり、メモリ上でどこにロードされるかを正確に特定します。
machdotout
関数の動作
machdotout
関数は、libmach_amd64
がMACH-Oバイナリを解析する際の主要なエントリポイントです。その役割は、MACH-Oファイルの低レベルな構造を読み取り、Goのデバッグツールが利用しやすい高レベルな情報(Fhdr
構造体)に変換することです。
-
ヘッダの読み込みと検証: 関数はまず、ファイルの先頭から
Machhdr
を読み込みます。このヘッダのcputype
、cpusubtype
、filetype
を検証し、AMD64アーキテクチャの実行可能ファイルであることを確認します。エンディアンの違いを吸収するためにバイトスワップ処理も行われます。 -
ロードコマンドの解析:
Machhdr
からncmds
(ロードコマンド数)とsizeofcmds
(ロードコマンドの合計サイズ)を取得し、ファイルからすべてのロードコマンドをメモリに読み込みます。その後、各ロードコマンドをループで処理します。-
LC_SEGMENT_64
(MACH_SEGMENT_64
) の処理: このコマンドが見つかると、MachSeg64
構造体として解析されます。特に、__TEXT
セグメントと__DATA
セグメントを探します。__TEXT
セグメント内では、__text
セクション(実行可能コード)のサイズを抽出します。__DATA
セグメント内では、__data
セクション(初期化済みデータ)と__bss
セクション(ゼロ初期化データ)のサイズを抽出します。 これらのセグメントとセクションの情報は、プログラムのコードとデータがメモリ上でどこに配置されるかをデバッガに伝えます。 -
MACH_SYMSEG
の処理: Goは、このロードコマンドタイプをシンボルテーブルとPC-lineテーブルの情報を格納するために再利用しています。machdotout
は、最初に見つかったMACH_SYMSEG
をシンボルテーブル(symtab
)として、次に見つかったものをPC-lineテーブル(pclntab
)として記録します。これらのテーブルは、デバッグシンボルやソースコードの行番号情報を含んでいます。
-
-
Fhdr
構造体へのマッピング: すべてのロードコマンドの解析が完了すると、machdotout
は収集した情報(テキストセクションの仮想アドレス、サイズ、ファイルオフセット、データセクションの仮想アドレス、サイズ、BSSセクションのサイズ、シンボルテーブルとPC-lineテーブルのファイルオフセットとサイズ)を、Goの内部的なファイルヘッダ構造体Fhdr
に設定します。settext(fp, text->vmaddr+sizeof(Machhdr) + mp->sizeofcmds, text->vmaddr, textsize, text->fileoff);
これは、テキストセクションの情報をFhdr
に設定します。text->vmaddr
はテキストセクションの仮想メモリ開始アドレス、textsize
はサイズ、text->fileoff
はファイル内オフセットです。エントリポイントは、ヘッダとロードコマンドの直後のアドレスとして計算されます。setdata(fp, data->vmaddr, datasize, data->fileoff, bsssize);
これは、データセクションとBSSセクションの情報をFhdr
に設定します。setsym(fp, symtab->filesize, 0, pclntab? pclntab->filesize : 0, symtab->fileoff);
これは、シンボルテーブルとPC-lineテーブルの情報をFhdr
に設定します。symtab->filesize
はシンボルテーブルのサイズ、symtab->fileoff
はファイル内オフセットです。pclntab
が存在すれば、そのサイズも設定されます。
このFhdr
構造体は、Goのデバッガがプログラムのメモリレイアウト、コードとデータの場所、そしてデバッグシンボルの位置を理解するための標準的なインターフェースとして機能します。machdotout
は、MACH-O固有の複雑さを抽象化し、デバッガが統一された方法で情報にアクセスできるようにする重要な役割を担っています。
関連リンク
- Go言語公式ドキュメント: https://go.dev/doc/
- Goのデバッグに関する情報: Go言語のデバッグツールやデバッグ方法に関する公式ドキュメントやチュートリアルを探すことで、このコミットの意義をより深く理解できます。
- Mach-O File Format Reference (Apple Developer Documentation): MACH-O形式の公式仕様は、その構造を詳細に理解するために不可欠です。
参考にした情報源リンク
- Go言語のソースコード:
src/cmd/6l/asm.c
src/cmd/6l/l.h
src/cmd/6l/obj.c
src/libmach_amd64/Makefile
src/libmach_amd64/executable.c
src/libmach_amd64/macho.h
src/libmach_amd64/sym.c
- MACH-O File Format:
- Go Toolchain:
- https://go.dev/doc/install/source (Goのビルドプロセスに関する一般的な情報)
- Debugging Go Programs:
- https://go.dev/doc/gdb (GoとGDBの連携に関する情報)
- https://go.dev/blog/debugging-go-programs (Goのデバッグに関するブログ記事)
- ELF (Executable and Linkable Format): MACH-Oと比較されることが多い実行可能ファイル形式。
- Portable Executable (PE): Windowsで使用される実行可能ファイル形式。
- DWARF Debugging Format: 多くのシステムでデバッグ情報を格納するために使用される標準形式。
- GDB (GNU Debugger): 多くのシステムで利用されるデバッガ。
- Rob Pike's contributions to Go:
- https://go.dev/blog/rob-pike (Rob Pike氏のGoへの貢献に関する情報)
- Go Project History:
- https://go.dev/doc/history (Go言語の歴史に関する情報)
- SVN to Git migration in Go:
- Goプロジェクトは元々SVNで管理されており、後にGitに移行しました。コミットメッセージの
SVN=122807
はその名残です。
- Goプロジェクトは元々SVNで管理されており、後にGitに移行しました。コミットメッセージの