IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

JavaScript はどのように実行されるか

JavaScript はどのように実行されるか

Safari*1 の実装を例に JavaScript はどのようにして実行されているかを書く。自分用のメモ。日本語の出来は気にしない

1. ブラウザを起動して以下のようなページを開いたとする
<html>
  <head>
    <script>
var a = 1;
var b = 2;
alert(a + b);
    </script>
  </head>
  <body>
  </body>
</html>
2. インターネットからデータが到着する

そうすると WebCore::FrameLoader::write という関数に生の文字列が渡される。型は char* だ。
http://trac.webkit.org/browser/trunk/WebCore/loader/FrameLoader.cpp#L990
この関数の中では、到着した文字の文字コードを解決し WebCore::Tokenizer に文字コード解決済みの HTML を渡す。
重要なところだけ抜粋すると

void FrameLoader::write(const char* str, int len, bool flush)
{
    // 文字コード解決
    String decoded = m_decoder->decode(str, len);

    // Tokenizer に渡す
    Tokenizer* tokenizer = m_frame->document()->tokenizer();
    tokenizer->write(decoded, true);
}
3. HTML が解析される

WebCore::Tokenizer は抽象クラスで実際には、 WebCore::HTMLTokenizer の write が呼ばれる。
このクラスと HTMLParser が連携して HTML は解析され、 DOM のノードが生成される。
この途中に script 要素の開始タグが現れると。。。

4. HTML の世界から JavaScript の世界への入り口へとどんどん進んでいく

script 要素が開始されると、 WebCore::HTMLTokenizer::scriptHandler が呼び出される。

