方舟编译器源码过一遍流程

不管是被带节奏还是啥,在年初放出方舟编译器的消息后,我真的很期待的,毕竟这是我本科一直很想去的华为编译器部门出品的,并且迫不及待地更新了最新的EMUI,体验一波所谓的方舟编译器。不过目前确实,没看到有啥实质性的、明眼可以看的东西。

跨语言编译的事,有一个比较成熟的graal在做了,其实也不算什么新思想。不过放在移动端,甚至是IoT领域,确实是前无古人。

昨天下载了代码,但是在火车上还没看,今天大致看了一下。

其实该吐槽的别人都吐槽了。

  • 文档啥的确实写得不怎么样,看完文档确实没懂应该怎么做才能跑起来;
  • 为啥是引用计数呢?
  • 后端的东西可能和华为的麒麟关系很大,需要脱敏可以理解,但是Java的前端有啥不好开源的?
  • 我对LLVM比较熟悉,说实话,我真的觉得二者好像啊,Phase和Pass的概念(感觉方舟的Phase开发没有Pass开发方便)、IR的设计等;(为什么不基于LLVM呢?小声BB……)
  • ……

目前开源的东西实在太少了,我其实很想知道以下是怎么做的(如果有内部人士提前透露一下就好了):

  1. 静态编译后如何保持动态特性(reflection,Java的invokedynamic等),特别是如果要支持JS的话,元信息等怎么处理;
  2. 内存管理部分,肯定不是单纯的引用计数吧;
  3. 多语言联合编译的话,是有一个统一的runtime提供所有语言的功能,还是用到一种语言就链接这个语言的runtime子集呢?还是两种结合的方式(一个提供基本功能的runtime,多个language-specific的runtime);
  4. 代码用方舟编译后在安卓运行,是私有格式,还是类似Xamarin的方式?(更新:这个可以参阅华为的方舟编译器主要修改了zygote

大概看了一些代码了,头晕。

可能因为我是C++菜鸡,有一些地方我实在看不懂为啥这么写……

源码结构

  • bin:编译好的可执行文件和一些脚本,很奇怪为什么要放在这里;
  • deplibs:因为有很多东西还没有开源,所以在这里提供了静态库;
  • huawei_secure_c:华为自己实现的xxx_s函数,很多只是简单的vxxx_s的封装(不是很理解为什么这么做);
  • maple_driver:编译器的驱动,就是把编译->各种Phase的转换和优化->链接->生成可执行文件这个流程串起来,目前好像不完整啊。在该目录下的defs/phases.def有所使用到的Phase的定义,有一些可以在src/mpl2mplsrc/maple_me中可以找到Phase的实现,有一些还没找到,不知道是没开源还是我漏了。
  • maple_ipa:ipa的实现,没细看,感觉是各种Phase的组合;这个部分应该不完整;
  • maple_ir:这次开源的重点,IR的生成、解析等;有一个手写的Lexer和一个手写的递归下降的Parser(牛批!将近3000行,写这个的兄弟写完估计眼睛就瞎了);primitive type很有意思;具体实现还没细看;
  • maple_me:me是啥的简写很迷啊,是maple emiting吗?功能应该和LLVM的emit类似;有几个Phase的实现在这里;一些中端优化;(更新:me是middle end的缩写);
  • maple_phase:Phase框架的实现,没有开源,只有三个头文件;
  • maple_util:工具类,没有开源;
  • mempool:内存池的实现,没有开源;有意思的是,定义了一个MapleString(果然每一个C++项目都会有自己的字符串实现啊hhh);
  • mpl2mpl:一些Phase的实现,主要是一些analysis、异常处理啥的,针对Java有一个native方法的stub的生成;有对Reflection分析的Phase,提取类型信息、方法信息、类字段信息(不限于Java,看定义可以支持C++、Python等),然后放在maple的metadata(定义在src/maple_ir/include/metadata_layout.h)中,metadata_layout.h中有一句说“metadata layout is shared between maple compiler and runtime”,所以Reflection需要runtime的支持是肯定的;有一些Phase的缩写太奇怪了,没看懂要干啥;
  • third_party:第三方库,只有一个zlib;应该是jbc2mpl用到了,用来解压jar文件;

MIR

看完MIR的文档,确实感觉有点不太舒服,和我印象中的IR应该有的样子不太一样,我一直觉得LLVM IR才是IR应该有的样子(太天真了点)。

看到有答主提到了MIR有师出同门的Open64的身影,我特意去了解了一下Open64的Whirl IR(Open64 Compiler Whirl Intermediate Representation),发现确实是的,可能这个就是Fred Chow老爷子的设计风格。

可以看到,MIR有很多和语言特性相关的opcode(例如Java的类声明、virtualcall等),甚至有if、while这种特别high level的opcode。如果参照Whirl IR的设计,其实这个是合理的。在Whirl IR中,IR是分为多种不同的level的,虽然都叫Whirl IR,但是囊括了从高级语言到底层机器码的过程中的所有可能需要的不同level的表示形式,可以方便进行不同层级的优化。high level的IR可以进行和语言特性相关的优化,low level进行和硬件特性相关的优化,类似这样。不过MIR的文档最好说明一下这么设计的原因,给个类似Whirl IR的这种流程图也可以,不然很容易被喷的。

如图,每一个转换过程都会将更high level的IR翻译为lower level的IR。

还没细看目前开源的实现中Phase的实现,不过像if、while这种high level、层级化的控制流opcode,经过Phase的转换、优化后,应该会变成goto、brfalse、brtrue等扁平化的控制流opcode(更新:在src/maple_ir/src/mir_lower.cpp有具体实现)。整体结构可以算是Open64的翻版,可能确实是老爷子的怨念吧hhh。

和Whirl IR不太一样的地方,就是MIR中有一些直接和前端语言特性强相关的opcode,目前看到的有类型里包含JS的数据类型、opcode中的Java Call和Java Class and Interface Declaration。这部分可能之后添加对JS等的支持后,还会有更多的opcode。

Java Class and Interface Declaration这个部分没啥好说的,Java Call应该是直接和Java Byte Code的invoke系列字节码(invokedynamic、invokespecial、invokeinterface、invokestatic、invokevirtual)对应了,不过按理来说适用其他语言。invokevirtual对应virtualcall、supercall,invokeinterface对应interfacecall,invokespecial和invokestatic编译之后进行name mangling可以当做普通函数(类似C++),对应的应该是MIR中Call系列。invokedynamic没有对应的,所以目前估计是没有直接解决这个问题,看后续的开源吧。

关于invokedynamic指令,我个人的一种猜测是,可能是runtime提供Callsite的构建,类似于在Java 7之前groovy的做法。不过这种方法效率挺低的,需要一些黑科技了。

更新:

关于invokedynamic指令,我觉得我之前肯定傻逼了,既然IR没有提供直接的支持,那就说明一个问题:在前端把invokedynamic翻译为了IR中有的指令。经过我的测试以及对jbc2mpl的逆向,确实是这样的,不过目前还不怎么清楚具体的机制,这里就不细说了。

上手

很惭愧,这么久了其实还没自己编译一遍,也没有上手试一下,只是大致看了代码。

试了一下,可以明显感觉开源的东西太少了,想要跑起来是不可能的,因为无论是编译期Runtime和执行时的Runtime都没有提供,这次开源的东西,确实只能看一下转换出来的IR(直接从Java字节码转换过来的,而这个jbc2mpl还没有开源)。

很多答主都说无法运行,这是因为没有提供Java的Runtime,所以想要能够生成.mpl,任何涉及到Java基本库中的类的都不要出现(所以不能System.out.println、不能测试异常,甚至不能出现main函数,因为main函数的参数有String,摊手.jpg)。

按照文档的指导,配置好Clang、gn、ninja之后,可以正常编译出来。这里要提一句,有人说开源的代码只有声明没有实现,这是不对的,那部分实现只是还没开源而已,提供了静态库,所以编译是没有问题的。

1.准备测试代码因为不能涉及任何基本库,所以基本算是残废的Java,这里我们就主要看Java字节码和MIR的对应关系。

以下面的斐波那契为例:public class HelloWorld {public static int fib(int n) {return n <= 2 ? 1 : fib(n - 1) + fib( n - 2);}}如果没有涉及基本库,是可以生成IR的:编译反编译得到java字节码2.生成对应的,这个过程中会打印一个警告信息:因为父类Object的构造函数没有定义(还是因为没有Runtime)。同时生成了两个文件:HelloWorld.mpl和HelloWorld.mplt。.mpl是IR,.mplt是符号表(?)3.分析根据官网的演示界面上的流程,后面还要跑maple优化IR和mplcg进行汇编生成的,但是我无论怎么跑都segmentation fault,遂放弃。

和Java字节码对比一下会发现基本是直译过来了(生成的IR还用注释给出了原始的Java字节码指令是啥,良心了)。

之前警告的方法是name mangling后的Object构造函数,这里将invokespecial指令翻译为了MIR的superclasscallassigned,和上面的猜测基本一致。这个地方要依赖Runtime,可能就是后续的步骤无法继续下去的原因吧。

曲线救国

按照官网的流程来说,需要先用maple进行一些中端优化的,但是maple实在跑不起来,所以想了一下,干脆直接进行汇编生成,测试一下整体的流程。

还是以上面的fib为例,用Maple IR写出来(其实是把上面的.mpl删了很多东西直接得到的),然后进行测试。

2. 使用mplcg生成汇编,直接看命令行参数可以发现mplcg的功能还是挺多的:

"mplcg fib.mpl"

汇编大概如下:..

一堆的load/store指令,生成的应该是aarch64的汇编。

同时还会生成一个.groots.txt文件和.primordials.txt文件,不知道干啥的,内容是空的。

自己测试了一下优化级别,用-O2来生成汇编:

diff比对一下,确实有不同,应该进行了尾递归优化(ARM汇编快忘了,没细看。更新:恶补了一下ARM汇编之后,发现并没有尾递归优化)。

3. 测试程序

生成了汇编以后,写个程序测试一下:

main.c   #include<stdio.h>extern int fib(int n);int main(int argc, char** argv) {printf("%d\n", fib(10)); return 0;}

4. 交叉编译

用Android NDK的工具链编译(按照我以前的经验,要加-static静态编译才行):aarch64-linux-android21-clang -static fib.s main.c -o fib会报错。undefined reference to `__mpl_personality_v0'

可以看到上面的汇编的下半部分,都是一些虚表、构造函数啥的section(很奇怪为什么一个fib函数会生成这些东西,这个应该是和对象结构相关的东西),目前没有用处,从".section .__muid_conststr,"aw",%progbits"开始直接全部删了。

再次编译成功。

5. 运行

不知道为啥qemu运行出错了,所以用adb连接安卓模拟器,然后把文件push到安卓模拟器之后再运行,这样是没有问题。

运行结果:55

整个流程就彻底算跑通了(但还并不是直接编译Java源码),概念上验证方舟是可行的(其实mplcg生成了aarch64汇编之后,后面的都是可以用传统的toolchain来,方舟可能也是这么做的)。

更新:

因为没有提供libjava-core.mplt,我想是不是可以把整个Java运行时用jbc2mpl转换一下呢(方舟团队肯定也不是手动编写这个libjava-core.mplt吧)?

于是下载了Java 5(怕太新的maple不支持,不过Java字节码也就在Java 7增加了一条invokedynamic指令)的JDK,提取出rt.jar,然后用jbc2mpl转换整个rt.jar。但是这种方法不行,会报错很奇怪的错:

Tid(7292): CHECK/CHECK_FATAL failure: false at [../../../jbc2mpl/src/jbcOpcode.cpp:138] Reversed opcode jsr

Reversed应该是typo,应该是Reserved吧)

其实仔细想一想,这种方法肯定会有问题,因为Java基本库并不是自洽的(基本库里调用了一些和具体虚拟机实现相关、私有的代码,com.sun包下的那些东西等等)。而且谷歌有先例在,Oracle肯定不放过,也不知道华为怎么避免专利问题。

不想折腾了,等开源更多的东西后一切都会明了吧。

更新:今天(2019.9.5)发现上面转换rt.jar获得libjava-core.mplt的方法已经有人做了,并且取得了一些成果(链接:贴吧用户wconly的帖子)。这个是苦力活,辛苦这位兄弟了。不过大家不要再尝试这种方法了,即使可以把rt.jar和相关的jar都通过jbc2mpl转换为了.mpl和.mplt,也是跑不起来的。以System.out.println为例,它最终会落在一个native方法上,而这个native方法会调用某个IO系统调用,这个并不是Java基本库本身提供的。要想跑起来,需要自己写这些Java基本库中的native方法的stub的实现,可能等研究完源码、自己写了stub的实现,华为已经把方舟都完整开源了。

2019.9.6最后一更(雾)

我先直接下一个结论:目前没办法不借助任何辅助手段直接通过开源的东西跑起来哪怕一个简单的HelloWorld。这里不借助任何辅助手段是指自己不写代码、也不使用任何自己找到的Runtime。

首先介绍方舟用到的两种重要文件格式.mplt和.mpl:

  • .mplt是声明(符号表),可以理解为.h头文件;
  • .mpl是定义,是具体实现,可以理解为.c实现,.mpl都会import相应的.mplt,相当于就是#include了;

因为Runtime是方舟很重要的一部分(很大概率不会开源的),这么设计,华为可以只给应用开发者提供.mplt文件,不给具体实现,最后链接到Runtime就好了。

而目前开源的东西里,没有给Runtime的实现,甚至连头文件(libjava-core.mplt)都没给。各位大大都想尽办法构建这个头文件:

通过这种方式,确实可以按照流程跑起来,并且最终得到汇编(但这个还是有限制)。

但是问题在于:

  • Java的基本库不是单纯由Java编写的,还有很多native方法;
  • 而HelloWorld中一个System.out.println涉及的模块很多,调用链错综复杂;(这个也是Java 9 的module系统解决的一个问题)

就以System.out.println为例(来源:How System.out.println() really works?):

 

整个的调用链最终落在了一个JNI调用,调用native方法访问操作系统提供的IO系统调用(打印出"Hello World!")。

问题就在于,这个native方法本身应该是在JVM中实现的,而现在如果我们要跑起来,我们就要自己实现这个方法。

而除了这些基本库本身的native方法外,方舟编译器其实还有一些自己私有的函数、symbol,仔细阅读生成的汇编,会发现__mpl_personality_v0(编译fib.s时出错的部分)这个方舟编译器插入的symbol,这个肯定不是Java基本库或者我们写的。还有很多类似的函数和symbol。

当然这个不是大问题,我们有时间的话,可以把涉及的native调用和能发现的方舟编译器的私有函数和symbol都自己实现了,最后链接进去。

但是还有一个问题:main函数入口。

回想一下C语言程序的链接过程,简单来说,在最终生成可执行文件时,是会链接进去一个crt0.o(或者其他类似的),包含一个_start入口,进行C语言Runtime的初始化,最终再跳转到我们写的main函数。

而在传统的Java程序的运行方式中,环境准备、跳转到main函数,这个是JVM干的事,但是现在方舟静态编译了Java,也就是说,需要我们进行main函数入口的指定和Runtime的初始化工作。

以一个不做任何事的HelloWorld.java为例:借助前面两位大大的方法,它生成的.mpl如下:因为JAVACLINITCHECK ()没有实现,直接mplcg生成汇编会有问题:把.mpl的倒数第四行删除后,可以生成有效的汇编(测试了一下,经过mplme和mpl2mpl转换之后可以不用删除,在src/mpl2mpl/src/class_init.cpp中有lower过程):可以看到Java中的main方法,只是进行了name mangling然后当做普通方法处理了,并没有我们喜闻乐见的main函数。

其实从这里可以知道,方舟编译器用的链接器应该是改写过的(mplcg命令行参数也有一个--maplelinker参数),至少也需要处理找Java的main函数这个问题(目前也没看到有对main函数进行标记)。

不能编译为可执行文件,但是也可以生成.so库文件,这个没有任何问题的,不过这个需要用上面提到的,自己写main.c调用。但是,C调用Java的方法的调用约定是怎么样的呢?这个也是个问题。

总之,我已经放弃用常规方式跑起来HelloWorld了,这是个很费劲的事,而这本来应该是方舟编译器做的事。

不过用几位大大的.mplt可以把生成.s这个流程完整跑下来了,把重点放在研究那几个Phase吧。

总结

  • 开源的东西太少了,只有一个IR是不行的;
  • 方舟编译器肯定不是PPT,整体是可以跑通的;
  • 需要等进一步的开源才能知道一些细节的东西(动态特性的处理、Runtime等等);

 

说实话,靠这样猜、看源码来摸清方舟的思路,是有点累的。

还是等进一步的开源吧。

 

 

 

 

https://www.zhihu.com/question/343431810

 

https://zhuanlan.zhihu.com/openarkcompiler

posted @ 2019-12-16 17:11  ation_work  阅读(739)  评论(1编辑  收藏  举报