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

[インデックス 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バイナリの構造を理解し、そこに含まれるデバッグシンボルやセクション情報を正しく解析できる機能が必要でした。

この変更の主な目的は以下の通りです。

  1. macOSでのデバッグサポートの実現: GoプログラムをmacOS上で開発・デバッグする際の障壁を取り除く。
  2. クロスプラットフォームデバッグの改善: dbツールがmacOSとLinuxの両方で機能するように、基盤となるライブラリ(libmach_amd64)の汎用性を高める。
  3. 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): ダイナミックリンカが使用する情報(エクスポート、インポートなど)を定義します。
  • 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
  */

-H7linux 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バイナリを解析します。

    1. ヘッダのバイトスワップと検証: Machhdr構造体を読み込み、エンディアンを調整(バイトスワップ)し、CPUタイプ、サブタイプ、ファイルタイプが期待通り(AMD64の実行可能ファイル)であることを検証します。
    2. ロードコマンドの読み込みと解析: Mach Headerのsizeofcmdsフィールドに基づいて、すべてのロードコマンドをメモリに読み込みます。
    3. ロードコマンドのループ処理: 各ロードコマンドを順に処理します。
      • MACH_SEGMENT_64: 64-bitセグメントコマンドを解析します。__TEXTセグメント(コード)と__DATAセグメント(データ)を探し、それぞれのセグメント内の__text__data__bssセクションのサイズを抽出します。
      • MACH_SYMSEG: このコマンドは、GoがシンボルテーブルとPC-lineテーブルのオフセットとサイズを格納するために再利用しています。最初に見つかったMACH_SYMSEGsymtab(シンボルテーブル)、次に見つかったものをpclntab(PC-lineテーブル)として記録します。
    4. ファイルヘッダ (Fhdr) の設定: 解析した情報(テキストセクションのアドレスとサイズ、データセクションのアドレスとサイズ、BSSセクションのサイズ、シンボルテーブルとPC-lineテーブルのオフセットとサイズ)を、Goの内部的なファイルヘッダ構造体Fhdrに設定します。これは、デバッガがバイナリの構造を理解するために使用されます。
    5. メモリ解放: 割り当てたメモリを解放します。

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_64MACH_CPU_SUBTYPE_X86MACH_EXECUTABLE_TYPEMACH_SEGMENT_64MACH_SYMSEGMACH_UNIXTHREADMACH_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つの部分に集約されます。

  1. src/libmach_amd64/macho.h の新規追加: MACH-Oバイナリ形式の構造をC言語で表現するためのヘッダファイルが導入されました。これにより、MACH-Oヘッダ、ロードコマンド、セグメント、セクションなどの各要素をプログラム的にアクセス・解析するための基盤が確立されました。
  2. 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)、cputypecpusubtypeはターゲットCPU(例: MACH_CPU_TYPE_X86_64MACH_CPU_SUBTYPE_X86)、filetypeはファイルの種別(例: MACH_EXECUTABLE_TYPE)、ncmdssizeofcmdsは後続のロードコマンドの数と合計サイズを示します。

  • MachCmd:

    typedef struct {
    	ulong	type;	/* type of load command */
    	ulong	size;	/* total size in bytes */
    } MachCmd;
    

    すべてのロードコマンドの基本構造です。typeフィールドはコマンドの種類(例: MACH_SEGMENT_64MACH_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テーブルのファイル内オフセットとサイズを格納するために使用されます。fileofffilesizeが、それぞれのテーブルの場所と大きさを指し示します。

  • 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構造体)に変換することです。

  1. ヘッダの読み込みと検証: 関数はまず、ファイルの先頭からMachhdrを読み込みます。このヘッダのcputypecpusubtypefiletypeを検証し、AMD64アーキテクチャの実行可能ファイルであることを確認します。エンディアンの違いを吸収するためにバイトスワップ処理も行われます。

  2. ロードコマンドの解析: 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)として記録します。これらのテーブルは、デバッグシンボルやソースコードの行番号情報を含んでいます。

  3. 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固有の複雑さを抽象化し、デバッガが統一された方法で情報にアクセスできるようにする重要な役割を担っています。

関連リンク

参考にした情報源リンク