0

C 代码是如何跑起来的

 3 years ago
source link: https://segmentfault.com/a/1190000039890899
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇「CPU 提供了什么」中,我们了解了物理的层面的 CPU,为我们提供了什么。

本篇,我们介绍下高级语言「C 语言」是如何在物理 CPU 上面跑起来的。

C 语言提供了什么

C 语言作为高级语言,为程序员提供了更友好的表达方式。在我看来,主要是提供了以下抽象能力:

  1. 变量,以及延伸出来的复杂结构体
    我们可以基于变量来描述复杂的状态。
  2. 函数
    我们可以基于函数,把复杂的行为逻辑,拆分到不同的函数里,以简化复杂的逻辑以。以及,我们可以复用相同目的的函数,现实世界里大量的基础库,简化了程序员的编码工作。

构建一个良好的示例代码,可以很好帮助我们去理解。
下面的示例里,我们可以看到 变量函数 都用上了。

#include "stdio.h"

int add (int a, int b) {
    return a + b;
}

int main () {
    int a = 1;
    int b = 2;
    int c = add(a, b);

    printf("a + b = %d\n", c);

    return 0;
}

毫无意外,我们得到了期望的 3

$ gcc -O0 -g3 -Wall -o simple simple.c
$ ./simple
a + b = 3

我们还是用 objdump 来看看,编译器生成了什么代码:

  1. 变量
    局部变量,包括函数参数,全部被压入了 里。
  2. 函数
    函数本身,被单独编译为了一段机器指令
    函数调用,被编译为了 call 指令,参数则是函数对应那一段机器指令的第一个指令地址。
$ objdump -M intel -j .text -d simple

# 截取其中最重要的部分

000000000040052d <add>:
  40052d:       55                      push   rbp
  40052e:       48 89 e5                mov    rbp,rsp
  400531:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi
  400534:       89 75 f8                mov    DWORD PTR [rbp-0x8],esi
  400537:       8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  40053a:       8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
  40053d:       01 d0                   add    eax,edx
  40053f:       5d                      pop    rbp
  400540:       c3                      ret

0000000000400541 <main>:
  400541:       55                      push   rbp
  400542:       48 89 e5                mov    rbp,rsp
  400545:       48 83 ec 10             sub    rsp,0x10
  400549:       c7 45 fc 01 00 00 00    mov    DWORD PTR [rbp-0x4],0x1
  400550:       c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2
  400557:       8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
  40055a:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  40055d:       89 d6                   mov    esi,edx
  40055f:       89 c7                   mov    edi,eax
  400561:       e8 c7 ff ff ff          call   40052d <add>
  400566:       89 45 f4                mov    DWORD PTR [rbp-0xc],eax
  400569:       8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
  40056c:       89 c6                   mov    esi,eax
  40056e:       bf 20 06 40 00          mov    edi,0x400620
  400573:       b8 00 00 00 00          mov    eax,0x0
  400578:       e8 93 fe ff ff          call   400410 <printf@plt>
  40057d:       b8 00 00 00 00          mov    eax,0x0
  400582:       c9                      leave
  400583:       c3                      ret
  400584:       66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  40058b:       00 00 00
  40058e:       66 90                   xchg   ax,ax

函数内的局部变量,为什么会放入栈空间呢?

这个刚好和局部变量的作用域关联起来了:

  1. 函数执行结束,返回的时候,局部变量也应该失效了
  2. 函数返回的时候,刚好要恢复栈高度到上一个调用者函数。

这样的话,只需要栈高度恢复,也就意味着被调用函数的所有的临时变量,全部失效了。

函数内的局部变量,一定会放入栈空间吗?

答案是,不一定。
上面我们是通过 -O0 编译的,接下来,我们看下 -O1 编译生成的机器码。

此时的局部变量直接放在寄存器里了,不需要写入到栈空间了。
不过,此时 main 都已经不再调用 add 函数了,因为已经被 gcc 内联优化了。
好吧,构建个合适的用例也不容易。

000000000040052d <add>:
  40052d:       8d 04 37                lea    eax,[rdi+rsi*1]
  400530:       c3                      ret

0000000000400531 <main>:
  400531:       48 83 ec 08             sub    rsp,0x8
  400535:       be 03 00 00 00          mov    esi,0x3
  40053a:       bf f0 05 40 00          mov    edi,0x4005f0
  40053f:       b8 00 00 00 00          mov    eax,0x0
  400544:       e8 c7 fe ff ff          call   400410 <printf@plt>
  400549:       b8 00 00 00 00          mov    eax,0x0
  40054e:       48 83 c4 08             add    rsp,0x8
  400552:       c3                      ret
  400553:       66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  40055a:       00 00 00
  40055d:       0f 1f 00                nop    DWORD PTR [rax]

禁止内联优化

我们用如下命令,关闭 gcc 的内联优化:

gcc -fno-inline -O1 -g3 -Wall -o simple simple.c

再来看下汇编代码,此时的机器码就符合理想的验证结果了。

000000000040052d <add>:
  40052d:       8d 04 37                lea    eax,[rdi+rsi*1]
  400530:       c3                      ret

0000000000400531 <main>:
  400531:       48 83 ec 08             sub    rsp,0x8
  400535:       be 02 00 00 00          mov    esi,0x2
  40053a:       bf 01 00 00 00          mov    edi,0x1
  40053f:       e8 e9 ff ff ff          call   40052d <add>
  400544:       89 c6                   mov    esi,eax
  400546:       bf f0 05 40 00          mov    edi,0x4005f0
  40054b:       b8 00 00 00 00          mov    eax,0x0
  400550:       e8 bb fe ff ff          call   400410 <printf@plt>
  400555:       b8 00 00 00 00          mov    eax,0x0
  40055a:       48 83 c4 08             add    rsp,0x8
  40055e:       c3                      ret
  40055f:       90                      nop
  1. 对于 C 语言的变量,编译器会为其分配一段内存空间来存储
    函数内的局部变量,放入栈空间是理想的映射方式。不过编译的优化模式下,则会尽量使用寄存器来存储,寄存器不够用了,才会使用栈空间。
    全局变量,则有对应的内存段来存储,这个以后可以再聊。
  2. 对于 C 语言的函数,编译器会编译为独立的一段机器指令
    调用该函数,则是执行 call 指令,意思是接下来跳转到执行这一段机器指令。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK