本节书摘来异步社区《HotSpot实战》一书中的第1章,第1.3节,作者:陈涛,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.3 实战:在HotSpot内调试HelloWorld
本节讲解的是Java入门程序HelloWorld在HotSpot上的执行过程。我们通过一个普通Java程序的运行过程,能够以点带面地讲解到涉及HotSpot内部实现的基础概念。
虽然是调试简单的HelloWorld程序,但在这个过程中会涉及HotSpot的基本数据结构以及环境准备等内容。理解这些,一方面使读者对HotSpot项目有个感性认识,其实调试HotSpot没有想象的那么困难,这利于我们增强驾驭HotSpot的自信心;另一方面,让我们正式接触到HotSpot的基本代码,并掌握HotSpot项目的基本调试方法。
调试准备过程如图1-6所示,具体步骤如下。
(1)选择调试器。
(2)配置GDB工作目录的绝对路径。
(3)配置JDK和动态链接库路径。
(4)定位Launcher。
(5)运行GDB初始化脚本,准备GDB运行环境。
(6)设置HotSpot项目断点。
(7)启动调试脚本。
(8)虚拟机运行HelloWorld程序,在断点处暂停。
(9)利用GDB命令调试HotSpot虚拟机程序的运行。
接下来,我们先了解一下如何使用GDB调试程序,然后开启我们的调试之旅。
1.3.1 认识GDB
本地程序(C/C++)的调试,一般使用GDB命令。对于Java程序员来说,GDB有些陌生,其实我们只需要掌握一些基本的调试命令,便足够应付HotSpot的调试任务了。
下面附上一些常用的GDB命令,包括断点、执行、查看代码、查看栈帧、查看数据等,如清单1-16所示。
清单1-16
断点:
break InitializeJVM:在InitializeJVM函数入口处设置断点
break java.c:JavaMain:在源文件java.c的InitializeJVM函数入口处设置断点
break os_linux.cpp:4380:在源文件os_linux.cpp的第4380行处设置断点
break *0x8048000:在地址为0x8048000的地址处设置断点
delete 1:删除断点1
delete:删除所有断点
执行:
step:执行1条语句,会进入函数
step n:执行n条语句,会进入函数
next:与step类似,但是不进入函数
next n:与step n类似,但是不进入函数
continue:继续运行
finish:运行至当前函数返回后退出
查看代码:
list n:查看当前源文件中第n行的代码
list InitializeJVM:查看InitializeJVM函数开始位置的代码
list:查看更多的行
list -:查看上次查看的代码行数之前的代码
默认,GDB打印10行。若需要调整,可使用:
set listsize n:调整打印行数为n行
查看栈帧:
frame n:从当前栈帧移动到#n栈帧
up n:从当前栈帧向上移动n个栈帧
down n:从当前栈帧向下移动n个栈帧
select-frame:查看更多的行
backstrace:查看整个调用栈
backstrace n:与backstrace类似,只不过只查看4个栈帧
backstrace full:查看整个调用栈,另外还打印出局部变量和参数
info args:查看函数参数
info locals:查看局部变量
查看数据:
print expr:查看expr的值,其中expr是源文件中的表达式
print /f expr n:以f指定的格式查看expr的值。其中f表示的格式可以为:
x:十六进制整数
d:有符号整数
u:无符号整数
o:八进制整数
t:二进制整数
c:字符常量
f:浮点数
s:字符串
r:原始格式
a:地址
x 0xbfffd034:查看内存地址为0xbfffd34的值
disassemble:查看汇编代码,反汇编当前函数
info registers:查看所有寄存器的值
print $eax:以十进制形式查看寄存器%eax的值
print /x $eax:以十六进制形式查看寄存器%eax的值
更多GDB的信息,可以参考GDB的官方教程1。
1.3.2 准备调试脚本
在HotSpot编译完成后,会在Jvmg目录下生成一个名为hotspot的脚本文件,如清单1-17所示。使用脚本可以替代大量重复性的输入,并且可以帮助我们准备好调试环境,为我们轻松调试系统创造了良好的环境。我们可以在此脚本文件的基础上调试HotSpot项目。
在启动调试之前,了解调试脚本究竟做了哪些工作是十分有益的,这有助于我们掌握独立分析和解决问题的能力,在出现问题时不致于手忙脚乱,可以利用自身所学知识解决问题。
清单1-17
来源:hotspot/src/os/posix/launcher/launcher.script
描述:调试脚本
#!/bin/bash```
首先是对传入的调试器名称参数进行转换,以便于定位到指定的调试器,支持的调试器包括GDB、GUD、DBX和VALGRIND等。
This is the name of the gdb binary to use
if [ ! "$GDB" ]
then
GDB=gdb
fi
This is the name of the gdb binary to use
if [ ! "$DBX" ]
then
DBX=dbx
fi
This is the name of the Valgrind binary to use
if [ ! "$VALGRIND" ]
then
VALGRIND=valgrind
fi
This is the name of Emacs for running GUD
EMACS=emacs`
用户可以通过调用该脚本时传入参数选择熟悉的调试器,这些参数可以是“-gdb”、“-gud”、“-dbx”或“-valgrind”。
# Make sure the paths are fully specified, i.e. they must begin with /.
SCRIPT=$(cd $(dirname $0) && pwd)/$(basename $0)
RUNDIR=$(pwd)
# Look whether the user wants to run inside gdb
case "$1" in
-gdb)
MODE=gdb
shift
;;
-gud)
MODE=gud
shift
;;
-dbx)
MODE=dbx
shift
;;
-valgrind)
MODE=valgrind
shift
;;
*)
MODE=run
;;
esac```
${MYDIR}是配置脚本的绝对路径:
Find out the absolute path to this script
MYDIR=$(cd $(dirname $SCRIPT) && pwd)`
${JDK}用来配置JDK路径,此外,还有一些链接库路径需要配置:
JDK=
if [ "${ALT_JAVA_HOME}" = "" ]; then
source ${MYDIR}/jdkpath.sh
else
JDK=${ALT_JAVA_HOME%%/jre};
fi
if [ "${JDK}" = "" ]; then
echo Failed to find JDK. ALT_JAVA_HOME is not set or ./jdkpath.sh is empty or not found.
exit 1
fi
# We will set the LD_LIBRARY_PATH as follows:
# o $JVMPATH (directory portion only)
# o $JRE/lib/$ARCH
# followed by the user's previous effective LD_LIBRARY_PATH, if
# any.
JRE=$JDK/jre
JAVA_HOME=$JDK
ARCH=i386
# Find out the absolute path to this script
MYDIR=$(cd $(dirname $SCRIPT) && pwd)
SBP=${MYDIR}:${JRE}/lib/${ARCH}
# Set up a suitable LD_LIBRARY_PATH
if [ -z "$LD_LIBRARY_PATH" ]
then
LD_LIBRARY_PATH="$SBP"
else
LD_LIBRARY_PATH="$SBP:$LD_LIBRARY_PATH"
fi
export LD_LIBRARY_PATH
export JAVA_HOME
JPARMS="$@ $JAVA_ARGS";```
${LAUNCHER}用作定位Launcher。关于Launcher,我们会在下一章中展开探讨。这里只需要知道它是虚拟机启动器程序便可:
Locate the gamma development launcher
LAUNCHER=${MYDIR}/gamma
if [ ! -x $LAUNCHER ] ; then
echo Error: Cannot find the gamma development launcher \"$LAUNCHER\"
exit 1
fi`
接下来是进行GDB自身初始化工作,包括配置工作路径以及信号等工作:
GDBSRCDIR=$MYDIR
BASEDIR=$(cd $MYDIR/../../.. && pwd)
init_gdb() {
# Create a gdb script in case we should run inside gdb
GDBSCR=/tmp/hsl.$$
rm -f $GDBSCR
cat >>$GDBSCR <<EOF
cd `pwd`
handle SIGUSR1 nostop noprint
handle SIGUSR2 nostop noprint
set args $JPARMS
file $LAUNCHER
directory $GDBSRCDIR```
在这里,可以设置断点。选择你感兴趣的HotSpot项目源代码位置,如JVM初始化模块“InitializeJVM”函数入口。接下来,便可以利用GDB的break命令设置断点,如:
Get us to a point where we can set breakpoints in libjvm.so
break InitializeJVM
run
Stop in InitializeJVM
delete 1
We can now set breakpoints wherever we like
EOF
}`
剩余配置代码我们可以不做调整:
case "$MODE" in
gdb)
init_gdb
$GDB -x $GDBSCR
rm -f $GDBSCR
;;
gud)
init_gdb
# First find out what emacs version we're using, so that we can
# use the new pretty GDB mode if emacs -version >= 22.1
case $($EMACS -version 2> /dev/null) in
*GNU\ Emacs\ 2[23]*)
emacs_gud_cmd="gdba"
emacs_gud_args="--annotate=3"
;;
*)
emacs_gud_cmd="gdb"
emacs_gud_args=
;;
esac
$EMACS --eval "($emacs_gud_cmd \"$GDB $emacs_gud_args -x $GDBSCR\")";
rm -f $GDBSCR
;;
dbx)
$DBX -s $MYDIR/.dbxrc $LAUNCHER $JPARAMS
;;
valgrind)
echo Warning: Defaulting to 16Mb heap to make Valgrind run faster, use -Xmx for larger heap
echo
$VALGRIND --tool=memcheck --leak-check=yes --num-callers=50 $LAUNCHER -Xmx16m $JPARMS
;;
run)
LD_PRELOAD=$PRELOADING exec $LAUNCHER $JPARMS
;;
*)
echo Error: Internal error, unknown launch mode \"$MODE\"
exit 1
;;
esac
RETVAL=$?
exit $RETVAL```
至此,调试脚本已经准备就绪,接下来,让我们开始HotSpot的调试吧。输入命令:
sh hotspot –gdb HelloWorld`
启动调试,将出现如图1-7所示的界面。
HotSpot运行在断点1(InitializeJVM)上停止下来,这时就可以利用前面提到的GDB命令尽情地控制HotSpot的运行了!
如果想让程序继续执行,输入continue命令使虚拟机正常运行下去,可以看到程序输出“Hello hotspot”并正常退出。感兴趣的读者可以亲自动手尝试一下。
建议读者结合源代码,利用GDB命令来跟踪调试HotSpot,查看系统运行时的内部数据和状态。这有两个好处:一方面,这能帮助我们将枯燥的阅读源码任务转换成有趣的虚拟机调试工作;另一方面,也能促进我们加深对HotSpot的理解。