mac 平台编译并调试 OpenJDK7 和 HotSpot

编译环境

下面是最终编译通过时的环境配置,中间踩了很多坑,光 boostrap JDK 就换过 1.6.0,1.7.0_71,1.7.0_80,1.8.0_131,1.7.0_40 等多个版本,主要是卡在 ant 和 JDK 版本不兼容,以及 openJdk和 boostrap JDK 不兼容等问题上

  • 系统环境 :osx 10.11.6
  • 编译器 :clang
  • openJdk :jdk7u-dev
  • boostrap JDK :oracle JDK 7u40
  • xcode version :8.0 (8A218a)
  • ant version :1.8.0
  • make 命令,请根据编译环境自行调整
sudo make ALT_BOOTDIR=/Library/Java/JavaVirtualMachines/jdk1.7.0_40.jdk/contents/home SPOT_OS_VERSION_CHECK=OK LANG=C ANT_HOME=/Users/alberthumbert/Downloads/apache-ant-1.8.0 WARNINGS_ARE_ERRORS=false ALLOW_DOWNLOADS=true CC=clang COMPILER_WARNINGS_FATAL=false LFLAGS='-Xlinker -lstdc++' USE_CLANG=true LP64=1 LANG=C  ARCH_DATA_MODEL=64 HOTSPOT_BUILD_JOBS=8


获取源码

  • 本文不会有安装和配置 brew , xcode 和 boostrap jdk(oracle jdk) 的内容,对于打算动手编译jdk的人,这部分内容太基础了,就不废话了
  • 首先确保 mercurial 已经安装,mercurial 和 git 的作用差不多
sudo easy_install mercurial
  • 使用 mercurial 提供的 hg 工具拉取远程仓库, 这里我拉取的是 jdk 7
hg clone http://hg.openjdk.java.net/jdk7u/jdk7u-dev
  • 现在还未真正获取到所有源码,在进行下一步之前,建议先确保你有足够的权限,在trust.rc中添加当前的用户名
sudo vim /etc/mercurial/hgrc.d/trust.rc
  • 一个可供参考的版本如下
[trusted]
users = alberthumbert 
groups = root
  • 接着正式获取源码,使用任何shell都可以, 如果执行后出现 xxx not trusted,说明上一步没有设置好
sudo zsh ./get_source.sh
  • 如果出现 abort: stream ended unexpectedly ,可能是网络的问题,我这里试了很多次都没有拉取成功,没办法只好走代理,在当前终端中设置代理,只需执行如下命令,只在当前终端生效
    export all_proxy=socks5://[host]:[port] # 同时配置 http 和 https代理
  • 如无意外,corba,jaxp,jaxws,langtools,jdk,hotspot 这些过程都将以 xxx files updated, 0 files merged, 0 files removed, 0 files unresolved 结束,不行就再试几次


编译环境

  • 安装llvm
brew install llvm
  • 安装 freetype
brew install freetype
  • 安装完freetype和llvm后需要设置链接
sudo ln -s /usr/bin/llvm-g++ /Applications/Xcode.app/Contents/Developer/usr/bin/llvm-g++


sudo ln -s /usr/bin/llvm-gcc /Applications/Xcode.app/Contents/Developer/usr/bin/llvm-gcc
  • 安装 ant , 编译过程中提示ant版本太旧,在apache官网上下载新版本,然后设置链接,建议不要使用版本太高的ant,最好不要跟你的 OpenJdk 版本有太大差距
sudo ln -s Users/alberthumbert/Desktop/apache-ant-1.8.0/bin/ant /usr/bin/ant
  • 安装 XQuartz,在官网下载dmg并手动安装 ,然后添加链接
sudo ln -s /opt/X11/include/X11 /usr/local/include/X11
  • 注意如果上一步出现 operation not permitted ,说明你的os版本比较高,有些操作被系统保护,需要重启 Command + R 进入 recover 模式,在终端中关闭保护, 输入如下命令然后重启即可
csrutil disable


配置参数

  • 下面是一份可供参考的参数配置,可以写在目录的build.sh当中然后执行,注意jdk路径和ant路径需要根据实际情况自行设置
# 语言选项,必须设置,否则编译好后会出现一个 HashTable 的 NPE错
export LANG=C

