FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Gem5のISAパーサについて勉強する

といってもひたすらISA Parserのページを翻訳して読んだだけ。一応メモのために残しておく。

www.gem5.org


ISAパーサ

gem5 ISA 記述言語は、gem5 が必要とするクラス定義とデコーダ関数を生成するために特別に設計されたカスタム言語です。このセクションでは、言語自体の実用的で非公式な概要を説明します。この言語のための正式な文法は、パーサーの「yacc」部分に埋め込まれています(isa_parser.pyでp_から始まる関数を探してください)。パーサーの2番目の主要なコンポーネントは、命令の特性を抽出するためにCのようなコード仕様を処理します。この側面は、セクションコード解析でカバーされています。

  • 最も高いレベル
    • ISA記述ファイル
    • 宣言セクションとデコードセクションの2つの部分に分割されます
  • decodeセクション
  • declarationsセクション
    • デコーダをサポートするために必要なグローバルな情報(クラス、命令フォーマット、テンプレートなど)を定義する。

decodeセクションは記述ファイルの焦点であるため、そこから議論を始めることにします。

デコード部

記述のdecodeセクションは、ネストされたデコードブロックの集合である。デコードブロックは、デコードする機械命令のフィールドを指定し、そのフィールドの特定の値に対して提供される結果を指定します。デコードブロックは、構文とセマンティクスの両方においてCのswitch文に類似しています。実際、記述ファイルの各デコードブロックは、結果のデコード関数の中にswitch文を生成します。まず、(少し単純化しすぎた)例から始めましょう。

decode OPCODE {
  0: add({{ Rc = Ra + Rb; }});
  1: sub({{ Rc = Ra - Rb; }});
}

デコードブロックは、キーワード decode の後に、デコードする命令フィールドの名前を付けて始めます。後者はファイルの宣言セクションでビットフィールド定義(ビットフィールド定義参照)を使って定義する必要があります。デコードブロックの残りの部分は、中括弧で囲まれたステートメントのリストです。最も一般的なステートメントは、整数の定数とコロンの後に命令定義が続くものです。この文は、C言語のスイッチにおける「case」文に相当します(ただし、簡潔さのために「case」キーワードは省略されていることに注意してください)。カンマで区切られた整数定数のリストを使用すると、1つのデコード文が一連のビットフィールド値のいずれにも適用されるようにすることができます。

命令定義は、C言語の関数呼び出しと同様の構文で、関数名の代わりに命令のニーモニックが使用されます。カンマで区切られた引数は、命令定義を処理するときに使用されます。上の例では、命令定義はそれぞれ1つの引数、「コード・リテラル」を取ります。コードリテラルは、操作的には文字列定数と似ていますが、二重の中括弧({{}})で区切られます。コードリテラルは行末文字をエスケープすることなく、複数行にまたがることができます。バックスラッシュのエスケープ処理は行われません (例えば、\t は文字通りに解釈され、タブは生成されません)。デリミタは、コードリテラルに含まれるC言語のようなコードがemacsのCモードでうまくフォーマットされるように選ばれました。

デコード文は、命令定義の代わりにネストされたデコードブロックを指定することができます。この場合、外側のブロックで指定されたビットフィールドが与えられた値と一致すると、内側のブロックで指定されたビットフィールドが調べられ、さらにスイッチが実行されます。

C言語のように、デフォルトの動作を定義するために、整数定数の代わりにキーワード default を使用することも合法です。しかし、以下の デコードブロックのデフォルト で説明するデコードブロックのデフォルト構文を使用する方がより一般的でしょう。

命令フォーマットの指定

ISA記述ファイルが処理されるとき、各命令定義は実際、デコードファイルのための適切なC++コードを生成するために関数呼び出しを行います。呼び出される関数は、命令フォーマットによって決定されます。命令フォーマットは、命令定義に与えられる引数の数とタイプ、および対応する出力を生成するためにそれらがどのように処理されるかを決定します。ここでいう「命令形式」とは、これらの定義処理機能の一つのみを指すものであり、ISAで定義された機械語命令形式と必ずしも一対一に対応するものではないことに注意されたい。前の例での1つの過度の単純化は、命令フォーマットが指定されていないことである。その結果、パーサは命令定義をどのように処理すればよいのかわかりません。

