C 代碼是如何跑起來的

上一篇「CPU 提供了什麼」中,咱們瞭解了物理的層面的 CPU,爲咱們提供了什麼。程序員

本篇,咱們介紹下高級語言「C 語言」是如何在物理 CPU 上面跑起來的。segmentfault

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 來看看,編譯器生成了什麼代碼:3d

  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

函數內的局部變量,爲何會放入棧空間呢?

這個恰好和局部變量的做用域關聯起來了:code

  1. 函數執行結束,返回的時候,局部變量也應該失效了
  2. 函數返回的時候,恰好要恢復棧高度到上一個調用者函數。

這樣的話,只須要棧高度恢復,也就意味着被調用函數的全部的臨時變量,所有失效了。內存

函數內的局部變量,必定會放入棧空間嗎?

答案是,不必定。
上面咱們是經過 -O0 編譯的,接下來,咱們看下 -O1 編譯生成的機器碼。作用域

此時的局部變量直接放在寄存器裏了,不須要寫入到棧空間了。
不過,此時 main 都已經再也不調用 add 函數了,由於已經被 gcc 內聯優化了。
好吧,構建個合適的用例也不容易。get

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 指令,意思是接下來跳轉到執行這一段機器指令。
相關文章
相關標籤/搜索