# Bootstrap JDK 解压路径,必须设置
export ALT_BOOTDIR=/Library/Java/JavaVirtualMachines/jdk1.7.0_40.jdk/Contents/Home
export ANT_HOME=/Users/alberthumbert/Desktop/apache-ant-1.8.0
export ANT_VERSION=1.7.1
# 允许自动下载
export ALLOW_DOWNLOADS=true

# 并行编译线程数
export HOTSPOT_BUILD_JOBS=4
export ALT_PARALLEL_COMPILE_JOBS=4

# 比较本次 build 出来的映像与先前版本的差异,对我们没有意义
# 必须设置为 false,否则 sanity 检查为报缺少先前版本 JDK 的映像的错误提示
export SKIP_COMPARE_IMAGE=false

# 使用预编译头文件,不加这个编译会变慢
export USE_PRECOMPILED_HEADER=true

# 要编译的内容
export BUILD_LANGTOOLS=true
export BUILD_HOTSPOT=true
export BUILD_JDK=true
# export BUILD_JAXWS=false
# export BUILD_JAXP=false
# export BUILD_CORBA=false

# 要编译的版本
# export SKIP_DEBUG_BUILD=false
# export SKIP_FASTDEBUG_BUILD=true
# export DEBUG_NAME=debug

# 把它设置为 false 可以避开 javaws 和浏览器 Java 插件之类的部分的 build
BUILD_DEPLOY=false

# 把它设置为 false 就不会 build 出安装包,因为安装包里有奇怪的依赖
# 但即使不 build 出它也能得到完整的 JDK 映像,所以还是别 build
BUILD_INSTALL=false
export WARNINGS_ARE_ERRORS=false
export COMPILER_WARNINGS_FATAL=false

# 编译结果所存放的路径
export ALT_OUTPUTDIR=/Users/alberthumbert/jdk7u-dev/build_result

# 这两个环境变量必须去掉,不然会发生奇怪的事情
# Makefile 检查到这两个变量就会提示警告
unset JAVA_HOME
unset CLASSPATH

make sanity


开始编译

no boostrap dir

  • 执行 make 进行编译,编译过程中可能提示找不到可用的 boostrap jdk ,虽然在上面已经配置过了,不太清楚为什么,只能再手动加上参数,注意这里也需要权限
sudo make ALT_BOOTDIR=/Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/content/home

ant 版本过低

  • 如果提示 ant 版本过低,但实际上你的 ant 又高于提示中的 ant 版本,有可能实际上是 boostrap jdk 版本太低,建议使用 oracle jdk 7u40 ,详见后面踩坑记录
ERROR: 
The version of ant being used is older than
the required version of '1.7.1'.
The version of ant found was '1.7.0'.

中文 与 ascii 乱码

  • 自动生成的代码(主要是AIDL文件)中注释会带有中文,这个时候由于配置中规定了使用 ascii ,这会导致编译器无法识别而编译不通过,解决办法主要有两种,一种使用shell命令或脚本,另一种直接修改文件
  • 使用shell命令替换可以参考下面, 有可能不成功
sudo find /Users/alberthumbert/jdk7u-dev/build/macosx-x86_64/corba/gensrc/com/sun/ -name '*.java' | while read p; do native2ascii -encoding UTF-8 $p > tmpj; mv tmpj $p ; done
  • 比较稳的做法是像这位老哥一样修改文件,看上去很多其实也没几个文件,善用字符搜索很快就能搞定

clang 不支持 -fpch-deps

  • 参考这一位老哥的做法,修改 hotspot/make/bsd/makefiles/gcc.make
# 注释216-218行
# Flags for generating make dependency flags.
# ifneq ("${CC_VER_MAJOR}", "2")
# DEPFLAGS = -fpch-deps -MMD -MP -MF $(DEP_DIR)/$(@:%=%.d)
# endif
# 在218行下添加下面代码
DEPFLAGS = -MMD -MP -MF $(DEP_DIR)/$(@:%=%.d)
ifeq ($(USE_CLANG),)
  ifneq ($(CC_VER_MAJOR), 2)
    DEPFLAGS += -fpch-deps
  endif
endif

clang: error: no such file or directory: 'false'

  • 仔细看发现是makefile生成的编译命令中某个参数只剩了一个false,不知道是哪个参数,很蛋疼,顺藤摸瓜找到生成参数的makefile
sudo vim /Users/alberthumbert/jdk7u-dev/hotspot/make/bsd/makefiles/vm.make
  • 注释掉下面个参数配置
#CFLAGS_WARN holds compiler options to suppress/enable warnings.
CFLAGS += $(CFLAGS_WARN/BYFILE)

# Do not use C++ exception handling
CFLAGS += $(CFLAGS/NOEX)

形参默认值问题

  • 还是参考这位老哥,修改 hotspot/src/share/vm/code/relocInfo.hpp
//修改374行
inline friend relocInfo prefix_relocInfo(int datalen);

//修改469行
inline relocInfo prefix_relocInfo(int datalen = 0) {
   assert(relocInfo::fits_into_immediate(datalen), "datalen in limits");
   return relocInfo(relocInfo::data_prefix_tag, relocInfo::RAW_BITS, relocInfo::datalen_tag | datalen);
}

Undefined symbols for architecture x86_64: "_attachCurrentThread"

  • 这个错误是在 debug_build 时才出现的,没有保存出错信息,简单说一下情况,提示说 ThreadUtilities中的 getJNIEnv 和 getJNIEnvUncached 函数引用了一个不存在的函数 attachCurrentThread,之前写过一点jni,感觉大概是一个封装了jni线程调度函数的工具类出了问题

  • 在osxapp目录找到这个文件

cd /Users/alberthumbert/jdk7u-dev/jdk/src/macosx//native/sun/osxapp
  • 实际上 ThreadUtilities.m 文件中可以找到这个函数,感觉是内联出了问题,把 inline 关键字去掉即可通过编译
inline void attachCurrentThread(void** env) {
    if ([NSThread isMainThread]) {
        JavaVMAttachArgs args;
        args.version = JNI_VERSION_1_4;
        args.name = "AppKit Thread";
        args.group = appkitThreadGroup;
        (*jvm)->AttachCurrentThreadAsDaemon(jvm, env, &args);
    } else {
        (*jvm)->AttachCurrentThreadAsDaemon(jvm, env, NULL);
    }
}


编译通过

  • 任务完成,耗时将近20分钟,纪念一下
>>>Finished making images @ Sat Apr  7 14:22:59 CST 2018 ...
########################################################################
##### Leaving jdk for target(s) sanity all docs images             #####
########################################################################
##### Build time 00:12:41 jdk for target(s) sanity all docs images #####
########################################################################

#-- Build times ----------
Target all_product_build
Start 2018-04-07 14:03:57
End   2018-04-07 14:22:59
00:00:16 corba
00:05:57 hotspot
00:00:02 jaxp
00:00:03 jaxws
00:12:41 jdk
00:00:03 langtools
00:19:02 TOTAL
-------------------------
  • 下面让我们来验证一下编译是否真的成功了
cd /Users/alberthumbert/jdk7u-dev/build/macosx-x86_64/j2sdk-image/bin
  • 验证一下版本
./java -version

#输出
openjdk version "1.7.0-internal"
OpenJDK Runtime Environment (build 1.7.0-internal-root_2018_04_07_14_03-b00)
OpenJDK 64-Bit Server VM (build 24.80-b07, mixed mode)
  • 写个 hello world, 用 ./javac Hello.java 编译
    public class Hello{
       public static void main(String[] args){
          System.out.println("Hello World !");
       }
   }
  • 用 ./java Hello 运行
Hello World !


调试源码

不单纯为了编译而去编译,在学习jvm的同时能跟着代码走才是最终目的

build debug_build fastdebug_build

  • 注意 在普通的编译模式下编译出来的jdk是不能调试的,它跟你平时使用的普通jdk是一个东西,为了能够支持调试 在make 命令后面需要加上 build_debug 参数,这时在 build/macosx-x86_64-debug/ 目录可以找到另一个编译版本,这里面的 jdk 和 hotspot是可调试的
  • 细心的玩家可能还会发现一个 macosx-x86_64-fastdebug 目录,如果你在配置文件中设置了 fastdebug 参数,那么就会有这个版本 jdk,简单说一下 fastdebug 是什么,它是由于 jdk 的开发人员无法忍受编译器极度缓慢的调试速度而打造的新调试版本,它的调试速度会比普通的 debug 快,不过替换掉了一些命令,所以它的表现和普通的 jdk 不太一样_

使用 Xcode 调试运行

本来是打算使用 Clion 调试的,无奈 cmake 太难配,看了别人使用 xcode 很顺畅,于是打开了万年不用的 xcode

  • 新建项目,名称随意,右键工作目录,点击 add files to (projectname),导入整个 jdk7u-dev,注意就是你用 hg 命令拉下来的整个仓库,不是 macosx-x86_64-debug 目录
  • 点击 product->scheme->edit scheme ,在 run -> info 中配置 executable 如下
