シミュレータを用いたArmデバッグ
V8のコード生成に取り組む際に、シミュレータとデバッガは非常に役立ちます。
- 実際のハードウェアにアクセスすることなく、コード生成をテストできるため便利です。
- クロスコンパイルやネイティブコンパイルは必要ありません。クロスコンパイル
- シミュレータは、生成されたコードのデバッグを完全にサポートしています。
このシミュレータはV8の目的のために設計されていることに注意してください。V8で使用される機能のみが実装されており、実装されていない機能や命令に遭遇する可能性があります。その場合は、自由に実装してコードを送信してください!
シミュレータを使用したArm向けコンパイル #
x86ホストではデフォルトで、gmを使用したArm向けコンパイルにより、シミュレータビルドが生成されます。
gm arm64.debug # For a 64-bit build or...
gm arm.debug # ... for a 32-bit build.
debug
構成は、特にV8テストスイートを実行する場合、少し遅くなる可能性があるため、optdebug
構成をビルドすることもできます。
デバッガの起動 #
n
命令後にコマンドラインからデバッガを直接起動できます。
out/arm64.debug/d8 --stop_sim_at <n> # Or out/arm.debug/d8 for a 32-bit build.
あるいは、生成されたコードにブレークポイント命令を生成することもできます。
ネイティブでは、ブレークポイント命令により、SIGTRAP
シグナルでプログラムが停止し、gdbを使用して問題をデバッグできます。ただし、シミュレータで実行している場合、生成されたコード内のブレークポイント命令は、代わりにシミュレータデバッガに制御を渡します。
Torque、CodeStubAssembler、TurboFanパス内のノードとして、またはアセンブラを直接使用して、複数の方法でブレークポイントを生成できます。
ここでは、低レベルのネイティブコードのデバッグに焦点を当てているため、アセンブラの方法を見てみましょう。
TurboAssembler::DebugBreak();
TurboFanでコンパイルされたadd
というJITコンパイルされた関数があるとします。そして、その開始時にブレークポイントを設定したいとします。test.js
の例を考えます。
// Our optimized function.
function add(a, b) {
return a + b;
}
// Typical cheat code enabled by --allow-natives-syntax.
%PrepareFunctionForOptimization(add);
// Give the optimizing compiler type feedback so it'll speculate `a` and `b` are
// numbers.
add(1, 3);
// And force it to optimize.
%OptimizeFunctionOnNextCall(add);
add(5, 7);
そのためには、TurboFanのコードジェネレータにフックし、アセンブラにアクセスしてブレークポイントを挿入します。
void CodeGenerator::AssembleCode() {
// ...
// Check if we're optimizing, then look-up the name of the current function and
// insert a breakpoint.
if (info->IsOptimizing()) {
AllowHandleDereference allow_handle_dereference;
if (info->shared_info()->PassesFilter("add")) {
tasm()->DebugBreak();
}
}
// ...
}
そして、実行してみましょう。
$ d8 \
# Enable '%' cheat code JS functions.
--allow-natives-syntax \
# Disassemble our function.
--print-opt-code --print-opt-code-filter="add" --code-comments \
# Disable spectre mitigations for readability.
--no-untrusted-code-mitigations \
test.js
--- Raw source ---
(a, b) {
return a + b;
}
--- Optimized code ---
optimization_id = 0
source_position = 12
kind = OPTIMIZED_FUNCTION
name = add
stack_slots = 6
compiler = turbofan
address = 0x7f0900082ba1
Instructions (size = 504)
0x7f0900082be0 0 d45bd600 constant pool begin (num_const = 6)
0x7f0900082be4 4 00000000 constant
0x7f0900082be8 8 00000001 constant
0x7f0900082bec c 75626544 constant
0x7f0900082bf0 10 65724267 constant
0x7f0900082bf4 14 00006b61 constant
0x7f0900082bf8 18 d45bd7e0 constant
-- Prologue: check code start register --
0x7f0900082bfc 1c 10ffff30 adr x16, #-0x1c (addr 0x7f0900082be0)
0x7f0900082c00 20 eb02021f cmp x16, x2
0x7f0900082c04 24 54000080 b.eq #+0x10 (addr 0x7f0900082c14)
Abort message:
Wrong value in code start register passed
0x7f0900082c08 28 d2800d01 movz x1, #0x68
-- Inlined Trampoline to Abort --
0x7f0900082c0c 2c 58000d70 ldr x16, pc+428 (addr 0x00007f0900082db8) ;; off heap target
0x7f0900082c10 30 d63f0200 blr x16
-- Prologue: check for deoptimization --
[ DecompressTaggedPointer
0x7f0900082c14 34 b85d0050 ldur w16, [x2, #-48]
0x7f0900082c18 38 8b100350 add x16, x26, x16
]
0x7f0900082c1c 3c b8407210 ldur w16, [x16, #7]
0x7f0900082c20 40 36000070 tbz w16, #0, #+0xc (addr 0x7f0900082c2c)
-- Inlined Trampoline to CompileLazyDeoptimizedCode --
0x7f0900082c24 44 58000c31 ldr x17, pc+388 (addr 0x00007f0900082da8) ;; off heap target
0x7f0900082c28 48 d61f0220 br x17
-- B0 start (construct frame) --
(...)
--- End code ---
# Debugger hit 0: DebugBreak
0x00007f0900082bfc 10ffff30 adr x16, #-0x1c (addr 0x7f0900082be0)
sim>
最適化された関数の開始時に停止し、シミュレータからプロンプトが表示されたことがわかります!
これは単なる例であり、V8は急速に変化するため、詳細は異なる場合があります。ただし、アセンブラが使用可能な場所であればどこでも、これを行うことができるはずです。
デバッグコマンド #
一般的なコマンド #
デバッガのプロンプトにhelp
と入力すると、使用可能なコマンドの詳細が表示されます。これには、stepi
、cont
、disasm
など、一般的なgdbのようなコマンドが含まれます。シミュレータがgdbで実行されている場合、gdb
デバッガコマンドはgdbに制御を渡します。その後、gdbからcont
を使用してデバッガに戻ることができます。
アーキテクチャ固有のコマンド #
各ターゲットアーキテクチャは独自のシミュレータとデバッガを実装しているため、エクスペリエンスと詳細は異なります。
printobject $register
(別名po
) #
レジスタに保持されているJSオブジェクトを記述します。
たとえば、今回は32ビットArmシミュレータビルドで例を実行しているとします。レジスタに渡された入力引数を調べることができます。
$ ./out/arm.debug/d8 --allow-natives-syntax test.js
Simulator hit stop, breaking at the next instruction:
0x26842e24 e24fc00c sub ip, pc, #12
sim> print r1
r1: 0x4b60ffb1 1264648113
# The current function object is passed with r1.
sim> printobject r1
r1:
0x4b60ffb1: [Function] in OldSpace
- map: 0x485801f9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x4b6010f1 <JSFunction (sfi = 0x42404e99)>
- elements: 0x5b700661 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype:
- initial_map:
- shared_info: 0x4b60fe9d <SharedFunctionInfo add>
- name: 0x5b701c5d <String[#3]: add>
- formal_parameter_count: 2
- kind: NormalFunction
- context: 0x4b600c65 <NativeContext[261]>
- code: 0x26842de1 <Code OPTIMIZED_FUNCTION>
- source code: (a, b) {
return a + b;
}
(...)
# Now print the current JS context passed in r7.
sim> printobject r7
r7:
0x449c0c65: [NativeContext] in OldSpace
- map: 0x561000b9 <Map>
- length: 261
- scope_info: 0x34081341 <ScopeInfo SCRIPT_SCOPE [5]>
- previous: 0
- native_context: 0x449c0c65 <NativeContext[261]>
0: 0x34081341 <ScopeInfo SCRIPT_SCOPE [5]>
1: 0
2: 0x449cdaf5 <JSObject>
3: 0x58480c25 <JSGlobal Object>
4: 0x58485499 <Other heap object (EMBEDDER_DATA_ARRAY_TYPE)>
5: 0x561018a1 <Map(HOLEY_ELEMENTS)>
6: 0x3408027d <undefined>
7: 0x449c75c1 <JSFunction ArrayBuffer (sfi = 0x4be8ade1)>
8: 0x561010f9 <Map(HOLEY_ELEMENTS)>
9: 0x449c967d <JSFunction arrayBufferConstructor_DoNotInitialize (sfi = 0x4be8c3ed)>
10: 0x449c8dbd <JSFunction Array (sfi = 0x4be8be59)>
(...)
trace
(別名t
) #
実行された命令のトレースを有効または無効にします。
有効にすると、シミュレータは実行中の逆アセンブルされた命令を出力します。64ビットArmビルドを実行している場合、シミュレータはレジスタ値の変更もトレースできます。
--trace-sim
フラグを使用してコマンドラインから有効にすることで、最初からトレースを有効にすることもできます。
同じ例で
$ out/arm64.debug/d8 --allow-natives-syntax \
# --debug-sim is required on 64-bit Arm to enable disassembly
# when tracing.
--debug-sim test.js
# Debugger hit 0: DebugBreak
0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (addr 0x7f1e00082be0)
sim> trace
0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (addr 0x7f1e00082be0)
Enabling disassembly, registers and memory write tracing
# Break on the return address stored in the lr register.
sim> break lr
Set a breakpoint at 0x7f1f880abd28
0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (addr 0x7f1e00082be0)
# Continuing will trace the function's execution until we return, allowing
# us to make sense of what is happening.
sim> continue
# x0: 0x00007f1e00082ba1
# x1: 0x00007f1e08250125
# x2: 0x00007f1e00082be0
(...)
# We first load the 'a' and 'b' arguments from the stack and check if they
# are tagged numbers. This is indicated by the least significant bit being 0.
0x00007f1e00082c90 f9401fe2 ldr x2, [sp, #56]
# x2: 0x000000000000000a <- 0x00007f1f821f0278
0x00007f1e00082c94 7200005f tst w2, #0x1
# NZCV: N:0 Z:1 C:0 V:0
0x00007f1e00082c98 54000ac1 b.ne #+0x158 (addr 0x7f1e00082df0)
0x00007f1e00082c9c f9401be3 ldr x3, [sp, #48]
# x3: 0x000000000000000e <- 0x00007f1f821f0270
0x00007f1e00082ca0 7200007f tst w3, #0x1
# NZCV: N:0 Z:1 C:0 V:0
0x00007f1e00082ca4 54000a81 b.ne #+0x150 (addr 0x7f1e00082df4)
# Then we untag and add 'a' and 'b' together.
0x00007f1e00082ca8 13017c44 asr w4, w2, #1
# x4: 0x0000000000000005
0x00007f1e00082cac 2b830484 adds w4, w4, w3, asr #1
# NZCV: N:0 Z:0 C:0 V:0
# x4: 0x000000000000000c
# That's 5 + 7 == 12, all good!
# Then we check for overflows and tag the result again.
0x00007f1e00082cb0 54000a46 b.vs #+0x148 (addr 0x7f1e00082df8)
0x00007f1e00082cb4 2b040082 adds w2, w4, w4
# NZCV: N:0 Z:0 C:0 V:0
# x2: 0x0000000000000018
0x00007f1e00082cb8 54000466 b.vs #+0x8c (addr 0x7f1e00082d44)
# And finally we place the result in x0.
0x00007f1e00082cbc aa0203e0 mov x0, x2
# x0: 0x0000000000000018
(...)
0x00007f1e00082cec d65f03c0 ret
Hit and disabled a breakpoint at 0x7f1f880abd28.
0x00007f1f880abd28 f85e83b4 ldur x20, [fp, #-24]
sim>
break $address
#
指定されたアドレスにブレークポイントを挿入します。
32ビットArmでは、ブレークポイントを1つしか設定できず、挿入するにはコードページの書き込み保護を無効にする必要があることに注意してください。64ビットArmシミュレータにはそのような制限はありません。
再び例で
$ out/arm.debug/d8 --allow-natives-syntax \
# This is useful to know which address to break to.
--print-opt-code --print-opt-code-filter="add" \
test.js
(...)
Simulator hit stop, breaking at the next instruction:
0x488c2e20 e24fc00c sub ip, pc, #12
# Break on a known interesting address, where we start
# loading 'a' and 'b'.
sim> break 0x488c2e9c
sim> continue
0x488c2e9c e59b200c ldr r2, [fp, #+12]
# We can look-ahead with 'disasm'.
sim> disasm 10
0x488c2e9c e59b200c ldr r2, [fp, #+12]
0x488c2ea0 e3120001 tst r2, #1
0x488c2ea4 1a000037 bne +228 -> 0x488c2f88
0x488c2ea8 e59b3008 ldr r3, [fp, #+8]
0x488c2eac e3130001 tst r3, #1
0x488c2eb0 1a000037 bne +228 -> 0x488c2f94
0x488c2eb4 e1a040c2 mov r4, r2, asr #1
0x488c2eb8 e09440c3 adds r4, r4, r3, asr #1
0x488c2ebc 6a000037 bvs +228 -> 0x488c2fa0
0x488c2ec0 e0942004 adds r2, r4, r4
# And try and break on the result of the first `adds` instructions.
sim> break 0x488c2ebc
setting breakpoint failed
# Ah, we need to delete the breakpoint first.
sim> del
sim> break 0x488c2ebc
sim> cont
0x488c2ebc 6a000037 bvs +228 -> 0x488c2fa0
sim> print r4
r4: 0x0000000c 12
# That's 5 + 7 == 12, all good!
いくつかの追加機能を備えた生成されたブレークポイント命令 #
TurboAssembler::DebugBreak()
の代わりに、追加機能を備えている点を除いて同じ効果を持つ低レベルの命令を使用できます。
stop()
(32ビットArm) #
Assembler::stop(Condition cond = al, int32_t code = kDefaultStopCode);
最初の引数は条件、2番目の引数は停止コードです。コードが指定され、256未満の場合、停止は「監視対象」と呼ばれ、無効化/有効化できます。また、カウンタはシミュレータがこのコードに何回ヒットしたかを追跡します。
このV8 C++コードに取り組んでいるとします。
__ stop(al, 123);
__ mov(r0, r0);
__ mov(r0, r0);
__ mov(r0, r0);
__ mov(r0, r0);
__ mov(r0, r0);
__ stop(al, 0x1);
__ mov(r1, r1);
__ mov(r1, r1);
__ mov(r1, r1);
__ mov(r1, r1);
__ mov(r1, r1);
デバッグセッションの例を次に示します。
最初の停止にヒットしました。
Simulator hit stop 123, breaking at the next instruction:
0xb53559e8 e1a00000 mov r0, r0
disasm
を使用して、次の停止を確認できます。
sim> disasm
0xb53559e8 e1a00000 mov r0, r0
0xb53559ec e1a00000 mov r0, r0
0xb53559f0 e1a00000 mov r0, r0
0xb53559f4 e1a00000 mov r0, r0
0xb53559f8 e1a00000 mov r0, r0
0xb53559fc ef800001 stop 1 - 0x1
0xb5355a00 e1a00000 mov r1, r1
0xb5355a04 e1a00000 mov r1, r1
0xb5355a08 e1a00000 mov r1, r1
少なくとも1回ヒットしたすべての(監視対象の)停止に関する情報を表示できます。
sim> stop info all
Stop information:
stop 123 - 0x7b: Enabled, counter = 1
sim> cont
Simulator hit stop 1, breaking at the next instruction:
0xb5355a04 e1a00000 mov r1, r1
sim> stop info all
Stop information:
stop 1 - 0x1: Enabled, counter = 1
stop 123 - 0x7b: Enabled, counter = 1
停止を無効化または有効化できます。(監視対象の停止でのみ使用可能です。)
sim> stop disable 1
sim> cont
Simulator hit stop 123, breaking at the next instruction:
0xb5356808 e1a00000 mov r0, r0
sim> cont
Simulator hit stop 123, breaking at the next instruction:
0xb5356c28 e1a00000 mov r0, r0
sim> stop info all
Stop information:
stop 1 - 0x1: Disabled, counter = 2
stop 123 - 0x7b: Enabled, counter = 3
sim> stop enable 1
sim> cont
Simulator hit stop 1, breaking at the next instruction:
0xb5356c44 e1a00000 mov r1, r1
sim> stop disable all
sim> con
Debug()
(64ビットArm) #
MacroAssembler::Debug(const char* message, uint32_t code, Instr params = BREAK);
この命令はデフォルトでブレークポイントですが、デバッガでtrace
コマンドを使用した場合と同様に、トレースを有効化および無効化することもできます。メッセージとコードを識別子として指定することもできます。
JS関数を呼び出すフレームを準備するネイティブビルトインから取得した、このV8 C++コードに取り組んでいるとします。
int64_t bad_frame_pointer = -1L; // Bad frame pointer, should fail if it is used.
__ Mov(x13, bad_frame_pointer);
__ Mov(x12, StackFrame::TypeToMarker(type));
__ Mov(x11, ExternalReference::Create(IsolateAddressId::kCEntryFPAddress,
masm->isolate()));
__ Ldr(x10, MemOperand(x11));
__ Push(x13, x12, xzr, x10);
DebugBreak()
を使用してブレークポイントを挿入し、実行時に現在の状態を調べることが役立つ場合があります。しかし、代わりにDebug()
を使用すると、このコードをさらにトレースできます。
// Start tracing and log disassembly and register values.
__ Debug("start tracing", 42, TRACE_ENABLE | LOG_ALL);
int64_t bad_frame_pointer = -1L; // Bad frame pointer, should fail if it is used.
__ Mov(x13, bad_frame_pointer);
__ Mov(x12, StackFrame::TypeToMarker(type));
__ Mov(x11, ExternalReference::Create(IsolateAddressId::kCEntryFPAddress,
masm->isolate()));
__ Ldr(x10, MemOperand(x11));
__ Push(x13, x12, xzr, x10);
// Stop tracing.
__ Debug("stop tracing", 42, TRACE_DISABLE);
これにより、作業中のコードスニペットのみのレジスタ値をトレースできます。
$ d8 --allow-natives-syntax --debug-sim test.js
# NZCV: N:0 Z:0 C:0 V:0
# FPCR: AHP:0 DN:0 FZ:0 RMode:0b00 (Round to Nearest)
# x0: 0x00007fbf00000000
# x1: 0x00007fbf0804030d
# x2: 0x00007fbf082500e1
(...)
0x00007fc039d31cb0 9280000d movn x13, #0x0
# x13: 0xffffffffffffffff
0x00007fc039d31cb4 d280004c movz x12, #0x2
# x12: 0x0000000000000002
0x00007fc039d31cb8 d2864110 movz x16, #0x3208
# ip0: 0x0000000000003208
0x00007fc039d31cbc 8b10034b add x11, x26, x16
# x11: 0x00007fbf00003208
0x00007fc039d31cc0 f940016a ldr x10, [x11]
# x10: 0x0000000000000000 <- 0x00007fbf00003208
0x00007fc039d31cc4 a9be7fea stp x10, xzr, [sp, #-32]!
# sp: 0x00007fc033e81340
# x10: 0x0000000000000000 -> 0x00007fc033e81340
# xzr: 0x0000000000000000 -> 0x00007fc033e81348
0x00007fc039d31cc8 a90137ec stp x12, x13, [sp, #16]
# x12: 0x0000000000000002 -> 0x00007fc033e81350
# x13: 0xffffffffffffffff -> 0x00007fc033e81358
0x00007fc039d31ccc 910063fd add fp, sp, #0x18 (24)
# fp: 0x00007fc033e81358
0x00007fc039d31cd0 d45bd600 hlt #0xdeb0