Android 调试原理

概述

在Android开发过程中,调试是不可避免的,在IDE的帮助下,只需要在IDE按钮上点击两下便可以进行调试。这让调试的工作变得十分简单方便,以至于开发者只需要熟记各种IDE的debug技巧,无需了解调试原理就可以完成程序的debug。

在调试的时候,开发者可以打断点调试、需改运行参数、dump虚拟机的堆栈信息、远程调试等,那这些都是怎么做到的呢?本文将带你一起探讨 Android 的调试原理。

要学习 Adb 的调试原理,需要从稍微简单一点的 Java 调试原理入手,因此首先介绍一下 Java 调试原理。

手动调试Java

在正式介绍Java的调试原理前,首先进行一次手动的 Java 程序调试。

第一步,编写 java 文件:

public class TestMain {

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            Thread.sleep(1000);
            String hello = hello("" + new Random().nextInt(100));
            System.out.println(hello);
        }
    }
    
    private static String hello(String hello) {
        return hello;
    }
}

第二步,将 java 文件,编译 class 文件:

$javac -g src/com/example/www/TestMain.java -d class

第三步,使用debug模式,运行 class 文件并监听8000端口,挂载 jdwp:

$java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 -cp class/ com.example.www.TestMain

第四步,使用调试工具 jdb 与 8000 端口进行通讯,开始调试:

$jdb -attach localhost:8000

第五步,在 TestMain.java的第十行上打断个点:

> stop at com.example.www.TestMain:10

效果如下:

sxxxx0@wxxxxxeMBP test % jdb -attach localhost:8000
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
正在初始化jdb...
> stop at com.example.www.TestMain:10
设置断点com.example.www.TestMain:10
> 
断点命中: "线程=main", com.example.www.TestMain.main(), 行=10 bci=6

main[1] run
> 
断点命中: "线程=main", com.example.www.TestMain.main(), 行=10 bci=6

main[1] clear com.example.www.TestMain:10
已删除: 断点com.example.www.TestMain:10
main[1] stop in com.example.www.TestMain.hello
设置断点com.example.www.TestMain.hello
main[1] run
> 
断点命中: "线程=main", com.example.www.TestMain.hello(), 行=16 bci=0

main[1] 

至此,手动调试已开启,并让调试过程停留在了 TestMain#hello 方法上(源码的第16行)。除了 stop at 可以打行断点外,如上,还可以通过 stop in 打上方法断点。同时,还可以使用 -source 指定源码路径,IDE默认设置的源码路径则是 $PROJECT_ROOT(项目根路径)。 打开 debug config ,可以在IDE中手动设置源码路径,告诉jdb该去哪里找到源码:

image

如果想了解更多的 java debug 指令可查阅官方文档

在上面五步的调试中,从第三步开始,可能大部分读者就比较生疏了,因为在调试程序调试时,按下IDE的 debug 按键后,IDE就在后台自动运行了该 class 文件,并且使用 jdb 帮我们将界面上的埋点转化为埋点指令,无需开发者手动调试。

现在,打开 Debug 后在 Console 这里输出的这些提示大概可以理解了吧 :-)

image

,这 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n 是什么玩意 ,还是有点懵:-) ,读完这篇文章后你就能知道这些具体是什么回事啦~

JDPA

下面一起看看 JVM 的调试原理吧!Java 的调试体系(Java Platform Debugger Architecture,以下简称:JDPA)定义了 JVM 调试的过程,学习 JVM 的调试原理,实际上就是学习JDPA。

图1

如上图,在JDPA中包含三个部分:

  • Java 虚拟机工具接口(JVMTI) : 可以通过『实现JVMTI可以获取、控制被调试虚拟机的运行状态』,JVMTI是调试的基础,JVMTI由JVM自身提供
  • Java 调试线协议(JDWP) : The Java Debug Wire Protocol (以下简称:JDWP)定义了调试者(Debugger)和被调试者(Debuggee)通讯协议
  • Java 调试接口(JDI) : 调试者通过实现 JDI ,调试者可以向JVM发送调试命令, 接受JVM运行时的状态信息(例如:jdb是JDI的一个实现)

调试的本质就是 Debugger 与 Debuggee 的之间的通讯,JDWP 则是通讯所用的协议。

