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

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

このコミットは、Go言語の初期段階におけるコードカバレッジツールの導入と、それに伴う関連ファイルの修正を記録しています。特に、6covという名称の新しいツールが追加され、既存のプロファイリングツールや低レベルの機械語操作ライブラリにも変更が加えられています。

コミット

commit 7832ab5ba0b53622b978acf1aacd8f61f2a44ca5
Author: Russ Cox <rsc@golang.org>
Date:   Fri Nov 14 10:45:23 2008 -0800

    code coverage tool
    
            $ 6cov -g 235.go 6.out
            235.go:62,62 main·main 0x27c9-0x2829 MOVL       $main·.stringo(SB),AX
            235.go:30,30 main·main 0x2856-0x285e ADDQ       $6c0,SP
            $
    
    and assorted fixes.
    
    R=r
    DELTA=743  (732 added, 8 deleted, 3 changed)
    OCL=19226
    CL=19243

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

https://github.com/golang/go/commit/7832ab5ba0b53622b978acf1aacd8f61f2a44ca5

元コミット内容

このコミットの主な目的は、Go言語のためのコードカバレッジツールを導入することです。コミットメッセージには、6covというツールが例示されており、特定のGoプログラム (235.go) とその出力ファイル (6.out) を引数として実行すると、コードの実行パスに関する情報(ファイル名、行番号、関数名、アドレス範囲、アセンブリ命令)が出力されることが示されています。また、「and assorted fixes.」とあるように、カバレッジツール以外の関連する修正も含まれています。

変更の背景

このコミットは2008年11月14日に行われており、Go言語が一般に公開される前の非常に初期の段階に当たります。Go言語は2009年11月に初めて公開されました。この時期は、Go言語の基本的なツールチェインやランタイムが構築されている最中であり、開発効率とコード品質を確保するための基盤ツールが不可欠でした。コードカバレッジツールは、テストがどれだけコードを網羅しているかを測定し、テストの品質を向上させる上で重要な役割を果たします。

Go言語の設計思想の一つに「実用性」があり、開発者が効率的にコードを書けるように、コンパイラ、リンカ、デバッガ、プロファイラ、そしてカバレッジツールといった開発支援ツール群が重視されていました。このコミットは、Go言語の初期開発において、コードの品質保証とテスト網羅率の可視化を目的として、カバレッジ測定機能が導入されたことを示しています。

また、Go言語の初期のツールは、Bell LabsのPlan 9オペレーティングシステムの影響を強く受けており、その設計思想や一部のライブラリが流用されていました。このコミットで追加されたsrc/cmd/covディレクトリ内のC言語コードや、libmach_amd64といったライブラリの利用は、その影響を色濃く反映しています。

前提知識の解説

このコミットを理解するためには、以下の技術的背景知識が役立ちます。

  1. コードカバレッジ:

    • プログラムのテスト時に、ソースコードのどの部分が実行されたか(網羅されたか)を測定する手法です。
    • 主なカバレッジの種類には、ステートメントカバレッジ(各文が実行されたか)、ブランチカバレッジ(各分岐が両方のパスを通ったか)、ファンクションカバレッジ(各関数が呼び出されたか)などがあります。
    • カバレッジツールは、通常、プログラムの実行中に特定のポイント(例: 各命令の開始点)にフックを仕込み、そのフックが実行されたかどうかを記録することでカバレッジを測定します。
    • Go言語では、go test -coverコマンドでコードカバレッジを測定できますが、このコミットはそれ以前の、より低レベルな実装の初期段階を示しています。
  2. アセンブリ言語と機械語:

    • 6covの出力例に見られるMOVLADDQは、x86-64アーキテクチャのアセンブリ命令です。
    • 0x27c9-0x2829のような表記は、メモリ上のアドレス範囲を示しており、特定の命令が配置されている場所を指します。
    • コードカバレッジツールは、実行ファイルの機械語レベルで命令の実行を追跡することがあります。これは、プログラムの実行中にブレークポイントを設定し、そのブレークポイントがヒットしたかどうかを監視するデバッガのようなメカニズムに似ています。
  3. Plan 9 from Bell Labs:

    • Unixの後継としてBell Labsで開発された分散オペレーティングシステムです。
    • Go言語の設計者の一部(Ken Thompson, Rob Pike, Russ Cox)はPlan 9の開発にも携わっており、Go言語のツールチェインや標準ライブラリにはPlan 9の設計思想(例: 全てをファイルとして扱う、シンプルなツールを組み合わせて複雑なタスクを解決する)が強く反映されています。
    • このコミットに見られるu.h, libc.h, bio.h, regexp9.hなどのヘッダファイルは、Plan 9の標準ライブラリに由来するものです。regexp9はPlan 9の正規表現ライブラリを指します。
    • libmach_amd64は、Plan 9のlibmachライブラリのx86-64アーキテクチャ版であり、実行ファイルの解析、シンボル情報の取得、プロセスのアタッチ/デタッチ、メモリの読み書きといった低レベルな機械語操作を行うための機能を提供します。
  4. ブレークポイントとプロセスアタッチ:

    • コードカバレッジツールは、実行中のプロセスにアタッチし、プログラムカウンタ(PC)の動きを監視することで、どのコードが実行されたかを判断します。
    • ブレークポイントは、特定のメモリアドレスに一時的に特別な命令(例: INT3命令)を書き込むことで、そのアドレスに到達した際にプログラムの実行を一時停止させるメカニズムです。カバレッジツールは、このブレークポイントのヒットを検出することで、コードの実行を追跡します。

