title: ‘Arm debugging with the simulator’

description: ‘The Arm simulator and debugger can be very helpful when working with V8 code generation.’

The simulator and debugger can be very helpful when working with V8 code generation.

  • It is convenient as it allows you to test code generation without access to actual hardware.
  • No cross or native compilation is needed.
  • The simulator fully supports the debugging of generated code.

Please note that this simulator is designed for V8 purposes. Only the features used by V8 are implemented, and you might encounter unimplemented features or instructions. In this case, feel free to implement them and submit the code!

Compiling for Arm using the simulator { #compiling }

By default on an x86 host, compiling for Arm with gm will give you a simulator build:

  1. gm arm64.debug # For a 64-bit build or...
  2. gm arm.debug # ... for a 32-bit build.

You may also build the optdebug configuration as the debug may be a little slow, especially if you want to run the V8 test suite.

Starting the debugger { #start_debug }

You can start the debugger immediately from the command line after n instructions:

  1. out/arm64.debug/d8 --stop_sim_at <n> # Or out/arm.debug/d8 for a 32-bit build.

Alternatively, you can generate a breakpoint instruction in the generated code:

Natively, breakpoint instructions cause the program to halt with a SIGTRAP signal, allowing you to debug the issue with gdb. However, if running with a simulator, a breakpoint instruction in generated code will instead drop you into the simulator debugger.

You can generate a breakpoint in multiple ways by using DebugBreak() from Torque, from the CodeStubAssembler, as a node in a TurboFan pass, or directly using an assembler.

Here we focus on debugging low-level native code, so let’s look at the assembler method:

  1. TurboAssembler::DebugBreak();

Let’s say we have a jitted function called add compiled with TurboFan and we’d like to break at the start. Given a test.js example:

{ #test.js }

  1. // Our optimized function.
  2. function add(a, b) {
  3. return a + b;
  4. }
  5. // Typical cheat code enabled by --allow-natives-syntax.
  6. %PrepareFunctionForOptimization(add);
  7. // Give the optimizing compiler type feedback so it'll speculate `a` and `b` are
  8. // numbers.
  9. add(1, 3);
  10. // And force it to optimize.
  11. %OptimizeFunctionOnNextCall(add);
  12. add(5, 7);

To do it, we can hook into TurboFan’s code generator and access the assembler to insert our breakpoint:

  1. void CodeGenerator::AssembleCode() {
  2. // ...
  3. // Check if we're optimizing, then look-up the name of the current function and
  4. // insert a breakpoint.
  5. if (info->IsOptimizing()) {
  6. AllowHandleDereference allow_handle_dereference;
  7. if (info->shared_info()->PassesFilter("add")) {
  8. tasm()->DebugBreak();
  9. }
  10. }
  11. // ...
  12. }

And let’s run it:

  1. $ d8 \
  2. # Enable '%' cheat code JS functions.
  3. --allow-natives-syntax \
  4. # Disassemble our function.
  5. --print-opt-code --print-opt-code-filter="add" --code-comments \
  6. # Disable spectre mitigations for readability.
  7. --no-untrusted-code-mitigations \
  8. test.js
  9. --- Raw source ---
  10. (a, b) {
  11. return a + b;
  12. }
  13. --- Optimized code ---
  14. optimization_id = 0
  15. source_position = 12
  16. kind = OPTIMIZED_FUNCTION
  17. name = add
  18. stack_slots = 6
  19. compiler = turbofan
  20. address = 0x7f0900082ba1
  21. Instructions (size = 504)
  22. 0x7f0900082be0 0 d45bd600 constant pool begin (num_const = 6)
  23. 0x7f0900082be4 4 00000000 constant
  24. 0x7f0900082be8 8 00000001 constant
  25. 0x7f0900082bec c 75626544 constant
  26. 0x7f0900082bf0 10 65724267 constant
  27. 0x7f0900082bf4 14 00006b61 constant
  28. 0x7f0900082bf8 18 d45bd7e0 constant
  29. -- Prologue: check code start register --
  30. 0x7f0900082bfc 1c 10ffff30 adr x16, #-0x1c (addr 0x7f0900082be0)
  31. 0x7f0900082c00 20 eb02021f cmp x16, x2
  32. 0x7f0900082c04 24 54000080 b.eq #+0x10 (addr 0x7f0900082c14)
  33. Abort message:
  34. Wrong value in code start register passed
  35. 0x7f0900082c08 28 d2800d01 movz x1, #0x68
  36. -- Inlined Trampoline to Abort --
  37. 0x7f0900082c0c 2c 58000d70 ldr x16, pc+428 (addr 0x00007f0900082db8) ;; off heap target
  38. 0x7f0900082c10 30 d63f0200 blr x16
  39. -- Prologue: check for deoptimization --
  40. [ DecompressTaggedPointer
  41. 0x7f0900082c14 34 b85d0050 ldur w16, [x2, #-48]
  42. 0x7f0900082c18 38 8b100350 add x16, x26, x16
  43. ]
  44. 0x7f0900082c1c 3c b8407210 ldur w16, [x16, #7]
  45. 0x7f0900082c20 40 36000070 tbz w16, #0, #+0xc (addr 0x7f0900082c2c)
  46. -- Inlined Trampoline to CompileLazyDeoptimizedCode --
  47. 0x7f0900082c24 44 58000c31 ldr x17, pc+388 (addr 0x00007f0900082da8) ;; off heap target
  48. 0x7f0900082c28 48 d61f0220 br x17
  49. -- B0 start (construct frame) --
  50. (...)
  51. --- End code ---
  52. # Debugger hit 0: DebugBreak
  53. 0x00007f0900082bfc 10ffff30 adr x16, #-0x1c (addr 0x7f0900082be0)
  54. sim>

We can see we’ve stopped at the start of the optimized function and the simulator gave us a prompt!

Note this is just an example and V8 changes quickly so the details may vary. But you should be able to do this anywhere where an assembler is available.

Debugging commands { #debug_commands }

Common commands

Enter help in the debugger prompt to get details on available commands. These include usual gdb-like commands, such as stepi, cont, disasm, etc. If the Simulator is run under gdb, the gdb debugger command will give control to gdb. You can then use cont from gdb to go back to the debugger.

Architecture specific commands

Each target architecture implements its own simulator and debugger, so the experience and details will vary.

printobject $register (alias po) { #po }

Describe a JS object held in a register.

For example, let’s say this time we’re running our example on a 32-bit Arm simulator build. We can examine incoming arguments passed in registers:

  1. $ ./out/arm.debug/d8 --allow-natives-syntax test.js
  2. Simulator hit stop, breaking at the next instruction:
  3. 0x26842e24 e24fc00c sub ip, pc, #12
  4. sim> print r1
  5. r1: 0x4b60ffb1 1264648113
  6. # The current function object is passed with r1.
  7. sim> printobject r1
  8. r1:
  9. 0x4b60ffb1: [Function] in OldSpace
  10. - map: 0x485801f9 <Map(HOLEY_ELEMENTS)> [FastProperties]
  11. - prototype: 0x4b6010f1 <JSFunction (sfi = 0x42404e99)>
  12. - elements: 0x5b700661 <FixedArray[0]> [HOLEY_ELEMENTS]
  13. - function prototype:
  14. - initial_map:
  15. - shared_info: 0x4b60fe9d <SharedFunctionInfo add>
  16. - name: 0x5b701c5d <String[#3]: add>
  17. - formal_parameter_count: 2
  18. - kind: NormalFunction
  19. - context: 0x4b600c65 <NativeContext[261]>
  20. - code: 0x26842de1 <Code OPTIMIZED_FUNCTION>
  21. - source code: (a, b) {
  22. return a + b;
  23. }
  24. (...)
  25. # Now print the current JS context passed in r7.
  26. sim> printobject r7
  27. r7:
  28. 0x449c0c65: [NativeContext] in OldSpace
  29. - map: 0x561000b9 <Map>
  30. - length: 261
  31. - scope_info: 0x34081341 <ScopeInfo SCRIPT_SCOPE [5]>
  32. - previous: 0
  33. - native_context: 0x449c0c65 <NativeContext[261]>
  34. 0: 0x34081341 <ScopeInfo SCRIPT_SCOPE [5]>
  35. 1: 0
  36. 2: 0x449cdaf5 <JSObject>
  37. 3: 0x58480c25 <JSGlobal Object>
  38. 4: 0x58485499 <Other heap object (EMBEDDER_DATA_ARRAY_TYPE)>
  39. 5: 0x561018a1 <Map(HOLEY_ELEMENTS)>
  40. 6: 0x3408027d <undefined>
  41. 7: 0x449c75c1 <JSFunction ArrayBuffer (sfi = 0x4be8ade1)>
  42. 8: 0x561010f9 <Map(HOLEY_ELEMENTS)>
  43. 9: 0x449c967d <JSFunction arrayBufferConstructor_DoNotInitialize (sfi = 0x4be8c3ed)>
  44. 10: 0x449c8dbd <JSFunction Array (sfi = 0x4be8be59)>
  45. (...)

trace (alias t) { #trace }

Enable or disable tracing executed instructions.

When enabled, the simulator will print disassembled instructions as it is executing them. If you’re running a 64-bit Arm build, the simulator is also able to trace changes to register values.

You may also enable this from the command-line with the --trace-sim flag to enable tracing from the start.

With the same example:

  1. $ out/arm64.debug/d8 --allow-natives-syntax \
  2. # --debug-sim is required on 64-bit Arm to enable disassembly
  3. # when tracing.
  4. --debug-sim test.js
  5. # Debugger hit 0: DebugBreak
  6. 0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (addr 0x7f1e00082be0)
  7. sim> trace
  8. 0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (addr 0x7f1e00082be0)
  9. Enabling disassembly, registers and memory write tracing
  10. # Break on the return address stored in the lr register.
  11. sim> break lr
  12. Set a breakpoint at 0x7f1f880abd28
  13. 0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (addr 0x7f1e00082be0)
  14. # Continuing will trace the function's execution until we return, allowing
  15. # us to make sense of what is happening.
  16. sim> continue
  17. # x0: 0x00007f1e00082ba1
  18. # x1: 0x00007f1e08250125
  19. # x2: 0x00007f1e00082be0
  20. (...)
  21. # We first load the 'a' and 'b' arguments from the stack and check if they
  22. # are tagged numbers. This is indicated by the least significant bit being 0.
  23. 0x00007f1e00082c90 f9401fe2 ldr x2, [sp, #56]
  24. # x2: 0x000000000000000a <- 0x00007f1f821f0278
  25. 0x00007f1e00082c94 7200005f tst w2, #0x1
  26. # NZCV: N:0 Z:1 C:0 V:0
  27. 0x00007f1e00082c98 54000ac1 b.ne #+0x158 (addr 0x7f1e00082df0)
  28. 0x00007f1e00082c9c f9401be3 ldr x3, [sp, #48]
  29. # x3: 0x000000000000000e <- 0x00007f1f821f0270
  30. 0x00007f1e00082ca0 7200007f tst w3, #0x1
  31. # NZCV: N:0 Z:1 C:0 V:0
  32. 0x00007f1e00082ca4 54000a81 b.ne #+0x150 (addr 0x7f1e00082df4)
  33. # Then we untag and add 'a' and 'b' together.
  34. 0x00007f1e00082ca8 13017c44 asr w4, w2, #1
  35. # x4: 0x0000000000000005
  36. 0x00007f1e00082cac 2b830484 adds w4, w4, w3, asr #1
  37. # NZCV: N:0 Z:0 C:0 V:0
  38. # x4: 0x000000000000000c
  39. # That's 5 + 7 == 12, all good!
  40. # Then we check for overflows and tag the result again.
  41. 0x00007f1e00082cb0 54000a46 b.vs #+0x148 (addr 0x7f1e00082df8)
  42. 0x00007f1e00082cb4 2b040082 adds w2, w4, w4
  43. # NZCV: N:0 Z:0 C:0 V:0
  44. # x2: 0x0000000000000018
  45. 0x00007f1e00082cb8 54000466 b.vs #+0x8c (addr 0x7f1e00082d44)
  46. # And finally we place the result in x0.
  47. 0x00007f1e00082cbc aa0203e0 mov x0, x2
  48. # x0: 0x0000000000000018
  49. (...)
  50. 0x00007f1e00082cec d65f03c0 ret
  51. Hit and disabled a breakpoint at 0x7f1f880abd28.
  52. 0x00007f1f880abd28 f85e83b4 ldur x20, [fp, #-24]
  53. sim>

break $address { #break }

Inserts a breakpoint at the specified address.

Note that on 32-bit Arm, you can have only one breakpoint and you’ll need to disable write protection on code pages to insert it. The 64-bit Arm simulator does not have such restrictions.

With our example again:

  1. $ out/arm.debug/d8 --allow-natives-syntax \
  2. # This is useful to know which address to break to.
  3. --print-opt-code --print-opt-code-filter="add" \
  4. test.js
  5. (...)
  6. Simulator hit stop, breaking at the next instruction:
  7. 0x488c2e20 e24fc00c sub ip, pc, #12
  8. # Break on a known interesting address, where we start
  9. # loading 'a' and 'b'.
  10. sim> break 0x488c2e9c
  11. sim> continue
  12. 0x488c2e9c e59b200c ldr r2, [fp, #+12]
  13. # We can look-ahead with 'disasm'.
  14. sim> disasm 10
  15. 0x488c2e9c e59b200c ldr r2, [fp, #+12]
  16. 0x488c2ea0 e3120001 tst r2, #1
  17. 0x488c2ea4 1a000037 bne +228 -> 0x488c2f88
  18. 0x488c2ea8 e59b3008 ldr r3, [fp, #+8]
  19. 0x488c2eac e3130001 tst r3, #1
  20. 0x488c2eb0 1a000037 bne +228 -> 0x488c2f94
  21. 0x488c2eb4 e1a040c2 mov r4, r2, asr #1
  22. 0x488c2eb8 e09440c3 adds r4, r4, r3, asr #1
  23. 0x488c2ebc 6a000037 bvs +228 -> 0x488c2fa0
  24. 0x488c2ec0 e0942004 adds r2, r4, r4
  25. # And try and break on the result of the first `adds` instructions.
  26. sim> break 0x488c2ebc
  27. setting breakpoint failed
  28. # Ah, we need to delete the breakpoint first.
  29. sim> del
  30. sim> break 0x488c2ebc
  31. sim> cont
  32. 0x488c2ebc 6a000037 bvs +228 -> 0x488c2fa0
  33. sim> print r4
  34. r4: 0x0000000c 12
  35. # That's 5 + 7 == 12, all good!

Generated breakpoint instuctions with a few additional features { #extra }

Instead of TurboAssembler::DebugBreak(), you may use a lower-level instruction which has the same effect except with additional features.

stop() (32-bit Arm) { #arm32_stop }

  1. Assembler::stop(Condition cond = al, int32_t code = kDefaultStopCode);

The first argument is the condition and the second is the stop code. If a code is specified, and is less than 256, the stop is said to be “watched”, and can be disabled/enabled; a counter also keeps track of how many times the Simulator hits this code.

Imagine we are working on this V8 C++ code:

  1. __ stop(al, 123);
  2. __ mov(r0, r0);
  3. __ mov(r0, r0);
  4. __ mov(r0, r0);
  5. __ mov(r0, r0);
  6. __ mov(r0, r0);
  7. __ stop(al, 0x1);
  8. __ mov(r1, r1);
  9. __ mov(r1, r1);
  10. __ mov(r1, r1);
  11. __ mov(r1, r1);
  12. __ mov(r1, r1);

Here’s a sample debugging session:

We hit the first stop.

  1. Simulator hit stop 123, breaking at the next instruction:
  2. 0xb53559e8 e1a00000 mov r0, r0

We can see the following stop using disasm.

  1. sim> disasm
  2. 0xb53559e8 e1a00000 mov r0, r0
  3. 0xb53559ec e1a00000 mov r0, r0
  4. 0xb53559f0 e1a00000 mov r0, r0
  5. 0xb53559f4 e1a00000 mov r0, r0
  6. 0xb53559f8 e1a00000 mov r0, r0
  7. 0xb53559fc ef800001 stop 1 - 0x1
  8. 0xb5355a00 e1a00000 mov r1, r1
  9. 0xb5355a04 e1a00000 mov r1, r1
  10. 0xb5355a08 e1a00000 mov r1, r1

Information can be printed for all (watched) stops which were hit at least once.

  1. sim> stop info all
  2. Stop information:
  3. stop 123 - 0x7b: Enabled, counter = 1
  4. sim> cont
  5. Simulator hit stop 1, breaking at the next instruction:
  6. 0xb5355a04 e1a00000 mov r1, r1
  7. sim> stop info all
  8. Stop information:
  9. stop 1 - 0x1: Enabled, counter = 1
  10. stop 123 - 0x7b: Enabled, counter = 1

Stops can be disabled or enabled. (Only available for watched stops.)

  1. sim> stop disable 1
  2. sim> cont
  3. Simulator hit stop 123, breaking at the next instruction:
  4. 0xb5356808 e1a00000 mov r0, r0
  5. sim> cont
  6. Simulator hit stop 123, breaking at the next instruction:
  7. 0xb5356c28 e1a00000 mov r0, r0
  8. sim> stop info all
  9. Stop information:
  10. stop 1 - 0x1: Disabled, counter = 2
  11. stop 123 - 0x7b: Enabled, counter = 3
  12. sim> stop enable 1
  13. sim> cont
  14. Simulator hit stop 1, breaking at the next instruction:
  15. 0xb5356c44 e1a00000 mov r1, r1
  16. sim> stop disable all
  17. sim> con

Debug() (64-bit Arm) { #arm64_debug }

  1. MacroAssembler::Debug(const char* message, uint32_t code, Instr params = BREAK);

This instruction is a breakpoint by default, but is also able to enable and disable tracing as if you had done it with the trace command in the debugger. You can also give it a message and a code as an identifier.

Imagine we are working on this V8 C++ code, taken from the native builtin that prepares the frame to call a JS function.

  1. int64_t bad_frame_pointer = -1L; // Bad frame pointer, should fail if it is used.
  2. __ Mov(x13, bad_frame_pointer);
  3. __ Mov(x12, StackFrame::TypeToMarker(type));
  4. __ Mov(x11, ExternalReference::Create(IsolateAddressId::kCEntryFPAddress,
  5. masm->isolate()));
  6. __ Ldr(x10, MemOperand(x11));
  7. __ Push(x13, x12, xzr, x10);

It might be useful to insert a breakpoint with DebugBreak() so we can examine the current state when we run this. But we can go further and trace this code if we use Debug() instead:

  1. // Start tracing and log disassembly and register values.
  2. __ Debug("start tracing", 42, TRACE_ENABLE | LOG_ALL);
  3. int64_t bad_frame_pointer = -1L; // Bad frame pointer, should fail if it is used.
  4. __ Mov(x13, bad_frame_pointer);
  5. __ Mov(x12, StackFrame::TypeToMarker(type));
  6. __ Mov(x11, ExternalReference::Create(IsolateAddressId::kCEntryFPAddress,
  7. masm->isolate()));
  8. __ Ldr(x10, MemOperand(x11));
  9. __ Push(x13, x12, xzr, x10);
  10. // Stop tracing.
  11. __ Debug("stop tracing", 42, TRACE_DISABLE);

It allows us to trace register values for just the snippet of code we’re working on:

  1. $ d8 --allow-natives-syntax --debug-sim test.js
  2. # NZCV: N:0 Z:0 C:0 V:0
  3. # FPCR: AHP:0 DN:0 FZ:0 RMode:0b00 (Round to Nearest)
  4. # x0: 0x00007fbf00000000
  5. # x1: 0x00007fbf0804030d
  6. # x2: 0x00007fbf082500e1
  7. (...)
  8. 0x00007fc039d31cb0 9280000d movn x13, #0x0
  9. # x13: 0xffffffffffffffff
  10. 0x00007fc039d31cb4 d280004c movz x12, #0x2
  11. # x12: 0x0000000000000002
  12. 0x00007fc039d31cb8 d2864110 movz x16, #0x3208
  13. # ip0: 0x0000000000003208
  14. 0x00007fc039d31cbc 8b10034b add x11, x26, x16
  15. # x11: 0x00007fbf00003208
  16. 0x00007fc039d31cc0 f940016a ldr x10, [x11]
  17. # x10: 0x0000000000000000 <- 0x00007fbf00003208
  18. 0x00007fc039d31cc4 a9be7fea stp x10, xzr, [sp, #-32]!
  19. # sp: 0x00007fc033e81340
  20. # x10: 0x0000000000000000 -> 0x00007fc033e81340
  21. # xzr: 0x0000000000000000 -> 0x00007fc033e81348
  22. 0x00007fc039d31cc8 a90137ec stp x12, x13, [sp, #16]
  23. # x12: 0x0000000000000002 -> 0x00007fc033e81350
  24. # x13: 0xffffffffffffffff -> 0x00007fc033e81358
  25. 0x00007fc039d31ccc 910063fd add fp, sp, #0x18 (24)
  26. # fp: 0x00007fc033e81358
  27. 0x00007fc039d31cd0 d45bd600 hlt #0xdeb0