/Users/alberthumbert/jdk7u-dev/build/macosx-x86_64-debug/bin/java
  • 同时在这个 executable 的目录下新建一个测试用的java文件,这里用回之前的 hello world
    public class Hello{
       public static void main(String[] args){
          System.out.println("Hello World !");
       }
   }
  • 在 run-> arguements 中配置参数为 hello,那么效果就是 ./java hello,所以你需要先 javac 一下
  • 在运行之前找到 main.c ,没错,这个就是你所以 java 程序的入口,路径在 /jdk/src/share/bin/main.c 找不到就 find . -iname main.c 一下
int
main(int argc, char **argv)
{
    int margc;
    char** margv;
    const jboolean const_javaw = JNI_FALSE;
#endif /* JAVAW */
#ifdef _WIN32
    {
        int i = 0;
        if (getenv(JLDEBUG_ENV_ENTRY) != NULL) {
            printf("Windows original main args:\n");
            for (i = 0 ; i < __argc ; i++) {
                printf("wwwd_args[%d] = %s\n", i, __argv[i]);
            }
        }
    }
    JLI_CmdToArgs(GetCommandLine());
    margc = JLI_GetStdArgc();
    // add one more to mark the end
    margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *)));
    {
        int i = 0;
        StdArg *stdargs = JLI_GetStdArgs();
        for (i = 0 ; i < margc ; i++) {
            margv[i] = stdargs[i].arg;
        }
        margv[i] = NULL;
    }
#else /* *NIXES */
    margc = argc;
    margv = argv;
#endif /* WIN32 */
    return JLI_Launch(margc, margv,
                   sizeof(const_jargs) / sizeof(char *), const_jargs,
                   sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
                   FULL_VERSION,
                   DOT_VERSION,
                   (const_progname != NULL) ? const_progname : *margv,
                   (const_launcher != NULL) ? const_launcher : *margv,
                   (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
                   const_cpwildcard, const_javaw, const_ergo_class);
}
  • 随便打几个断点,点击运行,先不要急着一头扎进源码,快速让调试结束,如果终端输出如下,恭喜你已经完成了所有任务
Hello World !
Program ended with exit code: 0


start the world

  • 接下来我们调试 HotSpot,在hotspot目录下的jni.cpp中找到 JNI_CreateJavaVM
/Users/alberthumbert/jdk7u-dev/hotspot/src/share/vm/prims/jni.cpp
  • 也是随便打上断点,如果你上一步已经成功,那么现在所有的配置参数都不需要改变,直接调试即可

现在,开始你的深入理解 java 虚拟机之旅吧!






踩坑记录

废话很多,如果运气好,上面的流程都通过了,下面就不用看了

ant 版本过低

ERROR: The version of ant being used is older than
   the required version of '1.7.1'.
   The version of ant found was '1.7.0'.
  • 使用 ant -version ,如果发现版本确实过低,那么下载新版 ant 即可
  • 然而我的机器上原先就没有 1.7.0 版本的 ant,反复使用 homebrew 安装了 ant,可以确定运行的 ant 版本是最新的,网上的说法是使用软连接将ant链接过去,看了一下源码,确实 openjdk 里面将 ant 目录写死在了/usr/bin/ant,问题是这个 ‘1.7.0’ 的ant到底是哪来的,为什么能找到这个版本的ant
sudo ln -s Users/alberthumbert/Desktop/apache-ant-1.10.3/bin/ant /usr/bin/ant
  • 再看一下make sanity的输出, 上面是我下载的ant,1.10.3 ,但下面的 ANT_VER 却是 1.7.0,这么说来这个 ant 的版本可能跟我指定的 ant 没有关系
    ANT_HOME = /Users/alberthumbert/Desktop/apache-ant-1.10.3
    …
    ANT_VER = 1.7.0 [requires at least 1.7.1]
  • 使用 mdfind 查找 这个ant version 是怎么来的,直接 mdfind “ANT_VER”
