[转]在Linux下编译OpenJdk并调试Hotspot
背景
对于大多数Java程序员来说,JVM就是一个黑盒子,我们一般不必关心它内部是怎么运作的。但是万一碰到JVM bug导致的Crash呢,又或者只是因为好奇想了解JVM的内部世界,那么就需要编译和调试JVM。
概念
1. JVM(Java Virtual Machine)
翻译过来就是Java虚拟机,所谓虚拟机是相对传统的计算机而言的。
传统的计算机有很多体系架构,比如x86,Sparc等等;一个体系架构一般有类似的指令集,比如586兼容所有386的指令集,但是又有自己扩充的指令。在Java出现之前,大部分编译型语言(我不知道是不是所有的)都会由编译器把源代码编译成特定机器下特定指令集的机器指令。从理论上来说所有的机器的表达能力都是一样的,等价于图灵机这个理论模型。但是就具体实现而言,在不同机器实现同一个逻辑的机器指令是不同的,这就导致在一台机器编译的二进制文件可能无法在另一台机器上运行(这里不考虑操作系统的差异)。当然写得比较好的c代码是可以在不同平台编译,但是一般不太可能把一个windows下的exe程序让linux运行。(当然除了机器指令的差别,不同系统的二进制格式也是不能兼容的,比如windows pe和linux elf)。即使同一个操作系统,比如linux,在x86下编译的程序也是没办法在Sparc下跑的,因为Sparc根本不认识x86的指令。
JVM就是一个虚拟机,它定义了自己的指令集(Java字节码),所有的Java程序编译成统一的字节码,然后JVM负责在不同的硬件和操作系统下解释或者编译字节码。
JVM本身只是一个概念,一种抽象
2. JRE(Java Runtime Environment)
Java运行时环境,首先必须实现JVM规范,实现同样的JVM规范可以有很多方法,比如Sun有Sun的实现,IBM有IBM的实现。
JVM当然得实现Java语言,也就是java.lang.*,认识int,知道class,方法,...
但是如果你发现没有java.util.* 或者没有java.io.*时,是不是会发现没法写Java程序了呢?
没有util,你当然可以自己实现那些常见的数据结构,这可以用java实现;那没有io呢?那似乎没法写出跨平台的代码了。
所以除了实现语言本身,Java还定义了很多跨平台的API,比如java.awt java.io等等。这些可能是我们非常常用的,所以Sun把它们叫作了Java Standard Edition(Java SE)。一般以java开头
另外有些API,比如JDBC,这是连接数据库的API。这些组成了Java Enterprise Edition(Java EE)。一般以javax包开头
这些API有些是用Java实现,但很多不跨平台的必须由C实现(JVM通过JNI规范来实现Java和c的数据传递),所以JRE必须实现这些
3. JDK(Java Development Kit)
* *开发者的工具,最常用的就是javac编译器。和vc或者gcc一样,必须要有工具把源代码编译成目标代码,c的目标代码是机器相关的,而Java的目标代码就是字节码。
* *当然实现javac就是实现一个传统的编译器,词法分析,语法分析和语义分析,然后翻译成字节码。这本身不是件简单的事情,所以编译器的开发者也开发了一些帮助开发编译器的工具,比如我们熟悉的lex/flex,yacc/bison。java也实现了类似的JavaCC和JFlex。
除了javac,javadoc和javah也是很常用的工具。此外Java的新特性,比如范型(Generics)和注解(Annotations),这些也需要由编译器来处理。(范型只影响编译,编译后被擦除了,但是Annotation可能会运行时能通过反射获取)。这些代码一般都是Java实现,打包到tools.jar。所以如果你需要动态编译Java代码(比如JSP),那么你可能要用到tools.jar
Linux下Build Open Jdk 7和Debug Hotspot
下面的内容大部分参考下面两篇文章:
http://weblogs.java.net/blog/simonis/archive/2008/01/hotspot_develop.html
http://weblogs.java.net/blog/simonis/archive/2008/01/hotspot_develop_1.html
不过在用NetBeans调试Hotspot时碰到了一些问题(可能是我用了比较新的netbeans的缘故),之前问过作者,不过没有得到满意的解答,后来自己使用了一些trick的手段。
1. 获取源代码
两种方法
1.1 使用 Mercurial 获取最新代码(其实就是svn git这样的scm工具)
http://hg.openjdk.java.net/jdk7/build/raw-file/tip/README-builds.html
速度比较慢,可能要花几个小时吧,如果你需要最新代码又不想等,可以找我
1.2 直接下载压缩的源代码包
http://download.java.net/openjdk/jdk7/
解压后就可以了,如果想更新代码,也可以运行 sh ./getSource.sh更新
1.3 源代码结构
hotspot
hotspot的源代码,完整的工程,包括makefile,文件按平台分类,比如 hotspot/src下有4个目录:os cpu os_cpu share
很明显,share放的是于平台无关的代码,os里放的是特定os的代码,cpu放的是特定cpu的代码,os_cpu放的是特定os和cpu的代码
jdk
java api的代码,如果我们对jdk的代码感兴趣,比如想看ConcurrentHashMap的实现,那么可以去src/share/classes/java/util/concurrent/ConcurrentHashMap.java
它的目录结果是 share,linux,solaris和windows,和前面类似,share放的是操作系统无关的代码,一般是用Java实现。Windows当然放的是windows的实现,不过*nix的都是放到solaris下实现的,不同的操作系统通过不同的#define区别,所以大部分代码都在solaris下,linux目录下只有一些文档。大部分代码都是c实现,然后通过JNI让java调用,所以大部分代码都在native下而不是classes下。
langtool
编译器工具,比如javac,javah,apt等等工具
make
makefile
其它
比如jaxp,corba等等,暂时不感兴趣
./share/classes/java/util/concurrent/ConcurrentHashMap.java
2. 编译和调试
大部分应该都装好了,比如gcc,make,ant,bootstrap jdk,还有一些需要安装,比如cups等Sun没有版权的代码,你必须自己从网上下载(或者通过Linux发行版的仓库安装)
由于我的机器很早以前编译的,当时编译时少一个装一个,所以忘了要装哪些了,碰到问题的可以google,也可以找我讨论(我也可能解决不了,我对Linux也不是很熟悉)
可以使用 make sanity 测试,请设置如下的环境变量(我的是bash):
export LANG=C
unset JAVA_HOME
export ALT_BOOTDIR=/home/lili/java/jdk1.6.0_26/
export ALLOW_DOWNLOADS=true
export USE_PRECOMPILED_HEADER=true
export SKIP_DEBUG_BUILD=false
export SKIP_FASTDEBUG_BUILD=true
export DEBUG_NAME=debug
2.1 编译JDK
如果make sanity通过,那么直接运行 make,如果你想保留日志,那么可以 make 2>&1 |tee ~/buildopenjdk7.log
可能会碰到问题,需要你解决。如果顺利的话,几个小时后应该能编译好。
2.2 编译结果
费了这么大的力气,我们看看编译的成果。
编译的结果放到了build/linux-i586-debug下,
bin
编译后的二进制文件,比如java,javac等等
lib
jar包,比如tools.jar
hotspot
我们最感兴趣的jvm,具体运行和调试会在下面介绍
classes
java api部分,包括按包组织的java类
2.3 运行一下自己编译的jdk
lili@lili-desktop:/media/d/openjdk7zip/openjdk/build/linux-i586-debug/j2sdk-image$ ./bin/java -version
openjdk version "1.7.0-internal-debug"
OpenJDK Runtime Environment (build 1.7.0-internal-debug-lili_2011_11_14_12_30-b00)
OpenJDK Server VM (build 21.0-b17-jvmg, mixed mode)
lili@lili-desktop:/media/d/openjdk7zip/openjdk/build/linux-i586-debug/j2sdk-image$
2.4 编译hotspot
前面说过了,我们感兴趣的是hotspot。如果想学习util里的实现,直接能看java代码,如果想学习io或者nio,那么可以直接看native c的实现(java通过JNI调用)。
比如我们修改了hotspot的代码,当然可以重新编译整个JDK,但这太慢,我们可以只重新编译hotspot自己
环境变量和前面一样,需要ALT_BOOTDIR和ALT_OUTPUTDIR
使用:cd hotspot/make && make jvmg jvmg1
target jvmg会编译Server版本的Hotspot,并且是带调试符号的,jvmg1会编译Client版本的,同样也是带调试符号的。因为优化可能会调整代码顺序或者去掉一些无用的代码等,这会给调试带来困难,所以如果我们是为了学习的话,用这两个target就好了。
如果需要优化的版本可以用target optimized或者optimized1,同样后面有1的表示Client;产品的target是product和product1
2.5 运行hotspot
* *需要增加两个环境变量LD_LIBRARY_PATH和JAVA_HOME,前者参考下面,里面包含我们编译好的libjvm.so,后者可以用我们自己编译的jdk
export LD_LIBRARY_PATH=/media/d/openjdk7zip/openjdk/build/hotspot_debug/linux_i486_compiler1/jvmg
export JAVA_HOME=/media/d/openjdk7zip/openjdk/build/linux-i586-debug/j2sdk-image
cd /media/d/openjdk7zip/openjdk/build/hotspot_debug/linux_i486_compiler1/jvmg
lili@lili-desktop:/media/d/openjdk7zip/openjdk/build/hotspot_debug/linux_i486_compiler1/jvmg$ ./gamma -versionlili@lili-desktop:/media/d/openjdk7zip/openjdk/build/hotspot_debug/linux_i486_compiler1/jvmg$ ./gamma -version
Using java runtime at: /media/d/openjdk7zip/openjdk/build/linux-i586-debug/j2sdk-image/jre
openjdk version "1.7.0-internal-debug"
OpenJDK Runtime Environment (build 1.7.0-internal-debug-lili_2011_11_14_12_30-b00)
OpenJDK Client VM (build 21.0-b17-internal-jvmg, mixed mode)Using java runtime at: /media/d/openjdk7zip/openjdk/build/linux-i586-debug/j2sdk-image/jreopenjdk version "1.7.0-internal-debug"
OpenJDK Runtime Environment (build 1.7.0-internal-debug-lili_2011_11_14_12_30-b00)
OpenJDK Client VM (build 21.0-b17-internal-jvmg, mixed mode
2.6 使用gdb调试hotspot
前面我们编译出来的hotspot已经是带有调试符号,所以可以使用gdb来调试了。
gdb ./gamma
GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
........
(gdb) break main
(gdb) r
Starting program: /media/d/openjdk7zip/openjdk/build/hotspot_debug/linux_i486_compiler1/jvmg/gamma
[Thread debugging using libthread_db enabled]
Breakpoint 1, main (argc=1, argv=0xbffff1a4)
at /media/d/openjdk7zip/openjdk/hotspot/src/share/tools/launcher/java.c:228
228 {
(gdb) (gdb)
可以看到,gamma测试程序的入口是hotspot/src/share/tools/launcher/java.c。
2.7 使用Netbeans调试hotspot
对于习惯GNU/Linux环境开发的c程序员来说,有源代码和Makefile,再配合vim或者emacs,使用它们的插件比如cscope来阅读代码,gdb来调试程序,是件很自然的事情。
不过对于习惯的IDE的Java程序员来说,这确实有的不爽。估计Sun的人也是这样的,所以他们内部是使用Netbeans来调试hotspot的。
HotSpot development on Linux with NetBeans - Part 2 这篇blog是基于Netbeans6,里面很多都是为了解决nb的bug,我使用的是Netbeans7,所以很多bug已经修正。
2.7.1 NB项目设置
新建项目->C/C+->基于现有源代码的C/C+项目
指定包含源代码的目录:/media/d/openjdk7zip/openjdk/hotspot,由于makefile不在这个目录下,而是在它的子目录下,所以选择“定制”模式。
然后选择好Makefile的位置。下一步进入build的配置
make的工作目录是make,
生成命令是${MAKE} -f Makefile jvmg,
清理命令是${MAKE} -f Makefile clean,
生成结果填写一个目录就可以了。
然后点击确定就会buildhotspot,不过这样会出现问题,因为缺少ALT_BOOTDIR环境变量,但是Netbeans好像只能设置运行时的环境变量,而不能设置build时的环境变量,参考
这篇文章 http://forums.netbeans.org/post-40251.html。我折腾了好久也没有搞定,最后也没搞定。最后只能使了个毛招:
因为它默认去/java/re/j2se/1.6.0/latest/binaries/linux-i586找jdk,所以可以建个符号链接
lili@lili-desktop:/java/re/j2se/1.6.0/latest/binaries$ ll linux-i586
lrwxrwxrwx 1 lili lili 28 Nov 14 18:14 linux-i586 -> /home/lili/java/jdk1.6.0_26/
这下可以点击确定,让它build。
2.7.2 删除“无用”的文件
因为hotspot是跨平台的项目,前面可以看到针对不同的平台会有不同的实现,但是很多文件是相同的,比如文件名相同,因此同一个方法可能在linux里有个实现,windows有个实现,
这样NB就不知道到底会是哪个了,这会对Code Assistance带来影响,另外我们查看定义也会带来问题。所以针对Linux平台,我们可以在NB里删除不需要的文件(这不是删除磁盘上的文件,而是在NB里删除)
可以删除的目录:
hotspot/agent/src/os/solaris
hotspot/agent/src/os/win32
hotspot/agent/src/share/native/jvmdi
hotspot/src/cpu/sparc
hotspot/src/os/solaris
hotspot/src/os/windows
hotspot/src/os_cpu/solaris_sparc
hotspot/src/os_cpu/solaris_x86
hotspot/src/os_cpu/windows_x86
hotspot/src/share/vm/utilities/globalDefinitions_sparcWorks.hpp
hotspot/src/share/vm/utilities/globalDefinitions_visCPP.hpp
可以删除的文件(我的机器是x86,所以把x86-64的那些文件可以删掉):
hotspot/src/cpu/x86/vm/assembler_x86_64.cpp
hotspot/src/cpu/x86/vm/assembler_x86_64.hpp
hotspot/src/cpu/x86/vm/assembler_x86_64.inline.hpp
hotspot/src/cpu/x86/vm/dump_x86_64.cpp
hotspot/src/cpu/x86/vm/interp_masm_x86_64.cpp
hotspot/src/cpu/x86/vm/interp_masm_x86_64.hpp
hotspot/src/cpu/x86/vm/interpreterRT_x86_64.cpp
hotspot/src/cpu/x86/vm/interpreter_x86_64.cpp
hotspot/src/cpu/x86/vm/jniFastGetField_x86_64.cpp
hotspot/src/cpu/x86/vm/runtime_x86_64.cpp
hotspot/src/cpu/x86/vm/sharedRuntime_x86_64.cpp
hotspot/src/cpu/x86/vm/stubGenerator_x86_64.cpp
hotspot/src/cpu/x86/vm/stubRoutines_x86_64.cpp
hotspot/src/cpu/x86/vm/stubRoutines_x86_64.hpp
hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp
hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp
hotspot/src/cpu/x86/vm/templateTable_x86_64.hpp
hotspot/src/cpu/x86/vm/vm_version_x86_64.cpp
hotspot/src/cpu/x86/vm/vm_version_x86_64.hpp
hotspot/src/cpu/x86/vm/vtableStubs_x86_64.cpp
hotspot/src/cpu/x86/vm/x86_64.ad
hotspot/src/os_cpu/linux_x86/vm/assembler_linux_x86_64.cpp
hotspot/src/os_cpu/linux_x86/vm/linux_x86_64.ad
hotspot/src/os_cpu/linux_x86/vm/linux_x86_64.s
2.7.3 在NB里调试
修改项目属性->运行
运行命令:"{OUTPUT_PATH}" -XX:StopInterpreterAt=1 -version (如果想调试某个Java类,那么配好-classpath和类名)
运行目录:make
环境变量:配置好JAVA_HOME和LD_LIBRARY_PATH
在java.c的main里头加个断点
右键点击项目->调试
have fun ?~