张墨轩的技术宅

不忘初心,方得始终

APK【Volume Power】逆向全过程

之前对音量键点亮屏幕比较感兴趣就分析了一个这样功能的APP,现在又分析了另一个名叫Volume Power的APP,感觉比较坑爹,权且当学习吧。我们开始,先看看包结构,如图:
文件很少,非常简单,唯一一个可疑的是empty.mp3文件,这个文件稍后分析中会提到。运行后主界面如下:
就是一个设置界面,有三个设置选项:
一:启用还是禁用音量键点亮屏幕
二:是否启动运行
三:通过通知栏的按钮关闭屏幕
接下来正式开始分析,这里只会讲重点部分,其他可以自己看代码。按惯例先看AndroidManifest.xml文件内容:
这里注册了几个广播接收器,还有一个服务。其中:
android.media.VOLUME_CHANGED_ACTION 会在调节音量的时候被触发。
android.intent.action.SCREEN_OFF 在关闭屏幕的时候触发
android.intent.action.SCREEN_ON   在点亮屏幕的时候触发
android.app.action.DEVICE_ADMIN_ENABLED 将程序设置成Android设备管理器或者取消的时候被触发
android.intent.action.BOOT_COMPLETED 系统启动后自动运行
其他都先不关注,按程序执行流程先看com.example.stayawake.BootReceiver,它主要是创建一个线程并且执行,如图:
那么线程具体做了什么,接着看 BootReceiver$1类,这个是个匿名类,如图:
这里注解的很清楚,这是一个内部类,没有名字,它存在于BootReceiver.onReceive中,线程主要就是运行起了SoundService服务:
然后我们来看看这个服务,这个服务最主要的作用就是做了一件坑爹的事,如图:
就是不停的循环播放empty.mp3文件,而empty.mp3实际上是一个空的没有声音的文件,所以用户并不知道在播放声音,但是后台确实一直在播放,那就意味着运行了这个程序后,电量将飞快的被消耗掉,真是坑爹啊。那为什么要这样做呢,主要目的就是保持这个APP一直存在于内存中不被kill掉,如果被kill了功能就丧失了,因为在Android系统内存低时系统会根据一些策略kill掉一些进程来腾出内存,所以作者这里用了这样一种坑爹的方式来保证APP能一直运行。
接下来当用户点击音量键的时候,android.media.VOLUME_CHANGED_ACTION所对应的广播接收器VolumeReceiver.onReceive会被执行,然后事情就简单了:
直接调用系统的电源管理器点亮屏幕。另外程序为了能有关闭屏幕的权限会在设置界面将自己提升为Android设备管理器:
好了,基本就是这样,这个APP非常简单也没什么太多要说的,具体的可以自行查看源代码。
全部代码已经上传到github上,源代码的github地址:
https://github.com/phonegapX/com.teliapp.powervolume


APK【解放电源键 1.6.0】逆向全过程

android手机在双击点亮屏幕功能出来以前,为了点亮屏幕得频繁的使用电源键,时间长了电源键容易损坏,所以聪明的人们就开发出用音量键来点亮屏幕的apk程序,安装这样的apk后,点击音量+键或者音量-键都可以点亮屏幕,这样就大大提高了电源键的使用寿命。最近突然对这类功能有点好奇,所以利用空闲时间研究了其中一个apk,叫做【解放电源键 1.6.0】,看了软件的关于信息,作者叫王龙,向他致敬。另外此程序需要root权限支持。首先来看看包结构,如图:
其中比较可疑的是assets/athena.dat文件,经证实这是一个可执行文件,只是取了一个.dat的后缀名迷惑人而已,利用ps查看进程可以得知,如图:
另外利用cat查看内存布局也确定lib/armeabi/librpkjni.so文件也已经被加载,如图:
那么到此分析的重点就在classes.dex,librpkjni.so,athena.dat三个文件上面。
对于dex文件有三个比较好的工具进行反汇编。
一个是 baksmali 或者直接用apktool(内部集成了baksmali ),它会将dex反汇编成smali文件。其实就是文本文件可以直接查看,我用设置了语法高亮的Notepad++查看。效果如图:
二个是用JEB,这是个收费软件 官方网站以前是http://www.android-decompiler.com/ 后来改成了https://www.pnfsoftware.com/ 效果如图:
三是用IDA,IDA Pro从6.1版本开始支持Android。包括Dalvik指令集的反汇编、原生库(ARM/Thumb代码)的反汇编、原生库(ARM/Thumb代码)的动态调试等,IDA 6.6新添加了对dex文件的动态调试支持,具体可以查阅相关文档。效果如图:

