DEV Community

Cover image for Let’s Understand Chrome V8 — Chapter 10: Ignition Execution Unit
灰豆
灰豆

Posted on • Originally published at Medium

Let’s Understand Chrome V8 — Chapter 10: Ignition Execution Unit

Original source: https://medium.com/@huidou/lets-understand-chrome-v8-chapter-10-ignition-execution-unit-66073e14f6aa
Welcome to other chapters of Let’s Understand Chrome V8


Execution is responsible for executing the bytecode. In this paper, we’ll talk about the workflow from S to E and several kernel functions.

Our test case:

function JsPrint(a){
    if(a >5){
        return "Debug";
    }
}
console.log(JsPrint(6));
Enter fullscreen mode Exit fullscreen mode

Workflow

The GetSharedFunctionInfoForScript() compiles JS source code into bytecodes.

0.  MaybeHandle<SharedFunctionInfo> Compiler::GetSharedFunctionInfoForScript(
1.      Isolate* isolate, Handle<String> source,
2.      const Compiler::ScriptDetails& script_details,
3.      ScriptOriginOptions origin_options, v8::Extension* extension,
4.      ScriptData* cached_data, ScriptCompiler::CompileOptions compile_options,
5.      ScriptCompiler::NoCacheReason no_cache_reason, NativesFlag natives) {
6.  //.omit.............
7.    // Do a lookup in the compilation cache but not for extensions.
8.    MaybeHandle<SharedFunctionInfo> maybe_result;
9.    IsCompiledScope is_compiled_scope;
10.    if (extension == nullptr) {
11.      bool can_consume_code_cache =
12.          compile_options == ScriptCompiler::kConsumeCodeCache;
13.      if (can_consume_code_cache) {
14.        compile_timer.set_consuming_code_cache();
15.      }
16.      // First check per-isolate compilation cache.
17.      maybe_result = compilation_cache->LookupScript(
18.          source, script_details.name_obj, script_details.line_offset,
19.          script_details.column_offset, origin_options, isolate->native_context(),
20.          language_mode);
21.      if (!maybe_result.is_null()) {
22.        compile_timer.set_hit_isolate_cache();
23.      } 
24.    }
25.  //.....omit..............
26.    if (maybe_result.is_null()) {
27.      ParseInfo parse_info(isolate);
28.      // No cache entry found compile the script.
29.      NewScript(isolate, &parse_info, source, script_details, origin_options,
30.                natives);
31.      // Compile the function and add it to the isolate cache.
32.      if (origin_options.IsModule()) parse_info.set_module();
33.      parse_info.set_extension(extension);
34.      parse_info.set_eager(compile_options == ScriptCompiler::kEagerCompile);
35.      parse_info.set_language_mode(
36.          stricter_language_mode(parse_info.language_mode(), language_mode));
37.      maybe_result = CompileToplevel(&parse_info, isolate, &is_compiled_scope);
38.      Handle<SharedFunctionInfo> result;
39.      if (extension == nullptr && maybe_result.ToHandle(&result)) {
40.        DCHECK(is_compiled_scope.is_compiled());
41.        compilation_cache->PutScript(source, isolate->native_context(),
42.                                     language_mode, result);
43.      } else if (maybe_result.is_null() && natives != EXTENSION_CODE) {
44.        isolate->ReportPendingMessages();
45.      }
46.    }
47.    return maybe_result;
48.  }
Enter fullscreen mode Exit fullscreen mode

In the 17th line of code, lookup compilation_cache, cache hit indicates that there are already bytecodes, and don’t need to recompile. The 26th line of code indicates that cache miss, so call the CompileToplevel on the 37th line to generate bytecodes.

Bytecode needs to be bound to a context before execution, as below:

0.  Local<Script> UnboundScript::BindToCurrentContext() {
1.    auto function_info =
2.        i::Handle<i::SharedFunctionInfo>::cast(Utils::OpenHandle(this));
3.    i::Isolate* isolate = function_info->GetIsolate();
4.    i::Handle<i::JSFunction> function =
5.        isolate->factory()->NewFunctionFromSharedFunctionInfo(
6.            function_info, isolate->native_context());
7.    return ToApiHandle<Script>(function);
8.  }
Enter fullscreen mode Exit fullscreen mode

After binding, we get an important member — JSFunction, which is the input expected by the execution.

