上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 ffcallq 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 ffcallq 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编译优化中也需要按照调用约定实现相关的代码。