JDPA工作流程

下图描述了JDPA工作流程:

图2
  1. Debugger

直接或者间接实现 JDI ,并使用 JDWP 定义的通讯规则发送或者接受来自 JDWP 的数据与命令。例如:JDB ,IDE 自带的调试工具。

  1. JDWP Agent

JJVMTI 的具体实现,开发时一般采用建立一个 Agent 的方式来使用 JVMTI,它使用 JVMTI 函数,设置一些回调函数,『接受或发送来自 Debugger 的数据与命令,并通过这些数据与命令去获取或者操作被调试虚拟机的运行状态』。

在Java 虚拟机启动时可以选择加载的 JDWP Agent ,例如,进行远程调试,我们需要指定加载jdwp:

$java -agentlib:jdwp=transport=dt_socket

上述参数,不仅指定需要加载 JDWP Agent ,并且指定了 JDWP Agent 与 Debugger 间使用socket进行通讯。

  1. JDWP

JDWP 规定了 JDI 与 JVMTI 之间的通讯协议,JDWP 并不包含传输层的实现,因此 JDWP 数据可以使用任意的传输方式传输,只需要数据格式满足 JDWP 所规定的格式即可。

  1. Target JVM

被调试的虚拟机。

JDWP协议

和Http协议一样,JDWP协议同样有握手和应答。

通讯握手

JDWP协议的通讯过程,由一个简单的握手开始,如下图所示:

image

Debugger 发送字符串”JDWP-Handshake”到 Target Java 虚拟机

Target Java 虚拟机回复”JDWP-Handshake”,握手成功

在 Target JVM 启动时,可以选择『监听指定调试端口』也可以将自己『直接连接到已有的调试端口』上去。再来看看上面那条长长的指令

$java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 -cp class/ com.example.www.TestMain

server参数用来控制启动选项,y表示监听指定『调试端口』,n则表示连接到已有的『调试端口』。

suspend参数则是是否在调试开始前暂停虚拟机。

所以,上述的指令表达的意思为:

运行 TestMain.class,并且监听8000这个调试端口,当外部(例如:jdb)向此端口发送一个"JDWP-Handshake"时,就表示对方在请求作为当前运行的虚拟机的调试端。希望了解更多相关内容的同学,可以参考官方文档

通讯数据包

握手完成后,debugger 就可以与 target Java 虚拟机相互发送数据了。JDWP中的数据包分为两种:命令数据包(CmdPacket)、回复数据包(ReplyPacket)。

CmdPacket

首先看 CmdPacket 的结构:

typedef struct {
    jint len;          // packet length
    jint id;          // packet id
    jbyte flags;      // value is 0
    jbyte cmdSet;      // command set
    jbyte cmd;          // command in specific command set
    jbyte *data;      // data carried by packet
} jdwpCmdPacket;

image
  • Length :是整个packet的长度,包括 length 部分。因为包头的长度是固定的11bytes,所以如果一个command packet没有数据部分,则length的值就是11。
  • Id :是一个唯一值,用来标记和识别reply所属的command。Reply packet与它所回复的command packet具有相同的Id,异步的消息就是通过Id来配对识别的。
  • Flags :目前对于command packet值始终是 0。
  • Command Set :相当于一个command的分组,一些功能相近的command被分在同一个Command Set 中。

Command Set的值被划分为 3个部分:

0-63: 从 debugger 发往 target Java 虚拟机的命令

64 – 127: 从 target Java 虚拟机发往 debugger 的命令

128 – 256: 预留的自定义和扩展命令

ReplyPacket

再看看 ReplyPacket :

typedef struct {
    jint len;          // packet length
    jint id;          // packet id
    jbyte flags;      // value 0x80
    jshort errorCode;      // error code
    jbyte *data;      // data carried by packet
} jdwpReplyPacket;
image

Flags : 目前对于 reply packet 值始终是0x80。我们可以通过 Flags 的值来判断接收到的packet 是 command 还是 reply 。

Error Code : 用来表示被回复的命令是否被正确执行了。零表示正确,非零表示执行错误。

Data : 内容和结构依据不同的 command 和 reply 都有所不同。比如请求一个对象成员变量值的 command ,它的 data 中就包含该对象的 id 和成员变量的 id 。而 reply 中则包含该成员变量的值。

