Научиться дебаггить дампы .net core процессов на Linux при помощи lldb и SOS плагина оказалось очень приятным опытом. Всё-таки «дамп процесса» звучит как нечто крайне низкоуровневое, а тут можно и на управляемые потоки посмотреть, и в памяти покопаться. В общем, приятное колдунство. И между прочим, lldb плагины были не только для .NET. Я видел ещё и для Python, и для Java. Интересно, а есть ли что-нибудь похожее для NodeJS и JavaScript?
А то!. Есть плагин под названием llnode
, который как раз с NodeJS и работает. Сегодня я не буду копать в нём слишком уж глубоко, всё-таки со скриптом в последнее время практически не работаю. Но посмотреть, как это вообще делается, интересно. Так что приступим.
Устанавливаем инструменты
За отсутствием другого железа сегодня я буду работать на живом Маке и macOS, так что не будет ни докера, ни всяких apt-get
. NodeJS v8.9.4 тут уже установлен, так что всего-то осталось доставить llnode
и lldb
. Если бы макось не стремилась превратиться в новую Windows, то сделать это можно было бы всего в две команды:
- brew update && brew install --with-lldb --with-toolchain llvm для lldb, и
- sudo npm install -g llnode для llnode.
Но реальность — беспощадная штука, так что макось жаловалась на всё. «Пожалуйста, установите XCode», «Пожалуйста, настройте сертификат для кода», «Доступ запрещён». В конечном итоге мне удалось добыть всё необходимое, но llnode
целиком так и не поставился. Это не такая уж и проблема, потому что сам по себе llnode — это шелл скрипт, который запускает lldb и загружает в него llnode плагин. Файл плагина успешно установился, загружать его в lldb я умею, так что того, что есть, должно хватить.
Последним штрихом включим автоматическое создание дампов упавших процессов — ulimit -c unlimited
— и можно начинать.
Добываем дамп процесса
Теоретически, следующий кусок JavaScript должен обязательно упасть:
1 2 3 |
setTimeout(function delayedFailure () { throw new Error("Fail not really fast"); }, 500); |
А отправив его в NodeJS вот таким образом, можно и дамп получить в придачу:
1 2 3 4 5 6 7 8 9 |
node --abort-on-uncaught-exception throw.js #Uncaught Error: Fail not really fast # #FROM #Timeout.delayedFailure [as _onTimeout] (/Users/pav/Documents/llnode/throw.js:1:1) #ontimeout (timers.js:1:1) #tryOnTimeout (timers.js:1:1) #Timer.listOnTimeout (timers.js:1:1) #Illegal instruction: 4 (core dumped) |
Создание дампа занимает некоторое время, потому что даже для такого мелкого скрипта размер дампа занял аж 1.7 гига бесценного SSD пространства. Не самый большой дамп в моей карьере, но и не самый маленький.
1 2 3 |
ls -lh /cores #total 3652896 #-r-------- 1 pav admin 1.7G Feb 21 06:42 core.11787 |
Отлично. Дамп есть, полезем внутрь ковыряться.
Смотрим внутрь
Вообще говоря, последовательность действий точно такая же, как и с .NET Core — открываем дамп, загружаем плагин:
1 2 3 4 5 |
lldb `which node` -c /cores/core.11787 # target create "/usr/local/bin/node" --core "/cores/core.11787" # Core file '/cores/core.11787' (x86_64) was loaded # (lldb) plugin load /usr/local/opt/llnode/llnode.dylib # (lldb) |
Так же как и SOS, llnode
добавляет в lldb несколько своих команд, все из которых начинаются с v8
. Запомнить легко. Если просто ввести v8
, то можно посмотреть, какие команды для JavaScript есть в принципе:
- bt
- findjsinstances
- findobjects
- findrefs
- inspect
- nodeinfo
- source
bt
bt
— обычно самая первая команда, которой смотрят, на чём же упал процесс. Для NodeJS/JavaScript это не исключение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
(lldb) v8 bt * thread #1: tid = 0x0000, 0x0000000100b26fa2 node`v8::base::OS::Abort() + 18, stop reason = signal SIGSTOP * frame #0: 0x0000000100b26fa2 node`v8::base::OS::Abort() + 18 frame #1: 0x000000010064bc13 node`v8::internal::Isolate::Throw(v8::internal::Object*, v8::internal::MessageLocation*) + 835 frame #2: 0x00000001007f6bab node`v8::internal::Runtime_Throw(int, v8::internal::Object**, v8::internal::Isolate*) + 59 frame #3: 0x00003f456918463d <exit> frame #4: 0x00003f456928443a <stub> frame #5: 0x00003f456923f9ce delayedFailure(this=0x00002d7d26e09e91:<Object: Timeout>) at /Users/pav/Documents/llnode/throw.js:1:97 fn=0x00002d7d26e09da9 frame #6: 0x00003f45692744c9 <stub> frame #7: 0x00003f456923f9ce ontimeout(this=0x00002d7d19602311:<undefined>, 0x00002d7d26e09e91:<Object: Timeout>) at timers.js:90:18 fn=0x00002d7d01b23c29 frame #8: 0x00003f4569274ed2 <stub> frame #9: 0x00003f456923f9ce tryOnTimeout(this=0x00002d7d19602311:<undefined>, 0x00002d7d26e09e91:<Object: Timeout>, 0x00002d7d26e0a821:<Object: TimersList>) at timers.js:90:18 fn=0x00002d7d01b23b09 frame #10: 0x00003f4569275002 <stub> #.... |
И на этом вскрытие можно было бы завершать, потому что имя проблемной функции уже видно в предсмертном стеке — delayedFailure
. Но мы поковыряемся ещё немного.
Смотрим в исходный код
Ковыряясь в стеке упавшего процесса, можно, оказывается, запросить исходный код для его фреймов. Например, проблемная функция delayedFailure
была в пятом фрейме:
1 2 3 |
# ... frame #5: 0x00003f456923f9ce delayedFailure(this=0x00002d7d26e09e91:<Object: Timeout>) at /Users/pav/Documents/llnode/throw.js:1:97 fn=0x00002d7d26e09da9 # ... |
На него можно переключиться при помощи frame select
и посмотреть, из чего же эта она сделана:
1 2 3 4 5 6 |
(lldb) frame select 5 #frame #5: 0x00003f456923f9ce #-> 0x3f456923f9ce: movq -0x20(%rbp), %rbx # 0x3f456923f9d2: movl 0x2b(%rbx), %ebx # 0x3f456923f9d5: leave # 0x3f456923f9d6: popq %rcx |
Она сделана не только из ассемблера. Есть команда v8 source list
, которая должна вернуть JavaScript, и которая, если верить StackOverflow, раньше работала как часы. Но не сейчас.
1 2 |
(lldb) v8 source list # error: USAGE: v8 source list |
Теперь, чтобы она заработала, нужно подавать ей на вход ещё и адрес фрейма:
1 2 3 4 5 |
(lldb) v8 source list 0x00003f456923f9ce # 1 (function (exports, require, module, __filename, __dirname) { setTimeout(function delayedFailure () { # 2 throw new Error("Fail not really fast"); # 3 }, 500); # 4 |
Как посмотреть на JavaScript объекты
В нашей программулине никаких пользовательских объектов не было, так что можно было бы на этом и остановиться. Но сам NodeJS наверняка создал что-нибудь от себя, так что можно посмотреть на его творения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
(lldb) v8 findjsobjects # Instances Total Size Name # ---------- ---------- ---- # 1 24 AssertionError # 1 24 AsyncResource # 1 24 FastBuffer #... # 1 24 console #... # 53 3392 NativeModule # 331 10592 (Array) # 624 35048 Object # 6814 77256 (String) # ---------- ---------- # 7910 131976 |
Для найденных объектов можно поискать их экземпляры. Делается это при помощи команды findjsinstances
. Например, чтобы найти все console
:
1 2 |
(lldb) v8 findjsinstances console #0x00002d7dbce9b829:<Object: console> |
Найденные экземпляры можно препарировать ещё дальше. Для этого есть команда inspect
:
1 2 3 4 5 6 7 8 9 10 |
(lldb) v8 inspect 0x00002d7dbce9b829 # 0x00002d7dbce9b829:<Object: console properties { # .debug=<unknown field type>, # .error=<unknown field type>, # .info=<unknown field type>, # .log=<unknown field type>, # .warn=<unknown field type>, # .dir=<unknown field type>, # .dirxml=<unknown field type>, #... |
Оказывается, экземпляры JavaScript объектов сделаны из… свойств. Внезапно.
Мораль
В общем, что работать с .NET Core дампами, что с NodeJS — разницы практически никакой. Что там, что там — стеки, память, и т.п. Но SOS плагин для .NET Core, правда, нёс с собой больше команд. Для работы с тем же сборщиком мусора и Just-in-time компиляции, например. И то, и другое есть и в NodeJS/JavaScript, так что, видимо, диагностировать их нужно при помощи чего-то другого. Но для базовой работы с дампами llnode должно вполне хватить.