修改,編譯,GDB調試openjdk8源碼(docker環(huán)境下)
歡迎訪問我的GitHub
這里分類和匯總了欣宸的全部原創(chuàng)(含配套源碼):https://github.com/zq2599/blog_demos
在上一章《在docker上編譯openjdk8》里,我們在docker容器內成功編譯了openjdk8的源碼,有沒有讀者朋友產生過這個念頭:“能不能修改openjdk源碼,構建一個與眾不同的jdk“,今天我們就來閱讀一些openjdk的源碼,再嘗試做些小改動并驗證。
我們先編譯openjdk:
首先通過命令git clone git@github.com:zq2599/centos7_build_openjdk8.git下載構建鏡像所需的文件,下載后打開控制臺進入centos7_build_openjdk8目錄,執(zhí)行
docker build -t bolingcavalryopenjdk:0.0.1 .
這樣就構建好了鏡像文件,再執(zhí)行啟動docker容器的命令(
命令中的參數(shù)“–security-opt seccomp=unconfined”有特殊用處,稍后會講到
):
docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1
然后執(zhí)行以下命令進入容器的控制臺:
docker exec -it jdk001 /bin/bash
進入容器的控制臺后執(zhí)行以下兩個命令開始編譯:
./configure --with-debug-level=slowdebug make all ZIP_DEBUGINFO_FILES=0 DISABLE_HOTSPOT_OS_VERSION_CHECK=OK CONF=linux-x86_64-normal-server-slowdebug
以上就是編譯openjdk的步驟了,請大家開始編譯吧,因為等會兒會用到,我們要用編譯好的jdk做調試。
現(xiàn)在開始看源碼吧,本次分析的目標是針對我們熟悉的java -version命令,當我們在終端敲下這個命令的時候,jvm到底做了些什么呢?
整個分析驗證的流程是這樣的:
準備工作:
在容器內通過vim看源碼是很不方便的,所以我這里是在電腦上復制了一份openjdk的源碼(-:http://www.java.net/download/openjdk/jdk8/promoted/b132/openjdk-8-src-b132-03_mar_2014.zip
),用sublime text3打開openjdk源碼,真正到了要修改的時候再去docker容器里通過vi修改。
尋找程序入口
第一步就是把程序的入口和源碼對應起來,先要找到入口main函數(shù),步驟如下:
在docker容器內的/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin目錄下,執(zhí)行命令以下命令可以進入GDB的命令行模式:
gdb --args ./java -version
效果如下圖,可以看到已進入GDB命令行模式,可以繼續(xù)輸入GDB命令了:
輸入b main命令,在main函數(shù)打斷點,此時GDB會返回斷點位置的信息,如下圖,main函數(shù)的位置在
/usr/local/openjdk/jdk/src/share/bin/main.c, line 97
:
再輸入l命令可以打印源碼,如下圖:
在容器外的電腦上,通過sublime text3或者其他ide打開main.c,如下圖,開始讀代碼吧:
順序閱讀代碼
main函數(shù)中的代碼并不多,但有幾個宏定義會擾亂我們思路,從字面上看#ifdef _WIN32這樣的宏應該是windows平臺下才會生效的,但總不能每次都靠字面推斷,此時打斷點單步執(zhí)行是最直接的方法,但是在打斷點之前,我們先解決前面遺留的一個問題吧,
此問題挺重要的
:
還記得我們啟動docker容器的命令么:
docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1
命令中的
–security-opt seccomp=unconfined
參數(shù)有什么用?為何要留在打斷點之前再次提到這個參數(shù)?
這個參數(shù)和Docker的安全機制有關,具體的文檔鏈接在這里,請讀者們自行參悟,本人的英文太差就不獻丑了,簡單的說就是Docker有個Seccomp filtering功能,以伯克萊封包過濾器(Berkeley Packet Filter,縮寫B(tài)PF)的方式允許用戶對容器內的系統(tǒng)調用(syscall)做自定義的“allow”, “deny”, “trap”, “kill”, or “trace”操作,由于Seccomp filtering的限制,在默認的配置下,會導致我們在用GDB的時候run失敗,所以在執(zhí)行docker run的時候加入
–security-opt seccomp=unconfined
這個參數(shù),可以關閉seccomp profile的功能;
我之前不知道seccomp profile的限制,用命令
docker run –name=jdk001 -idt bolingcavalryopenjdk:0.0.1
啟動了容器,編譯可以成功,但是在用GDB調試的時候出了問題,如下圖:
上圖中,黃框中的“進入GDB”和“b main”(添加斷點)兩個命令都能正常執(zhí)行,但是紅框中的”r”(運行程序)命令在執(zhí)行的時候提示錯誤
“Error disabling address space randomization: Operation not permitted”
,在執(zhí)行”n”(單步執(zhí)行)命令的時候提示程序不在運行中。
遺留問題已經澄清,可以繼續(xù)跟蹤代碼了,之前我們已經在GDB輸入了”b mian”,給main函數(shù)打了斷點,現(xiàn)在輸入”r”開始執(zhí)行,然后就會看到main函數(shù)的斷點已經生效,輸入”n”可以跟蹤代碼執(zhí)行到了哪一行,如下圖:
原來代碼執(zhí)行的位置分別是97,122,123,125這四行,和下圖的源碼完全對應上了:
有了GDB神器,可以愉快的閱讀源碼了:
main.c的main函數(shù)中,調用JLI_Launch函數(shù),在Sublime text3中,將鼠標放置在”JLI_Launch”位置,會彈出一個小窗口,上面是JLI_Launch函數(shù)的聲明和定義的兩個鏈接,如下圖:
點擊第一個鏈接,跳轉到JLI_Launch函數(shù)的定義位置:
//根據(jù)環(huán)境變量初始化debug標志位,后續(xù)的日志是否會打印靠這個debug標志控制了 InitLauncher(javaw); //如果設置了debug,就會打印一些輔助信息 DumpState(); if (JLI_IsTraceLauncher()) { int i; printf("Command line args:\n"); for (i = 0; i < argc ; i++) { printf("argv[%d] = %s\n", i, argv[i]); } AddOption("-Dsun.java.launcher.diag=true", NULL); } //如果設置debug標志位,就打印命令行參數(shù),并加入額外參數(shù) //選擇jre版本,在jar包的manifest文件或者命令行中都可以對jre版本進行設置 SelectVersion(argc, argv, &main_class); /* 設置一些參數(shù),例如jvmpath的值被設置成jdk所在目錄下的“l(fā)ib/amd64/server/l”子目錄,再加上宏定義JVM_DLL的值"libjvm.so",即:/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so */ CreateExecutionEnvironment(&argc, &argv, jrepath, sizeof(jrepath), jvmpath, sizeof(jvmpath), jvmcfg, sizeof(jvmcfg)); //記錄加載libjvm.so的起始時間,在加載結束后可以得到并打印出加載libjvm.so的耗時 ifn.CreateJavaVM = 0; ifn.GetDefaultJavaVMInitArgs = 0; if (JLI_IsTraceLauncher()) { start = CounterGet(); } //加載/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so if (!LoadJavaVM(jvmpath, &ifn)) { return(6); } if (JLI_IsTraceLauncher()) { end = CounterGet(); } JLI_TraceLauncher("%ld micro seconds to LoadJavaVM\n", (long)(jint)Counter2Micros(end-start)); ++argv; --argc; if (IsJavaArgs()) { /* Preprocess wrapper arguments */ TranslateApplicationArgs(jargc, jargv, &argc, &argv); if (!AddApplicationOptions(appclassc, appclassv)) { return(1); } } else { //classpath處理 /* Set default CLASSPATH */ cpath = getenv("CLASSPATH"); if (cpath == NULL) { cpath = "."; } SetClassPath(cpath); } //解析命令行的參數(shù) if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath)) { return(ret); }
到這里先不要繼續(xù)往下讀,我們進ParseArguments函數(shù)中去看看:
如上圖紅框所示,解析到”-version”參數(shù)的時候,會將printVersion變量設置為JNI_TRUE并立即返回。
繼續(xù)閱讀JLI_Launch函數(shù):
//如果有-jar參數(shù),就會根據(jù)參數(shù)設置classpath if (mode == LM_JAR) { SetClassPath(what); } //添加一個用于HotSpot虛擬機的參數(shù)"-Dsun.java.command" SetJavaCommandLineProp(what, argc, argv); /* Set the -Dsun.java.launcher pseudo property */ //添加一個參數(shù)-Dsun.java.launcher=SUN_STANDARD,這樣JVM就知道是他的創(chuàng)建者的身份 SetJavaLauncherProp(); //獲取當前進程ID,放入?yún)?shù)-Dsun.java.launcher.pid中,這樣JVM就知道是他的創(chuàng)建者的進程ID SetJavaLauncherPlatformProps(); return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
接下來在JVMInit函數(shù)中,ContinueInNewThread函數(shù)中會調用ContinueInNewThread0函數(shù),
并且把JavaMain函數(shù)做為入?yún)鬟f給ContinueInNewThread0,
ContinueInNewThread0的代碼如下:
//如果指定了線程棧的大小,就在此設置到線程屬性變量attr中 if (stack_size > 0) { pthread_attr_setstacksize(&attr, stack_size); } //創(chuàng)建線程,外部傳入的JavaMain也在此傳給子線程,子線程創(chuàng)建成功后,會先執(zhí)行JavaMain(也就是continuation參數(shù)) if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) { void * tmp; //子線程創(chuàng)建成功后,當前線程在此以阻塞的方式等待子線程結束 pthread_join(tid, &tmp); rslt = (int)tmp; } else { /* * Continue execution in current thread if for some reason (e.g. out of * memory/LWP) a new thread can't be created. This will likely fail * later in continuation as JNI_CreateJavaVM needs to create quite a * few new threads, anyway, just give it a try.. */ //若創(chuàng)建子線程失敗,在當前線程直接執(zhí)行外面?zhèn)魅氲腏avaMain函數(shù) rslt = continuation(args); } //不再使用線程屬性,將其銷毀 pthread_attr_destroy(&attr);
在閱讀ContinueInNewThread0函數(shù)源碼的時候遇見了下圖紅框中的注釋,這是我見過的最優(yōu)秀的注釋(僅代表個人見解),當我看到pthread_create被調用時就在想“創(chuàng)建線程失敗會怎樣?”,然后這個注釋出現(xiàn)了,告訴我“如果因為某些原因(例如內存溢出)導致創(chuàng)建線程失敗,當前線程還會繼續(xù)執(zhí)行JavaMain,但是在后續(xù)的操作中依然有可能發(fā)生錯誤,例如JNI_CreateJavaVM函數(shù)會創(chuàng)建一些新的線程,因此,在當前線程執(zhí)行JavaMain只是做一次嘗試”。
在恰當?shù)奈恢脤栴}說清楚,并對后續(xù)發(fā)展做適當?shù)奶崾荆玫拇a加上好的注釋真是讓人受益匪淺。
接著上面的分析,在新的線程中JavaMain函數(shù)會被調用,這個函數(shù)內容如下:
//windows和linux下,RegisterThread是個空函數(shù),mac有實現(xiàn) RegisterThread(); //記錄當前時間,統(tǒng)計JVM初始化耗時的時候用到 start = CounterGet(); //調用libjvm.so庫中的CreateJavaVM方法初始化虛擬機 if (!InitializeJVM(&vm, &env, &ifn)) { JLI_ReportErrorMessage(JVM_ERROR1); exit(1); } //調用java類的靜態(tài)方法(sun.launcher.LauncherHelper.showSettings),打印jvm的設置信息 if (showSettings != NULL) { ShowSettings(env, showSettings); CHECK_EXCEPTION_LEAVE(1); } /* 調用java類的靜態(tài)方法(sun.misc.Version.print),打印: 1.java版本信息 2.java運行時版本信息 3.java虛擬機版本信息 */ if (printVersion || showVersion) { PrintJavaVersion(env, showVersion); CHECK_EXCEPTION_LEAVE(0); if (printVersion) { LEAVE(); } }
讀到這里可以不用讀后面的代碼了,因為printVersion變量為true,所以在執(zhí)行完PrintJavaVersion后,會調用LEAVE()函數(shù)使虛擬機與當前線程分離,然后就是線程結束,進程結束。
此時,我們應該聚焦PrintJavaVersion函數(shù),來看看平時執(zhí)行”java -version”的內容是怎么產生的。
進入PrintJavaVersion函數(shù),內容并不多,但能學到c語言的jvm是如何執(zhí)行java類中的靜態(tài)方法的,如下:
static void PrintJavaVersion(JNIEnv *env, jboolean extraLF) { jclass ver; jmethodID print; //從bootStrapClassLoader中查找sun.misc.Version NULL_CHECK(ver = FindBootStrapClass(env, "sun/misc/Version")); /* 由于命令行參數(shù)中沒有-showVersion參數(shù),所以extraLF不等于JNI_TRUE,所以此處調用的是sun.misc.Version.print方法,如果命令是"java -showVersion",那么調用的就是pringlin方法了 */ NULL_CHECK(print = (*env)->GetStaticMethodID(env, ver, (extraLF == JNI_TRUE) ? "println" : "print", "()V" ) ); (*env)->CallStaticVoidMethod(env, ver, print); }
讀到這里,本次閱讀源碼的工作似乎要結束了,
但事情沒那么簡單
,讀者們請在openjdk文件夾下搜索Version.java文件,雖然能搜到幾個Version.java,可是包路徑符合sun/misc/Version.java的文件只有一個,而這個Version.java的上層目錄是test目錄,不是src目錄,顯然只是測試代碼,并不是上面的PrintJavaVersion函數(shù)中調用的Version類:
現(xiàn)在問題來了,真正的Version類到底在哪呢?
剛才搜索Version.java文件的時候,我們搜的是下載openjdk源碼解壓之后的文件夾,現(xiàn)在我們回到docker容器中的/usr/local/openjdk目錄下,輸入find ./ -name Version.java試試,結果如下圖,在build目錄下,發(fā)現(xiàn)了四個sun/misc/Version.java文件:
在上圖中,sun/misc/Version.java文件一共有四個,后三個Version.java文件的路徑中帶有get_profile_1,get_profile_2這類的路徑,此處猜測是在某些場景或者設置的前提下才會產生(實在對不起各位讀者,這是我的猜測,具體原因至今還么搞清楚,有知道的請告訴一些,謝謝啦),所以這里我們還是聚焦第一個文件吧:
/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/gensrc/sun/misc/Version.java
Version.java這個文件,在下載的源碼中沒有,而編譯成功后的build目錄下卻有,并且文件的路徑中有gensrc這個目錄,顯然是在編譯過程中產生的,好吧,我們從Makefile中去尋找答案去:
在Makefile文件中,會調用Main.gmk,如下圖:
Main.gmk中會調用BuildJdk.gmk,如下圖:
BuildJdk.gmk中會調用GenerateSources.gmk,如下圖:
GenerateSources.gmk中會調用GensrcMisc.gmk,如下圖:
打開GensrcMisc.gmk文件后,一切都一目了然了,如下圖中的代碼所示,以
/src/share/classes/sun/misc/Version.java.template
文件作為模板,通過sed命令將Version.java.template文件中的一些占位符替換成已有的變量,替換了占位符之后的文件就是Version.java
我們可以看到一共有五個占位符被替換:
@@launcher_name@@ 替換成 $(LAUNCHER_NAME) @@java_version@@ 替換成 $(RELEASE) @@java_runtime_version@@ 替換成 $(FULL_VERSION) @@java_runtime_name@@ 替換成 $(RUNTIME_NAME) @@java_profile_name@@ 替換成 $(call profile_version_name, $@)
先看看Version.java.template中是什么:
果然有五個占位符,然后有個靜態(tài)方法public static void init(),里面把占位符對應的內容設置到全局屬性中去了。
終于搞清楚了,原來Version.java源自Version.java.template文件,在編譯構建的時候被生成,生成的時候Version.java.template文件中的占位符被替換成對應的變量。
現(xiàn)在,在docker容器里,執(zhí)行命令
vi /usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/gensrc/sun/misc
,打開Version.java看看吧,如下圖:
果然全部被替換了,再配合static代碼塊中的init方法,也就意味著這個類被加載的時候,應用就有了這三個全局的屬性:java.version,java.runtime.version,java.runtime.name
搞清楚了Version.java的來龍去脈,還剩一個小問題要搞清楚,在GensrcMisc.gmk文件中,用sed命令替換Version.java.template文件中的占位符的時候,那些用來替換占位符的變量是哪里來的呢?或者說Version.java文件中java_version =”1.8.0-internal-debug”,java_runtime_name =”O(jiān)penJDK Runtime Environment”,java_runtime_version = “1.8.0-internal-debug-_2017_04_21_04_39-b00”這些表達式中的和”1.8.0-internal-debug”,“OpenJDK Runtime Environment””,“1.8.0-internal-debug-_2017_04_21_04_39-b00”究竟來自何處?
這時候最簡單的辦法就是用”RELEASE”,”FULL_VERSION”,”RUNTIME_NAME”去做全局搜索,很快就能查出來,我這來梳理一下吧:
openjdk/configure文件中調用common/autoconf/configure
common/autoconf/configure中調用autogen.sh
autogen.sh中有如下操作:
把configure.ac中的內容做替換后輸出到generated-configure.sh,其中用到了autoconfig做配置
configure.ac中調用basics.m4
basics.m4中調用spec.gmk.in
spec.gmk.in中明確寫出了JDK_VERSION,RUNTIME_NAME這些變量的定義,如下圖:
PRODUCT_NAME和PRODUCT_SUFFIX是autoconfig的配置項,在openjdk/common/autoconf/version-numbers文件中定義,這是個autoconfig的配置文件,如下圖:
變量的來源梳理完畢,接著看代碼吧,sun.misc.Version類的print方法,如下圖,一如既往的簡答明了,將一些全局屬性取出然后打印出來:
至此,java -version命令對應的源碼分析完畢,簡答的總結一下,就是入口的main函數(shù)中,通過調用java的Version類的print靜態(tài)方法,將一些變量打印出來,這些變量是通過autoconfig輸出到自動生成的java源碼中的;
既然已經讀懂了源碼,現(xiàn)在該親自動手實踐一下啦,這里我們做兩個改動,
記得是在docker容器中用vi工具去改
:
修改Version.java.template文件,讓java -version在執(zhí)行的時候多輸出一行代碼,如下圖紅框位置:
修改/usr/local/openjdk/common/autoconf/version-numbers,修改PRODUCT_SUFFIX的值,根據(jù)之前的理解,PRODUCT_SUFFIX修改后,輸出的runtime name會有變化,改動如下:
改動完畢,回到/usr/local/openjdk目錄下,執(zhí)行下面兩行命令,開始編譯:
./configure --with-debug-level=slowdebug make all ZIP_DEBUGINFO_FILES=0 DISABLE_HOTSPOT_OS_VERSION_CHECK=OK CONF=linux-x86_64-normal-server-slowdebug
編譯結束后,去/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin目錄執(zhí)行./java -version,得到的輸出如下圖,可以看到我們的改動已經生效了
至次,本次閱讀,修改,調試和編譯openjdk8的實踐就結束了,其實JavaMain函數(shù)做了很多事情,這次只是看到其中打印信息的那一部分而已,后面的加載class,執(zhí)行java類等都還沒有看到,有興趣的讀者可以先對java的類加載做個初步了解,再繼續(xù)閱讀JavaMain函數(shù),相信您會有更多收獲的。
歡迎關注華為云博客:程序員欣宸
學習路上,你不孤單,欣宸原創(chuàng)一路相伴…
Docker JDK
版權聲明:本文內容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內刪除侵權內容。