注:この質問はいくぶんバイトコードが「解析される」の正確さに関連していますが、それの複製ではありません。この質問では、バイトコードが「解析」される方法ではなく、バイトコードが生成される方法の特定の部分について尋ねています。
タイトルで述べたように、リテラル(文字列、整数など)はどのようにバイトコードファイルにエンコードされますか?私の混乱は、与えられたリテラルのバイト表現が可変長であるという事実から来ています。つまり、仮想マシンは、リテラル全体を読み取るために収集する必要のあるバイト数を把握できません。それでも私の質問がはっきりしない場合は、視覚的な例が私の混乱を説明するのに役立つと思います。
この例を考えてみましょう。パーサーが抽象構文ツリーを作成しました。式3 + 2
を次のように変換しました:
+
/ \
3 2
コンパイラ/インタプリタがツリーを歩き始めます。次のバイトコードを生成します。
Push 3 Push 2 ADD
| | | | |
|-----| |--------------| |-----| |----------| |-----|
b'\x00' b'\x00\x00\x00\' b'\x00' b'\x00\x00\' b'\x05'
次に、仮想マシンがバイトコードファイルの読み取りを開始します。最初のバイトを読み取り、オペコードプッシュであることを確認します。 argumentをオペコードPushに読み込む必要があります。
しかし、ここに問題があります。仮想マシンには、Pushへの引数全体を取得するために読み取る必要があるバイト数を知る方法がありません。 Pushの引数は可変バイト数であるため、仮想マシンは、各引数について読み取る必要があるバイト数を認識していません。上記の疑似バイトコードに見られるように、異なる値を表すために使用されるバイト数は変化する可能性があり、一貫性がありません。
上記の例では整数のみを使用していますが、これは他の場合にも適用できます。文字列、または識別子名の文字列表現など。
さまざまなブログを検索したり、一部の言語のバイトコードの公式ドキュメントを検索したりしましたが、リテラルのエンコード方法についての説明はまだわかりません。
私が遭遇したクローゼット情報は、ヘッダーでリンクした質問への この回答Ratchet Freak からの文でした。それは読む:
操作あたりのバイト数を非常に明示的にする例を示すために、SPIR-Vがあります。各命令の最初の4バイトワードは、2バイトの長さ+ 2バイトのオペコードとして構成されます。
彼が言っているのは、SPIR-V forcesのすべてのオペコードが2バイトを満たすように圧縮または拡張するための引数であることです。私はcouldでこれを行うと想定していますが、これが彼の意図したものではないと確信しています。
バイト表現が可変長であるリテラル値をバイトコードファイルにエンコードするときの一般的な方法は何ですか?もちろん、私はそれらが一般的な慣習だと思いますが、おそらく各言語で異なっていますか?
あなたの質問は、バイトコードシステムよりも広く、一般的な命令セットアーキテクチャ、ハードウェア、またはバイトコードに当てはまります。
バイト表現が可変長であるリテラル値をエンコードするときの一般的な方法は何ですか?
約半ダースの合理的なテクニックがあります。
オペコードは、オペコードに続くリテラルのバイト数を示します。つまり、通常は同じオペコードがいくつかあります。オペコードは、(何らかの形で)オペランド操作のサイズまたはタイプ(例:Push 32-bit int)をエンコードする必要があることに注意してください。これは、リテラルデータバイト(しばしばイミディエイトと呼ばれる)のサイズ/カウントのエンコードと一緒にまたは別々に実行できます。オペコードの後。これらが異なる場合(多くの場合、命令によって記述される直接のリテラルは、オペランドのタイプよりも短い)、オペコードの定義に従って(たとえば、符号拡張を使用して)、オペコードに続くバイトが拡張されます。オペランドタイプのサイズに指定された直接リテラルのサイズ。
オペコードの後に他のビットがありますが、オペコードとは別と見なされ、リテラルのサイズ(または(またはすべての)オペランドのフォーマット)を通知します。命令セットにサブオペコードがグループ化されている場合、メジャーオペコードを超えるビットがさまざまなオペランドに関することを示すことがあります。
最後のバリエーションは、各オペランドが独自の個別の記述子を持っていることです(おそらくリテラルの前にグループ化されます)。これらは、addl3(3つのオペランドのadd long)などの複数のオペランド命令を持つCISCスタイルのレジスタマシン(VAXなど)で一般的です。
リテラル自体に、リテラルのデータがさらに続くかどうかを示すビットがあります。たとえば、各バイトの1ビットは、より多くのバイトを示すために使用できます。つまり、各リテラルバイトは7ビットを生成し、次のバイトがリテラルであるか、リテラルが完了したかを示します。これは(ソフトウェア)解釈のパフォーマンスにやや敵対的ですが、ハードウェアはこれを素朴なアプローチが示すように見えるよりもうまくデコードできます。インタープリターの代わりにJITを実行している場合、これは問題なく機能する可能性があります。
何らかの間接参照が使用され、リテラルは別の場所に格納されます。これは、たとえば、Java/C#バイトコードの文字列の場合です。 Javaで、プッシュ文字列のオペコードは定数テーブルへのインデックスを使用します。アプリケーションのバイナリインターフェイスは、文字列やその他の32ビット、64などの大きな定数に対して、1つのマシンレジスタまたはアクセス可能なグローバルロケーションを指定します。ビット以上のblob定数。
時々、リテラルは、複数の命令を使用してリテラルをアセンブルするのに十分な大きさまたは複雑なビットパターンになる場合があります。一部のアーキテクチャは、リテラルオペランドを受け取り、それをレジスタ(またはスタック)の上位バイトに配置する即時ロードを提供します。次に、定期的な追加即値を使用して、リテラルの下位ビットを取り込みます。これは、固定サイズの命令を使用するアーキテクチャで時々見られます。
pushの引数は可変バイト数であるため、仮想マシンは、各引数について読み取る必要があるバイト数を認識していません。
通常、アーキテクチャでは、すべての引数が固定数のバイトであると規定されています。
Pushには複数のバリアントがあり、それぞれが異なるバイト数を取ることに注意してください。したがって、PUSHWORD、PUSHBYTE、PUSHSHORTがあり、それぞれに一意のオペコードがあります。それらはすべてPush in Assemblyと呼ばれる可能性がありますが、引数(たとえば、32ビットレジスタの代わりに16ビットレジスタを指定する)に十分なコンテキストが必要で、 uniqueオペコードプッシュは実際にオペコードです。
生成された命令は、次のようになります。
Push3 3 Push2 2 ADD
| | | | |
|-----| |--------------| |-----| |----------| |-----|
b'\x03' b'\x00\x00\x00\' b'\x02' b'\x00\x00\' b'\x05'
プッシュ命令は異なり、オペコードも異なることに注意してください。これはPushに限らず、算術演算と論理演算ごとに複数のオペコードを使用できるため、バイトまたはワードを追加するか、バイトのみ、またはワード全体をXORするかを指定できます。
文字列(または配列、構造体、リストなどの非アトミックデータ構造)は通常、イミディエイト(つまり、命令の一部)として提供されません。代わりに、それらはメモリ内の別の場所に格納され、メモリアドレスを介してポイントされます(これは固定サイズであり、したがって、命令の一部として提供できます)。
したがって、(バイトコードに文字列の印刷命令がある場合) `PRNT" Hello World "は次のようにはなりません。
PRNT "Hello World"
| |
|--| |------------------------------------------|
\x45 \x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64
代わりに次のようになります。
// data section
// This example assumes the string is loaded at address /xcafebeef.
// HWString is a label referring to that. The label is useful in
// Assembly, but probably not needed in the actual bytecode.
HWString: "Hello World"
|
|----------------------------------------------|
\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x00 // null terminator, if you're a fan of C-style strings.
// later in the file
// text section
PRNT HWString
| |
|--| |--------|
\x45 \xcafebeef
すべての命令が正確に32ビットであり、すべての命令が つの形式 のいずれかに適合するMIPS(32ビット)アーキテクチャを確認することをお勧めします。
Java は別の例です。特に、bipush
(byteimmediatePush)およびsipush
(short immediatePush)。前者は1バイトのオペランドを1つ取り、後者は常に2バイトのオペランドを1つ取ります。
リテラルオブジェクトは、バイトコードの外の配列に格納されます。そして、put
バイトコードはその配列にインデックスを付けるだけです。
A Rubyの例、
$ Ruby --dump insns -e '[nil,0,1,2,"str",/regexp/]'
== disasm: <RubyVM::InstructionSequence:<main>@-e>======================
0000 trace 1 ( 1)
0002 putnil
0003 putobject_OP_INT2FIX_O_0_C_
0004 putobject_OP_INT2FIX_O_1_C_
0005 putobject 2
0007 putstring "str"
0009 putobject /regexp/
0011 newarray 6
0013 leave
ご覧のように、異なるput
バイトコードがあります。
nil
に特化しており、0や1などの一般的な数値です。