干货 | 那些很难接触到的移动端APM产品研发技能

如需转载请联系听云College团队成员小尹 邮箱:yinhy#tingyun.com

听云研发总监江赛于10月20日在QCon2016上海站发表了题为《移动端APM产品研发技能》的演讲,现场介绍移动端APM产品底层技术细节与实现方法。

先自我介绍一下,我是来自听云的江赛,在听云主要负责移动端的整个SDK开发工作,因为听云的产品是为开发者服务的,所以相当于是面向技术工作者提供的一个技术平台。对技术要求比较高,今天有很多底层技术细节拿出来跟大家分享,可能是大家在自己的工作当中接触不到的也很难接触到的一些东西,希望对大家有所启发。

1.jpg

上图是一个普通的APM产品所具有最基本功能模块,所谓APM产品也就是做应用程序的相关分析。要做性能分析,第一个前提是什么?必须有采样的数据,而且必须是实时采集的。那么,我们现在需要采集什么的维度数据呢?所以需要在这样几个纬度展现:第一、网络数据;第二、交互行为;第三、稳定性能以及其他。

我们采集到这些数据以后会在不同维度下面去展现。想要看一个运营商的特殊接入方式、网络表现情况,想看某一种设备交互行为怎么样,在某一种设备上卡顿行为多吗,如果可以做到这些维度的展现,就能算是一款APM产品。但是会有一个问题,我们怎么采集这样多的数据?这个数据量非常大。举一个简单的例子,用户类似滑动这样的基本操作很多,我们不可能在每一个方法上都手工加一个点,也不可能在每一个调用里面加点,网络层面也是一样的。还有就是更专业的数据怎么采集?更别说我们采集数据都是真实的数据。

像稳定性这样的数据采集,有一些开源的项目是可以支持,类似崩溃、ANR这些东西采集难度不大,最重要就是这个上面这些数据。而且我们现在不仅可以做到数据采集工作,采集的过程也不需要开发人员,不需要程序员做任何的事情。所有的数据采集工作都是自动做的,包括在一些特定位置打点。所以我今天要讲的就是这一块儿就是APM是怎么工作的?如何采集到这么多数据的?这个是很多用户关心的地方。你在我们的APP里面采集这么多数据,会不会有安全的问题?或者产生其他性能问题?把技术细节分享出来以后,其实你会发现这些问题都不会存在。怎么做到刚刚说的维度数据采集,而且是自动采集呢?通过什么手段实现这个技术呢?基本就是下面几种:

2.jpg

那么,下面我们就开始来对每一个技术细节进行展开。

1APM实现——Bytecode

安卓程序员每天都会写大量的java代码,从java源代码到dalvik-bytecode,这个当中发生了什么事情?来看一下这个过程。

3.jpg

编译过程当中第一步,就是生成class文件,这个就是java bytecode,java bytecode就运行在java的虚拟机上面,安卓虚拟机没有办法跑java bytecode。下一步通过DX生成一个Dex文件。拿到源文件以后反编译成java代码,变成dex文件。然后,通过一些反编译工具,可以把dex转成java文件。这就是一个简单的编译过程。但是,这个东西看似很简单,如果你了解这个编译过程,就可以做很多的事情。 class文件生成了以后,还没有转成dex文件这一步。我们可以通过ASM技术,对java bytecode进行改写,从而插入要监控的代码。

下面举一个具体的例子

4.jpg

5.jpg

这是一个很简单的java源代码,里面有一个方法,方法只是对两个调用的参数做乘积,然后返回。这是一个很简单的方法。我们下一步通过javaC对这个方法进行编译,然后用javaP可以看一下java bytecode的指令。这就是一个很简单的java bytecode的指令,可以看一下,取得两个参数,然后做乘积。imul指令就是java bytecode的一个基本指令,之后就是把两个参数压栈,imul指令会pop出栈底两个数。

因为java bytecode没有办法在安卓手机上面跑,接下来要怎么做?需要将java bytecode继续通过DX把它编译成dalvik bytecode。很多时候大家都是通过一些编译工具来编译,没有手工做过这些编译,建议可以手工做一下。通过DX就可以把class文件编译成一个dex文件,然后通过dexdump命令,把DEX文件整个dump出来。大家可以发现,刚才的java bytecode里的几行乘法指令,在这儿就变成一行指令了。

