Let’s Understand Chrome V8--Chapter 18: Understanding Bytecode and How to Debug it

v8blink

灰豆

Posted on September 9, 2022

Let’s Understand Chrome V8--Chapter 18: Understanding Bytecode and How to Debug it

Original source: https://javascript.plainenglish.io/lets-understand-chrome-v8-understanding-bytecode-and-how-to-debug-it-2e470db67ba5
Welcome to other chapters of Let’s Understand Chrome V8


The bytecode is implemented using CodeStubAssembler (CSA), which can be roughly regarded as an assembly. Bytecode is loaded in a de-serialized manner during V8 startup, with no symbol table. So I want to tell you: CAS is obscure in a static analysis. Moreover, we can’t get the source code of the bytecode in debugging. This article will talk about how to debug bytecode at the assembly level and see its execution details.

Image description
Figure 1 shows the interpreter of bytecodes. The figure is detailed enough for JavaScript developers. But for V8 learners, Figure 1 lacks a lot of detail, such as: the ignition startup, loading the bytecode, and dispatch. Let’s debug bytecode to dive into those details.

1. Preparation

Note: Before debugging, you’d better understand stack frames, the encoding of bytecodes and registers, see here.

Below is the Invoke which is the last C++ function before debugging.

1.  V8_WARN_UNUSED_RESULT MaybeHandle<Object> Invoke(Isolate* isolate,
2.                                                  const InvokeParams& params) {
3.   if (params.target->IsJSFunction()) {
4.  //............omit...............
5.   }
6.    Object value;
7.     Handle<Code> code =
8.         JSEntry(isolate, params.execution_target, params.is_construct);
9.     {
10.       SaveContext save(isolate);
11.       SealHandleScope shs(isolate);
12.       if (FLAG_clear_exceptions_on_js_entry) isolate->clear_pending_exception();
13.       if (params.execution_target == Execution::Target::kCallable) {
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.     }
31.     //............omit...............
32.     return Handle<Object>(value, isolate);
33.   }
Enter fullscreen mode Exit fullscreen mode

Lines 14 to 23 initialize the JSFunction that will be executed, namely your JavaScript code. The following are five important members that you should remember. These members can help you to locate your position in debugging. They are constant in one debug context, but will be dissimilar in different debug.

(1) Line 7, code, it is the Builtin::JSEntry pointer, now the value is 1FA 0E06 ED30 (In my debug context).

(2) Line 18, stub_entry, it is the Ignition’s entrance. Below is the function that gets stub_entry from Builtin::JSEntry.

Address Code::OffHeapInstructionStart() const {
  DCHECK(is_off_heap_trampoline());
  if (Isolate::CurrentEmbeddedBlob() == nullptr) return raw_instruction_start();
  EmbeddedData d = EmbeddedData::FromBlob();
  return d.InstructionStartOfBuiltin(builtin_index());
}
Enter fullscreen mode Exit fullscreen mode

The entry is 1FA 1326 1840.

(3) Line21, func, it is the address of JSFunction, namely your JavaScript code. The value is 16 2BD8 15A9.

(4) The address of Builtin::InterpreterEntryTrampoline is 52 61C0 8A41.

(5) The dispatch_table is 1FA 0E08 CFB0.

Image description
Figure 2 is the call stack that is going into bytecode.

2. Debug bytecode

Let’s start to debug bytecode by stepping into stub_entry.Call(). I’m sorry I can’t go through the line-by-line since the assembly code doesn’t have context. I think the best way is to give the important states during the Ignition execution.

Image description
In Figure 3, the register RCX is 1FA 1326 1840(stub_entry). As I mentioned, the stub_entry is the entrance of Ignition.

Image description
In Figure 4, the register RBX is 1FA0E068A00 which is the first argument of stub_entry.Call(), that is isolate->isolate_data()->isolate_root().

Image description
Figure 5 is moving the R8 and calling RSI. The register R8 is func, and the RSI is stub_entry, below is the function to which the stub_entry is pointing.

1.  void Builtins::Generate_JSEntry(MacroAssembler* masm) {
2.    Generate_JSEntryVariant(masm, StackFrame::ENTRY,
3.                            Builtins::kJSEntryTrampoline);
4.  }
5.  //==================separation===========================
6.  void Generate_JSEntryVariant(MacroAssembler* masm, StackFrame::Type type,
7.                               Builtins::Name entry_trampoline) {
8.    Label invoke, handler_entry, exit;
9.    Label not_outermost_js, not_outermost_js_2;
10.    {  
11.      NoRootArrayScope uninitialized_root_register(masm);
12.      __ pushq(rbp);
13.      __ movq(rbp, rsp);
14.      __ Push(Immediate(StackFrame::TypeToMarker(type)));
15.      __ AllocateStackSpace(kSystemPointerSize);
16.      __ pushq(r12);
17.      __ pushq(r13);
18.      __ pushq(r14);
19.      __ pushq(r15);
20.  #ifdef _WIN64
21.      __ pushq(rdi);  // Only callee save in Win64 ABI, argument in AMD64 ABI.
22.      __ pushq(rsi);  // Only callee save in Win64 ABI, argument in AMD64 ABI.
23.  #endif
24.      __ pushq(rbx);
25.  #ifdef _WIN64 //Here is True, my OS is Win10!!!!!!!!!!!!!!!!!!
26.      // On Win64 XMM6-XMM15 are callee-save.
27.      __ AllocateStackSpace(EntryFrameConstants::kXMMRegistersBlockSize);
28.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 0), xmm6);
29.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 1), xmm7);
30.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 2), xmm8);
31.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 3), xmm9);
32.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 4), xmm10);
33.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 5), xmm11);
34.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 6), xmm12);
35.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 7), xmm13);
36.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 8), xmm14);
37.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 9), xmm15);
38.      STATIC_ASSERT(EntryFrameConstants::kCalleeSaveXMMRegisters == 10);
39.      STATIC_ASSERT(EntryFrameConstants::kXMMRegistersBlockSize ==
40.                    EntryFrameConstants::kXMMRegisterSize *
41.                        EntryFrameConstants::kCalleeSaveXMMRegisters);
42.  #endif
43.  //............omit...........................
44.  }
Enter fullscreen mode Exit fullscreen mode

In lines 12 to 37, we can see that they are the same as the assembly code in Figure 6. Actually, Generate_JSEntryVariant is executing. Builtin:JSEntry is responsible for organizing arguments by std::call standard. Later, the function Builtin::InterpreterEntryTrampoline will use these arguments.

Image description
Builtin::InterpreterEntryTrampoline lookups at the dispatch_table to find the target bytecode and call it, shown in Figure 7.

Image description
Let’s examine dispatch in a little more depth, see Figure 7.

  • mark 1: the machine register R15 is the dispatch_table.

  • mark 2: calculate the target bytecode address, that is dispatch_table + the encoding of the bytecode.

  • mark 3: call the target bytecode.

In my case, the target bytecode is LdaConstant.

1.  IGNITION_HANDLER(LdaConstant, InterpreterAssembler) {
2.    TNode<Object> constant = LoadConstantPoolEntryAtOperandIndex(0);
3.    SetAccumulator(constant);
4.    Dispatch();
5.  }
Enter fullscreen mode Exit fullscreen mode

When LdaConstant exits, lookup at the dispatch_table to find the next bytecode, shown in Figure 8.

Image description
At the assembly level, we can see the Ignition startup and bytecode execution, which can help us understand the V8 interpreter better.

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

💖 💪 🙅 🚩
v8blink
灰豆

Posted on September 9, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related