这三种工具对Dalvik指令的语法解析上都有些许不同,至于选择何种工具,可以根据自己的喜好来。 而对于librpkjni.so和athena.dat这2个原生程序的反汇编那毫无疑问肯定是用IDA了。
接下来正式进入流程,这里我们只关心关键点,至于其他的就不做过多说明了。通过分析AndroidManifest.xml文件可以得知入口activity是 com.wujianai.rpk.activity.Main,另外有一个服务
是 com.wujianai.rpk.service.RPKService。先看 com.wujianai.rpk.activity.Main的onCreate函数:

.method public onCreate(Bundle )V
          .registers 8
          .param p1, "savedInstanceState"
          .prologue
////////////////////////////////////////////////////
代码省略。。。。。。
////////////////////////////////////////////////////
000000A0  iget-object              v3, p0, Main->serviceIntent :Intent
000000A4  iget-object              v4, p0, Main->serviceConnection :ServiceConnection
000000A8  const/4                  v5, 0x1
000000AA  invoke-virtual           Main->bindService (Intent, ServiceConnection, I)Z, p0, v3, v4, v5
000000B0  invoke-direct            Main->hasRoot ()Z, p0
000000B6  move-result              v3
000000B8  if-nez                   v3, :C2
:BC
000000BC  invoke-direct            Main->doNoRoot ()V, p0
:C2
000000C2  return-void
.end method
这里会启动RPKService服务,同时会调用hasRoot函数检查是否有root权限。
来看看hasRoot函数是如何检查是否有root权限的:
.method private hasRoot()Z
          .registers 8
00000000  const/4                  v6, 0x0
          .prologue
