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:
#include <jni.h>
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#等,都能如此折腾!
我们按一个操作步骤一步一步来:
1.准备没有main方法但是期望成为java主类的类文件-Test.java
class Test {
public native void Wrapper();
static {
System.loadLibrary("from-java");
}
public static void sub(String[] args) {
new TestStunnel().Wrapper();
}
}
编译之:javac Test.java
并且用c实现一个so,实现Wrapper方法,它可以是stunnel,可以是openvpn,只需要将其main函数改为Wrapper并且编译成so而不是可执行文件即可!
2.准备一个汇编文件-loader.asm,它负责直接接管sys_execve内核系统调用返回用户态的第一时间的执行流:
global _start
extern not_main
section .text
_start:
call not_main
ret
用nasm编译之:nasm -f elf loader.asm
3.准备一个c文件-startjvm.c,它启动java虚拟机,并且执行java方法:
#include <jni.h>
#include <stdio.h>
int not_main() {
JavaVM *vm;
JNIEnv *env;
... //见上篇的代码或者直接参考java.c
}
用gcc编译之:gcc -c startjvm.c -I<jdk的include和include/linux目录>
4.将上述的链接在一起:gcc -nostartfiles startjvm.o loader.o <jre的jvm动态库路径>/libjvm.so -o no_main_main
必须使用-nostartfiles,因为只有这样,编译器才不会自动加入启动函数以及别的库级别的初始化,才不会调用标准链接器的_start,关于这个你可以通过ld --verbose来观看!
5.执行吧:./no_main_main
6.结果Wrapper被调用,整个过程完全自给掌控,没有main函数,也没有main方法!
7.有一种更方便的不调用main的方式,那就是gcc指定-e参数,后面跟你希望执行的函数名称,这样虽然能达到一定的效果,但是估计你除了多学习了一个gcc参数之外,什么也学不到。
8.整个过程中我们把所有的逻辑往下压了一层,c语言中不再从语言机制的main开始,而是从链接器机制的_start开始,java语言中不再以语言机制的main开始,而是从jvm的GetStaticMethodID和随后的CallStaticVoidMethod开始。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dog250/archive/2010/11/13/6007293.aspx