Первоисточник: https://medium.com/@huidou/lets-understand-chrome-v8-chapter-11-bytecode-dispatch-ab6415e32bbd
Добро пожаловать в другие главы «Давайте поймем Chrome V8».
Dispatch отвечает за планирование байткода, что эквивалентно регистру EIP++ и переходу к следующему байткоду. Диспетчер состоит из двух частей, одна из которых — таблица диспетчеризации, а другая — физический регистр. Таблица — это массив, который содержит все адреса байткода. V8 использует физический регистр для диспетчеризации байткода для достижения большей эффективности.
1. Диспетчерская таблица
Диспетчерская таблица — это массив указателей, который используется для хранения адреса программы обработки байткода. Тип массива — Code, а класс Code приведен ниже:
1. class Code : public HeapObject {
2. public:
3. NEVER_READ_ONLY_SPACE
4. using Flags = uint32_t;
5. #define CODE_KIND_LIST(V)
6. V(OPTIMIZED_FUNCTION)
7. V(BYTECODE_HANDLER)
8. V(STUB)
9. V(BUILTIN)
10. V(REGEXP)
11. V(WASM_FUNCTION)
12. V(WASM_TO_CAPI_FUNCTION)
13. V(WASM_TO_JS_FUNCTION)
14. V(JS_TO_WASM_FUNCTION)
15. V(JS_TO_JS_FUNCTION)
16. V(WASM_INTERPRETER_ENTRY)
17. V(C_WASM_ENTRY)
18. enum Kind {
19. #define DEFINE_CODE_KIND_ENUM(name) name,
20. CODE_KIND_LIST(DEFINE_CODE_KIND_ENUM)
21. #undef DEFINE_CODE_KIND_ENUM
22. NUMBER_OF_KINDS
23. };
24. static const char* Kind2String(Kind kind);
25. #ifdef ENABLE_DISASSEMBLER
26. const char* GetName(Isolate* isolate) const;
27. V8_EXPORT_PRIVATE void Disassemble(const char* name, std::ostream& os,
28. Address current_pc = kNullAddress);
29. #endif
30. //................omit................
31. };
Примечание: В изоляторе есть еще один массив указателей, который имеет тот же тип, что и таблица диспетчеризации. В нем хранятся все адреса Builtins, что может легко запутать новичков.
Диспетчерская таблица поддерживается BuildWithMacroAssembler, код приведен ниже:
1. Code BuildWithMacroAssembler(Isolate* isolate, int32_t builtin_index,
2. MacroAssemblerGenerator generator,
3. const char* s_name) {
4. HandleScope scope(isolate);
5. // Canonicalize handles, so that we can share constant pool entries pointing
6. // to code targets without dereferencing their handles.
7. CanonicalHandleScope canonical(isolate);
8. constexpr int kBufferSize = 32 * KB;
9. byte buffer[kBufferSize];
10. MacroAssembler masm(isolate, BuiltinAssemblerOptions(isolate, builtin_index),
11. CodeObjectRequired::kYes,
12. ExternalAssemblerBuffer(buffer, kBufferSize));
13. masm.set_builtin_index(builtin_index);
14. DCHECK(!masm.has_frame());
15. generator(&masm);
16. int handler_table_offset = 0;
17. // JSEntry builtins are a special case and need to generate a handler table.
18. DCHECK_EQ(Builtins::KindOf(Builtins::kJSEntry), Builtins::ASM);
19. DCHECK_EQ(Builtins::KindOf(Builtins::kJSConstructEntry), Builtins::ASM);
20. DCHECK_EQ(Builtins::KindOf(Builtins::kJSRunMicrotasksEntry), Builtins::ASM);
21. if (Builtins::IsJSEntryVariant(builtin_index)) {
22. handler_table_offset = HandlerTable::EmitReturnTableStart(&masm);
23. HandlerTable::EmitReturnEntry(
24. &masm, 0, isolate->builtins()->js_entry_handler_offset());
25. }
26. //.....................................................
27. //................omit.............................
28. }
Как отлаживать BuildWithMacroAssembler смотрите в главе 9. Когда значение параметра builtin_index равно 65, генератор указателей функций указывает на Generate_InterpreterEnterBytecodeDispatch, который отвечает за ведение таблицы диспетчеризации, код приведен ниже:
1. static void Generate_InterpreterEnterBytecode(MacroAssembler* masm) {
2. // Set the return address to the correct point in the interpreter entry
3. // trampoline.
4. Label builtin_trampoline, trampoline_loaded;
5. Smi interpreter_entry_return_pc_offset(
6. masm->isolate()->heap()->interpreter_entry_return_pc_offset());
7. DCHECK_NE(interpreter_entry_return_pc_offset, Smi::kZero);
8. // If the SFI function_data is an InterpreterData, the function will have a
9. // custom copy of the interpreter entry trampoline for profiling. If so,
10. // get the custom trampoline, otherwise grab the entry address of the global
11. // trampoline.
12. __ movq(rbx, Operand(rbp, StandardFrameConstants::kFunctionOffset));
13. __ LoadTaggedPointerField(
14. rbx, FieldOperand(rbx, JSFunction::kSharedFunctionInfoOffset));
15. __ LoadTaggedPointerField(
16. rbx, FieldOperand(rbx, SharedFunctionInfo::kFunctionDataOffset));
17. __ CmpObjectType(rbx, INTERPRETER_DATA_TYPE, kScratchRegister);
18. __ j(not_equal, &builtin_trampoline, Label::kNear);
19. __ movq(rbx,
20. FieldOperand(rbx, InterpreterData::kInterpreterTrampolineOffset));
21. __ addq(rbx, Immediate(Code::kHeaderSize - kHeapObjectTag));
22. __ jmp(&trampoline_loaded, Label::kNear);
23. __ bind(&builtin_trampoline);
24. // TODO(jgruber): Replace this by a lookup in the builtin entry table.
25. __ movq(rbx,
26. __ ExternalReferenceAsOperand(
27. ExternalReference::
28. address_of_interpreter_entry_trampoline_instruction_start(
29. masm->isolate()),
30. kScratchRegister));
31. __ bind(&trampoline_loaded);
32. __ addq(rbx, Immediate(interpreter_entry_return_pc_offset.value()));
33. __ Push(rbx);
34. // Initialize dispatch table register.
35. __ Move(
36. kInterpreterDispatchTableRegister,
37. ExternalReference::interpreter_dispatch_table_address(masm->isolate()));
38. // Get the bytecode array pointer from the frame.
39. __ movq(kInterpreterBytecodeArrayRegister,
40. Operand(rbp, InterpreterFrameConstants::kBytecodeArrayFromFp));
41. if (FLAG_debug_code) {
42. // Check function data field is actually a BytecodeArray object.
43. __ AssertNotSmi(kInterpreterBytecodeArrayRegister);
44. __ CmpObjectType(kInterpreterBytecodeArrayRegister, BYTECODE_ARRAY_TYPE,
45. rbx);
46. __ Assert(
47. equal,
48. AbortReason::kFunctionDataShouldBeBytecodeArrayOnInterpreterEntry);
49. }
50. // Get the target bytecode offset from the frame.
51. __ movq(kInterpreterBytecodeOffsetRegister,
52. Operand(rbp, InterpreterFrameConstants::kBytecodeOffsetFromFp));
53. __ SmiUntag(kInterpreterBytecodeOffsetRegister,
54. kInterpreterBytecodeOffsetRegister);
55. // Dispatch to the target bytecode.
56. __ movzxbq(r11, Operand(kInterpreterBytecodeArrayRegister,
57. kInterpreterBytecodeOffsetRegister, times_1, 0));
58. __ movq(kJavaScriptCallCodeStartRegister,
59. Operand(kInterpreterDispatchTableRegister, r11,
60. times_system_pointer_size, 0));
61. __ jmp(kJavaScriptCallCodeStartRegister);
62. }
63. //================================separation=========================
64. void Builtins::Generate_InterpreterEnterBytecodeDispatch(MacroAssembler* masm) {
65. Generate_InterpreterEnterBytecode(masm);
66. }
В приведенном выше коде Generate_InterpreterEnterBytecodeDispatch является точкой входа в диспетчер байткода, а Generate_InterpreterEnterBytecode является ключевым кодом, реализующим диспетчер байткода. Вот два важных понятия:
-
Generate_InterpreterEnterBytecodeDispatch является Builtin и его индекс равен 65. (Индекс может отличаться в разных версиях V8).
-
V8 использует физический регистр для управления таблицей диспетчеризации, имя регистра — kInterpreterDispatchTableRegister. Использование физического регистра позволяет избежать частого выталкивания и всплывания таблицы диспетчеризации, что повышает эффективность.
Вот ключевые строки кода:
(1) Строка 35, загружает таблицу диспетчеризации в регистр A. Строка 37 кода, извлекает адрес таблицы из изолятора. Реализация A заключается в следующих трех функциях.
ExternalReference ExternalReference::interpreter_dispatch_table_address(
Isolate* isolate) {
return ExternalReference(isolate->interpreter()->dispatch_table_address());
}
interpreter::Interpreter* interpreter() const {
return interpreter_;
}
Address dispatch_table_address() {
return reinterpret_cast<Address>(&dispatch_table_[0]);
}
(2) Строка 39 извлекает массив байткода из стека.
(3) Строка 51 извлекает смещение целевого байткода из массива байткода.
(4) Строки 56 и 58 вычисляют адрес целевого байткода и сохраняют его в регистре kJavaScriptCallCodeStartRegister.
(5) Строка 61 переходит к регистру kJavaScriptCallCodeStartRegister и выполняет байткод. На рисунке 1 представлен стек вызовов.
2. TailCall
Dispatch() является последним шагом в байткоде, поэтому его псевдоним — хвостовой вызов. Давайте рассмотрим несколько байткодов:.
1. IGNITION_HANDLER(StaGlobal, InterpreterAssembler) {
2. TNode<Context> context = GetContext();
3. //omit..............
4. Goto(&end);
5. Bind(&no_feedback);
6. CallRuntime(Runtime::kStoreGlobalICNoFeedback_Miss, context, value, name);
7. Goto(&end);
8. Bind(&end);
9. Dispatch();// !!! this is the tail call.
10. }
11. IGNITION_HANDLER(LdaContextSlot, InterpreterAssembler) {
12. TNode<Context> context = CAST(LoadRegisterAtOperandIndex(0));
13. TNode<IntPtrT> slot_index = Signed(BytecodeOperandIdx(1));
14. TNode<Uint32T> depth = BytecodeOperandUImm(2);
15. TNode<Context> slot_context = GetContextAtDepth(context, depth);
16. TNode<Object> result = LoadContextElement(slot_context, slot_index);
17. SetAccumulator(result);
18. Dispatch(); //!!! here also is.
19. }
20. IGNITION_HANDLER(LdaImmutableContextSlot, InterpreterAssembler) {
21. TNode<Context> context = CAST(LoadRegisterAtOperandIndex(0));
22. TNode<IntPtrT> slot_index = Signed(BytecodeOperandIdx(1));
23. TNode<Uint32T> depth = BytecodeOperandUImm(2);
24. TNode<Context> slot_context = GetContextAtDepth(context, depth);
25. TNode<Object> result = LoadContextElement(slot_context, slot_index);
26. SetAccumulator(result);
27. Dispatch(); //!!! here
28. }
Все три приведенных выше байткода заканчиваются Dispatch(). Ниже приведен код Dispatch:
1. void InterpreterAssembler::Dispatch() {
2. Comment("========= Dispatch");
3. DCHECK_IMPLIES(Bytecodes::MakesCallAlongCriticalPath(bytecode_), made_call_);
4. TNode<IntPtrT> target_offset = Advance();
5. TNode<WordT> target_bytecode = LoadBytecode(target_offset);
6. if (Bytecodes::IsStarLookahead(bytecode_, operand_scale_)) {
7. target_bytecode = StarDispatchLookahead(target_bytecode);
8. }
9. DispatchToBytecode(target_bytecode, BytecodeOffset());
10. }
Вторая строка — это функция комментария. При отладке мы можем использовать ее выходную информацию для определения позиции выполнения. В 4-й строке выводится смещение целевого (следующего) байткода. 5-я строка загружает целевой байткод. 9-я строка вызывает DspatchToBytecode(), которая в конечном итоге вызывает DispatchToBytecodeHandlerEntry().
void InterpreterAssembler::DispatchToBytecodeHandlerEntry(
TNode<RawPtrT> handler_entry, TNode<IntPtrT> bytecode_offset) {
// Propagate speculation poisoning.
TNode<RawPtrT> poisoned_handler_entry =
UncheckedCast<RawPtrT>(WordPoisonOnSpeculation(handler_entry));
TailCallBytecodeDispatch(InterpreterDispatchDescriptor{},
poisoned_handler_entry, GetAccumulatorUnchecked(),
bytecode_offset, BytecodeArrayTaggedPointer(),
DispatchTablePointer());
}
В DispatchToBytecodeHandlerEntry параметром 1 является целевой байткод, а 2 — смещение байткода. Параметр 1 используется для загрузки обработчика байткода, а 2 — для загрузки операндов. Затем вызовите команду TailCallBytecodeDispatch, которая приведена ниже:
0. template <class... TArgs>
1. void CodeAssembler::TailCallBytecodeDispatch(
2. const CallInterfaceDescriptor& descriptor, TNode<RawPtrT> target,
3. TArgs... args) {
4. DCHECK_EQ(descriptor.GetParameterCount(), sizeof...(args));
5. auto call_descriptor = Linkage::GetBytecodeDispatchCallDescriptor(
6. zone(), descriptor, descriptor.GetStackParameterCount());
7. Node* nodes[] = {target, args...};
8. CHECK_EQ(descriptor.GetParameterCount() + 1, arraysize(nodes));
9. raw_assembler()->TailCallN(call_descriptor, arraysize(nodes), nodes);
10. }
В приведенном выше коде 5-я строка создает дескриптор, как показано ниже:
0. CallDescriptor* Linkage::GetBytecodeDispatchCallDescriptor(
1. Zone* zone, const CallInterfaceDescriptor& descriptor,
2. int stack_parameter_count) {
3. const int register_parameter_count = descriptor.GetRegisterParameterCount();
4. const int parameter_count = register_parameter_count + stack_parameter_count;
5. DCHECK_EQ(descriptor.GetReturnCount(), 1);
6. LocationSignature::Builder locations(zone, 1, parameter_count);
7. locations.AddReturn(regloc(kReturnRegister0, descriptor.GetReturnType(0)));
8. for (int i = 0; i < parameter_count; i++) {
9. if (i < register_parameter_count) {
10. // The first parameters go in registers.
11. Register reg = descriptor.GetRegisterParameter(i);
12. MachineType type = descriptor.GetParameterType(i);
13. locations.AddParam(regloc(reg, type));
14. } else {
15. int stack_slot = i - register_parameter_count - stack_parameter_count;
16. locations.AddParam(LinkageLocation::ForCallerFrameSlot(
17. stack_slot, MachineType::AnyTagged()));
18. }
19. }
20. MachineType target_type = MachineType::Pointer();
21. LinkageLocation target_loc = LinkageLocation::ForAnyRegister(target_type);
22. const CallDescriptor::Flags kFlags =
23. CallDescriptor::kCanUseRoots | CallDescriptor::kFixedTargetRegister;
24. return new (zone) CallDescriptor( // --
25. CallDescriptor::kCallAddress, // kind
26. target_type, // target MachineType
27. target_loc, // target location
28. locations.Build(), // location_sig
29. stack_parameter_count, // stack_parameter_count
30. Operator::kNoProperties, // properties
31. kNoCalleeSaved, // callee-saved registers
32. kNoCalleeSaved, // callee-saved fp
33. kFlags, // flags
34. descriptor.DebugName());
35. }
В GetBytecodeDispatchCallDescriptor параметром 2 является целевой байткод, а параметром 3 — количество параметров стека. Эта функция используется для обращения к ресурсам регистра и создания CallDescriptor. CallDescriptor указывает, какие параметры необходимо предоставить при выполнении байткода. В комментариях к строкам 24-25 приведена его схема.
В TailCallBytecodeDispatch, TailCallN() на 9-й строке ниже, мы видим, что параметром 1 является CallDescriptor.
1. void RawMachineAssembler::TailCallN(CallDescriptor* call_descriptor,
2. int input_count, Node* const* inputs) {
3. // +1 is for target.
4. DCHECK_EQ(input_count, call_descriptor->ParameterCount() + 1);
5. Node* tail_call =
6. MakeNode(common()->TailCall(call_descriptor), input_count, inputs);
7. schedule()->AddTailCall(CurrentBlock(), tail_call);
8. current_block_ = nullptr;
9. }
В 5-й строке генерируется узел. В 7-й строке AddTailCall() добавляет узел в конец текущего основного блока. На этом диспетчеризация завершена и начинается выполнение следующего байткода. На рисунке 2 показан стек вызовов функций.
Итак, на этом мы заканчиваем этот раздел. Увидимся в следующий раз, ребята, всего доброго!
Пожалуйста, свяжитесь со мной, если у вас возникнут какие-либо вопросы. WeChat: qq9123013 Email: v8blink@outlook.com