08/06/15 | コンパイラ作ってみるかも編−5(flexで字句解析)[★★★◎◎◎] | ||||
ようやくC++で本格的なコンパイラを作ります。 作成したものはプログラム実験部屋のその5−JBCompilerにおいてあります。 1. はじめに コンパイラ作ってみるかも編−その1で始めたコンパイラ作成ですが、VBで軽く字句解析・構文解析の基礎を学んでみました。 そこで、ようやくここから実際のコンパイラを作成しようと思います。 まず、今回作成するコンパイラの大まかな仕様を決めておきます。
今回は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です。
さて、次に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++でコンパイル可能なソースを生成します。 この定義ファイルの大まかな構成は以下のとおりです。
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側で定義するのですが、先にここで載せておきます。
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のものを利用。
上がg++、下がVC++です。 上は最後にあるPc・d・iがなんとなくcharへのポインタ・double・intに関連してそうな雰囲気ですね。 下は後ろの方のH・N・PADが引数の型に関係しそうです。
なお、g++のコンパイル結果については、c++filtコマンドでわかりやすい形に変換できます。 と言ってもg++で生成されたシンボルにだけ有効で、VC++のものには効きません。
3. VC++のシンボルを解釈してもらう VC++のシンボルを自前で解釈することも出来ますが、実はWindows APIでこのシンボルの解釈が可能です。 UnDecorateSymbolName(MSDN) APIを使うと、mangling前の情報に変換することが出来ます。 OSとコンパイラが共通のシンボル解釈が出来るということは、Windows内の規格として決まっているんですかね。 たとえば、以下はコマンドラインの引数に与えた文字列を解釈するサンプルです。 "gcc a.c -limagehlp"でコンパイル可能。
実際にこのプログラムに、先ほどのVC++の出力したシンボルを処理させてみました。 確かに元に戻って見やすい形になりました。何気に__thiscallなんて情報も出力されました。
当然ながら、g++の出力したシンボルは変換できません。
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なんかも載っています。 ---(ここまで) まず、全体のルールは以下のとおり。
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(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の音を使い分けます。(後期のソフトでは、カートリッジ側でも音の操作をするものもあります)
特に矩形波はパラメータが色々あります。以降、これらを見ていきます。 2. 各チャンネルの共通点 個別の音を見る前に、複数のチャンネルで使える共通の機能について説明します。 240Hzのタイマー ファミコン内部では、音のパラメータ設定に関する240Hzのタイマーがあります。 後で音の再生の長さとか、音量変化の早さを指定する際に使います。 このタイマーは2つのモード(4stepと5step)があり、それぞれ以下のタイミングでカウントを行います。 最初の「- * - *」は240Hz4回で1セットで、2・4回目のタイマー時にカウンタが回ることを示します。 5stepモードのときは、5回タイマーが動くうち、2・4回目だけカウンタが回るので変則的な周期となります。
長さ指定 各音色は、音を発生する長さを指定できます。 指定しないこともでき、その場合は停止するまで音を発生し続けます。 この長さは、前述の変則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音分生成することができます。 ほとんどパラメータの無い三角波に加えると、波形やエフェクトを若干操作できるこちらは、もっと多様な表現ができます。 実際、大抵のメロディーはこちらの波形を利用しています。 矩形波では、共通の長さ指定と周波数のほかに、以下の指定をすることができます。
メインのメロディーとして利用されるのも納得ですね。 5. ノイズ ファミコンでは周期的なノイズ音を発生させます。 この周期を変化させることで、爆発音のような低音から、ドラム代わりに使われる高音を出すことができます。 まず、ノイズでは矩形波と共通で以下の2機能を持ちます。
長周期ノイズと短周期ノイズ ファミコン内部では、15bitの乱数用カウンタを持っており、後述の間隔ごとに、このカウンタが変化します。 このカウンタの変化パターンは2通りあり、32767周期の乱数と93周期の乱数が生成可能です。 カウンタは変化するたびに、カウンタは1ビット左シフトされ、追い出されたビットが音源に送られます。 また、そのときのカウンタの値を一部xorで計算した値が、カウンタの最下位ビットから入ります。 実際の計算式は以下のとおり。
この値が、音量指定の分だけ倍率がかけられ、音声再生に利用されます。 テーブルによる周波数指定 三角波・矩形波では、周波数の元となるブロック長を直接指定できましたが、ノイズではなぜかテーブルから指定します。 テーブルは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 さて、ここまで三角波・矩形波・ノイズとファミコンの基本的な音の仕組みを見てみました。 ファミコンは昔のハードとはいえ、割と色んな音を出しているイメージがありましたが、若干のパラメータがあるだけで実際はそこまでの表現力はありません。 それでも色んな名曲ができているのが興味深いですね。 なんとなくまとまったのでこれでおしまい。 情報の信憑性はあまり自信が無いので、誤りがありましたら指摘していただけるとありがたいです。 |