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

在上一章《在docker上編譯openjdk8》裏,咱們在docker容器內成功編譯了openjdk8的源碼,有沒有讀者朋友產生過這個念頭:「能不能修改openjdk源碼,構建一個不同凡響的jdk「,今天咱們就來閱讀一些openjdk的源碼,再嘗試作些小改動並驗證。java

咱們先編譯openjdk:首先經過命令git clone git@github.com:zq2599/centos7buildopenjdk8.git下載構建鏡像所需的文件,下載後打開控制檯進入centos7buildopenjdk8目錄,執行linux

docker build -t bolingcavalryopenjdk:0.0.1 .複製代碼

這樣就構建好了鏡像文件,再執行啓動docker容器的命令(命令中的參數「–security-opt seccomp=unconfined」有特殊用處,稍後會講到):git

docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1複製代碼

而後執行如下命令進入容器的控制檯:github

docker exec -it jdk001 /bin/bash複製代碼

進入容器的控制檯後執行如下兩個命令開始編譯:docker

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

如今開始看源碼吧,本次分析的目標是針對咱們熟悉的java -version命令,當咱們在終端敲下這個命令的時候,jvm到底作了些什麼呢?windows

整個分析驗證的流程是這樣的:centos

這裏寫圖片描述

準備工做:在容器內經過vim看源碼是很不方便的,因此我這裏是在電腦上覆制了一份openjdk的源碼(下載地址:http://www.java.net/download/openjdk/jdk8/promoted/b132/openjdk-8-src-b132-03mar2014.zip),用sublime text3打開openjdk源碼,真正到了要修改的時候再去docker容器裏經過vi修改。安全

尋找程序入口bash

第一步就是把程序的入口和源碼對應起來,先要找到入口main函數,步驟以下:

  1. 在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函數的位置在/usr/local/openjdk/jdk/src/share/bin/main.c, line 97:

這裏寫圖片描述

再輸入l命令能夠打印源碼,以下圖:

這裏寫圖片描述

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

這裏寫圖片描述

順序閱讀代碼

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

還記得咱們啓動docker容器的命令麼:

docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1複製代碼

命令中的–security-opt seccomp=unconfined參數有什麼用?爲什麼要留在打斷點以前再次提到這個參數?

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

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

這裏寫圖片描述

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

遺留問題已經澄清,能夠繼續跟蹤代碼了,以前咱們已經在GDB輸入了」b mian」,給main函數打了斷點,如今輸入」r」開始執行,而後就會看到main函數的斷點已經生效,輸入」n」能夠跟蹤代碼執行到了哪一行,以下圖:

這裏寫圖片描述

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

這裏寫圖片描述

有了GDB神器,能夠愉快的閱讀源碼了:

main.c的main函數中,調用JLILaunch函數,在Sublime text3中,將鼠標放置在」JLILaunch」位置,會彈出一個小窗口,上面是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函數,而且把JavaMain函數作爲入參傳遞給ContinueInNewThread0,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函數源碼的時候碰見了下圖紅框中的註釋,這是我見過的最優秀的註釋(僅表明我的看法),當我看到pthreadcreate被調用時就在想「建立線程失敗會怎樣?」,而後這個註釋出現了,告訴我「若是由於某些緣由(例如內存溢出)致使建立線程失敗,當前線程還會繼續執行JavaMain,可是在後續的操做中依然有可能發生錯誤,例如JNICreateJavaVM函數會建立一些新的線程,所以,在當前線程執行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);
}複製代碼

讀到這裏,本次閱讀源碼的工做彷佛要結束了,但事情沒那麼簡單,讀者們請在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文件的路徑中帶有getprofile1,getprofile2這類的路徑,此處猜想是在某些場景或者設置的前提下才會產生(實在對不起各位讀者,這是個人猜想,具體緣由至今還麼搞清楚,有知道的請告訴一些,謝謝啦),因此這裏咱們仍是聚焦第一個文件吧:

/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中是什麼:

這裏寫圖片描述

果真有五個佔位符,而後有個靜態方法public static void init(),裏面把佔位符對應的內容設置到全局屬性中去了。

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

如今,在docker容器裏,執行命令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文件中javaversion =」1.8.0-internal-debug」,javaruntimename =」OpenJDK Runtime Environment」,javaruntimeversion = 「1.8.0-internal-debug-201704210439-b00」這些表達式中的和」1.8.0-internal-debug」,「OpenJDK Runtime Environment」」,「1.8.0-internal-debug-2017042104_39-b00」究竟來自何處?這時候最簡單的辦法就是用」RELEASE」,」FULLVERSION」,」RUNTIMENAME」去作全局搜索,很快就能查出來,我這來梳理一下吧:

openjdk/configure文件中調用common/autoconf/configurecommon/autoconf/configure中調用autogen.shautogen.sh中有以下操做:

這裏寫圖片描述

把configure.ac中的內容作替換後輸出到generated-configure.sh,其中用到了autoconfig作配置

configure.ac中調用basics.m4basics.m4中調用spec.gmk.inspec.gmk.in中明確寫出了JDKVERSION,RUNTIMENAME這些變量的定義,以下圖:

這裏寫圖片描述

PRODUCTNAME和PRODUCTSUFFIX是autoconfig的配置項,在openjdk/common/autoconf/version-numbers文件中定義,這是個autoconfig的配置文件,以下圖:

這裏寫圖片描述

變量的來源梳理完畢,接着看代碼吧,sun.misc.Version類的print方法,以下圖,一如既往的簡答明瞭,將一些全局屬性取出而後打印出來:

這裏寫圖片描述

至此,java -version命令對應的源碼分析完畢,簡答的總結一下,就是入口的main函數中,經過調用java的Version類的print靜態方法,將一些變量打印出來,這些變量是經過autoconfig輸出到自動生成的java源碼中的;

既然已經讀懂了源碼,如今該親自動手實踐一下啦,這裏咱們作兩個改動,記得是在docker容器中用vi工具去改

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

這裏寫圖片描述

修改/usr/local/openjdk/common/autoconf/version-numbers,修改PRODUCTSUFFIX的值,根據以前的理解,PRODUCTSUFFIX修改後,輸出的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,獲得的輸出以下圖,能夠看到咱們的改動已經生效了

這裏寫圖片描述

至次,本次閱讀,修改,調試和編譯openjdk8的實踐就結束了,其實JavaMain函數作了不少事情,此次只是看到其中打印信息的那一部分而已,後面的加載class,執行java類等都尚未看到,有興趣的讀者能夠先對java的類加載作個初步瞭解,再繼續閱讀JavaMain函數,相信您會有更多收穫的。

歡迎關注個人公衆號

在這裏插入圖片描述

相關文章
相關標籤/搜索