0.  bool Shell::ExecuteString(Isolate* isolate, Local<String> source,
1.                            Local<Value> name, PrintResult print_result,
2.                            ReportExceptions report_exceptions,
3.                            ProcessMessageQueue process_message_queue) {
4.    bool success = true;
5.    {
6.      PerIsolateData* data = PerIsolateData::Get(isolate);
7.      Local<Context> realm =
8.          Local<Context>::New(isolate, data->realms_[data->realm_current_]);
9.      Context::Scope context_scope(realm);
10.      MaybeLocal<Script> maybe_script;
11.      Local<Context> context(isolate->GetCurrentContext());
12.      ScriptOrigin origin(name);
13.      if (options.compile_options == ScriptCompiler::kConsumeCodeCache) {
14.  //.......omit
15.      } else if (options.stress_background_compile) {
16.  //.......omit
17.      } else {
18.        ScriptCompiler::Source script_source(source, origin);
19.        maybe_script = ScriptCompiler::Compile(context, &script_source,
20.                                               options.compile_options);
21.      }
22.      maybe_result = script->Run(realm);
23.  //......omit
24.      if (options.code_cache_options ==
25.          ShellOptions::CodeCacheOptions::kProduceCacheAfterExecute) {
26.        // Serialize and store it in memory for the next execution.
27.        ScriptCompiler::CachedData* cached_data =
28.            ScriptCompiler::CreateCodeCache(script->GetUnboundScript());
29.        StoreInCodeCache(isolate, source, cached_data);
30.        delete cached_data;
31.      }
32.      if (process_message_queue && !EmptyMessageQueues(isolate)) success = false;
33.      data->realm_current_ = data->realm_switch_;
34.    }
35.    DCHECK(!try_catch.HasCaught());
36.    return success;
37.  }
Enter fullscreen mode Exit fullscreen mode

The above function is the entrance for executing JS. Line 19 compiles the JS to bytecode. Line 22 executes the bytecode. Debug line 22 to step into the Run(realm).

0.  MaybeLocal<Value> Script::Run(Local<Context> context) {
1.    auto isolate = reinterpret_cast<i::Isolate*>(context->GetIsolate());
2.    TRACE_EVENT_CALL_STATS_SCOPED(isolate, "v8", "V8.Execute");
3.    ENTER_V8(isolate, context, Script, Run, MaybeLocal<Value>(),
4.             InternalEscapableScope);
5.    i::HistogramTimerScope execute_timer(isolate->counters()->execute(), true);
6.    i::AggregatingHistogramTimerScope timer(isolate->counters()->compile_lazy());
7.    i::TimerEventScope<i::TimerEventExecute> timer_scope(isolate);
8.    auto fun = i::Handle<i::JSFunction>::cast(Utils::OpenHandle(this));
9.    i::Handle<i::Object> receiver = isolate->global_proxy();
10.    Local<Value> result;
11.    has_pending_exception = !ToLocal<Value>(
12.        i::Execution::Call(isolate, fun, receiver, 0, nullptr), &result);
13.    RETURN_ON_FAILED_EXECUTION(Value);
14.    RETURN_ESCAPED(result);
15.  }
Enter fullscreen mode Exit fullscreen mode

The 8th line casts this pointer to JSFunction. For our case, the variable fun is console.log(JsPrint(6)), then call the 12th line — call().

In call(), the Invoke() will be called which is below.

