关于c:C-代码是如何跑起来的

42次阅读

共计 2860 个字符,预计需要花费 8 分钟才能阅读完成。

上一篇「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 指令,意思是接下来跳转到执行这一段机器指令。

正文完
 0