00000002  const/16                 v4, 0x400
00000006  new-array                v0, v4, [C
:A
          .local v0, buf:[C
0000000A  invoke-static            Runtime->getRuntime ()Runtime
00000010  move-result-object       v4
00000012  const-string             v5, "su -c ls"
00000016  invoke-virtual           Runtime->exec(String )Process, v4, v5
0000001C  move-result-object       v2
          .local v2, exec:Ljava/lang/Process;
0000001E  new-instance             v3, InputStreamReader
00000022  invoke-virtual           Process->getErrorStream ()InputStream, v2
00000028  move-result-object       v4
0000002A  invoke-direct            InputStreamReader-><init> (InputStream)V, v3, v4
          .local v3, r:Ljava/io/InputStreamReader;
00000030  invoke-virtual           InputStreamReader->read([C)I, v3, v0
:36
00000036  move-result              v4
00000038  const/4                  v5, 0xFFFFFFFFFFFFFFFF
0000003A  if-ne                    v4, v5, :42
:3E
0000003E  const/4                  v4, 0x1
:40
00000040  return                   v4
:42
00000042  move                     v4, v6
00000044  goto                     :40
:46
00000046  move-exception           v4
00000048  move-object              v1, v4
          .local v1, e:Ljava/io/IOException;
0000004A  move                     v4, v6
0000004C  goto                     :40
          .catch IOException {:A .. :36} :46
.end method

原来是看"su -c ls"命令能否执行成功来判断的:)
 
接下来看看RPKService服务启动后会干些什么:
.method public run()V
          .registers 6
          .prologue
00000000  new-instance             v1, StringBuilder
00000004  invoke-direct            StringBuilder-><init> ()V, v1
:A
          .local v1, res:Ljava/lang/StringBuilder;
0000000A  invoke-static            Runtime->getRuntime ()Runtime
00000010  move-result-object       v2
00000012  new-instance             v3, StringBuilder
00000016  const-string             v4, "su -c ./"
0000001A  invoke-direct            StringBuilder-><init> (String)V, v3, v4
00000020  iget-object              v4, p0, RPKService$3->val$file :File
00000024  invoke-virtual           File->getAbsolutePath ()String, v4
0000002A  move-result-object       v4
0000002C  invoke-virtual           StringBuilder->append (String) StringBuilder, v3, v4
00000032  move-result-object       v3
00000034  invoke-virtual           StringBuilder->toString ()String, v3
0000003A  move-result-object       v3
0000003C  invoke-virtual           Runtime->exec(String )Process, v2, v3
:42
00000042  return-void
:44
00000044  move-exception           v2
00000046  move-object              v0, v2
          .local v0, ex:Ljava/lang/Exception;
00000048  const-string             v2, "wanghelong"
0000004C  invoke-virtual           StringBuilder->toString ()String, v1
00000052  move-result-object       v3
00000054  invoke-static            Log->e(String , String)I, v2, v3
0000005A  goto                     :42
          .catch Exception {:A .. :42} :44
.end method

.method public runScipt()V
          .registers 5
          .prologue
00000000  new-instance             v0, File
00000004  const-string             v2, "bin"
00000008  const/4                  v3, 0x0
0000000A  invoke-virtual           RPKService->getDir (String, I) File, p0, v2, v3
00000010  move-result-object       v2
00000012  const-string             v3, "athena.dat"
00000016  invoke-direct            File-><init> (File, String)V, v0, v2, v3
          .local v0, file:Ljava/io/File;
0000001C  new-instance             v1, RPKService$3
00000020  invoke-direct            RPKService$3-><init> (RPKService, File)V, v1, p0, v0
          .local v1, thread:Ljava/lang/Thread;
00000026  invoke-virtual           Thread->start()V, v1
0000002C  return-void
.end method
.method private static copyRawFile (Context, InputStream, File , String)V
          .registers 10
          .annotation system Throws
              value = {
                  IOException,
                  InterruptedException
              }
          .end annotation
          .param p0, "ctx"
          .param p1, "inputStream"
          .param p2, "file"
          .param p3, "mode"
          .prologue
00000000  new-instance             v2, FileOutputStream
00000004  invoke-direct            FileOutputStream-><init> (File)V, v2, p2
          .local v2, out:Ljava/io/FileOutputStream;
0000000A  const/16                 v3, 0x400
0000000E  new-array                v0, v3, [B
:12
          .local v0, buf:[B
00000012  invoke-virtual           InputStream->read([B)I, p1, v0
00000018  move-result              v1
          .local v1, len:I
0000001A  if-gtz                   v1, :7C
:1E
0000001E  invoke-virtual           FileOutputStream->close()V, v2
00000024  invoke-virtual           InputStream->close()V, p1
0000002A  invoke-static            Runtime->getRuntime ()Runtime
00000030  move-result-object       v3
00000032  new-instance             v4, StringBuilder
00000036  const-string             v5, "chmod "
0000003A  invoke-direct            StringBuilder-><init> (String)V, v4, v5
00000040  invoke-virtual           StringBuilder->append (String) StringBuilder, v4, p3
00000046  move-result-object       v4
00000048  const-string             v5, " "
0000004C  invoke-virtual           StringBuilder->append (String) StringBuilder, v4, v5
00000052  move-result-object       v4
00000054  invoke-virtual           File->getAbsolutePath ()String, p2
0000005A  move-result-object       v5
0000005C  invoke-virtual           StringBuilder->append (String) StringBuilder, v4, v5
00000062  move-result-object       v4
00000064  invoke-virtual           StringBuilder->toString ()String, v4
0000006A  move-result-object       v4
0000006C  invoke-virtual           Runtime->exec(String )Process, v3, v4
00000072  move-result-object       v3
00000074  invoke-virtual           Process->waitFor ()I, v3
0000007A  return-void
:7C
0000007C  const/4                  v3, 0x0
0000007E  invoke-virtual           FileOutputStream->write([B, I, I)V, v2, v0, v3, v1
00000084  goto                     :12
.end method

.method private initAssets ()V
          .registers 7
00000000  const-string             v5, "athena.dat"
          .prologue
00000004  invoke-virtual           RPKService->getAssets ()AssetManager, p0
0000000A  move-result-object       v0
          .local v0, assetManager:Landroid/content/res/AssetManager;
0000000C  new-instance             v2, File
00000010  const-string             v3, "bin"
00000014  const/4                  v4, 0x0
00000016  invoke-virtual           RPKService->getDir (String, I) File, p0, v3, v4
0000001C  move-result-object       v3
0000001E  const-string             v4, "athena.dat"
00000022  invoke-direct            File-><init> (File, String)V, v2, v3, v5
          .local v2, file:Ljava/io/File;
00000028  invoke-virtual           File->exists ()Z, v2
0000002E  move-result              v3
00000030  if-nez                   v3, :4A
:34
00000034  const-string             v3, "athena.dat"
00000038  invoke-virtual           AssetManager->open(String )InputStream, v0, v3
0000003E  move-result-object       v3
00000040  const-string             v4, "777"
00000044  invoke-static            RPKService->copyRawFile (Context, InputStream, File, String )V, p0, v3, v2, v4
:4A
0000004A  return-void
:4C
0000004C  move-exception           v3
0000004E  move-object              v1, v3
          .local v1, e:Ljava/lang/Exception;
00000050  invoke-virtual           Exception->printStackTrace ()V, v1
00000056  goto                     :4A
          .catch Exception {:34 .. :4A} :4C
.end method

.method private onServiceStart ()V
          .registers 1
          .prologue
00000000  invoke-direct            RPKService->initAssets ()V, p0
00000006  invoke-virtual           RPKService->runScipt ()V, p0
0000000C  return-void
.end method
总结起来就一句话:释放athena.dat这个可执行文件然后修改成可执行属性后再执行它。
RPKService服务还会启动一个线程:
.method public onStartCommand(Intent , I, I)I
          .registers 5
          .param p1, "intent"
          .param p2, "flags"
          .param p3, "startId"
          .prologue
00000000  invoke-super             Service->onStartCommand (Intent, I, I)I, p0, p1, p2, p3
00000006  iget-object              v0, p0, RPKService->thread :Thread
0000000A  invoke-virtual           Thread->isAlive ()Z, v0
00000010  move-result              v0
00000012  if-nez                   v0, :20
:16
00000016  iget-object              v0, p0, RPKService->thread :Thread
0000001A  invoke-virtual           Thread->start()V, v0
:20
00000020  const/4                  v0, 0x3
00000022  return                   v0
.end method
线程是在RPKService的构造函数里面定义的:
.method public run()V
          .registers 5
          .prologue
00000000  const/4                  v1, 0xFFFFFFFFFFFFFFFE
00000002  invoke-static            Process->setThreadPriority (I)V, v1
00000008  const/4                  v0, 0x0
:A
          .local v0, wakeLock:Landroid/os/PowerManager$WakeLock;
0000000A  iget-object              v1, p0, RPKService$2->this$0 :RPKService
0000000E  invoke-static            RPKService->access$3 (RPKService) RPKJNILoad, v1
00000014  move-result-object       v1
00000016  invoke-virtual           RPKJNILoad->fun1()V, v1
0000001C  invoke-static            Thread->interrupted ()Z
00000022  move-result              v1
00000024  if-eqz                   v1, :2A
:28
00000028  return-void
:2A
0000002A  iget-object              v1, p0, RPKService$2->this$0 :RPKService
0000002E  invoke-static            RPKService->access$4 (RPKService) PowerManager, v1
00000034  move-result-object       v1
00000036  const                    v2, 0x1000000A
0000003C  const-string             v3, "RPKService"
00000040  invoke-virtual           PowerManager->newWakeLock (I, String)PowerManager$WakeLock , v1, v2, v3
00000046  move-result-object       v0
00000048  const-wide/16            v1, 0x1388
0000004C  invoke-virtual           PowerManager$WakeLock->acquire (J)V, v0, v1, v2
00000052  goto                     :A
.end method
这个线程里面会调用librpkjni.so中的fun1函数,此时线程会阻塞在这个函数当中,一直等待,直到这个函数返回,线程就会调用PowerManager->newWakeLock 点亮屏幕(终于看到核心点了)。这里的PowerManager是电源管理器,在之前由getSystemService(Context.POWER_SERVICE)赋值。那么问题来了,librpkjni.so中的fun1函数内部到底做了什么呢?为了搞清楚这个问题我们接下来要开始分析librpkjni.so和athena.dat这两个原生程序了。
先看看librpkjni.so中的fun1函数(JNI相关的知识可自行查询资料),如图:
将其还原成伪代码大概就是下面这个样子:
其实本质上就是在做进程间通讯,和一个名叫com.my.MyService的服务通讯,而这个服务就是在athena.dat进程中实现的(android服务是一个复杂的系统,包含java层面的服务,原生层面的服务,服务管理器等等,具体的细节请自行查阅相关资料)。记住athena.dat进程是以root权限运行的,它会监听linux输入子系统,判断用户是否有按音量键,如图:
通过打开/dev/input/event系列设备进行监听,一旦用户按了音量键athena.dat进程就会捕获到,这样librpkjni.so中的fun1函数就会返回,应用中等待的线程就会继续执行调用
PowerManager->newWakeLock 点亮屏幕。完成一次点亮屏幕的过程。
最后总结一下:站在linux进程的角度来讲本程序运行后会产生2个进程,一个APK主进程,一个athena.dat进程(root权限运行),APK主进程中会调用librpkjni.so中的fun1函数与
athena.dat进程通讯并且一直等待通知。而在athena.dat进程中会监听linux输入子系统来判断用户是否按了电源键,如果用户按了电源键后 APK主进程中的librpkjni.so中的fun1函数
就会结束等待,代码就会执行到PowerManager->newWakeLock 从而点亮屏幕。具体详细可参阅源代码。
全部代码已经上传到github上,为了演示和编译的方便,源代码中进程通讯部分没有利用andorid的binder机制,只是简单的用了管道来实现,因为要实现
android服务需在android源代码环境下进行开发和编译,比较麻烦,所以就简单处理之。
补充说明:原版程序在android5.x的系统上是失效的,原因是athena.dat进程运行不起来,手动运行此文件会报错:error: only position independent executables (PIE) are supported.
原因是google给android5.0增加了新的安全特性,不支持PIE(position independent executables)的程序都不能运行。所以编译的时候要加上-pie -fPIE 的选项,这样才能在android5.x系统上正常运行。
源代码的github地址:
https://github.com/phonegapX/com.wujianai.rpk


android-arm逆向学习宏观知识点

一. linux原生层面:

包含三个层次:
1. c/c++语言层面
2. 汇编语言层面
3. 二进制指令层面
正向流程:
c/c++源文件==>gcc编译器==>s汇编源文件==>as汇编器==>包含arm二进制指令的elf格式文件[==>objcopy==>bin格式纯指令]
反向流程:
包含arm二进制指令的elf格式文件==>IDA, objdump==>arm反汇编输出==>人工阅读理解或者IDA-F5插件==>c/c++源文件
学习重点:
1. elf格式及其相关知识
2. 汇编语言(arm-asm语法,gun-asm语法)
3. arm指令二进制级别的编码和构成
4. 反汇编工具的学习和使用


二. android层面:
包含五个层次:
1. java语言层面
2. java汇编语言层面(暂称)
3. java字节码(二进制指令)层面
4. dalvik汇编语言层面(暂称)
5. dalvik字节码(二进制指令)层面
正向流程:
java源文件==>java编译器==>包含java字节码的class格式文件==>dx工具==>包含dalvik字节码的DEX格式文件
dalvik汇编源文件(smali格式的汇编语法)==>smali汇编工具==>包含davlik字节码的DEX格式文件
反向流程:
包含dalvik字节码的DEX格式文件==>baksmali工具==>dalvik反汇编(smali格式的汇编语法文件)==>人工阅读理解==>java源文件
包含dalvik字节码的DEX格式文件==>IDA工具==>dalvik反汇编(IDA格式的汇编语法)==>人工阅读理解==>java源文件
包含dalvik字节码的DEX格式文件==>dex2jar工具==>java字节码(多class文件打包进一个jar包)==>jd-gui工具==>java源文件
学习重点:
1. DEX格式及其相关知识
2. dalvik汇编语言(smali语法)
3. dalvik字节码二进制级别的编码和构成
4. 各种工具的学习和使用

补充:
android目前有两种执行环境:
1. dalvik运行时
2. art运行时
在Dalvik运行时中,APK在安装的时候,安装服务PackageManagerService会通过守护进程installd调用一个工具dexopt对打包在APK里面包含有Dex字节码的classes.dex进行优化,优化得到的文件保存在/data/dalvik-cache目录中,并且以.odex为后缀名,表示这是一个优化过的Dex文件。在ART运行时中,APK在安装的时候,同样安装服务PackageManagerService会通过守护进程installd调用另外一个工具dex2oat对打包在APK里面包含有Dex字节码进翻译。这个翻译器实际上就是基于LLVM架构实现的一个编译器,它的前端是一个Dex语法分析器。翻译后得到的是一个ELF格式的oat文件,这个oat文件同样是以.odex后缀结束,并且也是保存在/data/dalvik-cache目录中。
学习重点:
1. odex格式及其相关知识
2. ELF格式的oat文件相关知识


Android so 调试的几种途径

一:直接使用gdb进行调试,最原始。
二:用ndk-gdb工具进行调试。
三:在Eclipse中add native support后图形界面调试。
四:在Eclipse中利用Debug configurations=>c/c++ Remote Application进行调试(本质就是gdb调试,这时Eclipse作为gdb前端的一个图形外壳而已)
五:  利用IDA的远程调试功能
其中要考虑二种情况:
一:有源码,比如调试自己开发的程序。
二:无源码,比如逆向别人开发的程序。


关于ADB的一些有趣认知

当你有一台PC机假设是Windows系统,和一台android手机,你想让两者之间进行交流该怎么办呢?我们首先想到的肯定是通过wifi将二个设备连接到同一个局域网内,或者直接wifi-direct,但是可能的情况是环境里没有wifi,或者PC机压根就没有网卡等等情况,导致wifi连接并不可行。所以android手机和PC机默认连接方式是通过USB线缆将二者连接在一起,通过USB进行通讯。
OK,说到这里既然物理的通讯链路已经解决了,那么站在软件的角度来说是个什么样子呢?
ADB由两个物理文件组成:
     1. adb或adb.exe,运行于PC端,包括Linux、Windows、Mac OS等系统之中,通常是x86架构上。
     2. adbd,运行于Android设备的底层Linux之中,ARMv5架构上。
从运行实体角度来看adb主要分为三部分:
     1. adb client  (在adb.exe中实现,运行在PC机上)
     2. adb server (在adb.exe中实现,运行在PC机上)
     3. adbd (在adbd中实现,是一个linux deamon程序,它实现了adb service的功能,运行在android手机上)
交互流程:
      adb client  <==> adb server <==> adbd 
      [PC端]                 [PC端]                [android手机端]
下面为了行文方便,PC端统称为adb, 手机端称为adbd.
PC机上的adb(打开PC机的usb接口)与android上的adbd(打开手机的usb接口)进行通讯,从而达到PC机控制手机的目的。
我们来看看adb的功能,比较常用的功能如下:
第一:交互式shell。可以在PC机上登录到android手机(本质是linux),对android手机进行控制,就像通过ssh远程登录到linux下进行操作一样。其实原理上就是PC上的adb请求android手机上的adbd执行命令,然后把命令的执行结果返回给PC机上的adb,当然中间的通讯数据肯定是通过USB线缆进行传输的。
第二:文件传输。可以将PC上的文件push到手机上,也可以将手机上的文件pull到PC上。当然文件传输数据包同样是走的USB线缆。
第三:安装APK。原理上就是PC机首先把APK文件push到手机上,然后PC发<安装APK>的shell命令给adbd,说穿了还是adb与adbd通讯,通过USB线缆。
第四:调试辅助功能。这个功能是最让新手迷糊的一个功能,也是相对比较难理解的部分,所以要重点说说。adb是android debug bridge的缩写,证明这个程序本身就是以调试为重点的,既然是bridge,那就应当充当一个桥接的作用,事实上adb确实是起到了桥接的作用,接下来我们就来讨论一下。我们知道android上主要由两大类程序构成,JAVA程序和原生C/C++程序。下面分别讨论:

1. JAVA程序调试,了解JAVA调试体系的人都知道 JPDA(Java Platform Debugger Architecture),它是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP),以及 Java 调试接口(JDI)。
其中JDI由调试前端(front-end)实现,比如jdb工具,比如eclipseIDE,它的两个插件org.eclipse.jdt.debug.ui和org.eclipse.jdt.debug与其强大的调试功能密切相关,其中前者是eclipse调试工具界面的实现,而后者则是JDI的一个完整实现。JVMTI由后端(back-end)实现,比如android中的Davlik虚拟机就实现了JVMTI。 说穿了调试过程就是运行在PC上的前端(如jdb工具,eclipseIDE等) 与 运行在android手机上的后端(Davlik虚拟机)之间通过网络进行通信,通信用的协议就是JDWP。交互如下:

    jdb,eclipseIDE等 <==网络通信,用的JDWP协议==>  Davlik虚拟机 <==调试==> JAVA程序
   [PC机中]                                           [android手机中] 
从运行角度讲就是PC机中的jdb,eclipseIDE等进程与android手机中执行JAVA程序的Davlik虚拟机进程之间通过网络socket进行通信,其中Davlik虚拟机进程会bind并且listen某个端口作为调试通信用,而jdb,eclipseIDE等进程会去connect这个端口。

2.  原生C/C++程序,像有些APK中的so文件,android中的各种deamon进程都是属于这种,调试原生C/C++程序需要用到GDB,同样分为前端和后端,比如NDK中包含的
\toolchains\arm-linux-androideabi-4.6\prebuilt\windows\bin\arm-linux-androideabi-gdb.exe 就是一个前端 运行在PC机上,
而\prebuilt\android-arm\gdbserver\gdbserver就是一个后端,运行在android手机上,它是真正的调试器,用于调试原生C/C++程序。

运行在PC机上的arm-linux-androideabi-gdb.exe 与 运行在android手机上gdbserver 之间通过网络socket进行通信,其中gdbserver进程会bind并且listen某个端口
作为调试通信用,arm-linux-androideabi-gdb.exe进程会去connect这个端口。交互如下:

    arm-linux-androideabi-gdb.exe <==网络通信==>  gdbserver  <==调试==> 原生C/C++程序
   [PC机中]                                     [android手机中]        [android手机中] 

根据上面对JAVA程序调试和原生C/C++程序调试的分析我们可以得知,不管是哪种程序的调试,说的直白些,本质上就是 运行在PC中的A进程 与 运行在android手机中的B进程 之间的网络socket通信的过程。 既然本质是网络socket通讯,那问题就来了,前面已经说过PC和手机之间是通过USB线缆连接的,那怎么样才能进行网络socket通讯呢?ADB使用了一个巧妙的方法用USB模拟了网络通讯。

比如命令:
adb forward tcp:6100 tcp:7100 // PC上所有6100端口通信数据将被重定向到手机端7100端口server上

这个时候adb server会bind和listen 6100这个端口,PC上的其它应用程序,比如A程序,可以connect localhost:6100 ,adb server就会将这个连接请求通过USB线缆传给手机端
的adbd,adbd就会去connect localhost:7100,手机端的listen 7100端口的 B程序就会接到连接请求。交互如下:
A程序 ==连接本机6100==> adb server ==USB线缆==> adbd ==连接本机7100==> B程序
[--------------PC机中---------------]    [----------android手机中----------]
看上去就好像 A程序 直接与 B程序 通讯一样。
这样adb就完成了bridge的功能,通过USB线缆将原本不能进行socket连接的PC与手机,巧妙的桥接到了一起。
这种方法还有个特点:就是多条连接可以复用一条链路
比如android手机中 有三个程序 B1,B2,B3 分别listen 8001 8002 8003
那么我们可以
adb forward tcp:7001 tcp:8001 
adb forward tcp:7002 tcp:8002 
adb forward tcp:7003 tcp:8003
然后 PC机器中 假设有三个程序A1,A2,A3:
程序A1=>connect localhost:7001=>adb server=USB=>adbd=>connect localhost:8001=>B1
程序A2=>connect localhost:7002=>adb server=USB=>adbd=>connect localhost:8002=>B2
程序A3=>connect localhost:7003=>adb server=USB=>adbd=>connect localhost:8003=>B3
虽然实际有三条链路,但是却是共享了一条USB链路进行通信。
文章看到这里,估计就有很多小伙伴会在想,既然一切都是为了网络socket通信,那么如果PC机和手机已经连接到了同一个局域网内,双方之间完全可以直接进行网络socket通信了,是不是就不用那么麻烦了,是不是adb都可以不要了。从某种意义上来说,在这样的情况下 确实可以不用adb了,交互式shell可以直接在手机上装一个ssh server,然后PC上直接ssh,windows下还可以用SecureCRT等工具,文件传输可以直接SCP,调试直接可以用jdb,gdb和手机进行连接调试。事实上互联网上这方面的资料也有很多。但是你如果直接用eclipseIDE的话那还是需要adb,因为eclipseIDE内部会使用adb的功能。既然双方之间已经可以连通,这个时候adb如果再用USB线缆的话感觉有点多此一举,能不能不用USB线缆呢?答案是肯定的。adb支持用wifi连接代替USB连接。首先打开手机的wifi设置,使其连接到网络。然后,需要在手机上对adb连接端口进行设
置,这里需要有root权限的终端(terminal)应用,这种类型的应用在各个Market都有不少,选择一个适合的就可以了。
然后,在手机中打开这个终端(terminal)应用,比如输入如下命令:
        su
        setprop service.adb.tcp.port 5555
        stop adbd
        start adbd
接着,可以查看一下你的手机的IP地址,最后,在你的PC上,进入到<Android-SDK>/platforms目录,运行如下命令:
adb connect 手机的IP地址:5555 (例如:adb connect 192.168.1.115:5555)
如果前面的配置正确无误,则可以得到:connected to 192.168.1.115:5555的输出。
这样,就可以使用adb来对手机进行调试了,此时也可以在Eclipse的设备列表中看到已经连接的手机设备了。

从进程的角度来说 这个就是 运行在PC端的adb.exe进程和运行在手机端的adbd进程之间通过wifi建立的一条socket链路而已。但是站在adb的角度来说,我们要把这条socket链路看成是一条虚拟的USB线缆。要站在更高的高度来理解,这条socket链路就是一根USB线缆:)。 不管adb和adbd之间是通过何种模式通信,他们之间的行为都是一致的,不论是USB线缆链接,还是wifi socket链接。
最后,我们来看看android模拟器,在没有真实手机的情况下可以用android模拟器来调试程序,那么android模拟器和真实的android手机之间在连接方式上有什么区别呢?
android模拟器是qemu-base的,它运行起来后会绑定二个端口,绑定端口会从TCP:5554端口开始,比如机器上运行了2个模拟器,那么先运行的模拟器将绑定5554,5555二个端口,第二个运行的模拟器将绑定5556,5557二个端口,假如有第三个,第四个模拟器要运行就以此类推。其中奇数类的端口就是用于adb连接的。比如PC机上的ADB会去连接第一个模拟器进程的5555的端口,从而建立连接。同理,站在adb的角度来说,我们要把这条socket链路看成是一条虚拟的USB线缆。要站在更高的高度来理解,这条socket链路就是一根USB线缆:)。 
这里顺带讲讲偶数类的端口,比如模拟器绑定的5554端口,这个端口是用于控制台的,ddms会通过这个端口连接上模拟器,这样程序员可以通过ddms动态的设置模拟器中的电话状态,比如模拟一个电话呼入,可以设置GPS的数值,从而模拟手机在移动等等。


«12»

Powered By Z-Blog 2.2 Prism Build 140101

Copyright phonegap.me Rights Reserved.