0.  V8_WARN_UNUSED_RESULT MaybeHandle<Object> Invoke(Isolate* isolate,
1.                                                   const InvokeParams& params) {
2.  //...............omit
3.    Object value;
4.    Handle<Code> code =
5.        JSEntry(isolate, params.execution_target, params.is_construct);
6.    {
7.      SaveContext save(isolate);
8.      SealHandleScope shs(isolate);
9.      if (FLAG_clear_exceptions_on_js_entry) isolate->clear_pending_exception();
10.      if (params.execution_target == Execution::Target::kCallable) {
11.        // clang-format off
12.        // {new_target}, {target}, {receiver}, return value: tagged pointers
13.        // {argv}: pointer to array of tagged pointers
14.        using JSEntryFunction = GeneratedCode<Address(
15.            Address root_register_value, Address new_target, Address target,
16.            Address receiver, intptr_t argc, Address** argv)>;
17.        // clang-format on
18.        JSEntryFunction stub_entry =
19.            JSEntryFunction::FromAddress(isolate, code->InstructionStart());
20.        Address orig_func = params.new_target->ptr();
21.        Address func = params.target->ptr();
22.        Address recv = params.receiver->ptr();
23.        Address** argv = reinterpret_cast<Address**>(params.argv);
24.        RuntimeCallTimerScope timer(isolate, RuntimeCallCounterId::kJS_Execution);
25.        value = Object(stub_entry.Call(isolate->isolate_data()->isolate_root(),
26.                                       orig_func, func, recv, params.argc, argv));
27.      } else {
28.  //..................omit     
29.      }
30.    }
Enter fullscreen mode Exit fullscreen mode

Line 18 gets the address of the builtin, its index is 40, and its name is JSEntry. This builtin builds the execution environment for bytecodes. Then step into line 25, where the bytecode is executed. After that, we can only debug the assembly code, but c++. Figure 1 is the call stack.

Image description
Before stepping into bytecodes, Generate_JSEntryVariant will be called.

0.  void Generate_JSEntryVariant(MacroAssembler* masm, StackFrame::Type type,
1.                               Builtins::Name entry_trampoline) {
2.    Label invoke, handler_entry, exit;
3.    Label not_outermost_js, not_outermost_js_2;
4.    {  // NOLINT. Scope block confuses linter.
5.      NoRootArrayScope uninitialized_root_register(masm);
6.      // Set up frame.
7.      __ pushq(rbp);
8.      __ movq(rbp, rsp);
9.      // Push the stack frame type.
10.      __ Push(Immediate(StackFrame::TypeToMarker(type)));
11.      // Reserve a slot for the context. It is filled after the root register has
12.      // been set up.
13.      __ AllocateStackSpace(kSystemPointerSize);
14.      // Save callee-saved registers (X64/X32/Win64 calling conventions).
15.      __ pushq(r12);
16.      __ pushq(r13);
17.      __ pushq(r14);
18.      __ pushq(r15);
19.  #ifdef _WIN64
20.      __ pushq(rdi);  // Only callee save in Win64 ABI, argument in AMD64 ABI.
21.      __ pushq(rsi);  // Only callee save in Win64 ABI, argument in AMD64 ABI.
22.  #endif
23.      __ pushq(rbx);
24.  //.....................omit............................
25.    __ ret(0);
26.  }
27.  }  // namespace
28.  //==================separation===============================
29.  void Builtins::Generate_JSEntry(MacroAssembler* masm) {
30.    Generate_JSEntryVariant(masm, StackFrame::ENTRY,
31.                            Builtins::kJSEntryTrampoline);
32.  }
Enter fullscreen mode Exit fullscreen mode

Look at the 30th line of code, the generator function of the 40th builtin is B, and its analysis method refers to my previous paper. Figure 2 shows the corresponding assembly code.

Image description
You can also see a bit of C++ in Figure 2, but this is the last chance to see C++. After that, only the assembly code.

Our case uses the console.log which is a global function. It is below.

1.  #define RUNTIME_FUNCTION_RETURNS_TYPE(Type, InternalType, Convert, Name)      \
2.    static V8_INLINE InternalType __RT_impl_##Name(Arguments args,              \
3.                                                   Isolate* isolate);           \
4.                                                                                \
5.    V8_NOINLINE static Type Stats_##Name(int args_length, Address* args_object, \
6.                                         Isolate* isolate) {                    \
7.      RuntimeCallTimerScope timer(isolate, RuntimeCallCounterId::k##Name);      \
8.      TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.runtime"),                     \
9.                   "V8.Runtime_" #Name);                                        \
10.     Arguments args(args_length, args_object);                                 \
11.      return Convert(__RT_impl_##Name(args, isolate));                          \
12.    }                                                                           \
13.                                                                                \
14.    Type Name(int args_length, Address* args_object, Isolate* isolate) {        \
15.      DCHECK(isolate->context().is_null() || isolate->context().IsContext());   \
16.      CLOBBER_DOUBLE_REGISTERS();                                               \
17.      if (V8_UNLIKELY(TracingFlags::is_runtime_stats_enabled())) {              \
18.        return Stats_##Name(args_length, args_object, isolate);                 \
19.      }                                                                         \
20.      Arguments args(args_length, args_object);                                 \
21.      return Convert(__RT_impl_##Name(args, isolate));                          \
22.    }                                                                           \
23.                                                                                \
24.    static InternalType __RT_impl_##Name(Arguments args, Isolate* isolate)
25.  #define CONVERT_OBJECT(x) (x).ptr()
26.  #define CONVERT_OBJECTPAIR(x) (x)
27.  #define RUNTIME_FUNCTION(Name) \
28.    RUNTIME_FUNCTION_RETURNS_TYPE(Address, Object, CONVERT_OBJECT, Name)
29.  //=================separation==============================
30.  //==================separation===============================
31.  Object DeclareGlobals(Isolate* isolate, Handle<FixedArray> declarations,
32.                        int flags, Handle<JSFunction> closure) {
33.    HandleScope scope(isolate);
34.    Handle<JSGlobalObject> global(isolate->global_object());
35.    Handle<Context> context(isolate->context(), isolate);
36.    Handle<FeedbackVector> feedback_vector = Handle<FeedbackVector>::null();
37.    Handle<ClosureFeedbackCellArray> closure_feedback_cell_array =
38.        Handle<ClosureFeedbackCellArray>::null();
39.    if (closure->has_feedback_vector()) {
40.      feedback_vector =
41.          Handle<FeedbackVector>(closure->feedback_vector(), isolate);
42.      closure_feedback_cell_array = Handle<ClosureFeedbackCellArray>(
43.          feedback_vector->closure_feedback_cell_array(), isolate);
44.    } else {
45.      closure_feedback_cell_array = Handle<ClosureFeedbackCellArray>(
46.          closure->closure_feedback_cell_array(), isolate);
47.    }
48.    // Traverse the name/value pairs and set the properties.
49.    int length = declarations->length();
50.    FOR_WITH_HANDLE_SCOPE(isolate, int, i = 0, i, i < length, i += 4, {
51.      Handle<String> name(String::cast(declarations->get(i)), isolate);
52.      FeedbackSlot slot(Smi::ToInt(declarations->get(i + 1)));
53.      Handle<Object> possibly_feedback_cell_slot(declarations->get(i + 2),
54.                                                 isolate);
55.      Handle<Object> initial_value(declarations->get(i + 3), isolate);
56.      bool is_var = initial_value->IsUndefined(isolate);
57.      bool is_function = initial_value->IsSharedFunctionInfo();
58.      DCHECK_NE(is_var, is_function);
59.      Handle<Object> value;
60.      if (is_function) {
61.        DCHECK(possibly_feedback_cell_slot->IsSmi());
62.        Handle<FeedbackCell> feedback_cell =
63.            closure_feedback_cell_array->GetFeedbackCell(
64.                Smi::ToInt(*possibly_feedback_cell_slot));
65.        // Copy the function and update its context. Use it as value.
66.        Handle<SharedFunctionInfo> shared =
67.            Handle<SharedFunctionInfo>::cast(initial_value);
68.        Handle<JSFunction> function =
69.            isolate->factory()->NewFunctionFromSharedFunctionInfo(
70.                shared, context, feedback_cell, AllocationType::kOld);
71.        value = function;
72.      } else {
73.        value = isolate->factory()->undefined_value();
74.      }
75.      // Compute the property attributes. According to ECMA-262,
76.      // the property must be non-configurable except in eval.
77.      bool is_eval = DeclareGlobalsEvalFlag::decode(flags);
78.      int attr = NONE;
79.      if (!is_eval) attr |= DONT_DELETE;
80.      // ES#sec-globaldeclarationinstantiation 5.d:
81.      // If hasRestrictedGlobal is true, throw a SyntaxError exception.
82.      Object result = DeclareGlobal(isolate, global, name, value,
83.                                    static_cast<PropertyAttributes>(attr), is_var,
84.                                    is_function, RedeclarationType::kSyntaxError,
85.                                    feedback_vector, slot);
86.      if (isolate->has_pending_exception()) return result;
87.    });
88.    return ReadOnlyRoots(isolate).undefined_value();
89.  }
90.  //==================separation===============================
91.  //==================separation===============================
92.  RUNTIME_FUNCTION(Runtime_DeclareGlobals) {
93.    HandleScope scope(isolate);
94.    DCHECK_EQ(3, args.length());
95.    CONVERT_ARG_HANDLE_CHECKED(FixedArray, declarations, 0);
96.    CONVERT_SMI_ARG_CHECKED(flags, 1);
97.    CONVERT_ARG_HANDLE_CHECKED(JSFunction, closure, 2);
98.    return DeclareGlobals(isolate, declarations, flags, closure);
99.  }

Enter fullscreen mode Exit fullscreen mode

RUNTIME_FUNCTION(Runtime_DeclareGlobals) is a runtime function defined by a macro template. The function checks the parameters and then calls DeclareGlobals() to perform the corresponding operation.

In our case, DeclareGlobals() lookup the global object console, then takes out the log() from the console, and gets the address of console.log(). Finally, pass the parameter JsPrint(6) into log() which will start a new compilation and execution.


Okay, that wraps it up for this share. I’ll see you guys next time, take care!

Please reach out to me if you have any issues. WeChat: qq9123013 Email: v8blink@outlook.com

Top comments (0)