命令フォーマットは、2つの方法で指定することができます。明示的な形式指定は、次のようにダブルコロン(:)で区切って、ニーモニックの前に指定することができます。

decode OPCODE {
  0: Integer::add({{ Rc = Ra + Rb; }});
  1: Integer::sub({{ Rc = Ra - Rb; }});

この例では、どちらの命令定義もIntegerという書式で処理されます。より一般的な方法は、次のようにフォーマットブロックを使って一連の定義に対してフォーマットを指定する方法です。

デコード OPCODE {
  format Integer {
    0: add({{ Rc = Ra + Rb; }});
    1: sub({{ Rc = Ra - Rb; }});
  }
}

この例では、"Integer"というフォーマットが、中括弧内のすべての命令定義に適用されています。したがって、この2つの例は機能的に同等です。フォーマットブロックの使用には、ほとんど制約がありません。フォーマットブロックは、デコードブロック内のステートメントのサブセットのみを含むことができます。フォーマットブロックと明示的なフォーマット指定は自由に混在させることができ、後者が優先される。フォーマットブロックとデコードブロックは、互いに任意に入れ子にすることができる。閉じた中括弧は常に最も近いフォーマットブロックまたはデコードブロックと結合し、ブロックの内部に完全にネストしないフォーマットブロックまたはデコードブロックを生成することは構文上不可能であることに注意すること。

明示的なフォーマット指定がない命令定義が発生した場合、最も内側にあるフォーマットブロックに関連するフォーマットが使用されます。明示的な書式指定がなく、囲む書式ブロックもない定義が発生した場合、ランタイムエラーが発生する。

デコードブロックのデフォルト

デコードブロックのデフォルトケースは、C言語のswitch文のようにdefault:ラベルで指定することができます。しかし、ISAの記述では、未知の命令エンコーディングや不正な命令エンコーディングに対応するケースを指定しないことが一般的である。すべてのデコードブロックに default: ケースが必要なのを避けるために、この言語では、現在のデコードブロックと、明示的なデフォルトがないネストしたデコードブロックのデフォルトケースを指定する代替デフォルト構文が許可されています。この代替デフォルトは、ビットフィールドの指定の後(開始波括弧の前)に default キーワードと命令定義を与えることで指定します。一番外側のデコードブロックを次のように指定します。

decode OPCODE default Unknown::unknown() { {...
   [...]
}

は、デフォルトのケースを指定しないすべてのデコードブロックの中に default: Unknown::unknown(); を、デフォルトケースを指定しないすべてのデコードブロックの中に追加することと(ほぼ)同じです。

注: 適切なフォーマット定義(_フォーマット定義参照)は、命令定義に遭遇するたびに呼び出されます。したがって、単一のブロックレベルのデフォルトを持つことと、各ネストされたブロック内のデフォルトを持つことの間には、前者はフォーマット定義を1回呼び出すが、後者はフォーマット定義を複数回呼び出す結果になり得るという意味上の差異がある。フォーマット定義がヘッダー、デコーダー、exec出力を生成する場合、その出力は対応するファイルに複数回含まれることになり、通常、C++コンパイルされるときに多重定義エラーになります。一つの命令に対してフォーマット定義を複数回呼び出すことがどうしても必要な場合、フォーマット定義はデコードブロック出力のみを生成するように記述し、必要なすべてのヘッダー、デコーダー、exec出力は output blocks (_Output blocks 参照) を用いて一度に生成すべきです_。

プリプロセッサ指令の取り扱い

デコードブロックには、Cプリプロセッサー指令が含まれることがあります。これらのディレクティブはパーサーによって処理されません。その代わり、C++デコーダーがコンパイルされるときに処理されるように、C++出力に渡されます。パーサーは、特定のディレクティブを認識しません。最初の列に#がある行は、プリプロセッサーディレクティブとして扱われます。ディレクティブは、すべての出力ストリーム(ヘッダー、デコーダー、実行ファイル。 フォーマット定義参照)にコピーされます。ディレクティブは、デコードブロック内の命令定義によって生成されたコードとの相対的な 位置関係を維持します。その結果、例えば、一連の命令定義を囲む#ifdef/#endifペアは、それらの定義によって生成される宣言と、デコード関数内の対応するcaseステートメントの両方を囲むことになります。このように、#ifdefと同様の構成で、プリプロセッサ・シンボル(例えばFULL_SYSTEM)に基づいてシミュレータに条件付きでコンパイルされる命令定義を定義することができます。強調すべきは、#ifdefはISA記述パーサーに影響を与えないということです。ifdef/#else/#endif の構成では、条件の両方の部分にあるすべての命令定義が処理されます。その後のデコーダーのC++コンパイル時にのみ、どちらか一方の定義セットが選択されます。

declarationセクション

前述のように、ISA記述のdecodeセクション(単一の外側デコードブロックからなる)の前に、declarationセクションがある。declarationセクションの主な目的は、デコードブロックで使用される命令フォーマットやその他のサポート要素、および生成された出力にほぼそのまま渡されるサポートC++コードを定義することである。ここでは、宣言部に登場する構成要素について説明します。

フォーマット定義

命令フォーマットは基本的にPythonの関数で、命令定義(デコードブロック内で見つかる)から提供される引数を取り、最大4つのC++コードの断片を生成します。C++コードの断片は、生成された出力のどこに表示されるかによって区別されます。

  1. header outputは、生成されたすべてのソースファイル(decoder.ccおよびCPUモデルごとに実行されるすべての.ccファイル)に含まれるヘッダーファイル(decoder.hh)に出力されます。ヘッダ出力には、通常、命令に対応するC++クラス宣言が含まれます(もしあれば)。
  2. decoder outputは、同じソースファイル(decoder.cc)内のデコード関数の前に出力されます。この出力には、通常、execute()メソッドから見える必要がない定義が含まれます。インラインコンストラクタ定義、非インラインメソッド定義(例えば、ディスアセンブル用)、などです。
  3. exec outputには、CPUごとのモデル定義、すなわち命令クラスの execute()メソッドが含まれます。
  4. decode blockには、デコード関数に入るステートメントまたはステートメントのブロックが含まれる (対応するcaseステートメントのボディにある)。デコードブロックで指定されたビットパターンを認識すると制御を開始し、適切な命令オブジェクトを返す役割を担います。

命令フォーマットを定義するための構文は以下のとおりです。

def format FormatName(arg1, arg2) {{
    [コード省略]
}};

この例では、フォーマットを FormatName命名しています。(慣習として、命令フォーマット名は大文字から始まり、大文字と小文字が混在します)。このフォーマットを使用した命令定義は、2つの引数 (arg1arg2) を提供することが期待されます。この言語は、Pythonの変数引数のメカニズムもサポートしています。最後の引数がアスタリスクで始まる場合(例えば *rest) 、呼び出し元から、それ以外のすべての未束縛引数のリストを受け取ります。

フォーマット定義の最後から2番目の構文トークン(セミコロンの前)は、上で説明したように、 単なるコードリテラル(文字列定数)であることに注意すること。この場合、コードリテラル内のテキストは、Pythonのコードブロックです。このPythonコードは、指定された形式を使用する各命令定義で呼び出されます。

明示的な引数に加え、Pythonコードは2つの追加パラメータで提供されます。

フォーマットコードブロックでは、header_output, decoder_output, exec_output, decode_block という4つの特殊変数に文字列を代入して、生成されるコードを特定します。これらの変数に値が代入されない場合、対応するセクションのコードは生成されません。これらの文字列は、都合のよい方法で生成することができます。実際には、ほぼすべての命令フォーマットは、Cのようなコードスニペットから自動的に抽出された特性に基づいてコードテンプレートを特化するためにISA記述パーサによって提供されるサポート関数を使用しています。これらの機能の議論は、Code parsing ページに延期されます。

ISA記述は特定のシミュレータCPUモデルから完全に独立していますが、いくつかのC++コード(特にexec出力)は各モデルに対して若干特殊化する必要があります。この特殊化は、CPUモデル固有のシンボルの自動置換によって処理されます。これらのシンボルは CPU_ で始まり、パーサーによって特別に扱われます。現在のところ、モデル固有のシンボルは CPU_exec_context のみであり、これはモデルの実行コンテキストクラス名として評価される。テンプレートと同様に(See Template definitions) 、CPU固有のシンボルへの参照はPythonのキーベースのフォーマット文字列を使います。したがって、CPU_exec_contextシンボルへの参照は文字列の中で %(CPU_exec_context)s として表示されます。

header_outputdecoder_outputdecode_block に割り当てられた文字列が CPU 固有のシンボルの参照を含む場合、その文字列は CPU モデルごとに一度複製され、それぞれのインスタンスは CPU 固有のシンボルをそのモデルに従って置換される。得られた文字列を連結して、最終的な出力を形成する。exec_output` に割り当てられた文字列は、CPU固有のシンボル参照を含んでいるかどうかにかかわらず、常に各CPUモデルに対して一度だけ複製され、置換される。インスタンスは連結されず、別々に追跡され、CPUモデルごとに別々のファイル (例: simple_cpu_exec.cc) に配置されます。

テンプレート定義

上記のフォーマット定義で説明したように、命令フォーマットの目的は、命令定義の引数を処理して、いくつかのC++コードの断片を生成することです。これらのコード片は、通常、コード・テンプレートを特殊化することによって生成されます。記述言語はテンプレートを定義するための簡単な構文を提供します:キーワード def template, テンプレート名, テンプレート本体(コードリテラル), そしてセミコロンです。テンプレート名は大文字で始まり、大文字と小文字が混在し、"Declare" (宣言(ヘッダー出力)テンプレート)、 "Decode" (デコードブロックテンプレート)、 "Constructor" (デコーダー出力テンプレート)、 または "Execute" (実行出力テンプレート)で終わります。例えば、最も単純で有用なデコードテンプレートは以下の通りです。

def template BasicDecode {{
    return new %(class_name)s(machInst);
}};

命令フォーマットは、実際のクラス名を %(class_name)s に置き換えることで、このテンプレートを特定の命令用に特化させます。(テンプレートの特殊化は、Pythonの文字列フォーマット演算子 % に依存します。この %(class_name)s という用語は、シンボル class_name の値を代入することを示す、C言語%s フォーマット文字列の拡張です)。結果として得られるコードは、特定の命令が認識されたときに、C++のデコード関数に指定されたクラスの新しいオブジェクトを作成させることになります。

テンプレートはパーサーの中ではPythonのオブジェクトとして表現されます。テンプレートは、通常、テンプレートオブジェクトの subst() メソッドを呼び出すことで文字列を生成するために使われます。このメソッドは1つの引数を取り、テンプレート内の置換記号(例えば %(class_name)s )から特定の値へのマッピングを指定します。もし引数が辞書であれば、辞書自身がマッピングを指定します。そうでない場合は、引数は別のPythonオブジェクトでなければならず、そのオブジェクトの属性がマッピングとして使われます。実際には、 subst() の引数はほとんど常にパーサーの InstObjParams クラスのインスタンスです。InstObjParams クラス を参照してください。テンプレートは subst() 引数で指定されたシンボルに加えて、他のテンプレート (例えば %(BasicDecode)s) を参照することができます。

CPUモデル特有のシンボルへのテンプレート参照(フォーマット定義を参照)は subst() によって展開されず、そのまま通過します。この機能により、結果が exec_output や他の出力セクションに割り当てられるかどうかに応じて、後で適切に展開されます。しかし、CPUモデル固有のシンボルを含むテンプレートが他のテンプレートから参照された場合、 header_outputdecoder_output に割り当てられたテンプレートと同様に、前のテンプレートが複製されて一つの文字列に展開されてから補間が行われます。このポリシーは、CPUモデル固有のシンボルを直接含むテンプレートのみが複製され、間接的にそのシンボルを含むテンプレートは複製されないことを保証しています。この最後の特徴は、execute()メソッドのCPUごとの宣言を命令クラス宣言テンプレートに補間するために使用されます(Alpha ISAの説明のBasicExecDeclareテンプレートを参照してください)。

出力ブロック

出力ブロックは、ISA記述に、出力ファイルにほぼそのままコピーされるC++コードを含めることを可能にします。これらのブロックは、複数の命令オブジェクトの間で共有されるクラスとローカル関数を定義するのに便利です。出力ブロックは以下のフォーマットを持っています。

output <destination> {{
    [code ommitted]
}};

<destination> キーワードは headerdecoderexec のうちのいずれかでなければなりません。コードリテラル内のコードは、CPUモデル固有のシンボルの特別な処理を含めて、それぞれ命令フォーマット内の header_output decoder_output または exec_output 変数に割り当てられたものとして扱われます。コードリテラルに対して行われる唯一の追加処理は、命令定義で使用されるビットフィールド演算子の置換(【ビットフィールド演算子】(https://www.gem5.org/documentation/general_docs/architecture_support/isa_parser/#bitfield-operators)を参照)と、テンプレートへの参照の補間です。

Letブロック

Let ブロックはグローバルな Python コードを提供します。これらのブロックは、キーワード let の後にコードリテラル (ダブルブレイスの区切り文字列) とセミコロンが続くだけです。コードリテラルPython インタープリタによって直ちに実行されます。パーサは let ブロックの間で実行コンテキストを維持し、ある let ブロックで定義された変数や関数は、それ以降の let ブロックでもアクセスできるようにします。このコンテキストは、命令フォーマット定義を実行するときにも使用されます。letブロックの主な目的は、命令フォーマットで使用するための共有のPythonデータ構造と関数を定義することです。パーサはこの実行コンテキストに、定義されたテンプレートのセット (テンプレート定義 参照)、 InstObjParamsCodeBlock クラス (Code parsing 参照)、標準の Python stringre (regular expression) モジュールなどの限られた定義のセットを出力します。

ビットフィールドの定義

ビットフィールド定義は、機械命令内のビットフィールドの名前を指定します。これらの名前は、通常、デコードブロックのビットフィールドの仕様として使用されます。この名前は、命令クラス定義やデコードコードなど、デコーダーファイル内の他の C++ コード内でも使用されます。ビットフィールド定義の構文は、次の例で示されています。

def bitfield OPCODE <31:26>;
def bitfield IMM <12>;
def signed bitfield MEMDISP <15:0>;

指定されたビット範囲は両端を含み、ビット0が最下位ビットです。したがって、この例のOPCODEビットフィールドは、32ビット命令から最上位の6ビットを取り出します。1つのインデックス値で1ビットのフィールドIMMが抽出されます。抽出された値は、デフォルトではゼロ拡張されます。MEMDISPの例のように、符号付きキーワードを追加すると、抽出された値は符号拡張されます。ビットフィールドの実装は、プリプロセッサマクロとC++テンプレート関数に基づいているため、結果の値のサイズはコンテキストに依存します。

ビットフィールド定義の使用箇所を完全に理解するには、フードの下に少し潜る必要があります。ビットフィールド定義は、暗黙の変数 machInst から指定されたビットフィールドを抽出する C++ プリプロセッサー・マクロを生成するだけです。デコード関数のマシン命令パラメータも machInst と呼ばれます。したがって、デコード関数の内部で終わるビットフィールド名の使用 (デコードブロックの引数や命令フォーマットの出力のデコード部分など) は、現在デコード中の命令を暗黙に参照することになります。StaticInst オブジェクトに格納されているバイナリマシン命令も machInst という名前なので、命令オブジェクトのメンバ関数内でビットフィールド名を使用すると、この格納されている値を参照することになります。このデータメンバーは StaticInst のコンストラクタで初期化されますので、派生オブジェクトのコンストラクタでもビットフィールド名を使用しても安全です。

オペランドオペランド型の定義

これらの文は、命令の機能的な動作を表現するコードブロックで使用できるオペランド型を指定します。オペランド型修飾子](https://www.gem5.org/documentation/general_docs/architecture_support/isa_parser/#operand-type-qualifiers) と 命令解析 を参照してください。

名前空間宣言

宣言部の最後の構成要素は名前空間宣言であり、キーワード namespace の後に識別子とセミコロンを続けたものである。宣言部には、必ず1つの名前空間宣言を記述する。結果として得られる C++ デコード関数、デコードブロック内の命令定義から得られる宣言、および名前空間宣言の後に現れる declare 文の内容は、指定された名前の C++ 名前空間に配置されることになる。名前空間宣言の前に出現する declare 文の内容は、名前空間の外に置かれる。

コードパース

ISA記述機構の強力さと柔軟さは、デコードブロックで提供される短い命令定義から結果のC++コードへのマッピングが、汎用プログラミング言語Python)で行われることに大きく起因しています。(この機能は、前述のフォーマット定義で説明した「命令フォーマット」定義が担っています。技術的には、ISA記述言語は任意のPythonコードにこのマッピングを実行させることができます。しかし、パーサーは、その動作の簡単な説明から命令の特性を推測し、宣言とデコードテンプレートを埋めるために必要な文字列を生成するプロセスを自動化するために設計されたPythonのクラスと関数のライブラリを提供します。このライブラリはisa_parser.pyのコードの約半分を占めています。

命令の動作は、ビットフィールド演算子オペランド型修飾子という2つの拡張を持つC++を使って記述されます。ISA記述システムに完全なC++パーサーを構築することを避けるために(あるいは逆に、命令記述に使用できるC++を制限するために)、これらの拡張は正規表現マッチングと置換を使って実装されています。その結果、その使用にはいくつかの構文的制約がある。以下の2つのセクションでは、これらの拡張について順番に説明します。3番目のセクションでは、オペランド解析について説明します。オペランド解析は、パーサーがほとんどの命令の特性を自動的に推測するための技術です。最後の2つのセクションでは、命令フォーマットがライブラリと相互作用するためのPythonのクラスについて説明します。コードブロックは命令記述コードを解析し、カプセル化するクラスです。

ビットフィールド演算子

ビットフィールドは <:> というポストフィックス演算子を使って、rval に対してシンプルに抽出することができます。ビット番号はグローバルビットフィールドの定義で使用されているものと同じです(ビットフィールドの定義を参照してください)。例えば、Ra<7:0>レジスタ Ra の下位 8 ビットを抽出します。Rb<31:>` のように、後者のオペランドを削除することで、シングルビットフィールドを指定することができます。グローバルビットフィールドの定義とは異なり、コロンを削除することはできません。ビットフィールド演算子とテンプレート引数を区別することが難しくなりすぎるからです。また、ビットインデックスのパラメータは識別子か整数定数でなければならず、式は使用できません。ビット演算子は、左側の構文トークンか、そのトークンが閉じ括弧の場合は、括弧で囲まれた式に適用されます。

オペランド型修飾子

命令オペランドレジスタなど)の有効な型は、オペランド名にピリオドと型修飾子を付加して指定することができます。型修飾子のリストはアーキテクチャ固有であり、ISA記述の def operand_types ステートメントが指定に使用される。この仕様は、型拡張を型名にマッピングするPythonの辞書の形式をとっています。例えば、AlphaのISAの定義は以下の通りです。

def operand_types {{
    'sb' : 'int8_t',
    'ub' : 'uint8_t'。
    'sw' : 'int16_t',
    'uw' : 'uint16_t',
    'sl' : 'int32_t',
    'ul' : 'uint32_t'。
    'sq' : 'int64_t',
    'uq' : 'uint64_t'。
    'sf' : 'float',
    'df' : 'double'
}};

したがって、アルファ32ビットの加算命令addlは次のように定義できる。

Rc.sl = Ra.sl + Rb.sl;

演算は指定された型を使用して実行されます。結果は指定された型から適切なレジスタ値に変換されます(この場合、アルファの整数レジスタは64ビットサイズなので、32ビットの結果を64ビットに符号拡張して変換します)。

型修飾子は、認識された命令オペランドに対してのみ許可されます(命令オペランドを参照)。

命令オペランド

パーサーによる自動化のほとんどは、命令定義コードで使用されるオペランドを認識することに基づいています。浮動小数点命令と整数命令は使用されるレジスタで認識でき、メモリ位置から読み出す命令はロードであるなど、ほとんどの関連する命令の特性はオペランドから推測することができます。上記のビットフィールド・オペランドと型修飾子を組み合わせれば、ほとんどの命令を1行のコードで記述することができる。また、シミュレータのCPUモデル間の違いのほとんどはオペランドアクセスの仕組みにあり、これらのアクセスのためのコードを自動生成することにより、1つの記述で様々な状況に対応することができる。

ISA記述では、def operands文により、認識可能な命令オペランドとその特性の一覧を提供します。この文は、オペランド文字列を5つの要素のタプルにマップするPython辞書を指定します。タプルの要素はオペランドを以下のように指定する。

オペランドクラスは、文字列 "IntReg", "FloatReg", "Mem", "NPC", "ControlReg" のいずれかでなければならず、それぞれ整数レジスタ浮動小数レジスタ、メモリ位置、次のプログラムカウンター(NPC)、または制御レジスタを表わします。

  1. オペランドのデフォルトの型 (def operand_types ブロックで定義される拡張文字列)。 オペランドの特定のインスタンスがどのようにデコードされるかを示す指定子(例えば、ビットフィールド名)。
  2. オペランドが使用されたときに推測される命令フラグを示す文字列または文字列のトリプル。
  3. 分解時のオペランドの順序を制御するために使用されるソート優先順位。

例えば、Alpha ISAのオペランド特性マップの簡略化したサブセットは次のとおりです。

def operands {{
    'Ra': ('IntReg', 'uq', 'RA', 'IsInteger', 1),
    'Rb': ('IntReg', 'uq', 'RB', 'IsInteger', 2),
    'Rc': ('IntReg', 'uq', 'RC', 'IsInteger', 3),
    'Fa': ('FloatReg', 'df', 'FA', 'IsFloating', 1),
    'Fb': ('FloatReg', 'df', 'FB', 'IsFloating', 2),
    'Fc': ('FloatReg', 'df', 'FC', 'IsFloating', 3)となります。
    'Mem': ('Mem', 'uq', None, ('IsMemRef', 'IsLoad', 'IsStore'), 4),
    'NPC': ('NPC', 'uq', None, ( None, None, 'IsControl'), 4).
}};

Ra という名前のオペランドは整数レジスタで、デフォルトの型は uq (unsigned quadword) で、命令の RA ビットフィールドを使用し、 IsInteger 命令フラグを意味し、ソート優先度は 1 (オペランドのリストで最初に置かれる) です。

命令フラグ要素では、単一の文字列 ('IsInteger' など) は、無条件に推論された命令フラグを意味する。フラグオペランドがトリプルである場合、最初の要素は無条件に、2番目の要素はオペランドがソースであるときに推測され、3番目の要素はそれがデスティネーションであるときに推測されます。したがって、メモリ参照用の ('IsMemRef', 'IsLoad', 'IsStore') 要素は、メモリオペランドを持つすべての命令がメモリ参照としてマークされることを示す。さらに、メモリオペランドがソースである場合、その命令はロードとマークされ、オペランドがデスティネーションである場合、その命令はストアとマークされます。同様に、NPC オペランドに対する (None, None, 'IsControl') タプルは、NPC に書き込むすべての命令が制御命令であることを示しますが、単にソースとして NPC を参照する命令は、デフォルトフラグを受け取りません。

記述コード解析は正規表現を使用するため、パーサーが部分オペランドの性質を推論する能力は制限されることに注意してください。特に、宛先オペランドは、オペランドが代入演算子=)の左辺に現れるかどうかをテストすることによってのみ、ソースオペランドと区別されます。他の関数への参照渡しなど、別の方法で代入された目的オペランドも、正しく目的オペランドとして認識されるためには、代入演算子の左辺に表示されなければなりません。また、パーサーはC言語の複合代入(+=など)も認識しません。もしオペランドがソースとデスティネーションの両方であるなら、それは = の左辺と右辺の両方に現れなければなりません。

