前面我们看到,没有main的代码也能执行,之所以拥有main是为了遵循一种规定,遵循这个规定得到的益处是一切更加紧凑,数据冗余更小,程序更好维护,系统更好维护...如果抛开这些不谈,代码的编写其实是很灵活的,完全不需要遵循任何的附加规定,所需要遵循的只是冯氏机器的执行流程。本文就来整一套没有main的c代码和java代码的混合,旨在揭示代码运行的深层含义以及c语言和java语言的启动和层次关系。我们按一个操作步骤一步一步来:
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开始。