天天看點

java和c的本質--最重要的是啟動

java很神秘嗎?說什麼跨平台,虛拟機之類的。c#很神秘嗎?c很神秘嗎?作業系統呢?cpu呢?其實這些都不神秘,以前不懂作業系統的時候,看見個多線程就跟看見個神似的,現在呢?linux核心随便看,随便改,不就是程序管理那一堆事嘛,也沒有多少代碼。學習任何東西的時候,隻要靜态的架構以及動态的流程搞明白了,都不難的,這就和學英語一樣,靜态的東西其實就是字母表和單詞,動态的東西就是聽說讀寫,動靜結合,必有長進。

     下面以c和java為例說明其原理,特别說明c和java是如何啟動的,想必搞這個之前,每個人的c或者java的功底一定很高了。下面就一步一步地來:

寫c程式的時候,一般要寫個main:

int main(int argc, char **argv);

寫java的時候,一般也要寫個main:

public static void main(String argv[]);

在c語言的下面,有加載器,類似的,在java的下面,也有加載器,c的加載器必須脫離c語言程式設計的機制,但是仍然可以使用c的文法寫成,那麼啟動java的機制也必須脫離java程式設計的機制(加載器以下可能稱為連結器)。

     每一個編譯好的c程式都會在其elf檔案中被加入動态連結器的資訊,作業系統啟動新的elf檔案的時候(和程序沒有關系,程序是fork建立的,這裡僅僅是加載elf,也就是exec),首先調用連結器的入口,然後連接配接器中再調用c的main函數(如上),其實僅僅告訴作業系統一個入口點就可以了,随便一個都可以,作業系統會自動調轉到那裡的,但是一般而言每個作業系統的動态連結程式是固定的,比如在linux上就是ld-linux,這是為了減少資料備援,和重用元件的思想是一緻的,是以一般而言,固定的連結器調用固定的c入口,這個入口就被規定成了main函數,如果你自定義連結器的入口,那麼你完全可以調起來入口不是main的c程式。實際上,我們已經在使用這個辦法了,那就是加載動态庫并且調用動态庫的函數,這個意義上,動态庫就是一個可以沒有main的c程式,ld-linux.so這一類so本質上它們才是真正的程式入口,而我們編寫的帶有main的c程式可以了解成是ld-linux.so的一個動态庫。

     java的連結器或者加載器或者稱為啟動器其實也是一樣的道理,隻是它比c更上層了,它的啟動器不是作業系統直接調用的,而是c語言調用的,可以認為一個帶有main函數的c語言寫成的程式作為了java的啟動器,這個c啟動器可以調用别的動态庫,在這些本地環境中為java的執行建構了一個虛拟的“執行”環境,這就是java虛拟機,注意,這裡“執行”環境很重要,它導緻java是跨平台的,作業系統和c庫其實也做到了屏蔽下層的作用,然而它們都沒有能模拟一個執行環境,僅僅做到了接口相容而不是二進制相容,對于作業系統而言,比如linux完全向使用者空間暴露了機器指令,是以安裝在sparc上的linux和安裝在x86上的linux其上的應用程式是不相容的,c庫也是一樣的道理。另外就是作業系統本身的系統調用接口的不統一也會導緻程式無法即使在相同硬體但是不同作業系統上二進制跨平台。這種局面也許在作業系統和庫設計支援,對于跨平台執行沒有太大的需求,再者那時的硬體性能普遍很低,增加很多的虛拟層勢必會進一步降低性能,第三,那時的人們并沒有面對複雜應用的挑戰,是以和機器比較親近,軟體工程幾乎完全沒有被系統研究。

     ld-linux.so到底有什麼用以及怎麼用?它是到ld-2.x.so的軟連結,由于幾乎每一個正常且正規的程式都使用它,可以說,你把它删除了你的系統就起不來了,除非把磁盤mount到另一個系統上,然後拷貝一個過去,或者自己在别的系統上寫一個自定義連結器的程式...(還有一種辦法就是安裝sash-stand alone shell,它不依賴任何别的庫,靜态編譯的它是以也不依賴ld-linux,是以可以通過核心啟動參數init=/sbin/sash來啟動到它),然而如果你移動了它導緻系統找不到它引起的任何程式無法運作,隻要你知道把它搞到了哪裡,那就有救,做以下實驗:

#mv /lib/ld-2.7.so ./aaa

然後你會發現任何程式都沒有辦法運作了,此時幸好還有一個shell,隻要不關閉它,ld-2.7.so就一直在它的空間裡被映射着,這是因為linux是基于引用計數删除被移動的檔案的。隻要有shell就可以,執行:

#./aaa /bin/cp ./aaa /lib/ld-2.7.so