正規表現に基づくコード解析のもう一つの限界は、コードブロック内の制御フローが認識されないことです。これは、CPUモデルにおけるレジスタの更新方法の詳細と相まって、条件付きでデスティネーションを更新することができないことを意味します。特定のレジスタがデスティネーションレジスタとして認識された場合、そのレジスタexecute() メソッドの最後で必ず更新されます。したがって、コードはブロック内の可能な各コードパスに沿って、そのレジスタに有効な値を割り当てる必要があります。

CodeBlock クラス

命令フォーマットでは、命令記述コードを含む文字列をCodeBlockコンストラクタに渡すことで、その処理を依頼します。コンストラクタは必要な解析と処理をすべて行い、その結果を返されたオブジェクトに格納します。CodeBlock のフィールドは以下の通りである。

  • orig_code: オリジナルのコード文字列。
  • code: これは、ビットフィールド演算子を置換し、オペランドの型修飾子 (s/./_/) を有効な C++ 識別子にすることで、元のコードから派生したものです。
  • constructor: 命令オブジェクトのコンストラクタのコードで、オペランドの数やオペランドレジスタインデックスを含む様々な C++ オブジェクトフィールドを初期化します。
  • exec_decl: 実行エミュレーション関数で使用するために、オペランドに対応する C++ 変数を宣言するためのコードです。
  • *_rd: 実際のオペランド値を、ソースオペランドに対応する C++ 変数に読み込むコードです。名前の最初の部分は、関連する CPU モデル (現在は simple と dtld がサポートされている) を示している。
  • *_wb: C++ 変数の内容を、適切なレジスタまたはメモリ位置に書き戻すコードです。ここでも、名前の最初の部分はCPUモデルを反映している。
  • *_mem_rd, *_nonmem_rd, *_mem_wb, *_nonmem_wb: 上記と同様ですが、メモリオペランドと非メモリオペランドが分離されています。
  • flags: オペランドが意味する命令フラグのセット。
  • op_classオペランドの種類から、命令の演算クラス(OpClass を参照)を推測します。

InstObjParams クラス

InstObjParams クラスのインスタンスは、テンプレートの subst() メソッドの引数として使用されるコードテンプレートに代入するために必要なすべてのパラメータをカプセル化します (Template definitions を参照してください)。

class InstObjParams(object):
    def __init___(self, parser, 
                  mem, class_name, base_class = '',
                  snippets = {}, opt_args = []):

コンストラクタの最初の3つの引数は、オブジェクトの mnemonic, class_name, そして (オプションで) base_class のメンバを設定します。4番目の引数(オプション)はCodeBlockオブジェクトです。提供されたCodeBlockオブジェクトのすべてのメンバーは新しいオブジェクトにコピーされ、テンプレート置換のためにアクセスできるようになります。残りの引数は、追加の命令フラグ (CodeBlock 引数から継承された flags リストに追加されます。) か、オペレーションクラス (CodeBlock の op_class をオーバーライドします。) として解釈されます。