首页
社区
课程
招聘
[原创]从华为方舟编译器看一种JavatoC语言解释器的实现
发表于: 2019-5-2 16:48 14948

[原创]从华为方舟编译器看一种JavatoC语言解释器的实现

2019-5-2 16:48
14948

(一)背景介绍

最近网络上流传的华为方舟编译器很火,随着热度的提高,华为也将技术原理做了简单披露。我们来看官方的描述,原文链接:https://baijiahao.baidu.com/s?id=1632292071046828780&wfr=spider&for=pc。此时此刻,这套编译器华为还没有开源。


作为安全行业的从业者,我想大家很清楚安卓下的所谓C语言就是JNI(JAVA Native Interface)。安全开发者为了实现关键的安全防护功能自我保护,我们通常将功能实现在JNI里,然后用NDK编译成so文件,再通过对so加壳或者虚拟机化,进一步自我保护,这样可以极大增强上图提到的“安全性”。上图传达出的第二条信息是,该编译器将java和C(JNI)编译成一套可执行文件,到底是什么可执行文件呢?我们再往下看


什么是干掉虚拟机?我们知道,语言分成编译型语言和解释型语言,前者的典型是C,苹果是ObjectC,编译器将C语言直接编译成机器指令后由CPU执行,后者的典型就是Java语言,即由JVM里的解释器来执行java字节码,这个解释器就是虚拟机的核心之一。

所以我们不妨大胆猜测:所谓“干掉虚拟机”,是指将java代码转换成C代码,即将解释执行转换成编译后执行,由于C的执行速度比Java语言快,因为编译执行总是快于解释执行,从而实现了性能的提升。


上图传达出的信号是:这套编译框架甚至还可以优化java语言的执行效率,也就是java代码的编译优化。

上图所反映的法律和知识产权问题,据说谷歌和华为真正的老板其实是同一群(个)人,不多谈。

所以总结下来:华为的所谓“方舟”编译框架是:输入用户源代码,先将其中java语言进行优化,之后将它转换为C语言(JNI),从而提高执行效率。所以这套编译器的本质,我的猜测是:JAVA to C,或者JAVA to JNI,方舟编译器的本质是,一个基于编译器的语言解释器。


其实国外有很多成熟的JAVA to C/Native编译框架,不论是商用的还是开源的,比如Toba,Vortex,Marmot,IBM High performancefor Java Compiler,TowerJ

,superCede等等,都可以是可以提供大量的借鉴的。搜索java native compiler了解更多。另外国内很多安全厂商,据说几年前就能提供类似的且比较成熟的方案,不排除华为“借鉴”了友商的方案,工程框架确定后,凭借数量众多的工程师和强大的执行力,自身的产业号召力和国家资本的帮助迅速将其商业化和产业化。但是能否如华为宣传的数据如此之漂亮, 实际性能,用户的实际体验究竟能提升多少,作为从业人员,我本人并不那么乐观。而且这种转换很有可能降低系统的稳定性和兼容性。


那么接下来才是本文的重点:作为技术人员,我们要如何实现这套编译器呢?在这里,我提出一种方案,可以完整覆盖上述的所有特性,一样可以实现华为所宣传的方案,希望能抛砖引玉,分享不同的思路的效果。条条大路通罗马。

(二)JAVA语言的编译优化

第一步要实现华为宣称的“一个好的编译器,开发者一行代码都不需要修改,性能提升10%-20%”。

绝大部分安卓APP超过99%的代码都是JAVA语言写的,所以对JAVA的优化尤为重要。我们使用soot框架来优化,Soot是什么呢?它是由加拿大麦吉尔大学Sable Reasearch Group维护的,用来优化java程序的一套成熟的框架,这套框架的文档非常之全,可以让新人快速上手。

正如上图所说:java有很多优秀特性,但执行效率不如C/C++,为了优化java执行效率,soot应运而生。