读者如果希望更深入了解JDWP 协议,推荐阅读:JDWP 协议及实现

Android 调试原理

分析完了 Java 的调试原理,下面接着分析Android 调试原理 。Android的调试原理与Java的调试原理相比,要稍微复杂一些。

image

上图是adb的结构图:

Host 为PC端,在PC端运行着 Adb server 与 Adb clients, 同时运行着手机模拟器( Emulator )。

Target device 为手机, 无论是手机或者是手机模拟器,都运行着 Adbd (Adb daemon)和虚拟机(黄色的椭圆)

在Java的调试中,JDI 与 JVMTI 之间使用 JDWP 协议通讯来完成调试工作。在 Android 的调试>中,Adbd 与 虚拟机也是采用 JDWP 进行通讯,所以 ”Adbd 同 jdb 类似都是 JDI 的具体实现“。

构成介绍

Adb server 启动以后会一直监听本地的 5037 端口。adb client 通过本地的随机端口与 5037 端口建立连接。一个PC可以连接多台手机设备或虚拟机,一个手机也可以同时连接多台PC,这些设备的连接管理由 Adb server 完成。

Adb clients 可视为一个shell窗口(当使用 adb shell 命令时,可创建一个客户端)。当在执行输入 adb shell命令时,客户端会开启一个随机端口去与 5037 端口进行通讯,完成连接本地的服务端程序。如果 Adb server 没有启动,则启动一 Adb server 服务端程序。

image


如图,在运行 adb devices 命令时启动了 Adb server ,并开始监听 5037 端口,当运行 adb shell 命令后,再查看端口占用情况,可以看到 5037 端口与 53094 端口建立了连接,当关闭 adb shell 窗口后, 53094 端口关闭,这个 53094 端口即为上面所说的 Adb Client 产生的随机端口。

Adb daemon(adbd) 在模拟器或移动设备上运行的后台服务。当 Android 系统起机的时候,由 init 程序启动 adbd 。如果 adbd 挂了,则 adbd会由 init 重新启动。换言之,只要 Android 系统在运行,那 adbd 就是“不死的”,常年在伺服状态

通讯介绍

  • Adb clients 采用特定的格式的数据向 Adb server 发送各类命令

这些数据的格式为:Length(4字节) + commend

例如: 000Chost:version 中 000C 表示命令长度,实际命令为 host:version

Server收到Client的请求后,返回的数据遵循如下格式:

如果成功,则返回四个字节的字符串”OKAY“

如果失败,则返回四个字节的字符串”FAIL“和出错原因

如果异常,则返回错误码

当 Adb Client 发送命令并收到 Adb Server 返回的“OKAY”回复后,就可以继续发起操作命令了。

image
  • Adb Server 与 Adb daemon 之间采用的是 [『transport 协议』], Adb daemon 在手机(或模拟器)上启动后将一直监听手机(或模拟器)的 5555 端口。Adb Server启动后会试图与 5555 端口进行通讯,通讯采用无线(TCP协议)或者USB完成传输数据。

总结

至此 Android的调试原理介绍完成,在调试过程中,需要值得注意的是:

  • 开发可以设置断点位置外,还可设置源码路径。因此,在调试的时候运行的程序可以不是实际的源代码,只要 断点信息 相同,就可以进行调试。
  • 断点信息:行断点的信息(由包名、类名、行号组成);方法断点的信息(由包名、类名、方法名组成)
  • 方法断点相对于行断点比较重量级,在调试的时候如果发现程序运行非常缓慢甚至无响应,可以去掉所有的方法断点, 效果立竿见影。(如下图中的 Java Method Breakpoints)


    image

    (文章中的错误与不足之处还请广大读者评论留言,一起讨论,一起进步:-)

推荐阅读

Java Platform Debugger Architecture

Java Platform, Standard Edition Tools Reference

Java: slow performance or hangups when starting debugger and stepping

adb原理

JDWP 协议及实现

JPDA 体系概览

Android虚拟机调试器原理与实现

【Android】ADB工具原理探究

adb client, adb server, adbd原理浅析

Android中ADB-server、ADB-client和adbd的简介

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271