はじめに
前回の記事の続きです。
上記の記事ではまず下記4つの選択肢を挙げました。
(1) Microsoft Visual C/C++ コンパイラを C++ コンパイラとして使う。
(2) LLVM clang を使う。
(3) GCC を使う。
(4) 静的解析ツールを使う。
しかし結局のところ,どれも採用しないで5つめの選択肢をひねり出しました。
(5) msvc をプリプロセッサとして使い,LLVM clang を使う。
しかし,この方式で日本語が許されるのはコメント内のみで,プログラム中の文字列で使うのはもちろん NG ですし,ファイル名やディレクトリ名に日本語を使うのもダメです。つまり,使用できる場所がかなり限定されてしまうのです。
第6の選択肢~LLVMリンカを使えば検出できるか?
そういう訳で Microsoft Visual C コンパイラだけを使ってtentative definition を検出できる方法はないかと悪足掻きをします。
今回の目的は,同一コンパイル単位での tentative definition 検出ではなく,複数のコンパイル単位で重複宣言されたグローバル変数を検出することです。したがって Visual C++ コンパイラであれ,LLVM clang であってもコンパイル時ではなくリンク時にエラーを検出するはずです。であれば,Visual C コンパイラでコンパイルして,LLVM のリンカを使えば検出できるかもと思ったのです。
結論を先に申し上げると検出できません。試しに下記のソースコードを様々なコンパイラでコンパイルして生成したオブジェクトコードを見てみましょう。
static const int u = 0;
const int v = 0;
int w = 0;
int x;
extern int y;
static int z;
int getu( void ) { return u; }
int gety( void ) { return y; }
int getz( void ) { return z; }
初期値なしのグローバル変数を x の生成コードを知りたいのですが,比較のためさまざまな変数を用意しました。関数は最適化により未使用変数が削除されてしまうのを防ぐためのダミーです。
Visual C でコンパイルした場合
上記のC言語コードをコンパイルして生成されたオブジェクトファイルのシンボルテーブルを見てみると,初期値なしのグローバル変数 x のセクションは UNDEF となっています。これは COMMON セクションのことで tentative definition による仮定義の領域です。
Visual Studio ツールの dumpbin.exe に加えて,LVMM ツールの llvm-nm.exe の出力も見てみましょう。
Visual C++ でコンパイルした場合
C++ モードでコンパイルした結果を以下に示しますが,シンボルの名前が変わり,セクションも SECT3 と変わっています。これは BSS セクションで初期値なしのデータ領域です。
clang-cl でコンパイルした場合
clang-cl でコンパイルした結果を示します。先ほどと同様にセクションは SECT3 です。
clang-cl だと u が最適化により,削除されてしまったようです。
clang-cl に -fcommon オプションを付けた場合
-fcommon は初期値なしのグローバル変数を COMMON セクションに配置するオプションです。こうすると Visual C コンパイラと同じように UNDEF セクションに配置されます。
コンパイラ比較一覧
以上より,C++ や clang で tentative definition をリンク時に検出していますが,それが可能なのはコンパイル時に初期値なしグローバル変数を BSS セクションに配置しているからです。COMMON セクションに配置されたシンボルを BSS セクションに変更するようなツールがあれば良いのですが,残念ながら存在しないようです。
| コンパイルコマンド | 未初期化変数のセクション | tentative definitionの検出 |
|---|---|---|
cl |
UNDEF (COMMON) | × |
cl /TP |
SECT3 (BSS) | ○ |
clang-cl |
SECT3 (BSS) | ○ |
clang-cl -fcommon |
UNDEF (COMMON) | × |
第7の選択肢~オブジェクトから直接検出できる?
わざわざリンクしないでもオブジェクトファイルの段階で tentative definition を検出できそうなことに気づきました。msvc の dumpbin.exe あるいは LLVM のllvm-nm.exe を用いてオブジェクトファイルのシンボル情報を出力させ,これをスクリプトでパースして重複チェックすれば良いのです。
基本方針
- Tentative Definition CHECKer,略して TDCHECK と命名します。
- Windows マシンに最初からついてくること,文字列処理が楽なので WSH + JavaScript で作ります。
- シンボル出力のパースが簡単なので msvc の
dumpbin.exeではなく,LLVM のllvm-nm.exeを使います。
なお,デフォルトでは下記のようにシンボルが出力されます。
c:\Qiita>llvm-nm aho.obj
0104899a a @comp.id
80010191 a @feat.00
00000003 a @vol.md
00000000 T _getu
00000010 T _gety
00000020 T _getz
00000000 r _u
00000004 R _v
00000000 B _w
00000004 C _x
U _y
00000004 b _z
重複しているシンボル名に加えてファイル名も指摘できるようオプション -A を追加します。また,少しでもデータ量を減らしてパースの負担を減らすため,オプション -U を追加して U セクションを取り除きます。
c:\Qiita>llvm-nm -A -U aho.obj
aho.obj: 0104899a a @comp.id
aho.obj: 80010191 a @feat.00
aho.obj: 00000003 a @vol.md
aho.obj: 00000000 T _getu
aho.obj: 00000010 T _gety
aho.obj: 00000020 T _getz
aho.obj: 00000000 r _u
aho.obj: 00000004 R _v
aho.obj: 00000000 B _w
aho.obj: 00000004 C _x
aho.obj: 00000004 b _z
実装コード
実装コードを以下に示します。
var args = WScript.Arguments.Unnamed;
var ret = main(args);
try {
WScript.Quit(ret);
} catch(e) {
/* 何もしない */
}
function main(args) {
if(args.Count == 0) {
WScript.StdErr.WriteLine("tentative definition を検出します。");
WScript.StdErr.WriteLine();
WScript.StdErr.WriteLine("tdcheck(.js) [オブジェクトファイル名(.obj)] ...");
return -1;
}
var shell = WScript.CreateObject("WScript.Shell");
var command = "LLVM-NM.EXE -A -U"
var list = [];
for(var i = 0; i < args.Count; i++) {
var proc = shell.Exec(command + " " + args(i));
var buf = proc.StdOut.ReadAll();
while(proc.Status == 0)
WScript.Sleep(100);
var line = buf.split("\n");
for(var j = 0; j < line.length; j++) {
var m = line[j].match(/^(.+): [0-9A-Fa-f]{8} [BCDR] (_[A-Za-z_]\w*)$/);
if(m) list.push({symbol:m[2], filename:m[1]});
}
}
list.sort(function(a, b) {
return a.symbol.localeCompare(b.symbol);
});
var count = 0;
for(var i = 0, j = 1; j < list.length; i++, j++) {
if(list[i].symbol == list[j].symbol) {
var s = "シンボル *0 が *1 と *2 で重複しています!!";
s = s.replace("*0", list[i].symbol);
s = s.replace("*1", list[i].filename);
s = s.replace("*2", list[j].filename);
WScript.Echo(s);
count++;
}
}
if(count == 0)
WScript.Echo("シンボルの重複はありません!!");
else
WScript.Echo("シンボルの重複が " + count + " 個あります!!");
return 0;
}
実行例
引数なしで実行するとヘルプメッセージを表示します。
c:\Qiita>TDCHECK.JS
tentative definition を検出します。
tdcheck(.js) [オブジェクトファイル名(.obj)] ...
サンプルファイルを以下に示します。グローバル変数 v,w,x それぞれが重複しています。
実行例を以下に示します。オブジェクトファイル名を任意の数並べることができます。
c:\Qiita>tdcheck aho.obj baka.obj
シンボル _v が aho.obj と baka.obj で重複しています!!
シンボル _w が aho.obj と baka.obj で重複しています!!
シンボル _x が aho.obj と baka.obj で重複しています!!
シンボルの重複が 3 個あります!!
もちろんワイルドカードも使用できます。ワイルドカード展開は llvm-nm.exe 自体の機能です。
c:\Qiita>tdcheck *.obj
シンボル _v が aho.obj と baka.obj で重複しています!!
シンボル _w が aho.obj と baka.obj で重複しています!!
シンボル _x が aho.obj と baka.obj で重複しています!!
シンボルの重複が 3 個あります!!
感想
前回の記事では msvc のプリプロセッサ出力をパイプで clang に引き渡しましたが,流石にアクロバット過ぎたと反省しています。その方法に比べたら今回の方法はかなり穏当な方法だと思っていますが,LLVM ツールの llvm-nm.exe を使っていることから,msvc だけで実現できているとも言い難い訳です。であれば msvc ツールの dumpbin.exe を使えば良さそうなものですが,出力フォーマットがドキュメント化されていないことに加えて,加工するのもいろいろ面倒そうなので早々に選択肢から外してしまったのでした。msvc ツールは UNIX 系のツールと違ってパイプで繋いで処理することをあまり想定していないと思われます。