Welcome to other chapters of Let’s Understand Chrome V8
Debugging is undoubtedly the most efficient way to analyze V8, unfortunately, most of V8’s code is hard to debug. Most parts of the kernel code of V8 are implemented by CodeStubAssembler, namely CSA. You can roughly think that CSA is the assembly language.
Debugging assembly is not acceptable, let alone debugging in such a huge project V8.
Is there any way to make our debugging easier?
In this article, I’ll talk about my experience debugging builtins. Specifically, I’d like to talk about how to debug bytecode. Would you like to see the details of a bytecode execution? Let’s go! Absolutely, you also can skip these boring theories, and go to Section 3.
Tip: V8 is mainly implemented in two languages, C++ and assembly. Specifically, the bytecodes are implemented in CSA.
What is Runtime? Does it help with debugging bytecode? Of course, Yes!
Most of the Runtime in V8 is written in C++, and the Runtime and Bytecode can call each other. During a bytecode execution, we can manually call a Runtime to return to the C++ environment, which is easier for us to watch the details. This is the help that the Runtime gives.
Let’s learn about Runtime, and how to define our Runtimes.
1. #define FOR_EACH_INTRINSIC_GENERATOR(F, I) \
2. I(AsyncFunctionAwaitCaught, 2, 1) \
3. I(AsyncFunctionAwaitUncaught, 2, 1) \
4. I(AsyncFunctionEnter, 2, 1) \
5. I(AsyncFunctionReject, 3, 1) \
6. I(AsyncFunctionResolve, 3, 1) \
7. I(AsyncGeneratorAwaitCaught, 2, 1) \
8. I(GeneratorGetResumeMode, 1, 1)
9. #define FOR_EACH_INTRINSIC_MODULE(F, I) \ //omit....................
10. F(DynamicImportCall, 2, 1) \
11. I(GetImportMetaObject, 0, 1) \
12. F(GetModuleNamespace, 1, 1)
13. #define FOR_EACH_INTRINSIC_NUMBERS(F, I) \
14. F(GetHoleNaNLower, 0, 1) \
15. F(GetHoleNaNUpper, 0, 1) \
16. I(IsSmi, 1, 1) \
17. F(IsValidSmi, 1, 1) \
18. F(MaxSmi, 0, 1) \
19. F(NumberToString, 1, 1) \
20. F(StringParseFloat, 1, 1) \ //omit....................
21. F(StringParseInt, 2, 1) \
22. F(StringToNumber, 1, 1)
23. #define FOR_EACH_INTRINSIC_OBJECT(F, I) \
24. F(AddDictionaryProperty, 3, 1) \
25. F(NewObject, 2, 1)
The above code is a Runtime macro template, each line is a Runtime function, and you can see all the code in runtime.h.
I elaborate on the example in line 25. The keyword F
is a macro, the number 2
is the parameter counter, and the number 1
is the return value counter. The keyword I
also is a macro (see line 11).
The code of NewObject is below.
RUNTIME_FUNCTION(Runtime_NewObject) {
HandleScope scope(isolate);
DCHECK_EQ(2, args.length());
CONVERT_ARG_HANDLE_CHECKED(JSFunction, target, 0);
CONVERT_ARG_HANDLE_CHECKED(JSReceiver, new_target, 1);
RETURN_RESULT_OR_FAILURE(
isolate,
JSObject::New(target, new_target, Handle<AllocationSite>::null()));
}
In the above code, RUNTIME_FUNCTION is still a macro. We expand it to get the complete NewObject code, which is below.
1. static V8_INLINE Object __RT_impl_Runtime_NewObject(Arguments args,
2. Isolate* isolate);
3. V8_NOINLINE static Address Stats_Runtime_NewObject(int args_length, Address* args_object,
4. Isolate* isolate) {
5. RuntimeCallTimerScope timer(isolate, RuntimeCallCounterId::kRuntime_NewObject);
6. TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.runtime"),
7. "V8.Runtime_" "Runtime_NewObject");
8. Arguments args(args_length, args_object);
9. return (__RT_impl_Runtime_NewObject(args, isolate)).ptr();
10. }
11. Address Name(int args_length, Address* args_object, Isolate* isolate) {
12. DCHECK(isolate->context().is_null() || isolate->context().IsContext());
13. CLOBBER_DOUBLE_REGISTERS();
14. if (V8_UNLIKELY(TracingFlags::is_runtime_stats_enabled())) {
15. return Stats_Runtime_NewObject(args_length, args_object, isolate);
16. }
17. Arguments args(args_length, args_object);
18. return (__RT_impl_Runtime_NewObject(args, isolate)).ptr();
19. }
20. //.................分隔线...............
21. static Object __RT_impl_Runtime_NewObject(Arguments args, Isolate* isolate)
22. {
23. HandleScope scope(isolate);
24. DCHECK_EQ(2, args.length());
25. CONVERT_ARG_HANDLE_CHECKED(JSFunction, target, 0);
26. CONVERT_ARG_HANDLE_CHECKED(JSReceiver, new_target, 1);
27. RETURN_RESULT_OR_FAILURE(
28. isolate,
29. JSObject::New(target, new_target, Handle<AllocationSite>::null()));
30. }
The above code runs in a C++ environment, so you can debug it with C++ code.
I summarize the creation of the runtime as two points:
class Runtime : public AllStatic {
public:
enum FunctionId : int32_t {
#define F(name, nargs, ressize) k##name,
#define I(name, nargs, ressize) kInline##name,
FOR_EACH_INTRINSIC(F) FOR_EACH_INLINE_INTRINSIC(I)
#undef I
#undef F
kNumFunctions,
};
//omit.......................
If you define a new function, the name will also appear in this enumeration.
The runtime_table is a pointer array that holds the addresses of all runtime functions. This array is managed by v8::i::islolate. You need to choose a location to add your runtime. Below is my runtime.
#define FOR_EACH_INTRINSIC_TEST(F, I) \
F(Abort, 1, 1) \
F(AbortJS, 1, 1) \
F(AbortCSAAssert, 1, 1) \
F(ArraySpeciesProtector, 0, 1) \
F(ClearFunctionFeedback, 1, 1) \
F(ClearMegamorphicStubCache, 0, 1) \
F(CloneWasmModule, 1, 1) \
F(CompleteInobjectSlackTracking, 1, 1) \
F(ConstructConsString, 2, 1) \
F(ConstructDouble, 2, 1) \
F(ConstructSlicedString, 2, 1) \
F(MyRuntime,1,1) //Here is my runtime
The last line is my function, the name is MyRuntime, it has one parameter and one return value, and the code is below.
1.RUNTIME_FUNCTION(Runtime_MyRuntime) {
1. SealHandleScope shs(isolate);
2. DCHECK_EQ(1, args.length());
3. //function body,
4. return ReadOnlyRoots(isolate).undefined_value();
6.}
In the above code, line 1 RUNTIME_FUNCTION is a V8 macro. Line 2 is SealHandleScope (which will be explained in a later article). On line 4, you can write your code, namely the function body.
Below is the way that calls the MyRuntime in bytecode — LdaConstant.
IGNITION_HANDLER(LdaConstant, InterpreterAssembler) {
TNode<Object> constant = LoadConstantPoolEntryAtOperandIndex(0);
TNode<Context> context = GetContext();
SetAccumulator(constant);
CallRuntime(Runtime::kMyRuntime, context, constant);//Here is calling MyRuntime
Dispatch();
}
During LdaConstant is executing, our MyRuntime is called, Figure 1 is the call stack of MyRuntime.
In fact, V8 has several functions that can be used to debug builtins like bytecode, such as Runtime_DebugPrint:
1. RUNTIME_FUNCTION(Runtime_DebugPrint) {
2. SealHandleScope shs(isolate);
3. DCHECK_EQ(1, args.length());
4. MaybeObject maybe_object(*args.address_of_arg_at(0));
5. DebugPrintImpl(maybe_object);
6. return args[0];
7. }
8. //...........separation......................
9. void CodeStubAssembler::Print(const char* prefix,
10. TNode<MaybeObject> tagged_value) {
11. if (prefix != nullptr) {
12. std::string formatted(prefix);
13. formatted += ": ";
14. Handle<String> string = isolate()->factory()->NewStringFromAsciiChecked(
15. formatted.c_str(), AllocationType::kOld);
16. CallRuntime(Runtime::kGlobalPrint, NoContextConstant(),
17. HeapConstant(string));
18. }
19. // CallRuntime only accepts Objects, so do an UncheckedCast to object.
20. // DebugPrint explicitly checks whether the tagged value is a MaybeObject.
21. TNode<Object> arg = UncheckedCast<Object>(tagged_value);
22. CallRuntime(Runtime::kDebugPrint, NoContextConstant(), arg);
23. }
In the above code, the void Print(const char* s) is defined in code-stub-assembler.h. The Print is the Easter Egg I said. With the Print, you can output information in the bytecode execution. The technology behind Print is the call each other between Runtime and Bytecode that I mentioned above because the 22nd line calls Runtime_DebugPrint on the 1st line.
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: [email protected]
Also published here.