3 things to do before debugging V8 Link to heading

  1. Build d8 with debug flags from V8 source
  2. Install lldb
  3. A javascript file

Build d8 with debug flags from V8 source Link to heading

If you haven’t tried building V8 before, take a look at my other blog on how to build V8 from source on Ubuntu and Mac M1. Now we just need to make sure the d8 binary is built with debug flag instead of release. You can run this command gm.py arm64.debug.d8. Or incase you have built a release version and don’t want to rebuild everything. Open out/release/args.gn file and update following options.

is_debug = true
symbol_level = 2
v8_enable_backtrace = true
v8_optimized_debug = false

And then go to previous build directory and run ninja again.

cd out/release
ninja

Install lldb Link to heading

Run lldb -v to check if you have lldb installed.

lldb -v
lldb-1300.0.42.3

If not. You will be prompt to the installation.

Create a JS file Link to heading

For this first debugging session we will use simple script. Let’s create a file called debug.js in the same directory of d8 executable.

let a = 1;
var b = 2;
const c = 3;
console.log(a + b + c);

And we can run d8 with debug.js script.

$ ./d8 debug.js
6

Cool, it works!

Debug our script Link to heading

Let’s start up lldb debug session with command above.

$ cd out/arm64.debug
$ lldb -- d8 debug.js
(lldb) target create "d8"
Current executable set to '/Users/sytran/Code/google/v8/out/arm64.debug/d8' (arm64).
(lldb) settings set -- target.run-args "debug.js"

Our target to debug is d8 executable and its only argument is debug.js. To start the script, type run and hit enter.

(lldb) run
Process 80671 launched: '/Users/sytran/Code/google/v8/out/arm64.debug/d8' (arm64)
6
Process 80671 exited with status = 0 (0x00000000)

Our script is executed and printed out 6 and we expect. Now we need to set a breakpoint in V8 C++ code for debugging. We use breakpoint set --name main command to set our first breakpoint.

(lldb) breakpoint set --name main
Breakpoint 1: where = d8`main + 24 at d8.cc:5481:59, address = 0x000000010004ec70

As we are still in debug session, we just need to run again. But this time we use shortcut r for run.

(lldb) r
Process 80711 launched: '/Users/sytran/Code/google/v8/out/arm64.debug/d8' (arm64)
Process 80711 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x000000010004ec70 d8`main(argc=2, argv=0x000000016fdff330) at d8.cc:5481:59
   5478
   5479	}  // namespace v8
   5480
-> 5481	int main(int argc, char* argv[]) { return v8::Shell::Main(argc, argv); }
   5482
   5483	#undef CHECK
   5484	#undef DCHECK
Target 0: (d8) stopped.

d8 process is stopped right at main function (-> position), and we can see surrounding lines of C++ code. To step into v8::Shell:Main function, run step or s.

(lldb) s
Process 80711 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
    frame #0: 0x000000010004d484 d8`v8::Shell::Main(argc=2, argv=0x000000016fdff330) at d8.cc:5140:3
   5137	}  // namespace
   5138
   5139	int Shell::Main(int argc, char* argv[]) {
-> 5140	  v8::base::EnsureConsoleOutput();
   5141	  if (!SetOptions(argc, argv)) return 1;
   5142
   5143	  v8::V8::InitializeICUDefaultLocation(argv[0], options.icu_data_file);
Target 0: (d8) stopped.

This function parses given arguments, for our script, it will go to last else block.

5394
5395
5396
5397
5398
5399
5400
5401
5402
5403
5404
5405
5406
5407
5408
        printf("============ Run: Consume code cache ============\n");
        // Second run to consume the cache in current isolate
        result = RunMain(isolate, true);
        options.compile_options.Overwrite(
            v8::ScriptCompiler::kNoCompileOptions);
      } else {
        bool last_run = true;
        result = RunMain(isolate, last_run);
      }

      // Run interactive shell if explicitly requested or if no script has been
      // executed, but never on --test
      if (use_interactive_shell()) {
        RunShell(isolate);
      }

So let set a breakpoint at that position and continue

(lldb) b d8.cc:5401
Breakpoint 2: where = d8`v8::Shell::Main(int, char**) + 3792 at d8.cc:5401:14, address = 0x000000010004e264
(lldb) continue
Process 80711 resuming
Process 80711 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x000000010004e264 d8`v8::Shell::Main(argc=2, argv=0x000000016fdff330) at d8.cc:5401:14
   5397	        options.compile_options.Overwrite(
   5398	            v8::ScriptCompiler::kNoCompileOptions);
   5399	      } else {
   5400	        bool last_run = true;
-> 5401	        result = RunMain(isolate, last_run);
   5402	      }
   5403
Target 0: (d8) stopped.

Let’s see value of last_run and isolate variables. Because isolate is a pointer, we need to use *isolate to view its value.

(lldb) fr v last_run *isolate
(bool) last_run = true
(v8::Isolate) *isolate = {}

So we running new shell with an empty v8::Isolate (internal state). And our set up is complete. In order to use other lldb debug functions, check out their document.

Now we can dive deeper into V8 core 🤩