久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

docker環境下如何修改,編譯,GDB調試openjdk8源碼

157次閱讀
沒有評論

共計 10717 個字符,預計需要花費 27 分鐘才能閱讀完成。

丸趣 TV 小編給大家分享一下 docker 環境下如何修改,編譯,GDB 調試 openjdk8 源碼,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!

我們先編譯 openjdk:首先通過命令 git clone git@github.com:zq2599/centos7_build_openjdk8.git 下載構建鏡像所需的文件,下載后打開控制臺進入 centos7_build_openjdk8 目錄,執行

docker build -t bolingcavalryopenjdk:0.0.1 .

這樣就構建好了鏡像文件,再執行啟動 docker 容器的命令 (font color= red 命令中的參數“–security-opt seccomp=unconfined”有特殊用處,稍后會講到 /font):

docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1

然后執行以下命令進入容器的控制臺:

docker exec -it jdk001 /bin/bash

進入容器的控制臺后執行以下兩個命令開始編譯:

./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 做調試。

現在開始看源碼吧,本次分析的目標是針對我們熟悉的 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 函數,步驟如下:

在 docker 容器內的 /usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin 目錄下,執行命令以下命令可以進入 GDB 的命令行模式:

gdb --args ./java -version

效果如下圖,可以看到已進入 GDB 命令行模式,可以繼續輸入 GDB 命令了:

輸入 b main 命令,在 main 函數打斷點,此時 GDB 會返回斷點位置的信息,如下圖,main 函數的位置在 font color= red /usr/local/openjdk/jdk/src/share/bin/main.c, line 97 /font :

再輸入 l 命令可以打印源碼,如下圖:

在容器外的電腦上,通過 sublime text3 或者其他 ide 打開 main.c,如下圖,開始讀代碼吧:

順序閱讀代碼

main 函數中的代碼并不多,但有幾個宏定義會擾亂我們思路,從字面上看 #ifdef _WIN32 這樣的宏應該是 windows 平臺下才會生效的,但總不能每次都靠字面推斷,此時打斷點單步執行是最直接的方法,但是在打斷點之前,我們先解決前面遺留的一個問題吧,font color= red 此問題挺重要的 /font:

還記得我們啟動 docker 容器的命令么:

docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1

命令中的 font color= blue –security-opt seccomp=unconfined /font 參數有什么用?為何要留在打斷點之前再次提到這個參數?

這個參數和 Docker 的安全機制有關,具體的文檔鏈接在這里,請讀者們自行參悟,本人的英文太差就不獻丑了,簡單的說就是 Docker 有個 Seccomp filtering 功能,以伯克萊封包過濾器(Berkeley Packet Filter,縮寫 BPF)的方式允許用戶對容器內的系統調用(syscall)做自定義的“allow”,“deny”,“trap”,“kill”, or“trace”操作,由于 Seccomp filtering 的限制,在默認的配置下,會導致我們在用 GDB 的時候 run 失敗,所以在執行 docker run 的時候加入 font color= red –security-opt seccomp=unconfined /font 這個參數,可以關閉 seccomp profile 的功能;

我之前不知道 seccomp profile 的限制,用命令 font color= blue docker run –name=jdk001 -idt bolingcavalryopenjdk:0.0.1 /font 啟動了容器,編譯可以成功,但是在用 GDB 調試的時候出了問題,如下圖:

上圖中,黃框中的“進入 GDB”和“b main”(添加斷點) 兩個命令都能正常執行,但是紅框中的”r”(運行程序) 命令在執行的時候提示錯誤 font color= red“Error disabling address space randomization: Operation not permitted”/font,在執行”n”(單步執行) 命令的時候提示程序不在運行中。

遺留問題已經澄清,可以繼續跟蹤代碼了,之前我們已經在 GDB 輸入了”b mian”,給 main 函數打了斷點,現在輸入”r”開始執行,然后就會看到 main 函數的斷點已經生效,輸入”n”可以跟蹤代碼執行到了哪一行,如下圖:

原來代碼執行的位置分別是 97,122,123,125 這四行,和下圖的源碼完全對應上了:

有了 GDB 神器,可以愉快的閱讀源碼了:

main.c 的 main 函數中,調用 JLI_Launch 函數,在 Sublime text3 中,將鼠標放置在”JLI_Launch”位置,會彈出一個小窗口,上面是 JLI_Launch 函數的聲明和定義的兩個鏈接,如下圖:

