调试技术的起源
1947 年 9 月 9 日,一名美国的科学家格蕾丝.霍普和她的同伴在对 Mark II 计算机进行研究的时候发现,一只飞蛾粘在一个继电器上,导致计算机无法正常工作,当他们把飞蛾移除之后,计算机又恢复了正常运转。于是他们将这只飞蛾贴在了他们当时记录的日志上,对这件事情进行了详细的记录,并在日志最后写了这样一句话:First actual case of bug being found。这是他们发现的第一个真正意义上的 bug,这也是人类计算机软件历史上,发现的第一个 bug,而他们找到飞蛾的方法和过程,就是 debugging 调试技术。
调试原理
调试方式与权限管理
Chrome PC 浏览器
对于调试 Chrome PC 浏览器,可能大家经常使用的是用鼠标右键或者快捷方式(mac:option + command + J),唤起 Chrome 的控制台,来对当前页面进行调试。其实还有另外一种方法,就是使用一个 Chrome 浏览器调试另一个 Chrome 浏览器。Chrome 启动的时候,默认是关闭了调试端口的,如果要对一个目标 Chrome PC 浏览器进行调试,那么启动的时候,可以通过传递参数来开启 Chrome 的调试开关:
# for mac
sudo /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
复制代码
Chrome Android 浏览器
对于调试 Android 上的 Chrome 或者 WebView 需要连接 USB 线。打开调试端口的方法如下:
adb forward tcp:9222 localabstract:chrome_devtools_remote
复制代码
跟 Chrome PC 浏览器不同的是,对于 Chrome Android 浏览器,由于数据传输是通过 USB 线而不是 WIFI,实际上 Chrome Android 创建的一个 chrome_devtools_remote 这个 path 的 domain socket。所以,上面一条命令则是通过 Android 的 adb 将 PC 的端口 9222 通过 USB 线与 chrome_devtools_remote 这个 domain socket 建立了一个端口映射。
权限管理
Google 为了限制调试端口的接入范围,对于 Chrome PC 浏览器,调试端口只接受来自 127.0.0.1 或者 localhost 的数据请求,所以,你无法通过你的本地机器 IP 来调试 Chrome。对于 Android Chrome/WebView,调试端口只接受来自于 shell 这个用户数据请求,也就是说只能通过 USB 进行调试,而不能通过 WIFI。
开始调试
通过以上的调试方式的接入以及调试端口的打开,这个时候在浏览器中输入:
http://127.0.0.1:9222/json
复制代码
将会看到类似下面的内容
[
{
"description": "",
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e",
"id": "ebdace60-d482-4340-b622-a6198e7aad6e",
"title": "揭秘浏览器远程调试技术.mdown—/Users/harlen/Documents",
"type": "page",
"url": "http://127.0.0.1:51004/view/61",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e"
}
]
复制代码
其中,最重要的 2 个参数分别是 id 和 webSocketDebuggerUrl。Chrome 会为每个页面分配一个唯一的 id,作为该页面的唯一标识符。几乎对目标浏览器的所有操作都是需要带上这个 id。
Chrome 提供了以下这些 http 接口控制目标浏览器
获取当前所有可调式页面信息 http://127.0.0.1:9222/json
获取调试目标 WebView/blink 的版本号 http://127.0.0.1:9222/json/version
创建新的 tab,并加载 url http://127.0.0.1:9222/json/new?url
关闭 id 对应的 tab http://127.0.0.1:9222/json/close/id
webSocketDebuggerUrl 则在调试该页面需要用到的一个 WebSocket 连接。chrome 的 devtool 的所有调试功能,都是基于 Remote Debugging Protocol 使用 WebSocket 来进行数据传输的。那么这个 WebSocket,就是上面我们从 http://127.0.0.1:9222/json 获取的 webSocketDebuggerUrl,每一个页面都有自己不同的 webSocketDebuggerUrl。这个 webSocketDebuggerUrl是通过 url 的 query 参数传递给 chrome devtool 的。
chrome 的 devtool 可以从 Chrome 浏览器中进行提取 devtool 源码或者从 blink 源码中获取。在部署好自己的 chrome devtool 代码之后,下面既可以开始对 Chrome 进行调试, 浏览器输入一下内容:
http://path_to_your_devtool/devtool.html?ws=127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e
复制代码
其中 ws 这个参数的值就是上面出现的 webSocketDebuggerUrl。Chrome 的 devtool 会使用这个 url 创建 WebSocket 对该页面进行调试。
如何实现 JavaScript 调试
在进入 Chrome 的 devtool 之后,我们可以调出控制台,来查看 devtool 的 WebSocket 数据。这个里面有很多数据,我这里只讲跟 JavaScript 调试相关的。
{"id":6,"method":"Debugger.enable"}
复制代码
然后选中要调试的 JavaScript 文件,然后设置一个断点,我们再来看看 WebSocket 消息:
{
"id": 23,
"method": "Debugger.getScriptSource",
"params": {
"scriptId": "103"
}
}
{
"id": 24,
"method": "Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 2,
"url": "https://g.alicdn.com/alilog/wlog/0.2.10/??aplus_wap.js,spm_wap.js,spmact_wap.js",
"columnNumber": 0,
"condition": ""
}
}
复制代码
那么收到这几条消息之后,V8 做了些什么呢? 我们先来简单的看下 V8 里面的一小段源码片段:
// V8 Debugger.cpp
DispatcherImpl(FrontendChannel* frontendChannel, Backend* backend) : DispatcherBase(frontendChannel), m_backend(backend) {
m_dispatchMap["Debugger.enable"] = &DispatcherImpl::enable;
m_dispatchMap["Debugger.disable"] = &DispatcherImpl::disable;
m_dispatchMap["Debugger.setBreakpointsActive"] = &DispatcherImpl::setBreakpointsActive;
m_dispatchMap["Debugger.setSkipAllPauses"] = &DispatcherImpl::setSkipAllPauses;
m_dispatchMap["Debugger.setBreakpointByUrl"] = &DispatcherImpl::setBreakpointByUrl;
m_dispatchMap["Debugger.setBreakpoint"] = &DispatcherImpl::setBreakpoint;
m_dispatchMap["Debugger.removeBreakpoint"] = &DispatcherImpl::removeBreakpoint;
m_dispatchMap["Debugger.continueToLocation"] = &DispatcherImpl::continueToLocation;
m_dispatchMap["Debugger.stepOver"] = &DispatcherImpl::stepOver;
m_dispatchMap["Debugger.stepInto"] = &DispatcherImpl::stepInto;
m_dispatchMap["Debugger.stepOut"] = &DispatcherImpl::stepOut;
m_dispatchMap["Debugger.pause"] = &DispatcherImpl::pause;
m_dispatchMap["Debugger.resume"] = &DispatcherImpl::resume;
m_dispatchMap["Debugger.searchInContent"] = &DispatcherImpl::searchInContent;
m_dispatchMap["Debugger.setScriptSource"] = &DispatcherImpl::setScriptSource;
m_dispatchMap["Debugger.restartFrame"] = &DispatcherImpl::restartFrame;
m_dispatchMap["Debugger.getScriptSource"] = &DispatcherImpl::getScriptSource;
m_dispatchMap["Debugger.setPauseOnExceptions"] = &DispatcherImpl::setPauseOnExceptions;
m_dispatchMap["Debugger.evaluateOnCallFrame"] = &DispatcherImpl::evaluateOnCallFrame;
m_dispatchMap["Debugger.setVariableValue"] = &DispatcherImpl::setVariableValue;
m_dispatchMap["Debugger.setAsyncCallStackDepth"] = &DispatcherImpl::setAsyncCallStackDepth;
m_dispatchMap["Debugger.setBlackboxPatterns"] = &DispatcherImpl::setBlackboxPatterns;
m_dispatchMap["Debugger.setBlackboxedRanges"] = &DispatcherImpl::setBlackboxedRanges;
}
复制代码
你会发现,V8 有 m_dispatchMap 这样一个 Map。专门用来处理所有 JavaScript 调试相关的处理。 其中就有本文即将重点讲述的:
- Debuggger.enable
- Debugger.getScriptSource
- setBreakpointByUrl
这些都需要在 V8 的源码中找到答案。顺便给大家推荐一个查看 Chromium/V8 最正确的方式是使用 cs.chromium.org ,比 SourceInsight 还要方便。
Debugger.enable
void V8Debugger::enable() {
if (m_enableCount++) return;
DCHECK(!enabled());
v8::HandleScope scope(m_isolate);
v8::Debug::SetDebugEventListener(m_isolate, &V8Debugger::v8DebugEventCallback,
v8::External::New(m_isolate, this));
m_debuggerContext.Reset(m_isolate, v8::Debug::GetDebugContext(m_isolate));
compileDebuggerScript();
}
复制代码
这个接口的名称叫 Debugger.enable,但是收到这条消息,V8 其实就干了两件事情事情:
-
SetDebugEventListener: 给 JavaScript 调试安装监听器,并设置 v8DebugEventCallback 这个回调函数。JavaScript 所有的调试事件,都会被这个监听器捕获,包括:JavaScript 异常停止,断点停止,单步调试等等。
-
compileDebuggerScript: 编译 V8 内置的 JavaScript 文件 debugger-script.js。由于这文件比较长,我这里就不贴出来了,感兴趣的同学点击这个链接进行查看源码。debugger-script.js 主要是定义了一些针对 JavaScript 断点进行操作的函数,例如设置断点、查找断点以及单步调试相关的函数。那么这个 debugger-script.js 文件,被 V8 进行编译之后,保存在 global 对象上,等待对 JavaScript 进行调试的时候,被调用。
Debugger.getScriptSource
在 Chrome 解析引擎解析到 <script> 标签之后,Chrome 将会把 script 标签对应的 JavaScript 源码扔给 V8 编译执行。同时,V8 将会对所有的 JavaScript 源码片段进行编号并保存。所以,当 chrome devtool 需要获取要调试的 JavaScript 文件的时候,只需要通过 Debugger.getScriptSource,给 V8 传递一个 scriptId,V8 将会把 JavaScript 源码返回。我们再回头看看这个图中的消息: