[インデックス 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
といったライブラリの利用は、その影響を色濃く反映しています。
前提知識の解説
このコミットを理解するためには、以下の技術的背景知識が役立ちます。
-
コードカバレッジ:
- プログラムのテスト時に、ソースコードのどの部分が実行されたか(網羅されたか)を測定する手法です。
- 主なカバレッジの種類には、ステートメントカバレッジ(各文が実行されたか)、ブランチカバレッジ(各分岐が両方のパスを通ったか)、ファンクションカバレッジ(各関数が呼び出されたか)などがあります。
- カバレッジツールは、通常、プログラムの実行中に特定のポイント(例: 各命令の開始点)にフックを仕込み、そのフックが実行されたかどうかを記録することでカバレッジを測定します。
- Go言語では、
go test -cover
コマンドでコードカバレッジを測定できますが、このコミットはそれ以前の、より低レベルな実装の初期段階を示しています。
-
アセンブリ言語と機械語:
6cov
の出力例に見られるMOVL
やADDQ
は、x86-64アーキテクチャのアセンブリ命令です。0x27c9-0x2829
のような表記は、メモリ上のアドレス範囲を示しており、特定の命令が配置されている場所を指します。- コードカバレッジツールは、実行ファイルの機械語レベルで命令の実行を追跡することがあります。これは、プログラムの実行中にブレークポイントを設定し、そのブレークポイントがヒットしたかどうかを監視するデバッガのようなメカニズムに似ています。
-
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アーキテクチャ版であり、実行ファイルの解析、シンボル情報の取得、プロセスのアタッチ/デタッチ、メモリの読み書きといった低レベルな機械語操作を行うための機能を提供します。
-
ブレークポイントとプロセスアタッチ:
- コードカバレッジツールは、実行中のプロセスにアタッチし、プログラムカウンタ(PC)の動きを監視することで、どのコードが実行されたかを判断します。
- ブレークポイントは、特定のメモリアドレスに一時的に特別な命令(例:
INT3
命令)を書き込むことで、そのアドレスに到達した際にプログラムの実行を一時停止させるメカニズムです。カバレッジツールは、このブレークポイントのヒットを検出することで、コードの実行を追跡します。
技術的詳細
このコミットで導入された6cov
ツールは、Goプログラムのコードカバレッジを測定するために、以下の主要な技術要素と手順を採用しています。
-
実行ファイルの解析:
main.c
のmain
関数内で、crackhdr
とsyminit
関数が呼び出され、Goの実行ファイル(6.out
)のヘッダ情報とシンボルテーブルが解析されます。これにより、関数の開始アドレスやシンボル名などの情報が取得されます。loadmap
は、実行ファイルのテキストセクション(実行可能コード)をメモリマップとしてロードします。
-
プロセスのアタッチと制御:
startprocess
関数は、カバレッジ測定対象のGoプログラムをフォークして新しいプロセスとして起動します。ctlproc
関数(Plan 9由来のプロセス制御関数)を使って、起動したプロセスを「hang」(一時停止)させたり、「attached」(デバッガのようにアタッチ)したりします。attachproc
関数は、対象プロセスのメモリ空間にアタッチし、そのメモリを読み書きできるようにします。
-
ブレークポイントの設置:
cover
関数は、実行ファイルのテキストセクション全体にブレークポイントを設置します。treeput
関数を使って、breakpoints
というTree
構造体(赤黒木の実装)に、まだ実行されていないコード範囲(Range
構造体)を記録します。このTree
は、pc
(プログラムカウンタ)とepc
(終了プログラムカウンタ)で定義される範囲をキーとして、実行されていないコードブロックを管理します。put1
関数(libmach_amd64
の関数)を使って、各ブレークポイントのアドレスにmachdata->bpinst
(ブレークポイント命令、通常はINT3
など)を書き込みます。
-
コード実行の監視とカバレッジの記録:
go
関数は、対象プロセスを「startstop」モードで実行し、ブレークポイントにヒットするたびに停止させます。- ブレークポイントにヒットすると、
uncover
関数が呼び出されます。 uncover
関数は、ヒットしたブレークポイントの場所から、直線的なコードパス(ジャンプや制御フローの変更がない部分)を特定します。ran
関数は、特定されたコード範囲が実行されたことを記録します。具体的には、breakpoints
ツリーから該当する範囲を削除または調整します。これにより、breakpoints
ツリーには実行されなかったコード範囲のみが残ります。get1
とput1
を使って、ブレークポイント命令を元の命令に戻し、プログラムの実行を再開します。
-
カバレッジレポートの生成:
- プログラムの実行が終了した後、
walktree
関数がbreakpoints
ツリーを走査します。 missing
関数は、breakpoints
ツリーに残っている(つまり実行されなかった)各コード範囲について、そのソースファイル、行番号、関数名、アドレス範囲、および関連するアセンブリ命令を出力します。これにより、どのコードがテストによって網羅されなかったかが可視化されます。
- プログラムの実行が終了した後、
src/cmd/cov/tree.c
とsrc/cmd/cov/tree.h
は、このカバレッジツールが内部で使用する赤黒木(Red-Black Tree)の実装を提供しています。これは、実行されたコード範囲を効率的に管理し、残りの未実行コード範囲を特定するために使用されます。赤黒木は、挿入、削除、検索操作が対数時間で行えるバランスの取れた二分探索木であり、大量のコード範囲を扱うカバレッジツールに適しています。
src/libmach_amd64/darwin.c
とsrc/libmach_amd64/sym.c
の変更は、主にmacOS (Darwin) 上でのプロセスアタッチとシンボル解決の堅牢性を向上させるためのものです。特に、darwin.c
ではwaitpid
の再定義や、スレッドのサスペンド/レジュームに関する修正、プロセス終了時のエラーハンドリングの改善が見られます。sym.c
では、ファイルパスの解析における境界チェックの追加や、デバッグ情報からの行番号/アドレス変換ロジックの微調整が行われています。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下の新しいファイルと既存ファイルの修正に集中しています。
-
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
で定義された赤黒木構造体のヘッダファイル。
-
既存ファイルの修正:
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.c
のran
関数は、コードカバレッジの中核をなすロジックです。この関数は、特定のコード範囲[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言語の公式ウェブサイト: https://go.dev/
- Plan 9 from Bell Labs: https://9p.io/plan9/
- Russ CoxのGoに関するブログ記事や貢献: https://research.swtch.com/
参考にした情報源リンク
- Go言語の初期開発に関する情報(公式ドキュメントやブログ記事)
- Plan 9のドキュメントとライブラリに関する情報
- コードカバレッジツールの一般的な動作原理に関する情報
- 赤黒木(Red-Black Tree)のデータ構造に関する情報
- x86-64アセンブリ言語の基本に関する情報
libmach
ライブラリに関する情報 (Plan 9の文脈で)- Russ Coxの個人ブログやGoプロジェクトへの貢献に関する情報