點擊第一個鏈接,跳轉到 JLI_Launch 函數的定義位置:

// 根據環境變量初始化 debug 標志位,后續的日志是否會打印靠這個 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 標志位,就打印命令行參數,并加入額外參數
 // 選擇 jre 版本,在 jar 包的 manifest 文件或者命令行中都可以對 jre 版本進行設置
 SelectVersion(argc, argv,  main_class); 
 /*
  設置一些參數,例如 jvmpath 的值被設置成 jdk 所在目錄下的“lib/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);
 }
 // 解析命令行的參數
 if (!ParseArguments( argc,  argv,  mode,  what,  ret, jrepath))
 { return(ret);
 }

到這里先不要繼續往下讀,我們進 ParseArguments 函數中去看看:

如上圖紅框所示,解析到”-version”參數的時候,會將 printVersion 變量設置為 JNI_TRUE 并立即返回。

繼續閱讀 JLI_Launch 函數:

// 如果有 -jar 參數,就會根據參數設置 classpath
 if (mode == LM_JAR) { SetClassPath(what);
 }
 // 添加一個用于 HotSpot 虛擬機的參數 -Dsun.java.command 
 SetJavaCommandLineProp(what, argc, argv);
 /* Set the -Dsun.java.launcher pseudo property */
 // 添加一個參數 -Dsun.java.launcher=SUN_STANDARD,這樣 JVM 就知道是他的創建者的身份
 SetJavaLauncherProp();
 // 獲取當前進程 ID,放入參數 -Dsun.java.launcher.pid 中,這樣 JVM 就知道是他的創建者的進程 ID
 SetJavaLauncherPlatformProps();
 return JVMInit(ifn, threadStackSize, argc, argv, mode, what, ret);

接下來在 JVMInit 函數中,ContinueInNewThread 函數中會調用 ContinueInNewThread0 函數,font color= red 并且把 JavaMain 函數做為入參傳遞給 ContinueInNewThread0,/font ContinueInNewThread0 的代碼如下:

// 如果指定了線程棧的大小,就在此設置到線程屬性變量 attr 中
 if (stack_size   0) { pthread_attr_setstacksize( attr, stack_size);
 }
 // 創建線程,外部傳入的 JavaMain 也在此傳給子線程,子線程創建成功后,會先執行 JavaMain(也就是 continuation 參數)
 if (pthread_create( tid,  attr, (void *(*)(void*))continuation, (void*)args) == 0) {
 void * tmp;
 // 子線程創建成功后, 當前線程在此以阻塞的方式等待子線程結束
 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..
 */
 // 若創建子線程失敗,在當前線程直接執行外面傳入的 JavaMain 函數
 rslt = continuation(args);
 }
 // 不再使用線程屬性,將其銷毀
 pthread_attr_destroy(attr);

在閱讀 ContinueInNewThread0 函數源碼的時候遇見了下圖紅框中的注釋,這是我見過的最優秀的注釋(僅代表個人見解), 當我看到 pthread_create 被調用時就在想“創建線程失敗會怎樣?”,然后這個注釋出現了,告訴我“如果因為某些原因(例如內存溢出)導致創建線程失敗,當前線程還會繼續執行 JavaMain,但是在后續的操作中依然有可能發生錯誤,例如 JNI_CreateJavaVM 函數會創建一些新的線程,因此,在當前線程執行 JavaMain 只是做一次嘗試”。

在恰當的位置將問題說清楚,并對后續發展做適當的提示,好的代碼加上好的注釋真是讓人受益匪淺。

接著上面的分析,在新的線程中 JavaMain 函數會被調用,這個函數內容如下:

