修改編譯,GDB調試openjdk8源碼(docker環(huán)境下)

      網(wǎng)友投稿 782 2022-05-30

      歡迎訪問我的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看看吧,如下圖:

      修改,編譯,GDB調試openjdk8源碼(docker環(huán)境下)

      果然全部被替換了,再配合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小時內刪除侵權內容。

      上一篇:第十六屆全國大學生智能車競賽線上賽點賽道審核 - 廣東賽區(qū)(全國大學生智能車競賽賽區(qū))
      下一篇:OpenCV中的圖像處理 —— 圖像閾值+圖像平滑+形態(tài)轉換(opencv進行圖像處理)
      相關文章
      亚洲AV无码一区二区三区在线观看| 亚洲日韩精品无码专区| 亚洲国产成人久久一区二区三区| 亚洲视频一区在线播放| 无码乱人伦一区二区亚洲一| 亚洲AV无码专区国产乱码电影| 国产亚洲大尺度无码无码专线 | 亚洲乱码在线播放| 亚洲午夜久久久精品电影院| 亚洲六月丁香六月婷婷蜜芽| 亚洲Av无码一区二区二三区| 国产亚洲sss在线播放| 亚洲一本到无码av中文字幕 | 亚洲综合色丁香婷婷六月图片| 亚洲精品国产精品国自产网站| 亚洲va精品中文字幕| 亚洲精品国产国语| 亚洲性色AV日韩在线观看| 亚洲另类无码专区首页| 亚洲av午夜国产精品无码中文字 | 亚洲av无码专区首页| 亚洲av区一区二区三| 亚洲午夜福利精品久久| 在线亚洲午夜理论AV大片| 亚洲国产精品成人久久| 亚洲好看的理论片电影| 亚洲精品日韩中文字幕久久久| 亚洲人6666成人观看| 亚洲色无码专区一区| 国产大陆亚洲精品国产| 国产亚洲日韩在线三区| 日本亚洲成高清一区二区三区| 亚洲国产精品久久久久网站| 亚洲成av人片不卡无码| 亚洲精品无码你懂的| 国产亚洲精久久久久久无码AV| 精品国产_亚洲人成在线高清| 久久精品国产亚洲AV高清热 | 亚洲精品成人片在线播放| 亚洲性天天干天天摸| 亚洲人成电影院在线观看|