1. JVM主函数入口
众所周知,C语言的启动入口都是一个main方法,Hotspot既然是C语言实现的语言,那必然存在一个main方法。这个方法存在于main.c中。这个文件时唯一一个需要被其他工具反复编译的文件,其他文件都是通过链接的方式引入的。
// 注意笔者删除了与windows实现的相关代码,主要研究linux平台的实现
// 标准的main函数,
/**
* argc: 参数个数
* argv: 参数值
* argv[0]指向程序运行的全路径名
* argv[1]指向在DOS命令行中执行程序名后的第一个字符串
**/
int main(int argc, char **argv)
{
int margc;
char** margv;
const jboolean const_javaw = JNI_FALSE;
margc = argc;
margv = argv;
// 通过一些列的处理,进入关键函数入口
return JLI_Launch(margc, margv,
sizeof(const_jargs) / sizeof(char *), const_jargs,
sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
FULL_VERSION,
DOT_VERSION,
(const_progname != NULL) ? const_progname : *margv,
(const_launcher != NULL) ? const_launcher : *margv,
(const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
const_cpwildcard, const_javaw, const_ergo_class);
}
/**
* 入口点
*/
int JLI_Launch(int argc, char ** argv, /* main argc, argc */
int jargc, const char** jargv, /* java args */
int appclassc, const char** appclassv, /* app classpath */
const char* fullversion, /* full version defined */
const char* dotversion, /* dot version defined */
const char* pname, /* program name */
const char* lname, /* launcher name */
jboolean javaargs, /* JAVA_ARGS */
jboolean cpwildcard, /* classpath wildcard*/
jboolean javaw, /* windows-only javaw */
jint ergo /* ergonomics class policy */
)
{
int mode = LM_UNKNOWN;
char *what = NULL;
char *cpath = 0;
char *main_class = NULL;
int ret;
InvocationFunctions ifn;
jlong start, end;
char jvmpath[MAXPATHLEN];
char jrepath[MAXPATHLEN];
char jvmcfg[MAXPATHLEN];
_fVersion = fullversion;
_dVersion = dotversion;
_launcher_name = lname;
_program_name = pname;
_is_java_args = javaargs;
_wc_enabled = cpwildcard;
_ergo_policy = ergo;
// 一些简单的版本号的验证,确认JRE是正确运行的
SelectVersion(argc, argv, &main_class);
// 创建执行环境上下文, 主要是初始化一些默认的配置
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
jvmcfg, sizeof(jvmcfg));
ifn.CreateJavaVM = 0;
ifn.GetDefaultJavaVMInitArgs = 0;
// 加载动态链接库, 即解析CreateJavaVM和GetDefaultJavaVMInitArgs函数的地址
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
++argv;
--argc;
/* 命令行参数解析
*/
if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
{
return(ret);
}
if (mode == LM_JAR) {
SetClassPath(what); /* Override class path */
}
// 解析&设置java参数
SetJavaCommandLineProp(what, argc, argv);
SetJavaLauncherProp();
SetJavaLauncherPlatformProps();
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
int JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
ShowSplashScreen();
// 开启子线程运行,因为shell执行命令后,进程的参数都已经确定,且不可修改,子线程可以自定义/修改默认配置
return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
int ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
// 省略部分代码
{ /* 创建线程,执行Java main方法 */
JavaMainArgs args;
int rslt;
args.argc = argc;
args.argv = argv;
args.mode = mode;
args.what = what;
args.ifn = *ifn;
rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
return (ret != 0) ? ret : rslt;
}
}
/**
* 注意这里是通过pthred创建线程
**/
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
int rslt;
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
if (stack_size > 0) {
pthread_attr_setstacksize(&attr, stack_size);
}
if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
void * tmp;
// 主线程会等待子线程执行完成,才会退出
pthread_join(tid, &tmp);
rslt = (int)tmp;
} else {
/*
* 这里只是一个兜底,内存不足等导致线程创建失败,会在主线程中执行
*/
rslt = continuation(args);
}
// 销毁线程
pthread_attr_destroy(&attr);
return rslt;
}
总结:通过命令启动JVM进程的时候,通过C语言的函数入口main开始运行,做了如下的事情:
- 常规校验,运行的版本号,JRE等是否可以征程运行
- 解析环境变量:JDK,JRE
- 通过动态链接,解析函数入口(CreateJavaVM和GetDefaultJavaVMInitArgs)
- 这里通过调用dlopen和dlsym解析函数地址
- 解析JVM需要的参数
- 启动子线程执行JAVA的主函数JavaMain
2. C语言的链接(Linking)
链接的主要工作是将多个代码文件和数据文件整合在一起形成一个可执行文件的过程。链接可以发生的时机有:编译时由编译器链接,加载时由加载器链接,运行时链接(应用程序)。现代的系统,链接都是自动执行的。
2.1 C语言编译过程
- 预处理:main.c 和 sum.c 转换成ASCII中间文件main.i和sum.i,一言以蔽之:简化程序员的工作,编译器帮我们做一些重复的工作,拷贝头文件,展开宏定义,删除注释,格式化文件等
- 运行编译器,将中间文件转换成汇编文件
- 运行汇编器(assembly)将汇编代码翻译成可重定位的目标文件 *.o
- 通过链接器,将sum.o和main.o以及必要的系统类库文件,生成可执行程序
一个经典的案例,考虑如下代码:
// 预处理
[root@learn-node1 ~]# cpp main.c > main.i
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "main.c"
int sum(int *a, int n);
int array[2] = {1,2};
int main {
int val = sum(array,2);
return val;
}
[root@learn-node1 ~]# cpp sum.c > sum.i
# 1 "sum.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "sum.c"
int sum(int *a, int n) {
int i, s = 0;
for (i = 0; i < n; i++) {
s += a[i];
}
return s;
}
# 编译
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl $2, %esi
movl $array, %edi
call sum
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
leave
ret
sum:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -24(%rbp)
movl %esi, -28(%rbp)
movl $0, -8(%rbp)
movl $0, -4(%rbp)
jmp .L2
.L3:
movl -4(%rbp), %eax
cltq
leaq 0(,%rax,4), %rdx
movq -24(%rbp), %rax
addq %rdx, %rax
movl (%rax), %eax
addl %eax, -8(%rbp)
addl $1, -4(%rbp)
.L2:
movl -4(%rbp), %eax
cmpl -28(%rbp), %eax
jl .L3
movl -8(%rbp), %eax
popq %rbp
ret
// 汇编,生成ELF文件
as -o /tmp/main.o /tmp/main.s
链接,生成可执行文件
2.2 目标文件
Linux的目标文件的格式是ELF( Executable and Linkable Format ),文件主要包含三种类型:
- 可重定位目标文件:.o,包含代码段和数据段
- 可执行目标文件:.out
- 共享库文件:.so
ELF文件:
- 前后为ELF头和节表头(描述节),中间是各个节
- .text: 代码段
- .rodata: 只读数据,例如格式化的string
- .data:初始化的全局变量和static 变量
- .bss:未初始化的全局变量和static变量
- .symtab:符号表
- .rel.text:可重定位的代码,后面可以被修改入口的地址
- .debug:
- .line
- .strtab: .symtab 和 .debug节中的string表
2.3 符号和符号表
可重定位文件包含有三种类型的符号:
- 模块m定义的符号:其他模块可能引用的符号
- 被模块m引用的符号:在其他模块定义的符号
- 在模块m中定义,但是只在m中引用,static修饰的符号
注意,如果符号出现了冲突,编译器会处理,例如下面的代码
#include "stdio.h"
/*
* 注意statc修饰的符号,编译器分配在.bss或者.data分配空间
* 局部变量,在栈上分配空间
**/
int f() {
static int x = 0;
return x;
}
int g() {
static int x = -1;
return x;
}
// 编译后打开符号表如下:
Symbol table '.symtab' contains 23 entries:
Num: Value Size Type Bind Vis Ndx Name
12: 0000000000000000 0 FILE LOCAL DEFAULT ABS demo2.c
13: 0000000000403004 4 OBJECT LOCAL DEFAULT 10 x.2373
14: 0000000000403000 4 OBJECT LOCAL DEFAULT 9 x.2376
// 注意看x.2376位于.data节,x.2373位于.bss节(未初始化),如果g中改成static x = 1, .bss节将不会存在
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 9] .data PROGBITS 0000000000403000 00003000
0000000000000004 0000000000000000 WA 0 0 4
[10] .bss NOBITS 0000000000403004 00003004
0000000000000004 0000000000000000 WA 0 0 4
注意生成的x的符号有两个,分别为x.2373和x.2376
# 注意函数f中的x为x.2373, g中为x.2376
f:
pushq %rbp
movq %rsp, %rbp
movl x.2373(%rip), %eax
popq %rbp
ret
g:
pushq %rbp
movq %rsp, %rbp
movl x.2376(%rip), %eax
popq %rbp
ret
2.4 符号解析
- 只在本模块中引用的符号(截取的一段static的符号)
int g() {
static int x = -1;
return x;
}
从上一节中可知,在本模块的符号比较简单,直接从当前ELF文件中查到对应符号的偏移量即可
// 编译后打开符号表如下:
Symbol table '.symtab' contains 23 entries:
Num: Value Size Type Bind Vis Ndx Name
12: 0000000000000000 0 FILE LOCAL DEFAULT ABS demo2.c
13: 0000000000403004 4 OBJECT LOCAL DEFAULT 10 x.2373
14: 0000000000403000 4 OBJECT LOCAL DEFAULT 9 x.2376
-
全局符号引用
如果多个模块都有相同全局符号,怎么处理这些符号呢?
- 编译阶段,确定这些符号的是强符号还是弱符号:函数和已经初始化的全局变量为强符号,未初始化的全局变量为弱符号
- 根据符号的强弱制定优先级:
- 多个强符号是不允许的
- 一个强符号和多个弱符合,选择强符号
- 多个弱符号,任选一个
2.4.1 链接静态库的符号解析
在linux系统中,可重定位文件的格式为ELF, 静态库文件为.a文件,在链接的时候只会将引用符号对应的.o一起链接,节省了空间。
完成了符号解析后,就需要做重定位(符号引用转直接引用),重定位有两个步骤组成。
- 重定位节和符号定义:链接器合并节,根据类型分组合并
- 在节中重定位符号引用:链接器修改在代码节和数据节内容中的每一个符号引用,让他们指向正确的运行时地址。
2.4.2 动态符号解析
2.5 加载运行可执行文件
可执行文件格式:
如何执行?loading
调用execve函数的时候,加载器会从磁盘中的加载可执行文件中代码和数据(拷贝到内存),然后跳转到第一条指令(entry point)开始执行。这个过程就是loading。
每一个程序的运行视图,如图所示:
- 0x400000:早期的操作系统用0地址表示null, 这是一块特殊的区域, 而操作系统需要大页对齐,所以会保留4M
- 所以代码段从4M开始,代码段为readonly,数据段RW, 所以将readonly 跟4M放在一起,往上为数据段
- 虚拟地址空间内核空间为高地址空间,映射到物理地址空间的低地址空间
- 栈的分配向下扩展, 堆向上扩展,通过molloc分配内存
- 在堆里面通过mmap映射,其中共享库是其中一分部分
- brk为分配小内存128K以内
2.6 动态链接
共享库解决了静态库的弊端(如果想用最新的升级版本的函数,必须重新编译链接),可以在加载或者运行时链接。可以被加载到特定的内存(实际是不同进程通过mmap映射同一个物理地址)地址然后与程序进行链接,这其实就是动态链接。共享库文件,在linux系统中就是.so文件。
动态链接的处理过程如图所示:
![image-20220409200855805](image-
.png)
注意,除了运行时链接,linux系统也提供了接口允许应用程序在运行时加载和链接共享库,注意JVM启动的时候,就是调用dlopen和dlsym来获取动态链接库函数的实际地址。
void *dlopen(const char *filename, int flag);
void *dlsym(void *handle, char *symbol);
int dlclose (void *handle);
const char *dlerror(void);
共享库的主要目的是在内存中共享同一内存空间来减少内存的使用,那么多个进程是怎么共享这个内存空间的呢?
- 提前指定内存空间,让共享库都加载到那片空间。but,就算不需要使用,也得保留空间。支持的共享库的数量必然优先。很难控制
- 为了避免前面的问题,现代系统编译共享库的代码段,让他们在不需要修改链接器的情况下能够加载到任何地方,
这里需要两个关键的结构,GOT(global offset table)和PLT(Procedure Linkage Table)
- GOT: 在第一次运行后,会修改给真实的地址
- PLT:实际就是一个代码段,通过链接器可以修改GOT的实际地址
GOT和PLT如何配合工作?
看一个经典案例(注意printf为动态链接库的内容):
#include "stdio.h"
int main() {
printf("%s","1231321");
}
# main方法
0000000000401040 <__libc_start_main@plt>:
401040: ff 25 da 2f 00 00 jmpq *0x2fda(%rip) # 404020 <__libc_start_main@GLIBC_2.2.5>
401046: 68 01 00 00 00 pushq $0x1
# 首先跳转到.plt, 401020
40104b: e9 d0 ff ff ff jmpq 401020 <.plt>
# plt实际就是一段代码
0000000000401020 <.plt>:
# GOT压入栈中
401020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
# 跳转到动态链接器执行
401026: ff 25 e4 2f 00 00 jmpq *0x2fe4(%rip) # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
40102c: 0f 1f 40 00 nopl 0x0(%rax)
Disassembly of section .got.plt:
# .GOT
0000000000404000 <_GLOBAL_OFFSET_TABLE_>:
404000: 28 3e sub %bh,(%rsi)
404002: 40 00 00 add %al,(%rax)
...
404015: 00 00 add %al,(%rax)
404017: 00 36 add %dh,(%rsi)
404019: 10 40 00 adc %al,0x0(%rax)
40401c: 00 00 add %al,(%rax)
40401e: 00 00 add %al,(%rax)
404020: 46 10 40 00 rex.RX adc %r8b,0x0(%rax)
404024: 00 00 add %al,(%rax)
404026: 00 00 add %al,(%rax)
404028: 56 push %rsi
404029: 10 40 00 adc %al,0x0(%rax)
40402c: 00 00 add %al,(%rax)
3. JVM启动过程中动态加载libjvm.so
jboolean LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
void *libjvm;
// 打开动态链接库
libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
。。。// 省略判断地址
ifn->CreateJavaVM = (CreateJavaVM_t)
// 获取JNI_CreateJavaVM的地址
dlsym(libjvm, "JNI_CreateJavaVM");
if (ifn->CreateJavaVM == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
// 获取JNI_GetDefaultJavaVMInitArgs地址
ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
if (ifn->GetDefaultJavaVMInitArgs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
// 获取JNI_GetCreatedJavaVMs的地址
ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
dlsym(libjvm, "JNI_GetCreatedJavaVMs");
if (ifn->GetCreatedJavaVMs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
return JNI_TRUE;
}
获取到各个函数的共享库的动态地址后,就可以开始调用函数了