原文地址:https://www.usblog.cc/blog/post/justzhl/揭秘浏览器远程调试技术

调试技术的起源

1947 年 9 月 9 日,一名美国的科学家格蕾丝.霍普和她的同伴在对 Mark II 计算机进行研究的时候发现,一只飞蛾粘在一个继电器上,导致计算机无法正常工作,当他们把飞蛾移除之后,计算机又恢复了正常运转。于是他们将这只飞蛾贴在了他们当时记录的日志上,对这件事情进行了详细的记录,并在日志最后写了这样一句话:First actual case of bug being found。这是他们发现的第一个真正意义上的 bug,这也是人类计算机软件历史上,发现的第一个 bug,而他们找到飞蛾的方法和过程,就是 debugging 调试技术。

从格蕾丝调试第一个 bug 到现在,69 年的时间里,在计算机领域,硬件、软件各种调试技术都在不断的发展和演进。那么对于日新月异的前端来说,调试技术也尤其显得重要。淘宝前端团队也正在使用一些创新的技术和手段来解决无线页面调试的问题。今天先跟大家分享下浏览器远程调试技术,本文将用 Chrome/Webview 来作为案例。

调试原理

调试方式与权限管理

目前常规浏览器调试目标分为两种:Chrome PC 浏览器和 Chrome Mobile(Android 4.4 以后,Android WebView 其实就是 Chromium WebView)。

Chrome PC 浏览器

对于调试 Chrome PC 浏览器,可能大家经常使用的是用鼠标右键或者快捷方式(mac:option + command + J),唤起 Chrome 的控制台,来对当前页面进行调试。其实还有另外一种方法,就是使用一个 Chrome 浏览器调试另一个 Chrome 浏览器。Chrome 启动的时候,默认是关闭了调试端口的,如果要对一个目标 Chrome PC 浏览器进行调试,那么启动的时候,可以通过传递参数来开启 Chrome 的调试开关:

JavaScript
1
2

# for mac
sudo /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

Chrome Android 浏览器

对于调试 Android 上的 Chrome 或者 WebView 需要连接 USB 线。打开调试端口的方法如下:

JavaScript
1
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。

开始调试

通过以上的调试方式的接入以及调试端口的打开,这个时候在浏览器中输入:

JavaScript
1
http://127.0.0.1:9222/json

将会看到类似下面的内容:

JavaScript
1
2
3
4
5
6
7
8
9
10
11