一切恢複正常。

那麼ld-2.7.so到底是什麼呢?如果它作為連結器的話,它的連結器在哪裡呢?實際上它是一個靜态連結的so,并且它是可執行的,從它的man手冊上可以看出,它就是用來加載程式以及程式需要的動态庫的,然後執行程式,它本身是不依賴其它的so的,它隻要OS就能運作,是以任何程式都可以看起來這樣運作:

#/lib/ld-2.7.so XX [可執行檔案全路徑XX的參數]

比如:

#/lib/ld-2.7.so /bin/ls的效果和ls是一樣的。

隻不過為了友善,linux内置了對elf可執行檔案的直接支援,當執行exec的時候,OS自動地直接調用了ld-2.7.so(ld-2.7.so被動态連結進了elf可執行檔案,作為其一個so,可以通過ldd看出來),而實際上,更加一般的方法就是通過指令行ld-2.7.so XXX來執行elf可執行程式的(這樣ld-2.7.so就不需要連結進elf可執行檔案了)。既然c/c++寫出的代碼可以直接編譯成elf可執行檔案來直接執行,其它任何的語言寫出的代碼都應該可以直接執行,在linux中這是通過binfmt_misc來支援的,具體的可以參考核心源碼Documents目錄中的binfmt_misc.txt文檔。現在看一下java的執行:

#java XX(XX為類檔案去掉.class)

這裡的java就相當于一個連結器,和/lib/ld-2.7.so是類似的,隻是它做了更多,包括建構一個虛拟執行環境(建立java虛拟機)等,它啟動了java類XX。

     現在elf可執行檔案的執行方式以及java類的執行方式更加統一了,都是連接配接器來調用的:ld-2.7.so XX和java XX,它們的本質其實是一樣的。那麼它們的互操作就不成問題了,由于java程式本身就是一個elf可執行檔案,并且它是c寫成的,是以java.c就表達了如何在c語言中調用java方法,隻不過java.c調用了固定的java方法,那就是main,這和ld-2.7.so最終調用c語言的main函數是一樣的,完全是為了規定,沒有機制上的原因。既然java類本身是c語言寫的程式啟動的,是以對于本地代碼,它肯定能回調,這就是jni接口,可以在java類中調用本地c語言寫成的函數。在了解了機制以後,我們完全可以參考java.c檔案寫一個不調用main方法的新的java連結器:

代碼參考自:www.rgagnon.com首先定義一個類,沒有main函數:

class Test {

        public native void func();   //定義本地方法

        static {

                System.loadLibrary("func1");

        }

        public static void sub(String[] args) {

                new Test().func();

}

編寫一個c檔案-startjava.c:

#include <jni.h>

#include <stdio.h>

int main() {

        JavaVM *vm;

        JNIEnv *env; 

        JavaVMInitArgs vm_args;

        JavaVMOption options[1];   

        options[0].optionString = "-Djava.class.path=."; 

        vm_args.version = JNI_VERSION_1_2;

        vm_args.options = options;

        vm_args.nOptions = 1;

        vm_args.ignoreUnrecognized = 1;

        jstring jstr; 

        jobjectArray args; 

        jint res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);

        jclass cls = (*env)->FindClass(env, "Test");    //找到Test類,這個也可以通過指令行傳遞

        jmethodID mid = (*env)->GetStaticMethodID(env, cls, "sub", "([Ljava/lang/String;)V"); //調用sub方法,而不是main

        jstring argString = (*env)->NewStringUTF(env, ""); //empty arg list

        args = (*env)->NewObjectArray(env, 1, (*env)->FindClass(env, "java/lang/String"), jstr); 

        (*env)->CallStaticVoidMethod(env, cls, mid, args); 

        return 0;

然後定一個本地方法的實作-func1.c:

JNIEXPORT void JNICALL Java_TestStunnel_func(JNIEnv *env, jobject obj)

{

    ....//随意

最終startjava這個elf可執行檔案啟動了一個沒有main的java類,然後java類中又調用一個本地方法,startjava.c同樣也可以沒有main函數--将main改成abc,而是自己寫一個連結器來執行,這個連結器負責從OS核心接手使用者空間的執行(載入所需動态庫-libc/libjvm等的過程早在核心分析elf的時候就做過了,是以不需要這個連結器來做),然後調用其abc函數即可。這樣從最初從OS接手過來,每一個不管是elf可執行的本地檔案還是java類,沒有用到ld-2.7.so和java可執行程式,也沒有一個擁有main函數(或者方法),然而“....//随意”真的就執行了。

     java是這樣,其它的比如perl,python也是這樣,包括c#等,都能如此折騰!

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271173