技術的詳細

このコミットで導入された6covツールは、Goプログラムのコードカバレッジを測定するために、以下の主要な技術要素と手順を採用しています。

  1. 実行ファイルの解析:

    • main.cmain関数内で、crackhdrsyminit関数が呼び出され、Goの実行ファイル(6.out)のヘッダ情報とシンボルテーブルが解析されます。これにより、関数の開始アドレスやシンボル名などの情報が取得されます。
    • loadmapは、実行ファイルのテキストセクション(実行可能コード)をメモリマップとしてロードします。
  2. プロセスのアタッチと制御:

    • startprocess関数は、カバレッジ測定対象のGoプログラムをフォークして新しいプロセスとして起動します。
    • ctlproc関数(Plan 9由来のプロセス制御関数)を使って、起動したプロセスを「hang」(一時停止)させたり、「attached」(デバッガのようにアタッチ)したりします。
    • attachproc関数は、対象プロセスのメモリ空間にアタッチし、そのメモリを読み書きできるようにします。
  3. ブレークポイントの設置:

    • cover関数は、実行ファイルのテキストセクション全体にブレークポイントを設置します。
    • treeput関数を使って、breakpointsというTree構造体(赤黒木の実装)に、まだ実行されていないコード範囲(Range構造体)を記録します。このTreeは、pc(プログラムカウンタ)とepc(終了プログラムカウンタ)で定義される範囲をキーとして、実行されていないコードブロックを管理します。
    • put1関数(libmach_amd64の関数)を使って、各ブレークポイントのアドレスにmachdata->bpinst(ブレークポイント命令、通常はINT3など)を書き込みます。
  4. コード実行の監視とカバレッジの記録:

    • go関数は、対象プロセスを「startstop」モードで実行し、ブレークポイントにヒットするたびに停止させます。
    • ブレークポイントにヒットすると、uncover関数が呼び出されます。
    • uncover関数は、ヒットしたブレークポイントの場所から、直線的なコードパス(ジャンプや制御フローの変更がない部分)を特定します。
    • ran関数は、特定されたコード範囲が実行されたことを記録します。具体的には、breakpointsツリーから該当する範囲を削除または調整します。これにより、breakpointsツリーには実行されなかったコード範囲のみが残ります。
    • get1put1を使って、ブレークポイント命令を元の命令に戻し、プログラムの実行を再開します。
  5. カバレッジレポートの生成:

    • プログラムの実行が終了した後、walktree関数がbreakpointsツリーを走査します。
    • missing関数は、breakpointsツリーに残っている(つまり実行されなかった)各コード範囲について、そのソースファイル、行番号、関数名、アドレス範囲、および関連するアセンブリ命令を出力します。これにより、どのコードがテストによって網羅されなかったかが可視化されます。

src/cmd/cov/tree.csrc/cmd/cov/tree.hは、このカバレッジツールが内部で使用する赤黒木(Red-Black Tree)の実装を提供しています。これは、実行されたコード範囲を効率的に管理し、残りの未実行コード範囲を特定するために使用されます。赤黒木は、挿入、削除、検索操作が対数時間で行えるバランスの取れた二分探索木であり、大量のコード範囲を扱うカバレッジツールに適しています。

src/libmach_amd64/darwin.csrc/libmach_amd64/sym.cの変更は、主にmacOS (Darwin) 上でのプロセスアタッチとシンボル解決の堅牢性を向上させるためのものです。特に、darwin.cではwaitpidの再定義や、スレッドのサスペンド/レジュームに関する修正、プロセス終了時のエラーハンドリングの改善が見られます。sym.cでは、ファイルパスの解析における境界チェックの追加や、デバッグ情報からの行番号/アドレス変換ロジックの微調整が行われています。

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

このコミットのコアとなる変更は、主に以下の新しいファイルと既存ファイルの修正に集中しています。

  1. src/cmd/cov/ ディレクトリの新規追加:

    • src/cmd/cov/Makefile: 6covツールのビルド設定。libmach_amd64, libregexp9, libbio, lib9といったPlan 9由来のライブラリにリンクしている点が特徴です。
    • src/cmd/cov/main.c: 6covツールの主要なロジックを実装。プロセスの起動、アタッチ、ブレークポイントの設置と解除、カバレッジの記録、レポート生成などを行います。
    • src/cmd/cov/tree.c: 赤黒木(Red-Black Tree)のデータ構造とその操作(挿入、検索)の実装。main.cでコード範囲の管理に使用されます。
    • src/cmd/cov/tree.h: tree.cで定義された赤黒木構造体のヘッダファイル。
  2. 既存ファイルの修正:

    • src/cmd/prof/Makefile: コメントの修正。dbディレクトリがprofに改名されたことを反映。
    • src/libmach_amd64/darwin.c: macOS (Darwin) 環境でのプロセス制御とスレッド操作に関する修正。特に、waitpidの扱い、スレッドのサスペンド/レジュームのタイミング、エラーメッセージの改善が含まれます。
    • src/libmach_amd64/sym.c: シンボル情報の解析とデバッグ情報からのアドレス/行番号変換に関する修正。fileelem関数での境界チェックの追加や、pc2sp, pc2line, line2addrといった関数での微調整が行われています。

コアとなるコードの解説

src/cmd/cov/main.c (抜粋)

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

/*
 * code coverage
 */

#include <u.h>
#include <time.h>
#include <libc.h>
#include <bio.h>
#include <ctype.h>
#include <regexp9.h>
#include "tree.h"

#include <ureg_amd64.h>
#include <mach_amd64.h>
typedef struct Ureg Ureg;

// ... (usage, global variables, Range struct) ...

/*
 * comparison for Range structures
 * they are "equal" if they overlap, so
 * that a search for [pc, pc+1) finds the
 * Range containing pc.
 */
int
rangecmp(void *va, void *vb)
{
	Range *a = va, *b = vb;
	if(a->epc <= b->pc)
		return 1;
	if(b->epc <= a->pc)
		return -1;
	return 0;
}

/*
 * remember that we ran the section of code [pc, epc).
 */
void
ran(uvlong pc, uvlong epc)
{
	Range key;
	Range *r;
	uvlong oldepc;

	if(chatty)
		print("run %#llux-%#llux\n", pc, epc);

	key.pc = pc;
	key.epc = pc+1;
	r = treeget(&breakpoints, &key); // ブレークポイントツリーから該当範囲を検索
	if(r == nil)
		sysfatal("unchecked breakpoint at %#lux+%d", pc, (int)(epc-pc));

	// Might be that the tail of the sequence
	// was run already, so r->epc is before the end.
	// Adjust len.
	if(epc > r->epc)
		epc = r->epc;

	if(r->pc == pc) {
		r->pc = epc; // 範囲の先頭が実行された場合、先頭を更新
	} else {
		// Chop r to before pc;
		// add new entry for after if needed.
		// Changing r->epc does not affect r's position in the tree.
		oldepc = r->epc;
		r->epc = pc; // 範囲の末尾を更新
		if(epc < oldepc) {
			Range *n;
			n = malloc(sizeof *n);
			n->pc = epc;
			n->epc = oldepc;
			treeput(&breakpoints, n, n); // 残りの未実行部分を新しいエントリとして追加
		}
	}
}