[
  {
    "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 接口控制目标浏览器

JavaScript
1
2
3
4
5
6
7
8
9
10
11

# 获取当前所有可调式页面信息
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 进行调试, 浏览器输入一下内容:

JavaScript
1
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 调试相关的。

图中,对于 JavaScript 调试,有一条非常重要的消息,我蓝色选中的那条消息:

JavaScript
1
{"id":6,"method":"Debugger.enable"}

然后选中要调试的 JavaScript 文件,然后设置一个断点,我们再来看看 WebSocket 消息:

devtool 像目标 Chrome 发送了 2 条消息

JavaScript
1
2
3
4
5
6
7

{
  "id": 23,
  "method": "Debugger.getScriptSource",
  "params": {
    "scriptId": "103"
  }
}

JavaScript
1
2
3
4
5
6
7
8
9
10

{
  "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 里面的一小段源码片段:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// 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 最正确的方式是使用 https://cs.chromium.org,比 SourceInsight 还要方便。

Debugger.enable

JavaScript
1
2
3
4
5
6
7
8
9

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 解析引擎解析到 标签之后,Chrome 将会把 script 标签对应的 JavaScript 源码扔给 V8 编译执行。同时,V8 将会对所有的 JavaScript 源码片段进行编号并保存。所以,当 chrome devtool 需要获取要调试的 JavaScript 文件的时候,只需要通过 Debugger.getScriptSource,给 V8 传递一个 scriptId,V8 将会把 JavaScript 源码返回。我们再回头看看这个图中的消息:

    上面 id 为 23 的 scriptSource 就是 V8 返回的 JavaScript 源码,如此以来,我们就可以在 devtool 中看到我们要调试的 JavaScript 源码了。

Debugger.setBreakpointByUrl

所有准备工作都做好了,现在就可以开始设置断点了。从上面的几个图中,已经可以很清楚的看到,Debugger.setBreakpointByUrl 给目标 Chrome 传递了一个 JavaScript 的 url 和断点的行号。

首先,V8 会去找,是否已经存在了该 URL 对应的 JavaScript 源码了:

JavaScript
1
2
3
4
5
6
7
8
9

for (const auto& script : m_scripts) {
  if (!matches(m_inspector, script.second->sourceURL(), url, isRegex))
    continue;
  std::unique_ptr<protocol::Debugger::Location> location = resolveBreakpoint(
    breakpointId, script.first, breakpoint, UserBreakpointSource);
  if (location) (*locations)->addItem(std::move(location));
}
*outBreakpointId = breakpointId;

V8 给所有的断点,创建一个 breakpointObject。并将这些 braekpointObject 以 的形式存放在一个 Map 里面,而这个 Key,就是这个 JavaScript 文件的 URL。看到这里,已经可以解释很多同学在调试 JavaScript 遇到的一个问题:,>

有些同学为了防止页面的 JavaScript 文件不更新,对于一些重要的 JavaScript 文件的 URL 添加访问时间戳,对于这些添加了访问时间戳的 JavaScript 文件进行设置断点然后刷新调试的时候,Chrome 会打印一个 warnning,告诉你断点丢失。

原因很简单,在调试的时候,V8 发现这个 breakpointMap 里面找不到对应的 breakpointObject,因为 URL 发生了变化,这个 brakpointObject 就丢失了,所以 V8 就找不到了,无法进行断点调试。

根据我们的正常思维,你可能会认为 V8 会将断点设置在 C++ 中,其实一开始我也是这么认为。随着对 V8 的探索,让我看到了我时曾相识的一些函数名:

JavaScript
1
2
3
4
5
6
7

v8::Local<v8::Function> setBreakpointFunction = v8::Local<v8::Function>::Cast(
    m_debuggerScript.Get(m_isolate)
    ->Get(context, toV8StringInternalized(m_isolate, "setBreakpoint"))
      .ToLocalChecked());
v8::Local<v8::Value> breakpointId =
  v8::Debug::Call(debuggerContext(), setBreakpointFunction, info)
    .ToLocalChecked();

其中,m_debuggerScript,就是我前面提到的 debugger-script.js。随着对 V8 Debugger 的进一步探索,我发现,V8 实际上对这个对这个 breakpointObject 设置了 2 次。一次是通过在 C++ 中调用 m_debuggerScript 的 setBreakpoint 设置到 JavaScript 的 context 里面,也就是上面这段 C++ 逻辑做的事情。另一次是,m_debuggerScript 反过来将断点信息设置到了 V8 的 C++ Runtime 中,为要调试的 JavaScript 的某一行设置一个 JavaScript 的回调函数。

断点命中

由于 V8 对 JavaScript 是及时编译执行的,没有生成 bytecode,而是直接生成的 machine code 执行的,所以这个断点回调函数也会被设置到这个 machine code 里面。

最终触发断点事件,也是 V8 的 C++ Runtime。当用户刷新或者直接执行 JavaScript 的逻辑的时候,实际上是 V8 C++ Runtime 在运行 JavaScript 片段产生的 machine code,这个 machine code 已经包含了断点回调函数了。一旦这个 machine code 里面的回调函数被触发,接着就会触发之前 Debugger.enable 设置的调试事件监听器 DebugEventListener 的回调函数。并返回一条消息给 Chrome 的 devtool,告诉 Chrome devtool,当前 JavaScript 被 pause 的行号。到此为止,一个断点就被命中了。

关于 JavaScript 断点命中,其实是一个很复杂的过程。后面有时间的话,会专门讲讲 JavaScript 断点命中的详细逻辑。

总结

浏览器的调试,最终都落脚到引擎:渲染引擎和 JavaScipt 引擎。那么对于 JavaScript 调试来说,难点就在于 V8 如何给 JavaScript 某一行进行标记然后进行断点,这需要有一点 V8 的知识。

转载于:https://www.cnblogs.com/imhurley/p/5994636.html

揭秘浏览器远程调试技术相关推荐

  1. PC端chrome浏览器如何调试多点触控事件/chrome浏览器远程调试手机上的网页

    PC端chrome浏览器如何调试多点触控事件/chrome浏览器远程调试手机上的网页 最近学习移动端网页开发的时候,遇到了一个问题,如何在真机上看到控制台输出的内容. 虽然现在的桌面浏览器提供了模拟手 ...

  2. Chrome 浏览器远程调试

    打开浏览器的远程调试 Chrome浏览器的快捷方式,右键选择"属性",在目标一栏加上 --remote-debugging-port=9221,后面的端口可以自己定义,完成之后启动 ...

  3. 日志分析解决的问题不是问题?java远程调试技术之Remote

    远程调试产生的背景: 代码调试是程序员必备技能,可是在某些场景下,本地环境代码可以通,但是发到线上总会出现各种各样的问题,我们根据日志和控制台也无法分析出具体问题出现在哪里,对于分支流程控制比较复杂, ...

  4. 三星移动端浏览器远程调试

    Step1: 激活开发者选项 1. 点击[设置]. 2. 点击[关于手机]. 3. 点击[软件信息]. 4.连续点击[版本号]7次后,将提示"开发者模式已启用". 5.点击[←]图 ...

  5. 利用GDB进行远程调试

    在进行嵌入式系统开发中,受到嵌入式系统资源的限制,调试环境和通用桌面系统的调试环境有差别,引入了远程调试技术.这时,调试器运行于通过桌面系统,被调试的程序则运行于基于特定硬件平台的嵌入式系统(目标系统 ...

  6. 在海思芯片上使用GDB远程调试

    0.载GDB安装包,下载地址:Index of /gnu/gdb,使用8.2版本 1.见下面链接: 在海思芯片上使用GDB远程调试_大橙子疯的博客-CSDN博客_海思gdb调试 和 arm-linux ...

  7. 嵌入式软件调试技术 读书笔记

    第一章 软件调试概述 第二章 边界扫描测试技术 (JTAG) 第三章 学习使用GDB调试器 第四章 GDB远程调试技术 第五章 网络应用程序调试 第六章 多进程与多线程调试 第七章 静态库与动态库的调 ...

  8. HTML5铸就VMware新特性,WSX技术能让浏览器远程运行虚拟桌面

    HTML5铸就VMware新特性,WSX技术能让浏览器远程运行虚拟桌面 曾子嶒 发表于 2012/03/21-07:42 HTML5 /VMware /WSX 快成为第一个分享的人吧!  分享到  V ...

  9. java计算机毕业设计技术的游戏交易平台源程序+mysql+系统+lw文档+远程调试

    java计算机毕业设计技术的游戏交易平台源程序+mysql+系统+lw文档+远程调试 java计算机毕业设计技术的游戏交易平台源程序+mysql+系统+lw文档+远程调试 本源码技术栈: 项目架构:B ...

最新文章

  1. javascript权威指南学习笔记
  2. java继承关键字super_java学习笔记-继承中super关键字
  3. CTF杂项题型之画图-Miscellaneous-200
  4. aws ec2 选择可用区_AWS Messaging Services:选择合适的服务
  5. css3 多列布局使用
  6. java连接imserver_java后端IM消息推送服务开发——协议
  7. linux下使用TC模拟弱网络环境
  8. LeetCode刷题(Python)——汉明距离
  9. 苹果App Store曝出Bug 你遇到了么?
  10. 计算机设备管理中的其他设备,解决方法:计算机设备管理器中的“其他设备”驱动程序问题!...
  11. cmd打开记事本并写字_Windows中的记事本和写字板之间有什么区别?
  12. uniapp 开发小程序对象传递数据结构封装一层$orig,使组件中v-model绑定value失效
  13. android cts问题分析,一则CTS测试错误分析
  14. KMS激活报错0x8007000D
  15. Freeze the Discriminator a Simple Baseline for Fine-Tuning GANs
  16. Qt绘制旋转的轮播图
  17. 酒精测试仪全国产化电子元件推荐方案
  18. 联想R7000P莫名其妙黑屏问题记录
  19. 网络分层,图,易懂--对应 应用层、数据传输层、网络层、数据链路层
  20. 举个栗子!Tableau 技巧(176):快速添加 Server 受信任的 IP

热门文章

  1. 《通信技术导论(原书第5版)》——导读
  2. KY59 神奇的口袋【和某题相同原理!!】
  3. POJ3014(最小覆盖点;匈牙利算法)
  4. 配置windows iis
  5. 中国联通大幅下调国际漫游费
  6. 小米手环7外观曝光 相比小米手环6有大升级
  7. docker manifest 使用实战
  8. Pagehelper的详细文档
  9. additiv.sh
  10. oracle pns配置,oracle笔记大全