V8の実行環境が整い、V8がJSコードを実行し始めると、V8はまずJSコードをバイトコードにコンパイルし、バイトコードを解釈して実行するか、最適化が必要なバイトコードをバイナリにコンパイルし、バイナリコードを直接実行する必要があります。
はじめに
システムハードウェア構成モデル構造:主にCPU、メインメモリ、各種IOバス、外部デバイスで構成されます。
まず、プログラムの実行では、実行するプログラムがまずメモリにロードされます。CPUはメモリアドレスを使って、メモリからデータを読み出したり、メモリにデータを書き込んだりすることができます。
メモリ内の各記憶スペースには、対応する一意のアドレスがあり、これがメモリ・アドレスです。コードが実行可能ファイルにコンパイルされた後、実行可能ファイルはバイナリのマシンコードを含み、バイナリコードはメモリにロードされます。
バイナリコードがメモリにロードされると、CPUは命令をメモリから取り出し、命令を解析し、命令を実行します。この3つのプロセスをCPUクロックサイクルと呼びます。バイナリコードがメモリにロードされた後、システムはバイナリコードの最初の命令のアドレスをPCレジスタに書き込み、次のクロックサイクルで、CPUはPCレジスタのメモリアドレスに基づいてメモリから命令を取り出します。フェッチされた命令を分析し、異なるタイプの命令を識別し、さまざまなオペランドのメソッドを取得します。その後、CPUは命令の実行を開始します。
CPUからメモリへのアクセスは遅いため、CPU内部には汎用レジスタのような小さな記憶装置が増設されています。汎用レジスタとメモリの関係は、ポケットとバックパックの関係に似ています。汎用レジスタは小さくて読み書きが速く、メモリは大きくて読み書きが遅い。 汎用レジスタは少量のデータとポインタの両方を格納できます。例えば、PCレジスタは次に実行される命令などを格納するのに使われます。
よく使われるインストラクションの種類
- ロード命令は、指定された長さの内容をメモリから汎用レジスタにコピーし、レジスタの元の内容を上書きします。
- ロード命令とは対照的に、ストアド命令は、レジスタの内容をメモリ上のある場所にコピーし、メモリ上のその場所の元の内容を上書きします。
- 更新命令は、2つのレジスタの内容をALUにコピーするか、1つのレジスタと1つのメモリの内容をALUにコピーし、ALUが和の結果を1つのレジスタに格納し、元のレジスタの内容を上書きします。
- ジャンプコマンド。下図のように。
説明例
int main()
{
int x = 1;
int y = 2;
int z = x + y;
return z;
}
上記のCコードは、次のようにバイナリのマシンコードに変換されます:
pushq %rbp // rbpレジスタの値をメモリのスタック領域に書き込む
movq %rsp, %rbp // rspレジスタの値をrbpレジスタに書き込む
movl $0, -4(%rbp) // スタックフレームの最初の場所に0を書き込む
movl $1, -8(%rbp) // 定数1がスタックに押され、変数xに値が代入される。
movl $2, -12(%rbp) // 定数2をスタックに押し込み、y変数を代入する。
movl -8(%rbp), %eax // xeaxレジスタの値がスタックからeaxレジスタにコピーされる。
addl -12(%rbp), %eax // eaxxの値はすでにeaxに保存されている。この時点で、スタック内のyの値を取り出してxと加算する。
movl %eax, -16(%rbp) // eaxレジスタに保存された値を取り出してメモリに保存する
movl -16(%rbp), %eax // メモリ上の計算値をeaxレジスタにロードし、z変数を代入し、eaxレジスタの値を戻り値として使用する。
popq %rbp // 実行が完了し、現在の関数スタックをポップアップする
retq //
まとめ
- V8では、まずJSをバイトコードまたはバイナリコードにコンパイルしてから実行する必要があります。
- CPUがマシンコードを実行するロジック:まず、コンパイル後のバイナリコードをメモリにロードし、CPUが1行ずつ命令順に実行。CPUは命令を実行する過程でレジスタを導入し、中間データをレジスタに格納することで、CPUの実行を高速化。
- レジスタを使用するCPU命令:ロード命令、ストア命令、アップデート命令。レジスタとメモリ間、レジスタとレジスタ間のデータ転送に使用。
最後に書く
Geek Timeの李冰さんによる「図解 goole V8」コースから、V8関連の学習内容をまとめました。