// ... (missing, walktree, breakpoint, cover, uncover, startprocess, go, main functions) ...

main.cran関数は、コードカバレッジの中核をなすロジックです。この関数は、特定のコード範囲[pc, epc)が実行されたことを通知された際に、breakpointsという赤黒木から該当する未実行範囲を更新します。 rangecmp関数は、Range構造体(コードのPC範囲を表す)の比較関数であり、範囲がオーバーラップするかどうかを判定します。これは、赤黒木が範囲ベースの検索を効率的に行うために重要です。 ran関数は、実行された範囲が既存の未実行範囲とどのように重なるかに応じて、未実行範囲を分割したり、先頭や末尾を調整したりして、breakpointsツリーを正確に更新します。これにより、最終的にbreakpointsツリーに残る要素が、実際に実行されなかったコードブロックとなります。

src/cmd/cov/tree.c (抜粋)

// Renamed from Map to Tree to avoid conflict with libmach.

/*
Copyright (c) 2003-2007 Russ Cox, Tom Bergan, Austin Clements,
                        Massachusetts Institute of Technology
Portions Copyright (c) 2009 The Go Authors. All rights reserved.

// ... (License) ...

// Mutable map structure, but still based on
// Okasaki, Red Black Trees in a Functional Setting, JFP 1999,
// which is a lot easier than the traditional red-black
// and plenty fast enough for me.  (Also I could copy
// and edit fmap.c.)

#include <u.h>
#include <libc.h>
#include "tree.h"

#define TreeNode TreeNode
#define Tree Tree

enum
{
	Red = 0,
	Black = 1
};

// Red-black trees are binary trees with this property:
//	1. No red node has a red parent.
//	2. Every path from the root to a leaf contains the
//		same number of black nodes.

// ... (rwTreeNode, balance, ins0, ins1 functions) ...

void
treeput(Tree *m, void *key, void *val)
{
	treeputelem(m, key, val, nil);
}

void*
treeget(Tree *m, void *key)
{
	int i;
	TreeNode *p;

	p = m->root;
	if(m->cmp){ // 比較関数が指定されている場合
		for(;;){
			if(p == nil)
				return nil;
			i = m->cmp(p->key, key); // 比較関数を使ってキーを比較
			if(i < 0)
				p = p->left;
			else if(i > 0)
				p = p->right;
			else
				return p->value;
		}
	}else{ // ポインタ値で直接比較する場合
		for(;;){
			if(p == nil)
				return nil;
			if(p->key == key)
				return p->value;
			if(p->key < key)
				p = p->left;
			else
				p = p->right;
		}
	}
}

tree.cは、赤黒木の基本的な操作であるtreeput(挿入)とtreeget(検索)を実装しています。特にtreeget関数は、Tree構造体に設定された比較関数cmp(このコミットではrangecmp)を使用して、キー(Range構造体)を比較し、ツリー内を探索します。これにより、オーバーラップする範囲を効率的に見つけることができます。赤黒木は、挿入や削除の際に木のバランスを自動的に調整するため、検索性能が保証されます。

これらのコードは、Go言語の初期のツール開発が、C言語とPlan 9のライブラリを基盤として行われていたことを明確に示しています。低レベルなシステムプログラミングの知識と、効率的なデータ構造の利用が、このようなツールの開発には不可欠でした。

関連リンク

参考にした情報源リンク

  • Go言語の初期開発に関する情報(公式ドキュメントやブログ記事)
  • Plan 9のドキュメントとライブラリに関する情報
  • コードカバレッジツールの一般的な動作原理に関する情報
  • 赤黒木(Red-Black Tree)のデータ構造に関する情報
  • x86-64アセンブリ言語の基本に関する情報
  • libmachライブラリに関する情報 (Plan 9の文脈で)
  • Russ Coxの個人ブログやGoプロジェクトへの貢献に関する情報