//windows 和 linux 下,RegisterThread 是個空函數,mac 有實現
 RegisterThread();
 // 記錄當前時間,統計 JVM 初始化耗時的時候用到
 start = CounterGet();
 // 調用 libjvm.so 庫中的 CreateJavaVM 方法初始化虛擬機
 if (!InitializeJVM( vm,  env,  ifn)) { JLI_ReportErrorMessage(JVM_ERROR1);
 exit(1);
 }
 // 調用 java 類的靜態方法 (sun.launcher.LauncherHelper.showSettings),打印 jvm 的設置信息
 if (showSettings != NULL) { ShowSettings(env, showSettings);
 CHECK_EXCEPTION_LEAVE(1);
 }
 /*
  調用 java 類的靜態方法 (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,所以在執行完 PrintJavaVersion 后,會調用 LEAVE() 函數使虛擬機與當前線程分離,然后就是線程結束,進程結束。

此時,我們應該聚焦 PrintJavaVersion 函數,來看看平時執行”java -version”的內容是怎么產生的。

進入 PrintJavaVersion 函數,內容并不多,但能學到 c 語言的 jvm 是如何執行 java 類中的靜態方法的,如下:

static void
PrintJavaVersion(JNIEnv *env, jboolean extraLF)
 jclass ver;
 jmethodID print;
 // 從 bootStrapClassLoader 中查找 sun.misc.Version
 NULL_CHECK(ver = FindBootStrapClass(env,  sun/misc/Version));
 /*
  由于命令行參數中沒有 -showVersion 參數,所以 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);
}

讀到這里,本次閱讀源碼的工作似乎要結束了,font color= red 但事情沒那么簡單 /font,讀者們請在 openjdk 文件夾下搜索 Version.java 文件,雖然能搜到幾個 Version.java,可是包路徑符合 sun/misc/Version.java 的文件只有一個,而這個 Version.java 的上層目錄是 test 目錄,不是 src 目錄,顯然只是測試代碼,并不是上面的 PrintJavaVersion 函數中調用的 Version 類:

現在問題來了,真正的 Version 類到底在哪呢?

剛才搜索 Version.java 文件的時候,我們搜的是下載 openjdk 源碼解壓之后的文件夾,現在我們回到 docker 容器中的 /usr/local/openjdk 目錄下,輸入 find ./ -name Version.java 試試,結果如下圖,在 build 目錄下,發現了四個 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 文件后,一切都一目了然了,如下圖中的代碼所示,以 font color= blue /src/share/classes/sun/misc/Version.java.template /font 文件作為模板,通過 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 中是什么:

果然有五個占位符,然后有個靜態方法 public static void init(),里面把占位符對應的內容設置到全局屬性中去了。

終于搞清楚了,原來 Version.java 源自 Version.java.template 文件,在編譯構建的時候被生成,生成的時候 Version.java.template 文件中的占位符被替換成對應的變量。

現在,在 docker 容器里,執行命令 font color= blue vi /usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/gensrc/sun/misc /font,打開 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 =”OpenJDK 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 函數中,通過調用 java 的 Version 類的 print 靜態方法,將一些變量打印出來,這些變量是通過 autoconfig 輸出到自動生成的 java 源碼中的;

既然已經讀懂了源碼,現在該親自動手實踐一下啦,這里我們做兩個改動,font color= red 記得是在 docker 容器中用 vi 工具去改 /font:

修改 Version.java.template 文件,讓 java -version 在執行的時候多輸出一行代碼,如下圖紅框位置:

修改 /usr/local/openjdk/common/autoconf/version-numbers,修改 PRODUCT_SUFFIX 的值,根據之前的理解,PRODUCT_SUFFIX 修改后,輸出的 runtime name 會有變化,改動如下:

改動完畢,回到 /usr/local/openjdk 目錄下,執行下面兩行命令,開始編譯:

./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 目錄執行./java -version,得到的輸出如下圖,可以看到我們的改動已經生效了

看完了這篇文章,相信你對“docker 環境下如何修改,編譯,GDB 調試 openjdk8 源碼”有了一定的了解,如果想了解更多相關知識,歡迎關注丸趣 TV 行業資訊頻道,感謝各位的閱讀!

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-08-25發表,共計10717字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 呈贡县| 山西省| 兰考县| 武穴市| 额尔古纳市| 大方县| 钟山县| 兴安县| 沅陵县| 盱眙县| 黄骅市| 大竹县| 上虞市| 吐鲁番市| 滨州市| 元氏县| 广州市| 巴青县| 吉安市| 平武县| 台北县| 盱眙县| 宁安市| 孟津县| 五河县| 兴业县| 来宾市| 永兴县| 嘉善县| 仙游县| 左云县| 云龙县| 沅陵县| 会同县| 昌图县| 镇原县| 太康县| 和顺县| 防城港市| 汕尾市| 杨浦区|