购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 从C代码执行过程看编译器和操作系统协同工作

本节通过一个简单的C代码在Linux下执行的过程,介绍编译器和OS是如何分工、合作完成代码的执行。

1.2.1 从源代码到目标代码

一个简单的C示例如下:

int global_count = 10;

int add(int i, int j){
    return i + j;
}

main(){
    int i = 3;
    int j = 5;

    int result = add(i, j);
}

该示例非常简单,不存在动态链接,编译、链接完成后即可在OS中执行。但是程序要在OS上执行,需要符合OS的执行要求,主要包括:

以Linux系统为例,上面的源代码和编译生成的可执行文件(ELF格式)的对应关系如下图1-3所示。

图1-3 代码和ELF格式约定

以Linux/X86-64为例,通过gcc编译器对上述代码进行编译,产生目标文件。文件格式为ELF,可以使用objdump命令(或readelf命令)对编译后的目标文件进行解析。首先可以确认一下数据段的信息,如下所示:

Disassembly of section .data:

0000000000600868 <__data_start>:
  600868:       00 00                   add    %al,(%rax)
        ……

000000000060086c <global_count>:
  60086c:       0a 00                   or     (%rax),%al

在这个数据段中有两个变量__data_start和global_count,其中global_count是代码中定义的全局变量,可以看到该变量占用的空间为4字节,初始值为10;而__data_start是gcc在链接时创建的一个全局变量,该变量指向数据段开始的位置,该变量的大小也是4字节。

编译器除了满足OS对于可执行文件的约定规范外,其中一个重要的功能就是针对代码进行编译优化(当然也包含了内存数据的布局等)。接下来看一下代码段的内容。代码段非常长,这里只关注add函数的汇编代码,如下所示:

0000000000400474 <add>:
  400474:     55            push        %rbp
  400475:     48 89 e5      mov         %rsp,%rbp
  400478:     89 7d fc      mov         %edi,-0x4(%rbp)
  40047b:     89 75 f8      mov         %esi,-0x8(%rbp)
  40047e:     8b 45 f8      mov         -0x8(%rbp),%eax
  400481:     8b 55 fc      mov         -0x4(%rbp),%edx
  400484:     8d 04 02      lea         (%rdx,%rax,1),%eax
  400487:     c9            leaveq
  400488:     c3            retq

注意

在gcc编译过程中采用的是默认编译优化级别(默认编译优化级别为O0),如果采用不同的编译优化级别,生成的代码会略有不同。

在C/C++中,编译优化体现在源代码的编译时间长短不同,同时不同的编译代码执行效率也会不同。在JVM的执行过程中也存在同样的问题,并且因为JVM在编译代码执行过程中需要先等待编译代码完成后才能执行,所以编译时长会直接影响应用执行的性能。

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

点击中间区域
呼出菜单
上一章
目录
下一章
×