深入探索JVM垃圾回收:ARM服务器垃圾回收的挑战和优化
上QQ阅读APP看书,第一时间看更新

1.2.2 操作系统如何执行目标代码

OS首先读取ELF文件,按照进程执行时内存的布局把ELF文件的信息加载到内存中。在64位Linux环境下,文件到内存的映射以及加载后内存的布局如图1-4所示。

图1-4 Linux执行代码内存布局

代码的入口地址位于0x00400000处(32位系统位于0x08048000),本程序真正执行的地址开始于0x00400390(可以从objdump中看到该信息,此处对0进行了省略)。

architecture: i386:X86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0000000000400390

该地址对应的代码可以在代码段中找到。汇编代码如下:

0000000000400390 <_start>:
  400390:     31 ed                    xor        %ebp,%ebp
  400392:     49 89 d1                 mov        %rdx,%r9
  400395:     5e                       pop        %rsi
  400396:     48 89 e2                 mov        %rsp,%rdx
  400399:     48 83 e4 f0              and        $0xfffffffffffffff0,%rsp
  40039d:     50                       push       %rax
  40039e:     54                       push       %rsp
  40039f:     49 c7 c0 c0 04 40 00     mov        $0x4004c0,%r8
  4003a6:     48 c7 c1 d0 04 40 00     mov        $0x4004d0,%rcx
  4003ad:     48 c7 c7 89 04 40 00     mov        $0x400489,%rdi
  4003b4:     e8 c7 ff ff ff           callq      400380 <__libc_start_main@plt>
  4003b9:     f4                       hlt

该代码是gcc生成的,它作为入口地址,从此处开始执行。它将通过glibc的库函数_libc_start_main执行到源代码中的main函数中(具体细节可以参考其他书籍)。

在上面的代码示例中,main函数调用了add函数,这里简单演示一下从main函数到add函数的执行过程,主要关注栈的变化情况。main函数的汇编代码如下:

0000000000400489 <main>:
  400489:     55                       push       %rbp
  40048a:     48 89 e5                 mov        %rsp,%rbp
  40048d:     48 83 ec 10              sub        $0x10,%rsp
  400491:     c7 45 f4 03 00 00 00     movl       $0x3,-0xc(%rbp)
  400498:     c7 45 f8 05 00 00 00     movl       $0x5,-0x8(%rbp)
  40049f:     8b 55 f8                 mov        -0x8(%rbp),%edx
  4004a2:     8b 45 f4                 mov        -0xc(%rbp),%eax
  4004a5:     89 d6                    mov        %edx,%esi
  4004a7:     89 c7                    mov        %eax,%edi
  4004a9:     e8 c6 ff ff ff           callq      400474 <add>
  4004ae:     89 45 fc                 mov        %eax,-0x4(%rbp)
  4004b1:     c9                       leaveq
  4004b2:     c3                       retq

从main函数到执行callq指令之前,栈的情况如图1-5所示。

图1-5 main函数执行函数调用前的栈帧

从图1-5中可以看到,在调用add之前,main函数需要将参数以及add函数后的下一条指令地址入栈(由于此处add函数需要传递的参数比较少,因此直接使用寄存器传递。但是需要注意的是main函数中仍然有局部遍历i和j,它们也在栈中分配),其中传递的参数被add函数使用,返回地址用于add函数执行完成后继续返回main函数执行。当进入add函数中后,栈的情况如图1-6所示。

图1-6 main函数调用add函数后的栈帧

栈帧的变化是OS根据芯片的调用约定组织的,不同的芯片有不同的调用约定。在JVM编译优化中也需要按照调用约定实现相关的代码。