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

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

このコミットは、Goリンカー(cmd/ld)において、以前は存在したもののliblinkへの移行に伴い失われていた、コールグラフのダンプ機能(-cフラグ)を復元するものです。この機能は、特定の低レベルなコード(例: Plan 9のノートハンドラ内のSSE命令)の利用状況を監査する際に有用であることが示されています。

コミット

commit 1e2a61aee19e9f327950a19131b1349ebb5240e6
Author: Anthony Martin <ality@pbrane.org>
Date:   Wed Apr 16 22:42:02 2014 -0400

    cmd/ld: restore the call graph dump
    
    Before the switch to liblink, the linkers accepted the -c flag
    to print the call graph. This change restores the functionality.
    
    This came in handy when I was trying to audit the use of SSE
    instructions inside the Plan 9 note handler.
    
    LGTM=rsc
    R=golang-codereviews, bradfitz, rsc
    CC=golang-codereviews
    https://golang.org/cl/73990043

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

https://github.com/golang/go/commit/1e2a61aee19e9f327950a19131b1349ebb5240e6

元コミット内容

cmd/ld: restore the call graph dump

Before the switch to liblink, the linkers accepted the -c flag
to print the call graph. This change restores the functionality.

This came in handy when I was trying to audit the use of SSE
instructions inside the Plan 9 note handler.

LGTM=rsc
R=golang-codereviews, bradfitz, rsc
CC=golang-codereviews
https://golang.org/cl/73990043

変更の背景

この変更の背景には、Goリンカー(cmd/ld)の内部的なアーキテクチャ変更があります。具体的には、リンカーがliblinkというライブラリを使用するようにリファクタリングされた際、以前は-cフラグで利用可能だったコールグラフのダンプ機能が失われていました。

コミットメッセージによると、この機能の復元は、Plan 9のノートハンドラ内でSSE命令の使用を監査しようとした際に特に役立ったと述べられています。Plan 9のノートハンドラは、Unixのシグナルに似た非同期通知メカニズムであり、通常、浮動小数点演算の実行が制限されることがあります。このような制約のある環境で、意図せずSSE命令(多くの場合、浮動小数点演算を伴う)が使用されていないかを確認することは、システムの安定性や正確性を保証する上で重要です。失われた機能が特定のデバッグや監査のシナリオで必要とされたため、その復元が決定されました。

前提知識の解説

Goリンカー (cmd/ld)

Goのビルドプロセスにおいて、リンカー(cmd/ld)は非常に重要な役割を担います。コンパイラによって生成されたオブジェクトファイル(.oファイル)やアーカイブファイル(.aファイル)を結合し、実行可能なバイナリや共有ライブラリを生成します。この過程で、シンボルの解決、アドレスの割り当て、リロケーションの適用などが行われます。cmd/ldはGoツールチェインの一部であり、Goプログラムの最終的な実行形式を決定する段階を司ります。

コールグラフ

コールグラフとは、プログラム内の関数呼び出しの関係をグラフ構造で表現したものです。ノードが関数を表し、エッジがある関数から別の関数への呼び出しを表します。コールグラフは、プログラムの実行フローを理解したり、パフォーマンスのボトルネックを特定したり、特定の関数の呼び出し元や呼び出し先を分析したりする際に非常に有用です。リンカーが生成するコールグラフは、コンパイル後のバイナリレベルでの関数間の呼び出し関係を示します。

liblinkは、Goのリンカーの内部実装を抽象化し、再利用可能なライブラリとして提供するために導入されたものです。Goのツールチェインは進化の過程で、リンカーのコードベースをよりモジュール化し、保守性を高めるためにliblinkへの移行を行いました。この移行は、リンカーの機能の一部が一時的に失われる原因となることがあり、本コミットはそのような機能回帰の一つを修正するものです。

-c フラグ

Goリンカーにおける-cフラグは、コールグラフを標準出力にダンプするためのオプションです。このフラグをリンカーに渡すことで、リンク対象のプログラムがどのような関数呼び出し構造を持っているかをテキスト形式で確認できます。これは、プログラムの構造分析やデバッグに役立ちます。

SSE命令 (Streaming SIMD Extensions)

SSEは、Intelが開発したSIMD(Single Instruction, Multiple Data)命令セットの一種です。主に浮動小数点演算やメディア処理の高速化を目的としています。複数のデータを一度に処理できるため、画像処理、音声処理、科学技術計算など、大量のデータに対して同じ演算を繰り返すようなタスクで高いパフォーマンスを発揮します。CPUのレジスタや命令セットに直接アクセスするアセンブリコードや、コンパイラが生成する最適化されたコードを通じて利用されます。

Plan 9のノートハンドラ

Plan 9は、ベル研究所で開発された分散オペレーティングシステムです。その設計哲学の一つに「すべてがファイルである」というものがあります。Plan 9における「ノートハンドラ」は、Unixにおけるシグナルに似た非同期通知メカニズムですが、より柔軟性があります。ノートは文字列メッセージとしてプロセスに送信され、プロセスはatnotifyのようなシステムコールを使ってノートハンドラ関数を登録できます。