Soot的Github地址是https://github.com/Sable/soot,文档不限于以下https://github.com/Sable/soot/wiki/Tutorials

Soot的原理是什么呢?和LLVM相似,编译框架会将原始代码转换成一种中间语言。由于中间语言相当于一款编译器前端和后端的“桥梁”,无论是学习Soot还是LLVM,透彻了解他们的中间语言无疑是非常必要的工作。 Soot共有4种中间语言:BafBody,GrimpBody ,ShimpleBody  和 JimpleBody,每一种中间语言都可以进行优化。


BafBody:一种字节流形式的java字节码表示方法,易于操作和维护;

Jimple:一种规范的三地址码形式的中间表示;

Grimple:一种更加适合反编译的Jimple表示形式

对于Baf的表示如下所述

Baf是基于栈的表示,是一种较为初级的IR

在Jimple语言中,用的最多的就是statement型语句,这15种如上图所述。我们知道在java字节码中,总共有将近200多种指令,而现在的Jinple却只有15种statement,是不是大大简化了?并不是只有15中,而是其他种的指令要少很多,用到频率低。

那么soot的中间语言Jiimple长什么样的呢? 我们再看一个例子

这是转换之前的java源代码

这是转换之后的Jimple代码

是不是很容易就能看懂呢?我们可以简单总结一下Jimple的语法特性:

1.使用$开头的局部变量表示的是一个栈内的地址,它不表示一个真实的局部变量。而不以$开头的才是真正的局部变量。以$开头的变量由于不是原java代码里真实的变量,存在于堆中,所以是一种“堆媒介”

2类似int x = (f.bar(21) +a) * 长指令会被分隔成$i4 = $i3 + i0 和 i2 = $i4 * i1这样的三地址形式的指令。 什么是三地址码?就是形如x = y op z 型的指令

3参数值和this指针使用IdentityStmt形式的表达式被分配到一个具体的局部变量中,比如i0 := @parameter0: int; 以及r0:= @this: Foo。进一步讲,由于x := @parameter0符合形如identityStmt下的local := @paramrtern:type可知paramrter0就是第一个参数,local代表局部变量(或者立即数)了

要想继续推进,首先要对jimple代码足够熟悉。



上面的两个表,详细阐述了jimple语法的规范,从上面我们可以看到上面所说的15种statemnt和其他的几种指令,不得不说文档很齐全很细致,比我之前用的JS编译框架esprima好很多。总之只要根据官方提供的手册,可以很清楚搞清楚java和jimple的一一对应关系,自然就熟悉了Jimple语法。

这种形式的中间语言优势很多,因此也是很多自定义优化步骤和自由化工具处理的对象。我们的节点转换器也不例外,就是以Jimple代码为基础进行转换的

如果您熟悉编译器的优化,相信您已经被三地址码这种简洁又规整的形式吸引了。没错,但是soot还提供了一种更加适合编译优化的语言:shimple。它是一种静态单赋值型的语言(SSA),它保证了每一个局部变量只有一个赋值,将极大简化分析。除此之外,他和Jimple还有一个巨大的区别是它还引入了Phi节点,所以它是特别适合用来做优化和分析的一种语言了。但是这里我们不做过多介绍,编译优化,有大量成熟的算法可供借鉴,这里我们不多谈,我们还是选择Jimple作为转换对象.

关于Jimple的语法规范,官网上有详细的文档供查阅。

(三)JAVA to C


只要能将java字节码由解释器解释执行,转换为编译后直接由CPU执行,是不是自然就实现了上面的功能呢?就“干掉”虚拟机呢?但是怎么转化呢?由于前面已经能够将200多种字节码指令转换成20多种指令,如果我们将这不超过20种指令的JImple,做个等价转换,也就是一个节点(node)的解释器,是不是就可以实现JAVA toC呢?也就是说,首先将java代码进行简化,分成有限的几种指令,接着只要根据每一种指令,转换成JNI语法,最后再编译成so,让原本的java字节码指令转为执行so里的JNI,就实现了从java代码到C代码的转换