HTMLTokenizer::State HTMLTokenizer::scriptHandler(State state)
{
    // We are inside a <script>

そこで script を実行すべきか判定し WebCore::HTMLTokenizer::scriptExecution が呼び出される

HTMLTokenizer::State HTMLTokenizer::scriptExecution(const String& str, State state, const String& scriptURL, int baseLine)
{

str には JavaScript のソースが入っている
次に WebCore::FrameLoader::executeScript → WebCore::ScriptController::evaluate と呼び出されていく。引数はそのまま。
WebCore::ScriptController::evaluate では以下の事を行う。

5. VM の起動

WebCore::ScriptController::evaluate からは初回起動時だけ WebCore::FrameLoader::initScript という関数が呼ばれる。
その関数の中から、 JSC::Machine が new される。

6. JavaScript の実行

WebCore::ScriptController::evaluate から JSC::Interpreter::evaluate が呼び出され、 JavaScript は実行されます
JavaScript の実行は以下のフェーズで行われる。

順番に見て行きましょう

7. JavaScript を解析して構文木を生成する

JSC::Interpreter::evaluate の以下の行

RefPtr<ProgramNode> programNode = exec->globalData().parser->parse<ProgramNode>(exec, exec->dynamicGlobalObject()->debugger(), source, &errLine, &errMsg);

この programNode が構文木というものです。
parse 関数の中では bison というツールによって生成された kjsyyparse を実行している。
bison は kjs/grammer.y ファイルを読んで、そのパーサを自動で生成する。
この構文木の構造は

var a = 1;
var b = 2;

alert(a + b);

の場合、以下のようになる

- ProgramNode
  - VarStatementNode => "var"
    - AssignResolveNode => "a ="
      - NumberNode => "1"
  - VarStatementNode => "var"
    - AssignResolveNode => "b ="
      - NumberNode => "2"
  - ExprStatementNode
    - FunctionCallResolveNode => "alert"
      - ArgumentsNode => "(" ")"
        - ArgumentListNode
          - AddNode => "+"
            - ResolveNode => "a"
            - ResolveNode => "b"

この構造の節々にあるそれぞれのオブジェクトをノードという。ノードには親子関係があり、親ノード、子ノードという言葉も使う。
構文木の概要は以下のファイルにまとめて書いてある。
http://trac.webkit.org/browser/trunk/JavaScriptCore/kjs/nodes.h
http://trac.webkit.org/browser/trunk/JavaScriptCore/kjs/nodes.cpp

8. 構文木バイトコードにする

ここでは、構文木バイトコードに変換する
構文木のクラスは emitCode という関数を持っている。
この関数は

ということをする。
このとき、レジスタ番号が被らないように調整や、ノードがどんなバイトコードを生成すればいいかを管理しているのが JSC::CodeGenerator である。
以下の構文木に対して emitCode を呼び出すと

- ProgramNode
  - VarStatementNode => "var"
    - AssignResolveNode => "a ="
      - NumberNode => "1"
  - VarStatementNode => "var"
    - AssignResolveNode => "b ="
      - NumberNode => "2"
  - ExprStatementNode
    - FunctionCallResolveNode => "alert"
      - ArgumentsNode => "(" ")"
        - ArgumentListNode
          - AddNode => "+"
            - ResolveNode => "a"
            - ResolveNode => "b"

以下のようなバイトコードが生成される

enter

// ここから ProgramNode

// $0 には undefined が入っている
// $1 には 1 が入っている
// $2 には 2 が入っている

mov $-16 $0
mov $-17 $0
mov $3 $0

// $3 は this
// $-16 は a
// $-17 は b

// ここから VarStatementNode
// ここから AssignResolveNode
// ここから NumberNode

mov $-16 $1
// a = 1

// ここまで NumberNode
// ここまで AssignResolveNode
// ここまで VarStatementNode
// ここから VarStatementNode
// ここから AssignResolveNode
// ここから NumberNode

mov $-17 $2
// b = 2

// ここまで NumberNode
// ここまで AssignResolveNode
// ここまで VarStatementNode
// ここから ExprStatementNode
// ここから FunctionCallResolveNode

resolve_func $3 $4 #alert
// $4 に alert 関数が入る
// $3 に呼び出したときの this の値が入る

// ここから AargumentListNode
// ここから AddNode

add $6 $-16 $-17 [toInt]

// ここまで AddNode
// ここまで AargumentListNode

profile_will_call $4
call $3 $4 $3 $5 2 15
profile_did_call $4

// 2 というのは引数が二個あるという意味で
// 2 個目の引数が $6 (足し算の結果)になっている
// $3 に関数呼び出しの結果が入る

// ここまで FunctionCallResolveNode
// ここまで ExprStatementNode
// ここまで ProgramNode

end $3
// JavaScript 実行の結果は $3 に格納されている

バイトコードは、実際には配列のような形で持たれている。

9. バイトコードマシン語に変換できる場合は変換する

バイトコード化された JavaScript のコードは、 JSC::Machine::execute の以下の箇所で実行される。

#if ENABLE(CTI)
    if (!codeBlock->ctiCode)
        CTI::compile(this, newCallFrame, codeBlock);
    JSValue* result = CTI::execute(codeBlock->ctiCode, &m_registerFile, newCallFrame, scopeChain->globalData, exception);
#else
    JSValue* result = privateExecute(Normal, &m_registerFile, newCallFrame, exception);
#endif

ここで分かるのは、コンパイル時に CTI というフラグを立てておけば、マシン語に変換されてから実行されるということだ。
というわけで、この先は2つに道が別れる

10. バイトコードとして実行される場合

JSC::Machine::privateExecute の中では以下のようなことが行われる

  • バイトコードを一つ取り出して、 goto 文(goto 文で任意の場所に飛べないコンパイラでは switch を使う)でそのバイトコードの処理が書かれているところに飛ぶ
  • その処理を実行する
  • 淡々と上の2つを繰り返す。

JSC::Machine::privateExecute のコードは非常に分かり易い

11. マシン語に変換される場合

初回に CTI::compile が呼び出され、マシン語に変換される。(そのコードが一度マシン語されている場合は、呼び出されない)
CTI::compile は CTI::privateCompile を呼び出す。この関数では以下のようなっことを行う。

そして、最終的にバッファにたまったマシン語を callFrame->ctiCode に入れておく
最後にそのマシン語を呼び出す場合は以下のようになっている
まず、 CTI::execute はこんな感じ

        inline static JSValue* execute(void* code, RegisterFile* registerFile, CallFrame* callFrame, JSGlobalData* globalData, JSValue** exception)
        {   
            return ctiTrampoline(code, registerFile, callFrame, exception, Profiler::enabledProfilerReference(), globalData);
        }   

ctiTrampoline は C 言語の名前でリンケージされていて

    extern "C" {
        JSValue* ctiTrampoline(void* code, RegisterFile*, CallFrame*, JSValue** exception, Profiler**, JSGlobalData*);
        void ctiVMThrowTrampoline();
    };

以下のように code を呼び出している(gcc の場合)

asm(
".globl " SYMBOL_STRING(ctiTrampoline) "\n" 
SYMBOL_STRING(ctiTrampoline) ":" "\n" 
    "pushl %esi" "\n" 
    "pushl %edi" "\n" 
    "pushl %ebx" "\n" 
    "subl $0x20, %esp" "\n" 
    "movl $512, %esi" "\n" 
    "movl 0x38(%esp), %edi" "\n" // Ox38 = 0x0E * 4, 0x0E = CTI_ARGS_callFrame (see assertion above)
    "call *0x30(%esp)" "\n" // Ox30 = 0x0C * 4, 0x0C = CTI_ARGS_code (see assertion above)
    "addl $0x20, %esp" "\n" 
    "popl %ebx" "\n" 
    "popl %edi" "\n" 
    "popl %esi" "\n" 
    "ret" "\n" 
);

スタック %esp から code を取り出して call しているのだ

最後に


なんだか凄い時代に生きている気がしてきた