大家第一眼看会产生有什么感觉?首先,指令长度变小了,第二dalvike bytecode引入了寄存器的概念。而java bytecode的函数调用全部是通过栈来模拟的。这种方式对这个代码性能,以及代码结构大小有影响的,寄存器本身的性能要比栈高很多。

再看一下,刚刚那三行代码两次pop操作,一次乘积,一次push操作,变为这样一个操作。就是这个指令,这是目标计算器,源计算器,操作完以后,存在源计算机,就是变成这种形式了。

6.jpg

下面做一个对比:

7.jpg

刚刚这样的方法, java bytecode和dalvik bytecode的展现形式是不一样的,差距比较大。有时候面试一些程序员的时候,会问他java bytecode和dalvik bytecode有什么区别?其实有很多程序员是背下来的,就是找了一些资料背下来的。如果你实际的把这些指令比较过就可以理解了。理解通了就可以回答几个基本点,一个用栈,一个是用寄存器。就是说,所以,个人觉得学技术还是多动手。

这些对于自动插码技术有什么作用?前面提到的那个指令级插码又有什么作用?这些是基本工作,首先对于java bytecode要非常的熟悉,之后我们要了解整个编译过程。

8.jpg

这个代码就是我们通过动作分析java bytecode注入的,反编译出来就是这样的。我们需要分析一些关键的方法,还有特定方法,找到函数的头和尾,插入需要的代码,第一步为获取开始时间。第二,获取完成的时间,之后进行上报。如果像做一些错误处理,会对异常进行捕捉,我们可以自动分析你的Bytecode来做注入。

还有一个特殊的情况,我想要监控的是这个调用,或者说监控这个调用的反馈值,这些情况都是有的。但是,所有的变化都是基于对bytecode上下文的理解。插入对应的指令。这个其实已经是非常成熟的产品了。还有一个开源项目可以试试,就是ASM。

接下来的这个是通过一些smali反编译工具,转成smali文件,静态分析这些文件,然后重新打包,加一个名就可以用了。

9.jpg

这些大家都不陌生,写APP开发很多时候这些东西会帮助你分析一些事情。同样你也可以借鉴一些新的思路,通过这种方式分析APK。你认为存在恶意行为就分析一下。另外还可以做动态调试,把一些参数打出来。比如说我自己写了一个工程,网上可以看到类似的一些工程,可以做一个定制,写一个简单的SDK。分析一个APP的时候,需要分析其网络行为,就会把SDK注入进去,然后打包,之后看网络访问过程当中访问的什么主机,IP。如果是做加密,那就是另外一个话题需要对流做解密,普通来说传输什么数据都是可以看到的。

2APM实现——hook

下面讲一下hook,这个就是C和C++代码怎么做的问题,做到这个程度需要几个前提。因为大家都知道现在手机基本都是ARM架构的CPU,本身又分arm 32,thumb, thumb2, arm64指令。现在新的手机都是跑在arm64的CPU上。那么,对于这些代码,最后编译完以后都是机器指令。举一个例子,你们有个SO库,可以用一个16进制的编辑器,有一个特别好用的工具,叫做010editor。你可以看到,打开了以后又转成16进制,就是一串数字。

所以,我们现在做什么?我们现在会自动地把这些16进制,根据这个指令格式找到对应指令然后去做分析。看一下这个图

10.jpg

这是一个普通的调用关系,调用者调用被调用者。我们监测这个被调用的方法,想要拿到参数,还有这个方法执行多长时间,我们还想知道这个返回值,怎么做到?其实逻辑上很简单,我们把被调用方法头几行指令做一些修改。然后,把指令改成JMP指令,JMP到这个监控方法里面,通过hook的方式来做跳转。这里做一些参数、相关函数的记录,做完以后再重新按照这个轨迹返回。

那么,我们怎么做到这一步呢?首先,把头几行做跳转。需要对ARM指令,对各种架构比较熟悉才可以做到。其实大部分的程序员学过汇编指令,遇到的时候心里面发触,觉得很复杂。实际上并不是复杂,就是接触的少,其实ARM 32指令就是这样几个。根据后面3位,4位可以做区分。还有一些分值指令,数学预算指令。那么,我分析这些指令的时候,首先对于指令架构很熟悉,而且,要知道这个源计算机,目标计算器在哪里?比如说,最终跳转指令的时候,要知道跳转怎么计算,24位offset怎么跳转,24位怎么转换为绝对地址?如果把基本东西弄明白了,不要求会写,就可以做下面的事情了。

先看一下刚刚说的方法怎么做到的?

我们需要改写这个方法的头两行指令,头两行指令替换成这样的指令。PC指令就是当前运行时的逻辑地址,PC寄存器。因为arm 32做一个预加载,这个是会指向下两行指令。如果将PC指令减4,就是变为PC加4,这个操作是把下一行指令弄到PC寄存器中。如果你改写PC寄存器就实现了跳转,虽然只有两行代码,但是可以想到这一点其实要花很长的时间。需要了解arm指令,知道这个arm指令执行的过程,还要知道通过修改PC指令实现跳转。通过这种方式就可以实现跳转,头两行指令,通过改写,就可以把它跳转到任何地址。而且,这个地址就是4字节,32位,4G空间。可以跳转到任何函数。这就结束了吗?没有。把后两行做了以后,头两行挪到另外一个地方。但是,把指令去挪的时候因为一些指令本身就是依赖PC指令,所以就要去做指令的修复。更多的工作其实就是在修复这些被挪走的指令。这个例子是一个B指令修改,是写实际代码弄下来的一部分。

11.jpg

12.jpg

这一行代码什么意思呢?我们看一下,123,2个0是8位,8123,高位是0,0,F。如果是31到32位,我们现在取的值是哪一个?实际上就是取这4位,1234,取4位的值,通过这一行指令取这个值。通过4个值区分这些指令类型。取出来了以后,如果是这种这个是A,可以看一下这个B指令的这个方式,1010,这个是1010,一个是1,就是BR指令,跳出去再跳回来。如果无条件这里就是0,1010正好是多少就是10,就是A,如果是B指令。B指令跳转依赖寄存器,首先算出来这个地址,把绝对地址存在这里,头一行指令在这里。

13.jpg

如果真正把这个弄明白,你们去弄C代码可以做到,不难。如果做到这样觉得很有成就感,把系统的malloc,或者是new给hook住,可以监测所有的native内存申请和释放。这样的事情本身还是很有意思的。可以实际试下。

把hook技术应用在产品上面,我发现很多的产品是依赖这个技术的。比如做安全,他们很多产品也是通过这种方式做的。还有通过这种方式来做一些底层资源修改还有调度,这个可以用在很多的方面来做。怎么说呢?就是技术是为了产品服务的,只是把技术给弄明白就可以了,最终还是会产品化。像我这种做很多年技术的人切身的体会。有时候也是会沉迷在技术里面,总觉得做一些产品的工作就是浪费时间。现在想想,不是这样的。

最后一点,前面讲的这些,都是一些自动嵌码技术,就是Java应用,还有C++应用。这些数据都是自动采集的。编译的时候插码,还有运行时候的hook,这些都可以做,产品已经很成熟了。听云现在跑5亿终端,有一些大的电商类也已经在用听云的SDK。举一个例子,想通过听云对TCP层的监测结果来看一下那个负载均衡调度情况,同一个主机有一堆IP,正常情况下是没有办法拿到这个结果的。而我们不仅可以拿到DNS时间,还可以拿到DNS结果,真实IP是什么,通过这些情况可以看到负载均衡服务器,就是调度出来的结果什么情况以及IP分布情况,另外还有tcp三次握手时间,ssl握手时间等等。

这些数据其实都非常的有用。安卓程序员经常纠结使用哪些网络库,是urlconnection,还是okhttp。分别都有什么优缺点。这个我们就给你们做了一个强大的技术验证。第一个问题,比如说,我在程序里面连着发了10个request。现在http访问的传输层都是基于TCP,每发一次request都要做一次tcp连接吗?大家想一想,因为对于同一个地址肯定没有必要。这样做就是浪费时间。这个就是tcp复用技术,通过我们的技术,就可以监测对于一个同一个目标地址发生多少次的tcp connect操作,这个访问时间内就知道有没有复用之前的连接。所以,就可以出一个指标数据,就是发生了多少次tcp连接。

通过这种技术可以监测一些关键指标数据,因为采取底层原数据,很多点把这个原数据还原出应用场景,客户想出来的场景比我们多。这些原数据都是最宝贵的数据,并且最关键的是我们不需要你再去做额外的工作,也正是APM的价值所在。


想阅读更多技术文章,请访问听云技术博客,访问听云官方网站感受更多应用性能优化魔力。

关于作者

许小午

不是一个没有故事的女同学

我要评论

评论请先登录,或注册