转换成JNI后的代码不仅更加安全,还能提高执行效率,和上述完全吻合!

现在我们简单的看一两个转换示例:

假设我们有一个被测试的类,如下所示:

把它转为jimple

现在开始转换,由于涉及很多技术细节,比较敏感不详细解释。首先,将类下的方法名称 转换成JNI能识别的形式,

JNIEXPORT jobject JNICALL Java_demo_have_1a_1try_main

我们看后面的Java_demo_have_1a_1try_main,静态加载时,由于需要具体指明类所在的路径,会采取这种形式,这样就免去了动态加载时使用JNIOnload注册函数步骤,但需要注意,要避免重名,命名的不合法,区别构造函数,,一些特殊字符也需要特殊处理等等。从函数名到JNI函数名要不断试错,找出合适的命名规则

然后在参数传入this指针,JNI环境env,对象jobject,JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。

简单提一句:如果方法名里有各种特殊字符呢?该怎么处理?如果方法名里有$,在啊JNI里等价的对应着_00024

然后就是参数的转换了。需要注意这个方法是不是重载方法,是不是静态方法。重载方法需要在JNI里体现区别,一般JNI方法都有两个固定的参数,一个是JNIEnv *env 还有一个是this指针,但this需要根据静态方法和非静态方法区分成jobject this 和 jclass this .因为静态方法只返回类本身jclass,而不是类的实例jobject。

至于传入的数据类型,我们可以看下面的表格,对照下表翻译,更详细的可以看jni.h文件,具体的JNI函数如何写,这里不详细讲解。

上述操作只是给JNI函数的壳,但里面的怎么写,还需要更加深入的了解Soot框架,我想挑几个典型的数据结构,简单介绍一下soot的使用


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2019-5-9 13:43 被r0Cat编辑 ,原因:
收藏
免费 4
支持
分享
打赏 + 5.00雪花
打赏次数 1 雪花 + 5.00
 
