Отладка Arm с использованием симулятора
Симулятор и отладчик могут быть очень полезны при работе с генерацией кода V8.
- Это удобно, так как позволяет тестировать генерацию кода без доступа к реальному оборудованию.
- Нет необходимости в кросс-компиляции или компиляции для родной архитектуры.
- Симулятор полностью поддерживает отладку сгенерированного кода.
Обратите внимание, что этот симулятор разработан для целей V8. Реализованы только функции, используемые V8, и вы можете столкнуться с не реализованными функциями или инструкциями. В этом случае вы можете реализовать их и отправить код!
Компиляция для Arm с использованием симулятора
По умолчанию на хосте x86 компиляция для Arm с помощью gm создаст сборку симулятора:
gm arm64.debug # Для 64-битной сборки или...
gm arm.debug # ...для 32-битной сборки.
Вы также можете собрать конфигурацию optdebug
, так как debug
может работать немного медленно, особенно если вы хотите запустить набор тестов V8.
Запуск отладчика
Вы можете запустить отладчик сразу из командной строки после выполнения n
инструкций:
out/arm64.debug/d8 --stop_sim_at <n> # Или out/arm.debug/d8 для 32-битной сборки.
Кроме того, вы можете сгенерировать команду точки останова в сгенерированном коде:
На родной архитектуре команды точки останова вызывают останов программы с сигналом SIGTRAP
, позволяя вам отладить проблему с помощью gdb. Однако, если используется симулятор, команда точки останова в сгенерированном коде вместо этого перенесет вас в отладчик симулятора.
Вы можете создать точку останова несколькими способами, используя DebugBreak()
из Torque, из CodeStubAssembler, как узел в проходе TurboFan или напрямую с использованием ассемблера.
Здесь мы сосредоточимся на отладке низкоуровневого родного кода, поэтому давайте рассмотрим метод с использованием ассемблера:
TurboAssembler::DebugBreak();
Предположим, у нас есть функция, скомпилированная с помощью TurboFan, называемая add
, и мы хотим установить точку останова в самом начале. Возьмем пример test.js
:
// Наша оптимизированная функция.
function add(a, b) {
return a + b;
}
// Типичный чит-код, включенный через --allow-natives-syntax.
%PrepareFunctionForOptimization(add);
// Даем оптимизирующему компилятору обратную связь по типам, чтобы он предположил, что `a` и `b`
// являются числами.
add(1, 3);
// И заставляем его выполнить оптимизацию.
%OptimizeFunctionOnNextCall(add);
add(5, 7);
Для выполнения этого мы можем подключиться к генератору кода TurboFan и получить доступ к ассемблеру для вставки нашей точки останова:
void CodeGenerator::AssembleCode() {
// ...
// Проверяем, выполняется ли оптимизация, затем ищем имя текущей функции и
// вставляем точку останова.
if (info->IsOptimizing()) {
AllowHandleDereference allow_handle_dereference;
if (info->shared_info()->PassesFilter("add")) {
tasm()->DebugBreak();
}
}
// ...
}
И запускаем:
$ d8 \
# Включаем чит-код JS функций с `%`.
--allow-natives-syntax \
# Дизассемблируем нашу функцию.
--print-opt-code --print-opt-code-filter="add" --code-comments \
# Отключаем меры против Spectre для читаемости.
--no-untrusted-code-mitigations \
test.js
--- Исходный код ---
(a, b) {
return a + b;
}
--- Оптимизированный код ---
optimization_id = 0
source_position = 12
kind = OPTIMIZED_FUNCTION
name = add
stack_slots = 6
compiler = turbofan
address = 0x7f0900082ba1
Инструкции (размер = 504)
0x7f0900082be0 0 d45bd600 начальный пул констант (num_const = 6)
0x7f0900082be4 4 00000000 константа
0x7f0900082be8 8 00000001 константа
0x7f0900082bec c 75626544 константа
0x7f0900082bf0 10 65724267 константа
0x7f0900082bf4 14 00006b61 константа
0x7f0900082bf8 18 d45bd7e0 константа
-- Пролог: проверка регистра начала кода --
0x7f0900082bfc 1c 10ffff30 adr x16, #-0x1c (адрес 0x7f0900082be0)
0x7f0900082c00 20 eb02021f cmp x16, x2
0x7f0900082c04 24 54000080 b.eq #+0x10 (адрес 0x7f0900082c14)
Сообщение об отмене:
Неверное значение в регистре начала кода
0x7f0900082c08 28 d2800d01 movz x1, #0x68
-- Встроенный Трамплин для отмены --
0x7f0900082c0c 2c 58000d70 ldr x16, pc+428 (адрес 0x00007f0900082db8) ;; цель вне куча
0x7f0900082c10 30 d63f0200 blr x16
-- Пролог: проверка на деоптимизацию --
[ Декодирование сжатого указателя
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 (адрес 0x7f0900082c2c)
-- Встроенный траплин для CompileLazyDeoptimizedCode --
0x7f0900082c24 44 58000c31 ldr x17, pc+388 (адрес 0x00007f0900082da8) ;; вне кучи
0x7f0900082c28 48 d61f0220 br x17
-- Начало B0 (создание кадра) --
(...)
--- Конец кода ---
# Опция отладчика 0: DebugBreak
0x00007f0900082bfc 10ffff30 adr x16, #-0x1c (адрес 0x7f0900082be0)
sim>
Мы видим, что остановились в начале оптимизированной функции, и эмулятор выдал нам приглашение!
Обратите внимание, что это просто пример, и V8 часто обновляется, поэтому детали могут различаться. Но вы сможете использовать это там, где доступен ассемблер.
Команды для отладки
Общие команды
Введите help
в командной строке отладчика, чтобы получить подробности о доступных командах. Это включает обычные команды, похожие на gdb, такие как stepi
, cont
, disasm
и другие. Если эмулятор запускается под gdb, команда отладчика gdb
передаст управление gdb. Затем вы можете использовать cont
в gdb, чтобы вернуться к отладчику.
Команды, зависящие от архитектуры
Каждая целевая архитектура реализует свой эмулятор и отладчик, поэтому опыт и детали могут различаться.
printobject $register
(алиас po
)
Описание объекта JS, хранящегося в регистре.
Например, предположим, что на этот раз мы запускаем наш пример на сборке эмулятора Arm на 32 бита. Мы можем исследовать переданные аргументы, находящиеся в регистрах:
$ ./out/arm.debug/d8 --allow-natives-syntax test.js
Эмулятор остановился, прерывание на следующей инструкции:
0x26842e24 e24fc00c sub ip, pc, #12
sim> print r1
r1: 0x4b60ffb1 1264648113
# Текущий объект функции передается через 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;
}
(...)
# Теперь выведем текущий контекст JS, переданный через 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
)
Включает или отключает трассировку исполняемых инструкций.
Когда включено, эмулятор будет выводить дизассемблированные инструкции во время их выполнения. Если вы работаете со сборкой Arm на 64 бита, эмулятор также может трассировать изменения значений регистров.
Вы также можете включить это с помощью флага --trace-sim
в командной строке, чтобы активировать трассировку с самого начала.
Используя тот же пример:
$ out/arm64.debug/d8 --allow-natives-syntax \
# --debug-sim необходим для Arm на 64 бита для включения дизассемблирования
# при трассировке.
--debug-sim test.js
# Опция отладчика 0: DebugBreak
0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (адрес 0x7f1e00082be0)
sim> trace
0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (адрес 0x7f1e00082be0)
Включение вывода дизассемблирования, регистров и записи в память
# Установите точку останова на адрес возврата, хранящийся в регистре lr.
sim> break lr
Установлена точка останова на 0x7f1f880abd28
0x00007f1e00082bfc 10ffff30 adr x16, #-0x1c (адрес 0x7f1e00082be0)
# Продолжение позволит трассировать выполнение функции до возврата,
# что поможет понять, что происходит.
sim> continue
# x0: 0x00007f1e00082ba1
# x1: 0x00007f1e08250125
# x2: 0x00007f1e00082be0
(...)
# Сначала загружаются аргументы 'a' и 'b' из стека и проверяется,
# являются ли они числами с меткой. Это указывается младшим значимым битом, равным 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)
# Затем мы снимаем отметки и складываем 'a' и 'b' вместе.
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
# Это 5 + 7 == 12, всё верно!
# Затем мы проверяем на переполнение и снова отмечаем результат.
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)
# И наконец, помещаем результат в x0.
0x00007f1e00082cbc aa0203e0 mov x0, x2
# x0: 0x0000000000000018
(...)
0x00007f1e00082cec d65f03c0 ret
Остановлен и отключен брейкпоинт по адресу 0x7f1f880abd28.
0x00007f1f880abd28 f85e83b4 ldur x20, [fp, #-24]
sim>
break $address
Вставляет брейкпоинт по указанному адресу.
Обратите внимание, что на 32-битной Arm можно поставить только один брейкпоинт, и для его установки потребуется отключить защиту записи на кодовые страницы. 64-битный симулятор Arm таких ограничений не имеет.
С нашим примером снова:
$ out/arm.debug/d8 --allow-natives-syntax \
# Полезно знать, на какой адрес поставить брейкпоинт.
--print-opt-code --print-opt-code-filter="add" \
test.js
(...)
Симулятор остановлен, останавливаемся на следующей инструкции:
0x488c2e20 e24fc00c sub ip, pc, #12
# Ставим брейкпоинт на известный интересный адрес, где начинаем
# загрузку 'a' и 'b'.
sim> break 0x488c2e9c
sim> continue
0x488c2e9c e59b200c ldr r2, [fp, #+12]
# Мы можем посмотреть дальше с помощью '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
# И попробуем остановиться на результате первых инструкций `adds`.
sim> break 0x488c2ebc
установка брейкпоинта не удалась
# Ах, нужно сначала удалить предыдущий брейкпоинт.
sim> del
sim> break 0x488c2ebc
sim> cont
0x488c2ebc 6a000037 bvs +228 -> 0x488c2fa0
sim> print r4
r4: 0x0000000c 12
# Это 5 + 7 == 12, всё верно!
Сгенерированные инструкции для брейкпоинтов с добавлением некоторых дополнительных функций
Вместо TurboAssembler::DebugBreak()
можно использовать низкоуровневую инструкцию, которая имеет тот же эффект, но с дополнительными функциями.
stop()
(32-бит Arm)
Assembler::stop(Condition cond = al, int32_t code = kDefaultStopCode);
Первый аргумент — это условие, а второй — код остановки. Если задан код меньше 256, остановка считается «наблюдаемой», и её можно отключать/включать; также ведётся учёт количества раз, когда симулятор попал на этот код.
Представьте, что мы работаем над следующим C++ кодом для V8:
__ 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);
Вот пример сессии отладки:
Мы попадаем на первую остановку.
Симулятор остановлен на 123, останавливаемся на следующей инструкции:
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
Можно вывести информацию обо всех (наблюдаемых) остановках, которые были достигнуты хотя бы один раз.
sim> stop info all
Информация об остановках:
остановка 123 - 0x7b: Включена, счётчик = 1
sim> cont
Симулятор остановлен на 1, останавливаемся на следующей инструкции:
0xb5355a04 e1a00000 mov r1, r1
sim> stop info all
Информация об остановках:
остановка 1 - 0x1: Включена, счётчик = 1
остановка 123 - 0x7b: Включена, счётчик = 1
Остановки можно отключать или включать. (Доступно только для наблюдаемых остановок.)
sim> stop disable 1
sim> cont
Симулятор достиг остановки 123, прерывание на следующей инструкции:
0xb5356808 e1a00000 mov r0, r0
sim> cont
Симулятор достиг остановки 123, прерывание на следующей инструкции:
0xb5356c28 e1a00000 mov r0, r0
sim> stop info all
Информация об остановках:
остановка 1 - 0x1: Отключена, счетчик = 2
остановка 123 - 0x7b: Включена, счетчик = 3
sim> stop enable 1
sim> cont
Симулятор достиг остановки 1, прерывание на следующей инструкции:
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
в отладчике. Также можно передать сообщение и код в качестве идентификатора.
Представим, что мы работаем с этим кодом V8 C++, взятым из собственного встроенного модуля, который подготавливает фрейм для вызова функции JS.
int64_t bad_frame_pointer = -1L; // Неверный указатель на кадр, должна произойти ошибка, если он используется.
__ 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()
:
// Начать трассировку и журналирование дизассемблированного кода и значений регистров.
__ Debug("start tracing", 42, TRACE_ENABLE | LOG_ALL);
int64_t bad_frame_pointer = -1L; // Неверный указатель на кадр, должна произойти ошибка, если он используется.
__ 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);
// Остановить трассировку.
__ 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 (Округление до ближайшего)
# 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