/Users/alberthumbert/jdk7u-dev/build.sh
/Users/alberthumbert/jdk7u-dev/jaxp/src/com/sun/org/apache/xalan/internal/xslt/EnvironmentCheck.java
/Users/alberthumbert/Desktop/apache-ant-1.10.3/manual/api/org/apache/tools/ant/MagicNames.html
/Users/alberthumbert/Desktop/apache-ant-1.10.3/manual/api/index-all.html
/Users/alberthumbert/Desktop/apache-ant-1.10.3/manual/api/constant-values.html
  • 上面这个 EnvironmentCheck.java 很可疑,打开看下,有个检查 ant 版本的方法,这里用到反射,理论上查找的就是 ANT_HOME 当中 ant.jar 里的类,还是不明所以
 /**    * Report product version information from Ant.
   *    * @param h Hashtable to put information in
   */
  protected void checkAntVersion(Hashtable h)
  {
 
    if (null == h)
      h = new Hashtable();
    
    try
    {
      final String ANT_VERSION_CLASS = "org.apache.tools.ant.Main";
      final String ANT_VERSION_METHOD = "getAntVersion"; // noArgs
      final Class noArgs[] = new Class[0];
    
      Class clazz = ObjectFactory.findProviderClass(ANT_VERSION_CLASS, true);
    
      Method method = clazz.getMethod(ANT_VERSION_METHOD, noArgs);
      Object returnValue = method.invoke(null, new Object[0]);
    
      h.put(VERSION + "ant", (String)returnValue);
    }
    catch (Exception e)
    {
      h.put(VERSION + "ant", CLASS_NOTPRESENT);
    }
    }
  • 尝试更换 boostrap jdk 版本,这次使用 1.8.0 然后发现 make sanity 通过了,而输出的配置列表中 ANT_VER 就等于 1.8.0
     ANT_VER = 1.8.0 [requires at least 1.7.1]
  • 下意识的想起之前使用的boostrap jdk 是1.7.0 而提示的 ANT_VER 就是1.7.0,看了下我的机子里还有 1.6.0 的 jdk ,那么用它来编译一下… 黑人问号.jpg ,所以这个 ant 版本就等于 jdk 版本 ???WTF ???
    ERROR: The version of ant being used is older than
       the required version of '1.7.1'.
       The version of ant found was '1.6.0.'.
  • 总结一下,boostrap jdk 版本接近或者大于指定的ant版本,否则设定 ANT_HOME 没有作用,但是网上很多解决办法都是使用后者,可能是平台差异,具体原因不明, 推荐使用Oracle JDK 7u40

Bootstrap JDK 版本过高

  • 如果你尝试使用 JDK 8 去编译 OpenJDK 7 ,那么肯定是行不通的,在 OpenJDK 7 当中,许多 ant 的配置中都指定了 -werror 参数,这个参数意味着参与编译的文件在编译过程中导致的 warnning 都会被视为 error ,而在用 javac 编译 java 文件时又由于 Bootstrap JDK 过高而出现大量的 warnning ,最典型的比如下面这个
主版本 52 比 51 新, 此编译器支持最新的主版本。建议升级此编译器
  • 个人推荐 Oracle JDK 7u40 ,使用这个版本搭配 1.7.1 以上并接近这个版本的 ant

OS X 10.10 以上无法安装 Oracle JDK

  • 本来是觉得不需要写这部分内容的,但mac上的 Oracle JDK 有个bug ,在安装旧版本的 jdk 时你可能会收到这样一条提示
Java from Oracle requires Mac OS X 10.7.3 or later.
Your system has Mac OS X Version 10.11.6. This product can be installed on Version 10.7.3 or later.
Visit java.com/help for more information.
  • MDZZ,多么清奇的脑回路,而且这个问题直到 JDK 8 的一些版本都存在,但是我们说过 Bootstrap JDK 版本不能过高,那么怎么安装这个JDK呢,无奈只好帮Oracle 修一波 bug

  • 在 dmg 加载上来之后,用下面命令解压安装包, 第一个路径是你的 dmg 文件,第二个路径是解压目录的目标地址,命名随意

pkgutil --expand /Volumes/JDK\ 7\ Update\ 40/JDK\ 7\ Update\ 40.pkg
  • 进入解压目录在 Distribution 文件中找到 pm_instal_check() 函数,用什么方法都好,总之让它返回 true
function pm_install_check() {
//就是这么粗暴
  return true;
}
  • 重新打包, 然后用这个新的dmg进行安装即可
pkgutil --flatten /Users/alberthumbert/Desktop/740.unpkg /Users/alberthumbert/Desktop/jdk740.pkg

推荐阅读更多精彩内容