気まぐれな戯れ言 バックナンバー13


気まぐれな戯れ言バックナンバーです。全体の一覧はこちら

08/06/15 コンパイラ作ってみるかも編−5
08/03/15 Visual C++のマングリング規則編
07/12/26 ファミコン音源を再現しよう編

08/06/15 コンパイラ作ってみるかも編−5(flexで字句解析)[★★★◎◎◎]
ようやくC++で本格的なコンパイラを作ります。
作成したものはプログラム実験部屋のその5−JBCompilerにおいてあります。


1. はじめに

コンパイラ作ってみるかも編−その1で始めたコンパイラ作成ですが、VBで軽く字句解析構文解析の基礎を学んでみました。
そこで、ようやくここから実際のコンパイラを作成しようと思います。

まず、今回作成するコンパイラの大まかな仕様を決めておきます。

  • VisualC++2005で作成。
  • 字句解析にはflex、構文解析にはbisonを用いる。
  • 生成されるコードはJavaのクラスコード。

今回はx86のコードは生成せず、Javaのクラスコードを生成します。
この利点として、メモリ管理をJavaVMに丸投げできることや、多少変なコードでもPCの動作に影響を与えず実行できるという利点があります。
.exeを吐くコンパイラを作ると、まともに動くまで何度もおかしな動作しそうでちょっと怖いし(^^;


flex(Wikipedia)とbison(Wikipedia)は、それぞれ字句解析・構文解析を行うコードを生成してくれるプログラムです。
今回は、まずflexを使って字句解析用のコードを作成してみます。
ただ、flexとbisonは連携して使えるようになっており、逆に今回の例では、bisonのコードも準備しないとコンパイルできません。

2. 環境設定

ということで、まずはflex・bisonを入手します。
色々なバージョンがありますが、今回はUnix系の動作に近いものということで、cygwin版を利用します。
cygwinって何?という人はとりあえず検索してインストールしてみてください。

多分デフォルト設定ではflexとbisonはインストールされないため、パッケージ一覧で明示的にflex・bisonがInstallされるようにしてください。
こんな感じ。KeepかInstallになっていればokです。
なお、数年以上古いcygwinを使っている人は、flexのバージョンが2.5未満の可能性があります。
flexは2.5でC++対応など大きく変更されているので、2.5以上のバージョンにしておいてください。

cygwinのインストールが済んだら、環境変数Pathにcygwin\binディレクトリを追加しておき、コマンドプロンプトから実行できるようにしてください。
コマンドプロンプトで↓のように実行できるとokです。
C:\sample>flex -V
flex version 2.5.4

さて、次にVisualC++ 2005からflexを利用する方法です。
flexは一般的に拡張子.lのテキストファイルを処理して、C言語の字句解析器を生成します。
そこで、まず拡張子.lのテキストファイルをプロジェクトに登録し、次にカスタムビルド設定をします。
ソリューションエクスプローラで.lファイルを右クリックして"プロパティ"を選択、"構成プロパティ"の"全般"にある"ツール"の欄を"カスタム ビルド ツール"にします。
そして"カスタム ビルド ステップ"の"全般"を開き、"コマンドライン"の欄にflexのコマンドラインを記述します。
ついでに"説明"や"出力ファイル"の項目も埋めておきます。

たとえばこんな感じ。
flexコマンドでは-oの後に空白をつけずに出力ファイル名を書き、最後に入力ファイル名を書きます。
この例ではjbc.lを入力としてjbclexer.cppを生成しています。

この状態でjbc.lにflexが解釈できる適切なデータを持たせておくと、さしあたり1回ビルドさせるとjbclexer.cppが出来ます。
その後生成されたファイルをプロジェクトに追加すると良いでしょう。

3. flexの定義ファイルを書こう

さて、準備が整ったところでflexに処理させるデータを作成します。
この節では、flexの定義ファイルの一般的な書き方を示します。
コンパイラ作成固有の話は次の章以降に行うので、すでにflexを使ったことがある人はこの節は飛ばして大丈夫です。

Flex 字句スキャナ生成プログラムLex and YACC primer/HOWTOにFlexの日本語マニュアルがあるので、こちらも参考にすると良いでしょう。

今回はJBCompilerで使用したファイルをサンプルに説明していきます。
サンプルファイル(別ウインドウ)

flexは、定義ファイルを処理させることでC/C++でコンパイル可能なソースを生成します。
この定義ファイルの大まかな構成は以下のとおりです。
%{
Cのコード
}%

定義

%%

ルール

%%

Cのコード

2箇所あるCのコード部は、flexにより生成されるソースの先頭・末尾にそのままコピーされます。
生成後のファイルで必要な#includeや変数・関数宣言を入れておきます。
上記のサンプルファイルでは、STL関連のヘッダインクルードや、マクロ定義・関数宣言などをしています。
末尾のCコードは空にしてあります。

続く定義部には、基本的に正規表現に名前を与える処理を行います。
ルール部では、同じ正規表現を何度も使用することがあるため、ここで名前を与えておくと繰り返しがなくなって便利です。
なお、この定義はなくてもかまいません。
サンプルファイルの定義例を以下に挙げます。
ID             [a-zA-Z][_a-zA-Z0-9]*
digit          [0-9]
exponent       [eE][+-]?{digit}+
i              {digit}+
float_constant ({i}\.{i}?|{i}?\.{i}){exponent}?
左側が名前、右側が正規表現です。
正規表現名 ID は、1文字目はアルファベット、2文字目以降はアルファベット・数字・アンダースコアを任意の数繰り返せることを示します。
大抵のプログラム言語の変数名や関数名はこのような正規表現で表現できます。
他にも、数値を表す digit や小数を表す float_constant などを定義しています。
float_constant では、直前の定義 i exponent を利用しています。
他の正規表現を利用する場合は、 float_constant の例のように、前後を大括弧 { } で囲います。

他にも、定義部には、flexに与えるオプションを指示する %option や、後述のスタート状態を示す %x %s があります。
%option では、flexに対する指示をコマンドラインオプションで与える代わりに、同等の指示を定義ファイルから行えます。
これは上記のマニュアルを参照してください。

そして、残るはルール部です。
ここには、正規表現と、その正規表現にマッチした場合の処理を記述します。
基本的には、正規表現に対応して番号を振り、それをreturnすればokです。
このとき、bisonを利用する場合は、yylvalという変数に追加情報を格納できます。

今回のサンプルファイルでは、たとえば以下のような処理を記述しています。
sub		{ yylval.lineno=lineno(); return(T_SUB); }
function		{ yylval.lineno=lineno(); return(T_FUNCTION); }
dim		{ yylval.lineno=lineno(); return(T_DIM); }
左側は正規表現を記述しますが、この例では正規表現はアルファベットだけなので実質この文字列自体が来るかどうかを判定しています。
たとえば入力に sub function dim という文字列が来たとき、yylvalに値を代入して、return文で色々返しています。

つまり、flexは sub function dim という文字列を見つけるたび、呼び出し元に対応する整数T_SUB、T_FUNCTION、T_DIMを返します。
yylvalには、後のデバッグのためここでは元の入力の行番号を代入しておくことにします。
lineno()はflexの定義する関数で、読み込みの行番号を返します。

4. コンパイラ向けのflex定義ファイル

上では、一般的なFlexの定義ファイルを書いてきました。
ここでは、もう少し実践的に、コンパイラ向けの定義ファイルを書いていきます。

まず、字句解析結果の情報を格納するyylvalの型を決めます。
yylvalはbisonが管理する情報であり、flexではそれをちょっと利用するだけになります。
なので、yylvalの型はbison側で定義するのですが、先にここで載せておきます。

	struct{
		int lineno;
		union{
			int type;
			Varlist* vl;
			Expression* expr;
			Statement* stmt;
			ExprVec* exprvec;
			StmtVec* stmtvec;
		};
	};
今回は、行番号を覚えておくlinenoと、一部情報を覚えるのに使うexprしか使いません。
exprの型であるExpressionは式を表す構造体で、自前で作成したものです。
字句解析の結果、数値や文字列が出てきた場合、その値を格納するのに使います。
詳細は今後。

さて、字句解析のルールを書いていきます。
まず、予約語の類は上と同様の処理で構いません。
sub		{ yylval.lineno=lineno(); return(T_SUB); }
function		{ yylval.lineno=lineno(); return(T_FUNCTION); }
dim		{ yylval.lineno=lineno(); return(T_DIM); }
他にも、ifとかforとかcallとか色々作っています。


次に、演算子を処理していきます。
基本的には上と同じですが、順番には若干注意が必要です。
判定は上から順に行うため、 ++ += の判定はより短い + より先に行う必要があります。
+ を上にすると、 ++ += と書いてあっても先に + のルールで処理されてしまいます。

演算子 or のところには何もルールが書いてありませんが、この場合次のルールと同じ処理を行います。
つまり、 or || は全く同じ処理を行います。
"++"	{ yylval.lineno=lineno();return(T_INC);}
"--"	{ yylval.lineno=lineno();return(T_DEC);}
"+="	{ yylval.lineno=lineno();return(T_EQADD);}
"+"	{ yylval.lineno=lineno();return(T_PLUS);	}

"or"
"||"	{ yylval.lineno=lineno();return(T_BOOLOR);	}

次に、定義部で作成した正規表現を使う例です。
正規表現を利用する場合は、中括弧で囲います。
{ID} 予約語以外の文字列ということで、変数名や関数名が該当します。
ただ、これが変数か関数かは構文解析をしないとわかりません。
なお、yylvalを使ってこの文字列を保持することが出来ます。
yytextには正規表現にマッチした文字列が入るため、この例ではこの文字列を一旦リストに格納し、そのインデックスをyylvalに持たせています。
(「yylval.expr->index=GetIndexfromIDList(&IDlist,yytext);」の部分。

{int_constant} は10進数で記述された整数を示す正規表現であるため、整数を示すT_INTをreturnしています。
ここでは、yytextからsscanfを利用して数値を読み取り、yylval.expr->ivalに格納しています。
サンプルファイル中では16進数や浮動小数点の数値も処理しています。
なお、これらの数値処置はよくある処理なので、flexのマニュアル中にも数字の処理として記載されています。

{ID}	{ yylval.expr=new Expression;
	yylval.expr->lineno=lineno();
	yylval.expr->exprtype=T_ID;	
	yylval.expr->index=GetIndexfromIDList(&IDlist,yytext);	return(T_ID); }

{int_constant}   { yylval.expr=new Expression;
		sscanf(yytext,"%ld",&yylval.expr->ival); 
		yylval.expr->lineno=lineno();
		yylval.expr->type=T_INT;
		yylval.expr->exprtype=T_CONST;
		return(T_INT);}
同様に複雑なものとして、「"」で囲まれた文字列の処理があります。
これは長いので、flexのマニュアルにある文字列リテラルの処理を参照ください。


次に、空白・改行文字の処理をしておきます。
{ws} はスペースまたはタブです。
まず最後の2行、 {ws} . はルールが記述されていません。
これは何もしないことを示します。すなわち、字句解析中に空白文字や任意文字が来た場合は無視します。
. は最後のルールとして書くべきでしょう。そうしないと、どんな文字もここにマッチしてしまいます。

"_"{ws}*\n		{nowline=lineno()+1;}
^{ws}*\n		{nowline=lineno()+1;}
\n		{nowline=lineno()+1;yylval.lineno=lineno();return '\n';}
{ws}
.
上3つは改行に関わる処理です。
今回作るコンパイラは、VBのように1行で一区切りとしています。ただ、行末に"_"が来たときだけは次の行と連結します。
このため、途中で改行記号 \n が出てきたら、そのまま数値'\n'を返します。
bisonではこれを行区切りとして処理します。
ただ、1つ目のルールでは、"_"のあとに空白がきて、その後改行が来た場合、'\n'を返しません。
すなわち、bisonには行の存在が認識されない=改行を無視して次の行と連結して処理されます。
2つ目は、行の先頭から空白文字と改行文字しかない、すなわち空行ですね。この場合も何も返しません。

ただ、この3つの処理ではnowlineという変数を更新しています。
これはあとで構文解析でエラーが発生したときに、発生箇所を特定するのに使います。

最後にコメントの処理をします。
これもflexのマニュアルのコメントの処理の欄にあるものに少し手を入れています。

ここでは、定義部で定義したスタート状態 %x が使われています。
先頭の2行だけはルール部ではなく定義部にあると思ってください。
%x comment_line
%x comment_block


"'"	{ BEGIN(comment_line);}
"//"	{ BEGIN(comment_line);}
"/*"	{ BEGIN(comment_block);}

<comment_line>[^\n]+ {nowline=lineno()+1;}	
<comment_line>[\n]	{ BEGIN(INITIAL);}

<comment_block>"*/" { BEGIN(INITIAL);}
<comment_block>[*/]	
<comment_block>[^*/\n]+	{nowline=lineno()+1;}
<comment_block>[\n]	{nowline=lineno()+1;}
ルールの最初の3つは、コメント処理の開始を示します。
これらの文字列を受けると、BEGIN文により指定したスタート状態 comment_line comment_block に入ります。
これらのスタート状態に入ると、以後先頭に <comment_line> <comment_block> が付いた行だけが処理対象のルールに入ります。

たとえば、 ' // により comment_line 状態に入った場合を考えます。
以後、改行以外の文字が来たときは変数nowlineを更新するだけで、改行が来るとBEGIN文により初期スタート状態 INITIAL に戻ります。
これにより、改行までをコメント化することが出来ます。

comment_block も同様で、コメントの終端 */ が来るまではnowlineの更新以外の処理を行いません。
5. まとめ

今回は早くもこれで終わりです。
実際プログラムを動かすには、bison側の設定も必要なので…

flexの役割は、正規表現にマッチする文字列を見つけたら対応する整数値をreturnすること。
その際追加情報があればyylvalに格納すること。
ここさえ覚えておけば、あとは正規表現やスタート状態を適切に設定するだけで字句解析が出来ます。

コンパイラを作る場合、どんな言語でもflexは似たような記述になると思います。
(演算子が「:-)」で、予約語が「B-)」みたいな変な言語を考えない限りは(^^;)
次回、bisonでは構文解析を行いますが、このあたりから作ろうとする言語の特徴が出てきますのでお楽しみに。


参考文献

08/03/15 Visual C++のマングリング規則編[★★★★◎]
なんとなくVisual C++のマングリング規則について調査してみました。

1. はじめに   2. 他コンパイラとの比較   3. VC++のシンボルを解釈してもらう
4. VC++の関数mangling規則の詳細   5. VC++の変数mangling規則   6. まとめ
参考文献

1. はじめに

C++では、関数のオーバーロードにより、同じ名前の関数を複数作成することが出来ます。
プログラムを書く側は、単に異なる型を持つ同名関数を複数作るだけでよいですが、じゃあそれをコンパイル・リンクするときにどうなるか、というのを知っておくと便利です。

C++では、プログラムをコンパイルすると、関数名は引数の型情報を含むシンボル文字列に変換されます。
これをmanglingとかdecorationと言います。日本語だと名前修飾(wikipedia)とも呼ばれるようですね。

このおかげで、複数のファイルをコンパイルしたときも、ちゃんとリンク時に正しい引数の型を持つ関数呼び出しを参照することができます。
逆にmanglingされたシンボル名を元に戻すことを、demanglingとかundecorationと呼びます。

このmanglingの方法は特に規格で定められてもいないため、コンパイラによって異なります。
gccのmanglingについては資料が見つかるのですが、Visual C++の公式資料が見つからないので、ネット上で探してみました。


2. 他コンパイラとの比較

まずは実際に、コンパイラ間の違いを見るということで、以下の簡単なC++のコードをgcc3.4.4とVisualC++2005でコンパイルし、binutilsのnmコマンド(wikipedia)でシンボル検索をしてみました。
見てわかるとおり、クラスhogeのメンバ関数fooは3通りの引数型を持ちます。

ちなみに、g++およびnmはcygwinのものを利用。

#include <iostream>

class hoge {
	void foo(char* b) {  std::cout << b << std::endl; };
	void foo(int i) {    std::cout << i << std::endl; };
	void foo(double d) { std::cout << d << std::endl; };
};

int main() {
	hoge a;
	a.foo("Hello, World!");
	a.foo(1);
	a.foo(3.14);
	return 0;
}
両コンパイラの生成するオブジェクトファイルから、nmコマンドでクラスhoge関連のシンボルを出力してみました。
上がg++、下がVC++です。
上は最後にあるPc・d・iがなんとなくcharへのポインタ・double・intに関連してそうな雰囲気ですね。
下は後ろの方のH・N・PADが引数の型に関係しそうです。

> nm test.o | grep hoge
00000000 t .text$_ZN4hoge3fooEPc
00000000 t .text$_ZN4hoge3fooEd
00000000 t .text$_ZN4hoge3fooEi
00000000 T __ZN4hoge3fooEPc
00000000 T __ZN4hoge3fooEd
00000000 T __ZN4hoge3fooEi

> nm test.obj | grep hoge
00000000 T ?foo@hoge@@QAEXH@Z
00000000 T ?foo@hoge@@QAEXN@Z
00000000 T ?foo@hoge@@QAEXPAD@Z

なお、g++のコンパイル結果については、c++filtコマンドでわかりやすい形に変換できます。
と言ってもg++で生成されたシンボルにだけ有効で、VC++のものには効きません。
> nm test.o | grep hoge | c++filt
00000000 t .text$_ZN4hoge3fooEPc
00000000 t .text$_ZN4hoge3fooEd
00000000 t .text$_ZN4hoge3fooEi
00000000 T hoge::foo(char*)
00000000 T hoge::foo(double)
00000000 T hoge::foo(int)

> nm test.obj | grep hoge | c++filt
00000000 T ?foo@hoge@@QAEXH@Z
00000000 T ?foo@hoge@@QAEXN@Z
00000000 T ?foo@hoge@@QAEXPAD@Z


3. VC++のシンボルを解釈してもらう

VC++のシンボルを自前で解釈することも出来ますが、実はWindows APIでこのシンボルの解釈が可能です。
UnDecorateSymbolName(MSDN) APIを使うと、mangling前の情報に変換することが出来ます。
OSとコンパイラが共通のシンボル解釈が出来るということは、Windows内の規格として決まっているんですかね。

たとえば、以下はコマンドラインの引数に与えた文字列を解釈するサンプルです。
"gcc a.c -limagehlp"でコンパイル可能。

#include <stdio.h>
#include <windows.h>
#include <imagehlp.h>

int main(int argc,char** argv){
	int i;
	char buf[256];
	for(i=1;i<argc;i++){
		if(UnDecorateSymbolName(argv[i],buf,255,UNDNAME_COMPLETE))
			printf("%s : %s\n",argv[i],buf);
	}
	return 0;
}

実際にこのプログラムに、先ほどのVC++の出力したシンボルを処理させてみました。
確かに元に戻って見やすい形になりました。何気に__thiscallなんて情報も出力されました。

> ./a.exe ?foo@hoge@@QAEXH@Z ?foo@hoge@@QAEXN@Z ?foo@hoge@@QAEXPAD@Z
?foo@hoge@@QAEXH@Z : public: void __thiscall hoge::foo(int)
?foo@hoge@@QAEXN@Z : public: void __thiscall hoge::foo(double)
?foo@hoge@@QAEXPAD@Z : public: void __thiscall hoge::foo(char *)

当然ながら、g++の出力したシンボルは変換できません。
> ./a.exe __ZN4hoge3fooEPc
__ZN4hoge3fooEPc : __ZN4hoge3fooEPc


4. VC++の関数mangling規則の詳細

さて、上のUnDecorateSymbolNameを使うとmanglingされたシンボル名を読めるようになります。
とはいえ、単に盲目的にこのAPIを使うのではなく、せっかくなので変換規則を調べてみました。

Microsoft公式の資料は見つかりませんでしたが、"C++ Name Mangling/Demangling"のページに説明がありました。
Microsoft Visual C++ Name Mangling(wikipedia)には上記サイトの情報を下にBNF化した文法を載せています。
以下は、両サイトの情報を自分なりに解釈したものを載せていきます。

(以下2008/03/31追記)---
他にVisual C++のmangling規則に関するページとして、Microsoft C++ Name Mangling Schemeがあるようです。
このページは若干読みにくいですが、情報の網羅性では上の"C++ Name Mangling/Demangling"やWikipedia、(そしてこのページ)より上ですね。
テンプレートのmanglingなんかも載っています。
---(ここまで)

まず、全体のルールは以下のとおり。
  1. 先頭は必ず"?"
  2. 関数名 (演算子やコンストラクタも特殊な名前で記述。後述)
  3. クラス名 (クラスに属さない普通のグローバル関数の場合は省略)
  4. 関数の種類
  5. 呼び出し規則
  6. 関数の返り値の型
  7. 関数の引数の型
  8. 終端は必ず"Z"
この時点で、"?foo@hoge@@QAEXH@Z" "?foo@hoge@@QAEXN@Z" "?foo@hoge@@QAEXPAD@Z"がいずれも先頭に"?"、終端に"Z"を持つ理由がわかりますね。

関数名
基本的には、関数名そのものに、最後"@"をつけたものです。
先ほどの例だと"?foo@hoge@@QAEXH@Z"の部分ですね。
ただし、クラスの演算子については、「"?"+1文字」で決まっています。
この1文字の部分は、上記のWikipediaのページのOperatorCodeの項目に書いてあります。
例を挙げると、コンストラクタは"?0"、operator+は"?H"、operator()は"?R"というようになっています。

たとえば、"myclass::myclass(int x)"は"??0myclass@@QAE@H@Z"となります。

クラス名
関数が何らかのクラスのメンバ関数である場合、「クラス名 + "@"」となります。
上記のhoge::foo()の例だと、"?foo@hoge@@QAEXH@Z"となります。
クラスがネストしている場合、「クラス名 + "@"」を繰り返します。
"int myclass::nested::F(int bar)"だと"?F@nested@myclass@@QAEHH@Z"になります。

クラスのメンバ関数ではない普通の関数ではこの項目は省略されます。

関数の種類
「@+1文字」で関数の種類を表します。
関数がクラスに属さない普通の関数の場合は、"@Y"となります。
関数がクラスのメンバ関数の場合、(private/protected/public)×(普通のメンバ関数/staticメンバ関数/仮想関数)で9通りをあらわします。
上記のhoge::fooの様にpublicなメンバ関数は"@Q"、privateな仮想関数は"@E"というようになります。
"char f(void)"なら"?f@@YADXZ"、hoge::foo()の例だと"?foo@hoge@@QAEXH@Z"となっていますね。

他の種類の詳細はWikipediaのページのTypeCodeの欄を参照。


呼び出し規則
_cdeclなら"A"、_fastcallなら"I"、_stdcallなら"G"、_thiscallなら"AE"です。
上のUnDecorateSymbolNameの結果では、hoge::fooが_thiscallになっていますが、確かに"?foo@hoge@@QAEXH@Z"
ですね。
AとAEは共通のprefixを持つのに、どう見分けるんだという気がしますが、AEとなるのはクラスのメンバ関数だけなので、最初に上の関数の種類を調べればわかりそうですね。

関数の返り値の型
関数の引数の型
これはどちらも似たような形になります。
返り値は、voidなら"X"、それ以外は後述の変数の型を記述します。
引数の型は、voidならこちらも"X"、それ以外は、後述の変数の型を引数の数だけ並べます。
そして最後は"@"で終わります。
ただし、最後が可変長引数の場合は"@"ではなく"Z"で締めます。

変数の型
さて、あとは関数の返り値と引数の型の表記方法がわかれば完了です。
ただ、これがなかなかに面倒。

単純な型
まず、charやunsigned intと言った単純な型は、"D"や"I"と言った1文字が決まっています。
他の型はWikipediaのページのSimpleDataTypeの欄を参照。
また、若干特殊な型として、__int64、unsigned __int64、bool、__wchar_tの4つも決まった2文字があります。
これはそれぞれ"_J"、"_K"、"_N"、"_W"となっています。

共用体・構造体・クラス・列挙体
これらはそれぞれ"T"、"U"、"V"、"W4"の後に名前を記述し、最後に"@"で締めます。
名前自体にも終端を"@"で締める必要があり、通常は最後"@@"と2つの"@"が付きます。
このとき、"T"、"U"、"V"、"W4"の後に名前ではなく0-9の数値を書くと、過去に出てきた名前を参照することが出来ます。
参照先は、全体の最初の関数名やクラス名を含め、文字列を記述したものを左端から0,1,2…と割り当てます。
この場合は名前自体の終端"@"は不要なので、型全体の最後は"@"1個で終わります。

たとえば、こんなメンバ関数を作成してみます。

class aaaa;
class bbbb;
class cccc {
public:
	int*** hoge(int,bbbb*,cccc*,aaaa*,aaaa*,aaaa**,bbbb**);
};

この出力は"?hoge@cccc@@QAEPAPAPAHHPAVbbbb@@PAV1@PAVaaaa@@2PAPAV3@PAPAV2@@Z"です。
最初の下線部"PAV1@"は第3引数のcccc*に相当します。
このシンボル名では、最初の名前がhoge、次に出てくる名前がccccなので、"V1@"は2番目の"cccc"を表します("PA"はポインタであることを表す。後述。)

同様に、第6引数aaaa**を示す"PAPAV3@"の"V3@"は4番目にでてきた"aaaa"を表し、第7引数bbbb**を示す"PAPAV2@"の"V2@"は3番目に出てきた"bbbb"の名前を示します。


ポインタ・参照型
まず最初にポインタ・参照の属性を表す1文字が入ります。
参照なら"A"、普通のポインタは"P"、constポインタは"Q"、volatileポインタは"R"です。
ポインタのポインタ…というようにネストしたポインタの場合、その回数だけポインタなら"AP"、constポインタは"BQ"、volatileポインタは"CR"を繰り返します。
次に、ポイント・参照先の型をつけますが、その前にconst性を示す1文字をつけます。通常は"A"、constなら"B"、volatileなら"C"です。
そして最後にポイント・参照先の型をつけます。

先ほどの"?hoge@cccc@@QAEPAPAPAHHPAVbbbb@@PAV1@PAVaaaa@@2PAPAV3@PAPAV2@@Z"を見てみます。
まず返り値の型の"PAPAPAH"は非constなint(AH)のポインタ(AP)のポインタ(AP)のポインタ(P)です。
最後の型の"PAPAV2@"は非constなクラスbbbb(AV2@、3番目の名前)のポインタ(AP)のポインタ(P)です。

関数ポインタ
他に複雑な型として、関数ポインタがあります。
最初は普通のポインタ同様、"A"、"P"、"Q"、"R"を付け、ネストしたポインタならさらに"AP"、"BQ"、"CR"をつけます。
変数の場合は次に"A"、"B"、"C"をつけますが、通常の関数の場合は"6"、メンバ関数なら"8"になります。
その後は再帰的な記述になりますが、クラス名・呼び出し規則・返り値の型・引数の型・最後の"Z"で締めます。

たとえば、intを引数にとってintを返す関数ポインタを引数にしてみます。

typedef int (*x)(int); 
int F(x fnptr)

このシンボルは"?F@@YAHP6AHH@Z@Z"であり、下線部が「返り値int(H)、引数int(H)な__cdecl(A)呼び出しの関数ポインタ(P6)」を示します。

型の省略記法
さて、上で共用体やクラスなどの名前の省略記法を紹介しましたが、それとは別に、型にも省略記法があります。
引数型のところで単に数字1文字を書くと、過去に引数リストで出てきた1文字で表せない型(参照・ポインタ型または共用体・クラス・構造体・列挙型または"_J"、"_K"、"_N"、"_W")を参照できます。
返り値の型はカウントしませんし、参照できません。
上の例の"?hoge@cccc@@QAEPAPAPAHHPAVbbbb@@PAV1@PAVaaaa@@2PAPAV3@PAPAV2@@Z"がそうです。
この下線を引いた2は第5引数のaaaa*を示します。
ここまで第5引数までに、1文字で表せない型としてbbbb*(PAVbbbb@@)とcccc*(PAV1@)、aaaa*(PAVaaaa@@)があり、2は3つ目のaaaa*を参照します。


5. VC++の変数mangling規則

せっかくなので、オマケで関数だけではなく変数のmanglingの規則も見てみます。
とはいえ、ここまでの話を踏まえると簡単です。

まず、"?"の後に変数名・クラス名を書くところは関数と同じ。
関数の場合はその後"@Q"とか"@Y"とか書きましたが、変数の場合はメンバ変数は"@2"、普通の変数は"@3"になります。
その後、上で書いた変数の型を書き、最後にconst性を示す"A"、"B"、"C"を書きます。

たとえば、"int aiueo"なら"?aiueo@@3HA"、"hoge* ptest"なら"?ptest@@3PAVhoge@@A"、staticメンバ変数の"int kakikukeko::data"なら"?data@kakiku@@2HA"です。


6. まとめ

色々と複雑ですが、これでようやく説明を終わりです。
ここまでの知識とWikipediaのページの情報をあわせると、最初に出てきたシンボルも理解できるようになります。

?foo@hoge@@QAEXH@Z : public: void __thiscall hoge::foo(int)
?foo@hoge@@QAEXN@Z : public: void __thiscall hoge::foo(double)
?foo@hoge@@QAEXPAD@Z : public: void __thiscall hoge::foo(char *)

たとえば最後の例を見ると、関数名はfoo(foo@)で、クラスhoge(hoge@)のpublicメンバ関数(@Q)であり、呼び出し規則は__thiscall(AE)。
返り値の型はvoid(X)で、引数は非const(A)なchar(D)へのポインタ(P)である、と。

WikipediaのBNFだけ見ても、なかなか意味を追うのがしんどいので、ここの情報が参考になれば幸いです。
"C++ Name Mangling/Demangling"の最後に、沢山サンプル例があるので、こちらも見比べてみると良いでしょう。


参考文献

07/12/26 ファミコン音源を再現しよう編[★★◎◎◎]
昨今ゲームではMIDIが使われることも減り、フリーのゲームすらWAVファイルやそれを圧縮したMP3やOGG形式ファイルが使われるようになってきました。
とはいえ、ファミコン音源やFM音源には独自のよさもあります。
ということで、ファミコン音源の内容を調査して実装にチャレンジしてみました。
なお、実際に作成したプログラムは、ミニミニソフト集の「★その56−ファミコン音源再現ソフト FamiWave v1.0」にあります。

注意:ここの内容はネット内で調べた情報をまとめた物です。
正規のファミコンの資料によるものではないので、正確さは微妙。
目的は音の再現であり、それと関係ない部分は省いています。

1. はじめに   2. 各チャンネルの共通点   3. 三角波
4. 矩形波   5. ノイズ   6. 最終的な音の合成とまとめ


1. はじめに

ファミコンの音源はPSG(Programmable Sound Generator)と呼ばれる音源の一種です。(wikipedia)
通常は本体が持つ以下の5chの音を使い分けます。(後期のソフトでは、カートリッジ側でも音の操作をするものもあります)

  1. 三角波 : 正弦波に近く、滑らかな感じの音。ベースとしての利用が多く、メロディーは少ない。
    使用例:DQ3のアレフガルドメロディ、DQ4のほこらメロディ、FF3戦闘曲最初の「デデデデ デデデデ×2」。
  2. 矩形波 × 2ch : 重い感じの音。大概のメロディーはこれ。
  3. ノイズ : 爆発音やドラムに利用。
  4. DPCM : 波形をそのまま出力できる。ファミコン後期のゲームで多い。この記事では詳細は省略。
たった4種類の割にいろいろな音が出てるように思えますが、パラメータの調整によってさまざまな音色が出せます。
特に矩形波はパラメータが色々あります。以降、これらを見ていきます。

2. 各チャンネルの共通点

個別の音を見る前に、複数のチャンネルで使える共通の機能について説明します。

240Hzのタイマー

ファミコン内部では、音のパラメータ設定に関する240Hzのタイマーがあります。
後で音の再生の長さとか、音量変化の早さを指定する際に使います。

このタイマーは2つのモード(4stepと5step)があり、それぞれ以下のタイミングでカウントを行います。
最初の「- * - *」は240Hz4回で1セットで、2・4回目のタイマー時にカウンタが回ることを示します。
5stepモードのときは、5回タイマーが動くうち、2・4回目だけカウンタが回るので変則的な周期となります。

モード用途タイミング周波数
4step長さカウンタ・sweep速度 - * - * 120Hz
5step * - * - -変則96Hz
4step三角波長さカウンタ・エンベロープ * * * *240Hz
5step * * * * -変則192Hz

長さ指定

各音色は、音を発生する長さを指定できます。
指定しないこともでき、その場合は停止するまで音を発生し続けます。
この長さは、前述の変則96Hzまたは120Hzのタイマーで以下のカウント数分から選択できます。
2,4,6,8,10,12,14,16,18,20,24,26,28,30,40,48,60,72,80,96,160,192,254
大体0.02〜2秒程度ですね。

周波数

三角波・矩形波では、波形の周波数を指定できます。
ただ、指定方法は周波数そのものではなく、約1789773Hzのタイマーに対し、何クロックで波形が1ブロック進むかを指定します。
三角波は32ブロックで1周、矩形波は16ブロックで1周になります。

指定は11bit値でできるので、三角波なら1789773÷32〜1789773÷32÷2047(11bitの最大値)の周波数が指定できることになります。


3. 三角波

一番単純なのが三角波です。
ファミコンの三角波は、若干階段状の波形を生成します。
16段階の段差を持ち、0,1,2,3, ... , 13,14,15,15,14,13, ... 2,1,0,0,1,2 ... という波形になります。
図で表すとこんな感じ。

音はこんな感じ(ogg形式)です。

パラメータは限られており、周波数と長さ指定のみ可能であり、音量指定すらできません。
一方で、なぜか長さ指定は2通り可能。
ひとつは上で示した各波形共通の長さ指定方法、もうひとつは三角波専用の長さ指定です。

前述の変則192Hzまたは240Hzのタイマーを使って、1〜127クロックまで任意の値を指定できます。
共通の長さ指定では決まった長さから選ぶのに対し、こちらは細かく指定できます。
でも、こんな似たパラメータ作るなら別の機能がほしかったなぁ(^^;


4. 矩形波

矩形波は、名前のとおり四角い波で、波形は高い位置と低い位置を交互に出力します。
ファミコンの矩形波は、2音分生成することができます。
ほとんどパラメータの無い三角波に加えると、波形やエフェクトを若干操作できるこちらは、もっと多様な表現ができます。
実際、大抵のメロディーはこちらの波形を利用しています。

矩形波では、共通の長さ指定と周波数のほかに、以下の指定をすることができます。
  • 4種類のDuty比
    波形の高い時間と短い時間の比を、1:1、1:3、1:7、3:1の4つから選択可能。
    最初の3つを図で表すとこんな感じ。

    duty比を変えると、音はこんな感じで変わります。
    Duty比1:1(ogg形式)Duty比1:3(ogg形式)Duty比1:7(ogg形式)と音色が変化します。
    Duty比が1:1に近いほど波形がsin波に近く、滑らかな音に聞こえますね。

  • Sweep機能
    矩形波では、音の高さを段々高くor段々短くするSweep機能があります。
    Sweep機能では、変化の間隔、方向(加算・減算)、変化量を指定できます。
    前述のとおり、ファミコンでは周波数そのものではなく、波形が1ブロック進むまでのクロック数で音の高さを管理しており、Sweep機能ではこの値を操作します。

    変化の間隔は前述の変則96Hzまたは120Hzを用いて、1〜8クロックの間隔を指定できます。
    方向は、周波数を加算するか増減するかの設定です。これにより、音を段々高くするか低くするか指定できます。
    変化量は、現在のクロック数に対し、右何bitシフトしたものを加算or減算するかを0〜7で指定します。
    なお、減算するときには2chの矩形波では若干処理が異なり、1ch目では普通に減算しますが、2ch目は1だけ余分に値を減算します。

    つまり、変化後のクロックはこんな計算式で決まります。
    方向およびch計算式
    加算NewClock = OldClock + (OldClock >> Sweep)
    減算(ch1)NewClock = OldClock - (OldClock >> Sweep)
    減算(ch2)NewClock = OldClock - (OldClock >> Sweep) - 1
    加算Sweep例(ogg形式)はこんな感じです。
    もっとゆっくり変化させたものは、ジャンプ音などによく利用されますね。

  • 音量指定・変化
    三角波では音量指定ができませんでしたが、矩形波では固定音量またはフェードアウト指定ができます。
    固定音量では、0〜15の16段階で音量を指定します。
    フェードアウト指定では、最初音量は最大の15で始まり、以後前述の変則192Hzまたは240Hzのタイマーが指定クロックを刻むたびに、音量が1ずつ下がります。
    この指定クロックには1〜16が指定可能です。最長の16にすると、240Hzのタイマーで音が出なくなるまで1秒かかりますね。
    また、音量が0になった後また音量15にするループ指定も可能です。
ここまで挙げたように、矩形波では割と色々な操作が可能になっています。
メインのメロディーとして利用されるのも納得ですね。


5. ノイズ

ファミコンでは周期的なノイズ音を発生させます。
この周期を変化させることで、爆発音のような低音から、ドラム代わりに使われる高音を出すことができます。

まず、ノイズでは矩形波と共通で以下の2機能を持ちます。
  • 共通の長さ指定
  • 音量指定・変化

長周期ノイズと短周期ノイズ

ファミコン内部では、15bitの乱数用カウンタを持っており、後述の間隔ごとに、このカウンタが変化します。
このカウンタの変化パターンは2通りあり、32767周期の乱数と93周期の乱数が生成可能です。
カウンタは変化するたびに、カウンタは1ビット左シフトされ、追い出されたビットが音源に送られます。

また、そのときのカウンタの値を一部xorで計算した値が、カウンタの最下位ビットから入ります。
実際の計算式は以下のとおり。
周期計算式
32767NewRand = ((OldClock<<1) | ((OldClockの15bit目) ^ (OldClockの14bit目))) & 0xFFFF
93NewRand = ((OldClock<<1) | ((OldClockの15bit目) ^ (OldClockの9bit目))) & 0xFFFF
音源側に伝わる乱数の精度は、OldClockの最上位から追い出された1bitだけです。
この値が、音量指定の分だけ倍率がかけられ、音声再生に利用されます。

テーブルによる周波数指定

三角波・矩形波では、周波数の元となるブロック長を直接指定できましたが、ノイズではなぜかテーブルから指定します。
テーブルは16エントリあり、1789773Hzのタイマーがこのエントリ数クロック分を刻むごとに、前述の計算式を用いて新たな乱数が生成されます。
テーブルのエントリは4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068となっています。

最後に生成例を。
長周期ノイズ例(ogg形式)。爆発音とかでよく聞く感じですね。
短周期ノイズ例(ogg形式)。ここでは2秒も生成していますが、通常こんな長く使うことはなく、もっと効果音的な使い方が主でしょう。


6. 最終的な音の合成とまとめ
ここまで出てきた波形は、以下の計算式で合成されるそうです。
なんか複雑な式ですね。どっからこんな値が出てきたんでしょう。
          95.88                               159.79
 -----------------------          ------------------------------
        8128                  +               1
 ----------------- + 100          ------------------------ + 100
 矩形波1 + 矩形波2                三角波     ノイズ   DMA
                                  -------- + ----- + -----
                                    8227     12241   22638

さて、ここまで三角波・矩形波・ノイズとファミコンの基本的な音の仕組みを見てみました。
ファミコンは昔のハードとはいえ、割と色んな音を出しているイメージがありましたが、若干のパラメータがあるだけで実際はそこまでの表現力はありません。
それでも色んな名曲ができているのが興味深いですね。

なんとなくまとまったのでこれでおしまい。
情報の信憑性はあまり自信が無いので、誤りがありましたら指摘していただけるとありがたいです。


一覧へ戻る