Nodejs 调试

最近看 node 源码,记录一下怎么调试

Debugger 调试

Debugger 是 Node.js (当前版本 v12)自带的独立于运行进程之外的一个监听客户端,基于 v8 inspect 实现。

debugger 一个 node inspect myScript.js 

Bash
➜  Node.js node inspect myScript.js
< Debugger listening on ws://127.0.0.1:9229/1645d9a3-b0c1-44f9-aad2-0e2a6c751508
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in myScript.js:2
  1 // myscript.js
> 2 global.x = 5;
  3 setTimeout(() => {
  4   debugger;
debug>

可以看到 debugger 会起一个监听进程,默认情况下侦听 127.0.0.1:9229,并且有一个 uuid 标识。

debugger 相关命令:

  • cont, c: Continue execution
  • next, n: Step next
  • step, s: Step in
  • out, o: Step out
  • pause: Pause running code (like pause button in Developer Tools)

如果代码执行完了可以用 debug>restart 重启脚本

  • run: Run script (automatically runs on debugger’s start)
  • restart: Restart script
  • kill: Kill script

设置断点的方法

1、通过 debug> 提示符使用 setBreakpoint ( sb ) https://nodejs.org/api/debugger.html#debugger_breakpoints 函数:setBreakpoint(module_file_name, line_number)

Bash
debug> sb(5)
  1 // myscript.js
  2 global.x = 5;
  3 setTimeout(() => {
  4   debugger;
> 5   console.log('world');
  6 }, 1000);
  7 console.log('hello');
debug>

2、代码中加 debugger

获取所有断点

Bash
debug> breakpoints
#0 myScript.js:5

清除断点

Bash
debug> cb(5)
Could not find breakpoint at 5:undefined
debug> cb('myScript', 5)
debug>

查看变量值

Bash
debug> repl
Press Ctrl + C to leave debug repl
> global.x
undefined
debug> n
break in myScript.js:3
  1 // myscript.js
  2 global.x = 5;
> 3 setTimeout(() => {
  4   debugger;
* 5   console.log('world');
debug> repl
Press Ctrl + C to leave debug repl
> global.x
5
>

watch 变量 watch('expr'), expr 是变量名,watch 接收参数 字符串

Bash
➜  Node.js node inspect myScript.js
< Debugger listening on ws://127.0.0.1:9229/14c83ac5-c5f5-4a19-a30c-2c573222f2f2
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in myScript.js:2
  1 // myscript.js
> 2 global.x = 5;
  3 setTimeout(() => {
  4   debugger;
debug> watch('global')
debug> watch('global.x')
debug> watch('window')
debug> n
break in myScript.js:3
Watchers:
  0: global =
    { global: global,
      clearInterval: ,
      clearTimeout: ,
      setInterval: ,
      setTimeout: ,
      ... }
  1: global.x = 5
  2: window =
    ReferenceError: window is not defined
        at eval (eval at <anonymous> (/Users/albertaz/Documents/learn/Node.js/myScript.js:3:1), <anonymous>:1:1)
        at Object.<anonymous> (/Users/albertaz/Documents/learn/Node.js/myScript.js:3:1)
        at Module._compile (internal/modules/cjs/loader.js:1154:14)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:1177:10)
        at Module.load (internal/modules/cjs/loader.js:1001:32)
        at Function.Module._load (internal/modules/cjs/loader.js:900:14)
        at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
        at internal/main/run_main_module.js:18:47

  1 // myscript.js
  2 global.x = 5;
> 3 setTimeout(() => {
  4   debugger;
  5   console.log('world');
debug>

修改变量值

Bash
➜  Node.js node inspect myScript.js
< Debugger listening on ws://127.0.0.1:9229/ced21af3-6f9c-4bee-8757-769ebb4ff3ea
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in myScript.js:2
  1 // myscript.js
> 2 global.x = 5;
  3 setTimeout(() => {
  4   debugger;
debug> watch('global.x')
debug> n
break in myScript.js:3
Watchers:
  0: global.x = 5

  1 // myscript.js
  2 global.x = 5;
> 3 setTimeout(() => {
  4   debugger;
  5   console.log('world');
debug> repl
Press Ctrl + C to leave debug repl
> global.x = 7
7
debug> n
break in myScript.js:7
Watchers:
  0: global.x = 7

  5   console.log('world');
  6 }, 1000);
> 7 console.log('hello');
debug>

vscode 调试

原理还是先运行node进程,再使用 VS Code 去连接 Node 进程,具体做法可以通过在 vscode中添加一个 launch.json 文件

https://code.visualstudio.com/docs/editor/debugging

https://code.visualstudio.com/docs/nodejs/nodejs-debugging

vscode-debug-node

vscode-debug-node-1

vscode-debug-node-2

chrome dev tool 调试

node --inspect myScript.js 注意是 **--inspect**

Bash
➜  Node.js node --inspect myScript.js
Debugger listening on ws://127.0.0.1:9229/c4018064-1abd-4e6f-b0de-bf2dc7c3170d
For help, see: https://nodejs.org/en/docs/inspector

打开 Chrome 并在地址栏中输入 about:inspect

chrome-debug-node

image.png

chrome-debug-node-2

chrome devtool 没法调 worker threads。调 worker threads 可以用 ndb 

火焰图

speedscope

https://github.com/jlfwong/speedscope/wiki/Importing-from-Node.js

Nodejs 源码调试

首先看 build.md ,编译node debug版本的执行文件,因为发行版本的 node 是不支持调试的,所以我们需要自己通过源码构建一份可调试的 node

支持的平台(有多个级别)

  • t1:包括所有测试的工具链,测试必须通过
  • t2:包括所有测试的工具链,测试必须通过,工具链的 issue 必须修复才会发布
  • Experimental:测试不通过不会阻碍发布

重点关注下:

Unix

  • gcc and g++ >= 6.3 or newer, or
  • GNU Make 3.81 or newer
  • Python (see note above)
    • Python 2.7
    • Python 3.5, 3.6, 3.7, and 3.8.

CentOS 下配好依赖

Bash
sudo yum install python gcc-c++ make

Mac

  • Xcode Command Line Tools >= 10 for macOS
  • Python (see note above)
    • Python 2.7
    • Python 3.5, 3.6, 3.7, and 3.8.

项目构建通过 make 进行管理,源码中已经有了 configure 文件

Bash
$ ./configure # 确认 xcode clt 和 python
$ ./configure --debug # 默认的编译配置是没有启用调试模式的,因此,我们需要在执行 ./configure 时加上 --debug 就可以生成可调试的编译配置项
$ make -j4 # 让 make 运行四个并行编译job,加速编译
#测试覆盖率编译
$ ./configure --coverage
$ make coverage

./configure --debug  之后会生成两份二进制文件 一分在 out/Release/node  一份在 out/Debug/node 
为了方便构建,建了一个 debug-build.sh 

Bash
#!/bin/bash
./configure --debug
make -C out  BUILDTYPE=Debug -j4
echo "let's go"

并给他可执行的权限再执行:

Bash
chmod +x debug-build.sh
./debug-build.sh

mac 需要留下足够空间,编译构建比较耗资源,如果只执行 ./configure (只生成 out/Release/node)需要10g,./configure --debug 会干掉 20g (我的剩余空间从 39g 到 19g)
同时也比较耗时间。


build.md 说构建后给out目录下的 node 可执行文件设置 firewall rules,避免测试时不断弹出连接网络的提示,并可以在项目根目录下 创建 node 符号执行 。

Bash
sudo ./tools/macos-firewall.sh

但我还没遇到。

调试 js

创建一个用于调试的项目,随便写点,作为入口的:

Javascript
console.log('hello world');

用编译好的 node来执行:

Bash
./out/Debug/Node --inspect-brk=9229 demo/helloWorld.js #inspect-brk让他在程序加载前就进断点

会看到

Bash
➜  node git:(master) ✗ ./out/Debug/Node --inspect-brk=9229 demo/helloWorld.js
Debugger listening on ws://127.0.0.1:9229/33cc9b77-fa44-4b4c-adae-7b5126c088c6
For help, see: https://nodejs.org/en/docs/inspector

然后打开 vscode

vscode 启动 debug 需要配置 launch.json, 其中 request 支持两种:

  • launch: vscode 会打开这个程序然后进入调试,比较适合用 chrome dev tool 调
  • attach:已经打开了程序,然后接通 node 的内部调试协议进行调试,比较适合用终端起

image.pngimage.png

调试 c/c++ (libuv or v8)

vscode 需要先装好 c++ 插件
重新创建或添加 launch.json,type 选 cppdbg

调试总结

通过以上操作基本可以感觉到,无论哪种防范,调试思路都是一致的:

在 nodejs 侧启动一个 websocket 服务提供运行时信息,在用户调试侧再启动一个 websocket 服务客户端触发各种调试操作。两个 websocket 之间通过调试协议通信。

用 debugger 调试时,前面提到他是基于 v8-inspect 实现的,v8-inspect 的主要作用就是实现 inspect protocal 调试协议。 对源码调试时,通过 --inspect-brk 也是同样触发 v8-inspect, 实现调试协议。

而由于 v8 inspect 本身是从 chrome 中提取出来的,支持 chrome devtools protocal,所以可以通过 --inspect 触发 v8-inspect 在 chrome 上调试。