js层面和d8调试技巧以及底层原理
这个大主题会分两个小专题叙述,因为js
层面的调试和c++
的调试有很大不同,这一篇文章会先从js
的调试谈起,揭秘我们日常nodejs
调试时,发生了什么? 为了与时俱进,我这边就只讲目前最流行的V8 Inspector
,在开始之前,我先抛出几个问题,让我们带着疑问去看完这篇文章:
- 当我们输入
--inspector
去启动node进程时,chrome dev tool
或者开发IDE(比如说VScode)
是怎么实现与node
内置的debugger端
交互通信的? - 交互通信的内容是什么?
- 看完了这篇文章后,你是否有能力自己实现一个
client
代替其他调试客户端(chrome dev tool
或者开发IDE(比如说VScode)
)来调试nodejs
代码?
ok,让我们带着这几个问题开始揭秘吧。
ps:本文作为先导文不会太深究node::inpector
等模块下的实现细节,不会去介绍进程调试session
的概念,单纯从发生的现象来谈实现过程,很细节的部分放到后面的文章来叙述。
准备工作
假设我们有一份index.js
代码需要被调试,大致内容如下:
console.log(
开始动手
目前为止,我们还没有设置任何断点,为了让脚本可以直接停止被编译执行,我们直接用--inspect-brk
暂停脚本:node --inspect-brk=9222 index
这个时候终端上会有类似这样的输出:
Debugger listening on ws://127.0.0.1:9222/8c34ca65-fa52-4fef-81b1-e0e4f8453de0
第一个问题
问: 当我们输入--inspector
去启动node进程时,chrome dev tool
或者 开发IDE(比如说VScode)
是怎么实现与node
内置的debugger端
交互通信的?
答:
- 看第一行的文本的输出:
Debugger listening on ws://127.0.0.1:9222/8c34ca65-fa52-4fef-81b1-e0e4f8453de0
,nodejs
启动了一个Debugger端
进程,它监听着9222
端口,并且应用层协议是ws
。我们猜想它是通过ws
进行通信的,其实事实上就是基于ws
通信的. - 至于为什么要选用
ws
,这其实和Chrome DevTools Protocol
有很大的关系,搞调试的那批人,想要复用Chrome DevTools
对nodejs
进行调试,这个时候传统的http协议
就存在跨域的问题,ws
跨域绕过去,所以就选了ws
。
第二个问题
问:交互通信的内容是什么?
答:我们在上一个问题的回答中提到了V8 Inspector Protocol
的诞生其实是为了让nodejs
的调试能完美配合Chrome DevTools
一起使用,其目的之一是为了增加可移植性。为了证实V8 Inspector
和Chrome DevTools Protocol
是息息相关的,我将在接下来的实战中,会向debugger端
,发送一些Chrome DevTools Protocol
格式的消息,用来启动调试,控制调试等。所以尽管你可能不太了解Chrome DevTools Protocol
,没关系,如果在文中看到一些陌生的名词,建议你直接谷歌,八成就是Chrome DevTools Protocol
相关的内容。
第一步: 我们先搭一个ws
客户端,不管三七二十一,先连上去再说。连上去之后,你会发现什么都没发生,哈哈哈😁。
第二步: 我们先大概给debugger端
发送如下内容,具体含义可以自己去官方文档里搜。
[
Runtime.enable(),
Debugger.enable(),
Runtime.runIfWaitingForDebugger(),
];
上面这些js代码转成ws
的消息体后,变成这个样子:
[
{"id":1,"method":"Runtime.enable"},
{"id":2,"method":"Debugger.enable"},
{"id":3,"method":"Runtime.runIfWaitingForDebugger"}
];
在你发完这些消息给debugger端
后,开始有回音啦,首先我这边收到一条消息:
我把payload
美化一下大致长这个样子:
{
"method":"Runtime.executionContextCreated",
"params":{
"context":{
"id":1,
"origin":"",
"name":"node[11738]",
"auxData":{
"isDefault":true
}
}
}
}
它对应的就是Runtime.enable()
,表示一个可执行上下文被创建起来了。当然每次请求,debugger端
都会有相应的返回,就像上面这个返回一样,如果你仔细观察返回的内容,你可以发现大概有两种格式:
- 一种就是上面的那种,返回一个method和params字段。
{
"method":"xxxx",
"params":{...},
}
- 另外一种就是带个id和result字段。
{
id: 1,
result: {…}
}
我猜想上面第一种返回是告诉我们v8虚拟机正在做什么事情,第二种就是我们请求debugger端
后,debugger端
做完后返回的结果。
为了验证猜想,我继续调试代码,努力找到一个样本,很快,我找到了一个:
熟悉nodejs
源码的同学大概知道'internal/bootstrap/loaders.js'
是nodejs
加载内置模块的加载器代码,不管是你主线程
还是worker线程
,启动一个nodejs实例
都会调用到它。
然后我继续debug,因为我们的index.js
启动时,在第一行代码上设置了一个断点,所以debugger端
会通过ws
发送一个类似这样的消息:
告诉我们 file:///Users/huenchao/study/cnm/index.js
这个地方有个断点,此时,我们需要把这部分的代码从debugger端
拿过来,所以我就 发送如下信息给debugger端
:
"id":
很快,debugger端
就返回给我们消息,正是我们想要调试的代码片段:
好了,我已经获得了我们要调试的代码片段,接下来的就是不断的打断点,获取上下文信息,下一步,如此重复下去。。。。。
不如,我简单再演示一下,怎么继续打断点,继续next吧。
我现在给第一行代码设置一个断点:
{"id":6,"method":"Debugger.setBreakpoint","params":{"location":{"scriptId":"66","lineNumber":0}}}
然后debugger端
设置好断点后,返回我类似下面的信息,我处理了一下:
然后我想执行next的动作,ok,我再发送这样的信息给debugger端
,看看会发生点什么:
"id":
等debugger端
返回消息:
然后我们被调试的代码已经跑到下一行了:
后面更多花式技巧,我就不演示了,因为我本人就是一个张嘴就来,有口就行 的工程师,实战的部分留给你们后浪吧~😄
第三个问题
问:看完了这篇文章后,你是否有能力自己实现一个client
代替其他调试客户端(chrome dev tool
或者 开发IDE(比如说VScode)
)来调试nodejs
代码?
答:如果你从头到尾看完我这篇文章,做一个调试客户端,起码内置一个ws
服务,然后基于CDP
通信就行,因为做一个客户端,你起码要实现一个watcherLists吧?你起码要可以追溯上下文吧?你起码要做一下多进程链接调试吧?要有个简单可交互的界面吧?行,有兴趣就做一个呗,有了它,什么云端ide调试,跨端app调试不就弄起来了吗???
最后
这是调试文章的第一篇,其实很简单,后面大概还有会3篇文章,大致介绍Inpector的源码实现,以及编译V8后生成的D8怎么调试,怎么通过 D8观察我们js代码在存储状态,以及可能扩展到GC部分的内容,当然以上内容全部都是我yy的,我有时间就会研究,并写出来分享给大家。😁