重要な点として、Plan 9のノートハンドラは、通常、浮動小数点演算の実行が制限されるという特性があります。これは、ノートハンドラが非同期に、かつ予測不能なタイミングで実行される可能性があるため、浮動小数点の状態を安全に管理することが難しいことに起因します。したがって、ノートハンドラ内でSSE命令(多くの場合、浮動小数点演算を伴う)が意図せず使用されていないかを監査することは、システムの整合性を保つ上で非常に重要になります。

技術的詳細

このコミットは、Goリンカーにコールグラフダンプ機能を復元するために、主に以下の3つのファイルにコードを追加しています。

  1. src/cmd/ld/lib.c: コールグラフを生成し、出力するcallgraph関数が追加されました。
  2. src/cmd/ld/lib.h: callgraph関数のプロトタイプ宣言が追加されました。
  3. src/cmd/ld/pobj.c: リンカーのメイン処理からcallgraph関数を呼び出すコードが追加されました。

callgraph関数の実装は、リンカーが持つシンボル情報(特にテキストセクションのシンボル)を走査し、各シンボル(関数)からのリロケーション情報を確認します。リロケーションタイプがR_CALLまたはR_CALLARM(関数呼び出しを示す)であり、かつリロケーションの対象シンボルがテキストセクションのシンボル(別の関数)である場合に、呼び出し関係を「呼び出し元関数 calls 呼び出し先関数」の形式で出力します。この処理は、リンカーのデバッグフラグdebug['c']が設定されている場合にのみ実行されます。

これにより、リンカーは-cフラグが指定された際に、リンク対象のバイナリに含まれる関数間の呼び出し関係を抽出し、標準出力に表示できるようになります。

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

src/cmd/ld/lib.c の追加

+void
+callgraph(void)
+{
+	LSym *s;
+	Reloc *r;
+	int i;
+
+	if(!debug['c'])
+		return;
+
+	for(s = ctxt->textp; s != nil; s = s->next) {
+		for(i=0; i<s->nr; i++) {
+			r = &s->r[i];
+			if(r->sym == nil)
+				continue;
+			if((r->type == R_CALL || r->type == R_CALLARM) && r->sym->type == STEXT)
+				Bprint(&bso, "%s calls %s\n", s->name, r->sym->name);
+		}
+	}
+}

src/cmd/ld/lib.h の追加

+void	callgraph(void);

src/cmd/ld/pobj.c の追加

 	deadcode();
+	callgraph();
 	paramspace = "SP";	/* (FP) now (SP) on output */

コアとなるコードの解説

src/cmd/ld/lib.ccallgraph 関数

この関数は、コールグラフのダンプ処理の本体です。

  • if(!debug['c']) return;: リンカーのデバッグフラグ'c'が設定されていない場合、関数は何もせずに終了します。これは、-cフラグが指定された場合にのみコールグラフを生成するためのガードです。
  • for(s = ctxt->textp; s != nil; s = s->next): ctxt->textpは、リンカーが認識しているすべてのテキストセクション(つまり、実行可能なコードを含む関数)のシンボルを指すリストの先頭です。このループは、プログラム内のすべての関数シンボルsを順に走査します。
  • for(i=0; i<s->nr; i++): 各関数シンボルsには、その関数が参照する外部シンボルやリロケーション情報がs->r配列に格納されています。この内部ループは、sに関連付けられたすべてのリロケーションrを走査します。
  • if(r->sym == nil) continue;: リロケーションが有効なシンボルに関連付けられていない場合はスキップします。
  • if((r->type == R_CALL || r->type == R_CALLARM) && r->sym->type == STEXT): ここがコールグラフの検出の核心です。
    • r->type == R_CALL || r->type == R_CALLARM: リロケーションのタイプがR_CALL(一般的な関数呼び出し)またはR_CALLARM(ARMアーキテクチャ特有の関数呼び出し)であるかをチェックします。これにより、関数呼び出しに関連するリロケーションのみを対象とします。
    • r->sym->type == STEXT: 呼び出し先のシンボルr->symがテキストセクションのシンボル(つまり、別の実行可能な関数)であるかをチェックします。これにより、データ参照などではなく、純粋な関数間の呼び出し関係を抽出します。
  • Bprint(&bso, "%s calls %s\n", s->name, r->sym->name);: 上記の条件が満たされた場合、Bprint関数を使用して、標準出力(bsoはバッファリングされた出力ストリーム)に「呼び出し元関数名 calls 呼び出し先関数名」の形式で呼び出し関係を出力します。s->nameは呼び出し元関数の名前、r->sym->nameは呼び出し先関数の名前です。

src/cmd/ld/lib.hcallgraph 関数プロトタイプ宣言

この行は、lib.cで定義されたcallgraph関数が、他のファイル(特にpobj.c)から呼び出せるようにするための前方宣言です。C言語では、関数を使用する前にそのプロトタイプを宣言する必要があります。

src/cmd/ld/pobj.cmain 関数からの呼び出し

 	deadcode();
+	callgraph();
 	paramspace = "SP";	/* (FP) now (SP) on output */

pobj.cmain関数は、Goリンカーのエントリポイントです。deadcode()関数の呼び出しの後、callgraph()関数が追加されました。これにより、リンカーの通常の処理フローの中で、デッドコードの除去が行われた後にコールグラフのダンプ処理が実行されるようになります。この位置に配置することで、最終的なリンク結果に基づいてコールグラフが生成されることが保証されます。

関連リンク

参考にした情報源リンク