최종 업데이트 : 2013-10-21 20:15:46
첫 번째 예제. 디버깅은 어떻게 할 수 있을까?
- 분명 노드 커미터 애들도 이런식으로 외부 라이브러리를 만들어 쓸꺼다. 그런데 디버깅을 하는 방법이 콘솔출력 뿐이라고?? 아닐것 같다.
- 분명 자신들이 짠 라이브러리가 노드에서 어떻게 돌아기는지 확인하는 방법이 있을 것이다.
- 회사에서
jdb
를 통해 안드로이드 apk를 소스없이 신나게 디버깅 했기에, 다른 디버거들에 대한 호기심이 증폭되고 있었음!! (lldb 등등) - 무엇보다도, 여기서 막하는게 싫었다.
function () { [native code] }
- 노드 런타임에 스택 출력
console.trace()
로 스택 트레이스를 찍었는데, 헐 네이티브 호출은 찍히지도 않는다.
- 노드 디버거는
var hello = addon.hello();
라인은 step in이 되지않고 그대로 넘어가 버린다.
당연한 듯 하다. hello는 자바스크립트의 영역이 아니고, c++ 함수를 바로 호출하는 코드 이니까.
즉, 첫 번째 예제는 (당연하게도!!) 자바스크립트 레이어에선 디버깅이 불가능 !!
node.js는 이름엔 js가 붙어있는데,
거의 70% 이상이 C/C++ 로 이루어져 있다.(linguist)
$ linguist node/
38% C++
32% C
29% JavaScript
0% D
0% Python
0% Shell
0% Objective-C
0% Ruby
0% R
즉, 우린 C/C++ 로 구성된 거대한 프로젝트에 addon 된 코드를 디버깅 해야됨.
컴파일된 라이브러리를 디버깅하려고 하니 바로 떠오르는건 gdb
!!
GDB, the GNU Project debugger, allows you to see what is going on `inside' another program while it executes.
gdb
로 C/C++ 디버깅을 하려면 노드를 직접 빌드해 설치해야 한다.
난 최신 stable 버전이 좋으니까 ㅋ
$ git clone https://github.com/joyent/node.git
$ git checkout -b <new_branch_name> origin/v0.8.18-release
$ ./configure --debug
$ make -j4 -C out BUILDTYPE=Debug
$ sudo make install
configure
에서 사용할 수 있는 옵션들은./configure --help
를 통해 더 확인할 수 있다.- 그냥 빌드하면 릴리즈 모드로 빌드되는데, 통상 릴리즈 모드로 빌드 한다는건 디버깅에 필요한 심볼정보들을 빼고 빌드한다는 거다.
- 이는
binutils
에 포함된gobjdump
를 통해 확인할 수 있다.
$ brew install binutils
$ gobjdump -e <object_file>
- 근데, release 모드로 빌드해도
-gdwarf
옵션으로 웬만한 바이너리는 디버깅 정보가 들어가 있다;;
node
프로젝트를 release
로 빌드했을 때와 debug
로 빌드했을 때의 차이
항목 | release | debug |
---|---|---|
make 에 걸리는 시간 | 2m36.488s | 3m13.670s |
out 디렉토리의 크기 | 280Mb | 311Mb |
tested on Mid 2011 MacBook Air with 1.8GHz i7, 4GB ram, OSX 10.8.2
인스톨이 끝났으면, 첫 번째 예제 를 테스트 하던 디렉토리로 가서 다음의 명령어를 입력해 보자
$ gdb -d <your_path_to/node/src/> --args node <target.js>
gdb
가 시직된다!!! 그럼, 우리가 hello.node
로 빌드한 hello.cc
파일의 7번째 라인에 break point를 걸어보자
(gdb) b hello.cc:7
(gdb) run
Breakpoint 1, Method (args=@0x7fff5fbfef40) at ../hello.cc:7
(gdb) p scope
(gdb) p scope->isolate_
(gdb) ptype scope
자주 사용할만한 주요 명령어를 정리해 보면
명령어 | 설명 |
---|---|
b symbol | symbol에 breakpoint 설정 |
s | step |
n | next |
c | continue |
finish | 함수를 끝까지 실행하고 빠져나간다. |
return | 함수를 실행하지 않고 빠져나간다.(return 값을 줄 수 있음) |
i line | 현재 break 된 라인에 대한 정보 |
i threads | 스레드에 대한 정보 출력 |
i source | 현재 break된 소스에 대한 정보 출력 |
i types | 현재 로드된 모든 타입 출력 |
i variables | 현재 로드된 모든 변수들, 주소 출력 |
i b | 설정된 break point 확인 |
i locals | 로컬 변수들의 내용 확인 |
l | break 된 소스의 내용 출력 |
l - | 출력된 소스의 이전 내용 출력 |
l + | 출력된 소스의 이후 내용 출력 |
p | 변수의 값 출력 |
p $(number) | 레지스터 값 출력 |
p *a@3 | a 배열의 내용을 3번째까지 출력 |
bt | 스택 프레임 출력 |
$ brew install cgdb
$ cgdb -- -d ~/git/node_project/node/src/ --args node addon.js
키 | 설명 |
---|---|
esc | 소스 pane 으로 |
space | 소스 pane 에서 breakpoint 설정 |
i | 소스 pane 에서 gdb 터미널로 |
gdb 가 켜진 김에 module 동작 확인.
- 우선 node의 디버거로 javascript 함수들이 어떻게 호출되는지 따라가 보자
- require 가 호출되면 require 인자의 종류 (node, json, js) 에 따라 다른 일이 일어나는데,
- dynamic lib 인
.node
파일들은process.dlopen
에 의해 로드됨. - 근데 process.js 는 어디에??
- process 는 100% 네이티브로 구현되어 있음
node.cc
V8은 현재 스크립트의 상황에 따라 어셈코드를 동적으로 막 생성한다. 이로 인해 평상시 노드를 gdb로 디버깅하면서 bt
를 찍으면,
3 0x000011d0e750618e in ?? ()
4 0x000011d0e7531eac in ?? ()
5 0x000011d0e75316ef in ?? ()
6 0x000011d0e752b528 in ?? ()
7 0x000011d0e7532907 in ?? ()
8 0x000011d0e75248c7 in ?? ()
9 0x000011d0e7511317 in ?? ()
10 0x00000001001713cd in Invoke (is_construct=<value temporarily unavailable, due to optimizations>, argc=1, args=0x7fff5fbff4f8, has_pending_exception=0x7fff5fbff4b7) at ../dep
s/v8/src/execution.cc:118
이렇게 중간에 ??
프레임이 찍힌다.
중간에 조금이라도 정보를 더 보고자 한다면, 여기 참고 (아직 노드 빌드시 저 옵션을 켜는 방법은 모르겠다.)
gdb 연결을 위해 노스 소스를 보면서 발견한 분석해볼만한
포인트
- node 의
make
시스템 v8
과 다른 라이브러리간의 인터페이스(특히 libuv >> tick >> api call)- 잃어버린 심볼들??
_________________________________
< let your hacking begin with GDB >
---------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
If I were using Chrome, I'd simply record the timeline, and capture the spike
in code, see the line numbers and start to refactor (or some similar approach).
The problem I have is jsbin has a few 1000 line codebase with no obvious
infinite loop problem. I can't replicate it myself, but I know that some user
is able to replicate the issue that triggers the process to go in to 99% cpu
usage, and all things stop.
Obviously things like nodetime won't work, because the process is in a tight
loop and won't respond to nodetime's requests.
I've also tried using `strace` on the box, but it actually looks idle, rather
than looping - which really doesn't make sense to me, but it's quite possible
that I'm not reading the strace output correctly.
So, clever people, any ideas how to capture and find the source of these kinds
of issues?
Clever or crazy ideas welcome!
여기에 이전에 v8에서 일했던 Vyacheslav Egorov 가 해결책을 제시했다.
+Remy Sharp
if it is a node application then you can just take a full memory core dump
once it goes into infinite loop and then you extract stack trace from there.
iI you don't want to unroll JavaScript stack manually you can write a bit of
Python, unfortunately V8 does not come with good GDB helpers out of the box.
https://code.google.com/p/v8/source/browse/trunk/tools/gdb-v8-support.py <-
that's everything it has, support for postmortem debugging on Windows is much
more substantial, and Joyent has contributed postmortem debugging tools for mdb
.
I used to have some stack unrolling helper for GDB, but I am not sure I will
be able to find it.
Now with respect to tight loops: V8 actually allows built-in debugger to
interrupt them (generated code for JavaScript loops must always contain an
interrrupt check which allows debugger to stop execution, if it does not ---
that's a bug in V8.). So in theory you should be able to break any loop and
see where you are.
+Remy Sharp
I unfortunately did not find my stack walking helper but I remembered an
easier (but nastier) trick to get stack trace out of V8
Tried it with node v0.10.8 that I have on my box and it seems to work:
https://gist.github.com/mraleph/6453431
[but it might be a bit toolchain dependent, as it requires certain symbols to
be visible to GDB]
+Vyacheslav
Egorov oh, so you're breaking with gdb, then throwing an arbitrary error so
that you can capture a full stack trace. Is that right?
+Remy Sharp
kind of.
I break in a random place in gdb (somewhere in jit compiled native code), then
I put a break point into V8 internals in a place a) which is inside runtime,
so JS stack is traversable b) which I should be able to hit even if JS code is
stuck in the infinite loop. This place is inside code that handle V8 stack
guard interrupts.
Then I ask V8 to schedule a termination interrupt via stack guard and resume
execution of the process.
Once execution is resumed it is bound to hit a stack guard and fall into
runtime where I put breakpoint.
At that point I am in a place where V8 itself can traverse the stack.
Unfortunately because debugging symbols are absent I can't call a function
that would just print a stack trace, but I can call another function (V8_Fatal)
that is used to report & crash process on fatal errors in V8. Among things it
reports is current JS stack trace. (I could not use this function immediately
when I broke inside JS code because V8 would not be able to unwind the stack
correctly from the random place, hence all the dance with stack guard).
방법인 즉슨 gdb로 무한루프에 빠진 노드 프로세스에 붙은 뒤 아래의 조건을 충족하는 곳에 브레이크 포인트를 설정하고 V8_Fatal 로 에러를 던져 현재 스택트레이스를 찍는 것 ㄷ
- which is inside runtime, so JS stack is traversable
- which I should be able to hit even if JS code is stuck in the infinite loop.
실제로 이 방법으로 remy sharp는 무한루프에 빠진 지점을 찾았다고...