赞赏  junkboy   +5.00 2019/05/02 感谢分享~
最新回复 (39)
雪    币: 248
活跃值: (3789)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
根据方舟编译器的官宣ppt,工程编译生成apk后,apk里面没有dex,只有so
2019-5-2 17:10
0
雪    币: 11716
活跃值: (133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
支持         
2019-5-2 17:21
0
雪    币: 229
活跃值: (70)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
4
有技术前景
2019-5-2 18:49
0
雪    币: 10845
活跃值: (1054)
能力值: (RANK:190 )
在线值:
发帖
回帖
粉丝
5
如何验证方舟编译器在兼容性上的风险?
如果把这个编译器作用在安装apk的过程中,是否能有效降低兼容性风险?
2019-5-2 19:30
0
雪    币: 471
活跃值: (4063)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
6
等emui9.1正式上线 erofs+方舟编译过的程序
2019-5-2 20:48
0
雪    币: 62
活跃值: (89)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
7
关键字:llvm
2019-5-3 05:44
0
雪    币: 9479
活跃值: (757)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
这么发展下去,加固厂商还能活下去吗?
2019-5-3 05:57
0
雪    币: 248
活跃值: (3789)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
无边 这么发展下去,加固厂商还能活下去吗?
加固厂商也得跟着转变思路
否则当然活不下去
2019-5-3 21:28
0
雪    币: 916
活跃值: (3434)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
10

Java2JNI跟脱离虚拟机还是有很大差距的,Java Native Interface归根结底跑的还是虚拟机,除了运算指令,若是调用、变量读写等依然还是使用JNI,那所谓的性能提升其实很有限,执行时其实更多的可能是虚拟机对对象的操作令得耗时。

 

做了一个小测试,分别使用Java和JNI来调用一个Java方法十万次,结果是JNI的耗时的Java的十倍以上,而运算指令是native比Java快2-3倍,当然并不能说明什么,因为都同样是微秒级别,并且全编完应该是JNI调JNI了,而不是JNI调Java,那样速度应该会快不少。

23:43:55.340  E/profile__: [static] java start: 1556898235326, end: 1556898235339, time: 13
23:43:55.443  E/profile__: [static] jni start: 1556898235343, end: 1556898235442, time: 99
23:44:04.287  E/profile__: [static] java start: 1556898244279, end: 1556898244287, time: 8
23:44:04.384  E/profile__: [static] jni start: 1556898244288, end: 1556898244383, time: 95
23:44:08.535  E/profile__: [static] java start: 1556898248528, end: 1556898248535, time: 7
23:44:08.636  E/profile__: [static] jni start: 1556898248536, end: 1556898248636, time: 100
23:44:10.917  E/profile__: [static] java start: 1556898250908, end: 1556898250917, time: 9
23:44:11.016  E/profile__: [static] jni start: 1556898250918, end: 1556898251015, time: 97
23:44:14.959  E/profile__: [static] java start: 1556898254950, end: 1556898254958, time: 8

23:51:12.300 E/profile__: [add] java start: 1556898672287, end: 1556898672299, time: 12
23:51:12.303 E/profile__: [add] jni start: 1556898672301, end: 1556898672303, time: 2
23:51:13.924 E/profile__: [add] java start: 1556898673916, end: 1556898673923, time: 7
23:51:13.926 E/profile__: [add] jni start: 1556898673924, end: 1556898673926, time: 2
23:51:16.125 E/profile__: [add] java start: 1556898676117, end: 1556898676124, time: 7
23:51:16.127 E/profile__: [add] jni start: 1556898676125, end: 1556898676127, time: 2
23:51:16.613 E/profile__: [add] java start: 1556898676607, end: 1556898676613, time: 6
23:51:16.615 E/profile__: [add] jni start: 1556898676613, end: 1556898676615, time: 2

不过总的来说,想要做Java2Native完全脱离虚拟机其实还是有很多东西需要去思考和解决,比如Java原生库(java.lang.*, Set, Map等)是否也需要2native编译一遍,还是按需编译,还是依然调用虚拟机,还是直接改Runtime。
再比如Java的一些特性,比如是否需要再搞一个自动回收?但如果编译器做的东西太多那就相当于做了个静态的解释器在代码里,只能白白增大体积,

 

华为公布的内容没怎么看,不过我猜这个可能是以编译器+Runtime的形式存在,现在看到的好像都是一些对公众公布的原理,没有说到具体细节,也不知道是否真的“完全脱离虚拟机”。

最后于 2019-5-4 00:15 被葫芦娃编辑 ,原因:
2019-5-4 00:10
1
雪    币: 314
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
你不皮,我皮
2019-5-4 21:50
0
雪    币: 3425
活跃值: (1479)
能力值: ( LV9,RANK:320 )
在线值:
发帖
回帖
粉丝
12
有些标题党了,本文本质上介绍了编译器/反编译器的一种中端IR,好像和方舟编译器没多大关系吧?实际上逆向分析JAVA相关字节码,WALA库比Soot库更有效。还以为有SSA相关优化、垃圾回收、寄存器分配、代码调度等方面的内容呢?
2019-5-5 00:22
0
雪    币: 60
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
2019-5-5 10:27
0
雪    币: 53
活跃值: (106)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
如果只是Java2JNI的话,效率会受很大的影响,按照方舟编译器公开的资料显示,它的做法应该更像dex2oat。
2019-5-5 10:50
0
雪    币: 47
活跃值: (418)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
15
j2c的过程如果调用 jni 实现的话,就像楼上一些大佬所说的,效率是很大问题,因为仍然没有脱离java 环境。楼主上面所说的方式和现在的一些 vmp 的实现是相似的,方法标记为 native,进入 native 执行(j2c会导致编译的 so 过大,采用解释执行的 vmp 是现在加固厂商使用的主要方式)。方舟编译器应该是做到了完全 native 化了,具体还是等放出 demo 看吧
2019-5-5 11:26
0
雪    币: 19
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
坐等华为开源方舟编译器
2019-5-5 14:48
0
雪    币: 1385
活跃值: (5609)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
17
新的系统应该是集成了另外一个环境,完全独立运行在c上面了,就和普通的Ubuntu程序差不多吧。
2019-5-5 15:27
0
雪    币: 8715
活跃值: (8619)
能力值: ( LV13,RANK:570 )
在线值:
发帖
回帖
粉丝
18
看场雪 如何验证方舟编译器在兼容性上的风险? 如果把这个编译器作用在安装apk的过程中,是否能有效降低兼容性风险?
如果是安装过程中,就是华为手机的runtime自带了编译器,这个思路就是编译器和运行时深度融合了,是很有野心的想法
不知道诶,编译放到用户侧会暴露源码,编译也非常耗时,如果在厂家的电脑里用编译器编译出apk,然后用户那边只有运行时,可以避免这些问题吧
我自己感觉,兼容性风险体现在,从java转移成JNI的过程是不是等价的,
2019-5-6 10:44
0
雪    币: 8715
活跃值: (8619)
能力值: ( LV13,RANK:570 )
在线值:
发帖
回帖
粉丝
19
幽灵娃娃
感谢兄弟,已经改正了
2019-5-6 10:45
0
雪    币: 8715
活跃值: (8619)
能力值: ( LV13,RANK:570 )
在线值:
发帖
回帖
粉丝
20
vasthao 有些标题党了,本文本质上介绍了编译器/反编译器的一种中端IR,好像和方舟编译器没多大关系吧?实际上逆向分析JAVA相关字节码,WALA库比Soot库更有效。还以为有SSA相关优化、垃圾回收、寄存器分配 ...
编译优化我的确是不太懂你留下的线索我去百度一下
2019-5-6 10:46
0
雪    币: 8715
活跃值: (8619)
能力值: ( LV13,RANK:570 )
在线值:
发帖
回帖
粉丝
21
葫芦娃 Java2JNI跟脱离虚拟机还是有很大差距的,Java Native Interface归根结底跑的还是虚拟机,除了运算指令,若是调用、变量读写等依然还是使用JNI,那所谓的性能提升其实很有限,执行时 ...
娃哥一针见血,不愧是老司机,其实我也觉得应该不见得是完全脱离虚拟机体系,有些地方可以适当用反射来实现JNI掉JAVA方法
最后于 2019-5-6 18:20 被r0Cat编辑 ,原因:
2019-5-6 10:49
0
雪    币: 8715
活跃值: (8619)
能力值: ( LV13,RANK:570 )
在线值:
发帖
回帖
粉丝
22
无边 这么发展下去,加固厂商还能活下去吗?
只是听说啊,几年前某维安全就实现了类似的编译器
2019-5-6 10:51
0
雪    币: 8715
活跃值: (8619)
能力值: ( LV13,RANK:570 )
在线值:
发帖
回帖
粉丝
23
Zkeleven 如果只是Java2JNI的话,效率会受很大的影响,按照方舟编译器公开的资料显示,它的做法应该更像dex2oat。
贴一篇大佬的文章:
华为公布的方舟编译器到底对安卓软件生态会有多大影响? - weishu的回答 - 知乎
https://www.zhihu.com/question/319688949/answer/648358786
2019-5-6 10:52
0
雪    币: 8715
活跃值: (8619)
能力值: ( LV13,RANK:570 )
在线值:
发帖
回帖
粉丝
24
junkboy 支持
感谢JB大神,感谢兄弟
2019-5-6 10:53
0
雪    币: 27
活跃值: (622)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
25
还是看代码/效果吧...感觉这样做问题很多,看不到代码大家都在猜
2019-5-6 13:59
0
游客
登录 | 注册 方可回帖
返回
//