--- title: 'C 概念 & 編譯' disqus: kyleAlien --- C 概念 & 編譯 === ## Overview of Content C 是**關注程式** 的語言(命令範式),重順序性、演算法 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/) ::: [TOC] ## 程式概念 解決問題的方法稱為**演算法 (`algorithm`)**,它是解決問題的一種「**思路、方案**」 表達問題的解決方法稱為 **程式 (`program`)**,它是將人類的思路與方案,透過電腦表達出來的一種「**方式**」 ### 低階語言概述 * 以組合語言(也可稱為彙編語言)來說它必須符合兩個規定 1. **一對一性**:**不同程式皆是針對於不同的電腦設備**,因為`不同的處理器有不同的指令`,不可兼容於其它的處理器 2. **不可攜帶性**(`portable`):**基於`不可兼容`,攜帶到其他設備也沒有用** 3. 編譯是透過「組譯器」 ### 高階語言概述 * `C`、`C++`、`Java` 或是 `Kotlin` 它不需依賴於處理器(CPU)的不同作編譯不同的程式,是一種可攜帶性語言 1. 不再關注特定電腦的體系結構,**不依賴於指令集** 2. **語法的標準化**,在不同電腦上很少需要修改即可運行 3. 編譯是透過「編譯器、直譯器」 ## 作業系統 & 編譯器 & 直譯器 ### 作業系統 OS * 控制電腦系統的程式,所有給予電腦的命令,都需要透過作業系統`分配資源` and `引導`才能正常執行 * **Unix 主要就是用 C 語言編寫**,並**對電腦架構做了很少的假設**(抽象化作的很好),所以可以成功的移植到不同電腦系統中 ### 編譯器 * 編譯器及是 **翻譯高階語法** 給處理器知道的程式 :::success * 副檔名 ? 副檔名是`.c`,這只是一個協定(讓電腦知道它是 C 程式),並不是要求 副檔名是`.out`,,這是 Unix 系統下的執行檔 ::: ### 直譯器 - intepreted * 使用直譯器的語言有像是 `JavaScript`、`BASIC`、`Python` & `Unix's shell`… 等等,其特點如下: * 不需經過編譯即可執行,**運行時++同時++ ++分析++ 與 ++執行++** * 速度較慢,因為不會轉換成低階型式 * 直譯器運行的概念是將特定語言的文字形式的原始碼,在「運行時逐行」解釋成機器語言 ### C 語言的編譯器 * 常見的 C 語言編譯器: 1. **`gcc` 編譯器**:使用GNU通用公共許可證(GPL)等自由軟件許可證發布 ```shell= sudo apt install -y gcc ``` 2. **`clang` 編譯器**:用一個更寬鬆的許可證(`University of Illinois` / `NCSA Open Source License`)發布,並它基於 LLVM ```shell= sudo apt install -y clang ``` :::success * **LLVM** (`Low-Level Virtual Machine`)? 它是一個開源的編譯器基礎建設項目 LLVM 的主要目標是提供一個靈活、高效、模塊化的編譯器基礎架構,以便用於各種不同的編譯和代碼優化任務 > 一般來說 Clang 的編譯速度較快 **它的架構允許前端(負責解析源代碼並生成中間表示)和後端(負責將中間表示轉換為目標平台的機器代碼)能夠獨立地進行擴展和優化** > 相較之下 GCC 則是前、後端共同開發 > 由於切開維護使得拓展變得容易並清晰,也使得在不同的語言和目標平台上進行編譯變得更加容易 ::: ### 編譯的過程 - 概述 :::info 編譯指令 `gcc` 的選項可以透過 `man gcc` 查看 ![](https://i.imgur.com/gZpt2s0.png) ::: * C **編譯的過程** (Building) 一般來說會經過幾個階段:預編譯、編譯、匯編、連結 ```c= // Hello.c #include <stdio.h> int main(void) { printf("Hello C"); return 0; } ``` 1. **cpp 預編譯**:一般副檔名為 `.i`;編譯 #define、#if、#ifndef、#endif...等等預編譯指令 > cpp 並不是說 C++ 檔案,它的意思是 `C Preprocessor` ```shell= ## 指令 gcc -E Hello.c -o Hello.i ``` 預編譯結果較長,這裡只擷取部分,其中可以看到 `#include <stdio.h>` 被替換為具體的 `stdio.h` 檔案 ```c= ... 省略一大部分 extern int __uflow (FILE *); extern int __overflow (FILE *, int); # 902 "/usr/include/stdio.h" 3 4 # 2 "Hello.c" 2 # 3 "Hello.c" int main(void) { printf("Hello C"); return 0; } ``` > ![](https://i.imgur.com/i9gbktw.png) 2. **cc 編譯(組合)**:一般副檔名為 `.s`;^1.^ 檢查語法、語意是否正確、^2.^ 將高階語言翻譯成低階語言(指令集、機械指令) ```shell= ## 以下兩個指令都可以運行 gcc -S Hello.c -o Hello.s gcc -S Hello.i -o Hello.s ``` 編譯結果如下 ```shell= .file "Hello.c" .text .section .rodata .LC0: .string "Hello C" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rdi movl $0, %eax call printf@PLT movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 9.4.0-5ubuntu1) 9.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4: ``` > ![](https://i.imgur.com/TBOcGfe.png) 3. **as 匯編**:一般副檔名為 `.o`,將上一個步驟中的組合語言轉為轉為 CPU 可了解的二進制碼 ```shell= ## 指令 gcc -c Hello.s -o Hello.o ``` 由於編譯出來的是二進位文件,所以一般人是看不懂的 > ![](https://i.imgur.com/9cpvuGj.png) :::info * 目標檔案(`.o` 擴展名)通常都是以二進制形式表示的,而不是純文本;這些二進制檔案的內容對於人類而言可能不是可讀的字符 ::: 4. **ld 連結** (Linked):一般副檔名為 `.out`;^1.^ 搜尋 Library 函式庫的程式,與程式碼連接、^2.^ 連接上其他相關被編譯的檔案、^3.^ 將編譯的程式碼編譯成可執行檔案 ```shell= ## 指令 gcc Hello.o -o Hello.out ``` 執行 `Hello.out` 檔案 ```shell= ./Hello.out ``` > ![](https://i.imgur.com/eE1E0Dz.png) * 各家編譯器會有不同的編譯流程,下圖是**瑞薩 RL 系列 的 CCRL** > ![](https://i.imgur.com/y8lkIG4.png) ### GCC 常見擴展名 | 擴展名 | 含意 | | -------- | -------- | | `.c` | C Source Code | | `.C`/`.cpp` | C++ Source Code | | `.m` | Objective-C Source Code | | `.h` | C 或 C++ 的頭文件 | | `.i` | C 已經預處理過的檔案 | | `.ii` | C++ 已經預處理過的檔案 | | `.s` | 編譯後的文件檔,**之後編譯不再進行預處理操作** | | `.S` | 編譯後的文件檔,**之後編譯可以再進行預處理操作** | | `.o` | 匯譯後的文件檔 | | `out` | 最後鏈結,變成一個平台可執行檔案 | | `a` | 靜態 Library | | `.so` | 動態 Library | ## 預處理 cpp > cpp: `C Preprocessor` 1. 頭文件替換 `#include`:`.h` 檔案的內容會被原封不動的替換進 `.c` 檔案 2. 宏定義替換 `#define` 3. 條件替換 `#if`、`#else`、`#elif`、`#endif`、`#ifndef`、`#ifdef` ... 等等 4. keep 特殊處理 `#pragma` ... 等等 5. **移除注釋** ### 預編譯 define & typedef * 要證明 `define` & `typedef` 是否都在預編譯處理,使用以下程式進行預編譯,測試他們是否都是預編譯時會處理的關鍵字 ```c= // Typedef_Test.c #define dChar_t char* typedef char* tChar_t; int main(void) { dChar_t c1, c2; tChar_t c3, c4; return 0; } ``` * 對上面程式進行 cc、選項 `-i` 進行預編譯: ```shell= gcc -E Typedef_Test.c -o Typedef_Test.i ``` 從結果來看 **可以知道 `typedef` 是在編譯時期處理**,**而不是在預編譯時期** ( `#define` 才在預編譯時處理 ) > ![](https://i.imgur.com/kDnmMmD.png) ### 預處理 - include * 有關於 `include` 會使用到兩個符號 1. **尖括號 `<>`**:編譯器直接去系統指定目錄尋找; > 像是 Unix 就會去 `/usr/include` 目錄尋找 ```shell= # 在編譯時也可以使用 `-I` 選項來指定目錄 cc -c -I <指定目錄> 源碼.c ``` 2. **雙引號 `""`**:^1.^ 編譯器會去 **當前文件目錄下尋找**,^2.^ 找不到才去系統目錄找 * include 進來的頭文件會 **直接替換** 進源檔案中 1. `H_Test.h`:宣告三個變數 ```c= int apple; short book; char name; ``` 2. `H_Test.c`:引用 `H_Test.h` ```c= #include <stdio.h> #include "H_Test.h" int main(void) { int c = a + b; printf("%d", c); return 0; } ``` > 下圖省略 `stdio.h` > > 可以看到 `H_Test.h` 是直接被替換上 Source code > ![](https://i.imgur.com/d1A8Eyw.png) ### 宏定義 - 解析 :::info 宏也就是 `#define`,可以用來 **減少函數的開銷** ::: * `#define` 只是 **原封不動的替換**;但要注意它是可以遞迴進行替換的,直到不末端不再是宏為止 * `#define` 宏 **可以帶參數**:**每個參數在宏中都必須括號**,最後整體再括號 (**==括號==相當重要**) 1. 無參數宏: ```c= #define SEC_YEAR (365*24*60*60UL) // UL: unsigned long ``` 2. 有參數宏: ```c= #define MAX(a, b) (((a) > (b)) ? (a) : (b)) // 括號相當重要,不使用會容易出錯 ``` * 以下範例就是一個有參宏缺少括號造成的問題,導致跟原來要表達的意思完全不同 ```c= // 缺少 括號 #define ADD_1(X, Y) X+Y // 每個參數都有括號 #define ADD_2(X, Y) ((X)+(Y)) int main(void) { int a = 10, b = 30; int c = 3 * ADD_1(a, b); int d = 3 * ADD_2(a, b); return 0; } ``` 預編譯 ```shell= gcc -E Define_Test.c -o Define_Test.i ``` 預編譯 結果 ```shell= # 1 "Define_Test.c" # 1 "<built-in>" # 1 "<command-line>" # 31 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 32 "<command-line>" 2 # 1 "Define_Test.c" int main(void) { int a = 10, b = 30; int c = 3 * a+b; ## 錯誤 int d = 3 * ((a)+(b)); ## 正確 return 0; } ``` ### 宏定義 v.s inline 內聯 | \ | 一般函數 | 宏定義 | inline 內聯函數 | | - | -------- | -------- | -------- | | 優點 | 編譯器會進行類型檢查 | 在預編譯時期就解決,不會消耗內存 | 使用時進行替換,沒有 Stack開銷 | | 缺點 | **耗費 Stack 空間**,效率較低 | **不進行類型檢查**,並建議不要進行 `++`/`--` 操作 | **耗費內存**,如果有循環就更耗費時間 | * inline 內聯特色 1. 必須定義時使用,加在函數宣告是沒有用的 2. 編譯時會進行參數類型檢查(靜態語言的特性) ```c= // 在宣告使用時沒用 inline int add(int a, int b); int add(int a, int b) { return a + b; } // 必須在定義時使用 inline int subtraction(int a, int b) { return a - b; } ``` ### 條件編譯 * 條件宏 `#if`、`#else`、`#elif`、`#endif`、`#ifndef`、`#ifdef` ... 等等 ```c= #define NUM_1 #define NUM_2 1 #define NUM_3 1 int main(void) { int a = 0, b = 0; #ifdef NUM_1 a = 100; // 有定義,預編譯後會顯示 #endif #undef NUM_1 // 取消定義 #ifndef NUM_1 a = 200 // 預編譯後會顯示 #endif #if NUM_1 a = 300; #endif #if (NUM_1 && NUM_2) // 多定義判斷,由於 NUM_1 沒有定義,所以條件不符合 b = 300; #elif NUM_2 b = 301; // 預編譯後會顯示 #elif NUM_3 b = 302; #else b = 303; #endif return 0; } ``` :::warning * **`with no expression` 錯誤** !? `#if`、`#elif` 後面不但會檢查是否有定義,**還會檢查 ++定義值++**,如果沒有定義值就會報這個錯誤 > 但像是 `#define` 就不要求需要定義值 ::: 預編譯後結果,可以看到 **不符合宏判斷條的原始碼就會被忽略**,無法進入下一個編譯階段 ```shell= int main(void) { int a = 0, b = 0; a = 100; a = 200; b = 301; return 0; } ``` > ![](https://i.imgur.com/sNKSX9F.png) ### 編譯時加入定義 * 預編譯的條件不一定要寫在源碼內,可以透過編譯時指令指定要使用的巨集;格式如下 ```shell= # 添加以下選項,動態決定巨集 -D<聚集名>[=數值] ``` 範例如下 ```java= #include <stdio.h> int main(void) { // 源碼內沒有定義 HELLO 巨集 #ifdef HELLO printf("Hello~\n"); #else printf("Hi~\n"); #endif return 0; } ``` 1. 尚未添加 `-D` 選項去指定巨集 ```shell= cc main.c -o mainWithOutD.o ``` > ![](https://hackmd.io/_uploads/SkB25bwhh.png) 2. 添加 `-D` 選項去指定巨集 ```shell= cc main.c -DHELLO=1 -o mainWithD.o ``` > ![](https://hackmd.io/_uploads/Bk9ksZvn2.png) ## 函數 * 函數有幾個特點 1. 入參建議不要超過 4 個,超過建議使用 struct 包裹起來(或是使用指標) > 否則可能會造成 stack 的負擔 2. 傳入參數大小也不建議過大,避免超過 Stack Size,較大參數建議使用 Pointer 傳遞 3. 編譯完後函數 **會存在 elf 中的 `.text` 段** :::info * 可以使用 `readelf` 指令查看 > `readelf -S Hello.out` > ![](https://i.imgur.com/EDkKEiJ.png) ::: ### 函數 - 聲明、定義、調用 * 函數宣告 * 首先要先知道 **編譯器在編譯程式時,是以 ++文件為單位++**,所以在哪個文件裡面調用,就要在哪個文件內聲明 編譯器在編譯時會按照文件中的先後順序進行編譯,所以 **如果沒有宣告該函數,就必須按照順序進行撰寫** * 宣告主要是告訴編譯器函數的原型 > 大多數都聲明在 `.h` 標頭檔案內 :::success * **函數聲明可以重複! 但函數定義不可重複** ::: * 函數定義: * 當函數定義出來後,就 **表明了該函數的地址在哪** * 可以不用宣告就直接定義,但是要注意順序;函數定義在後面,前面的函數就無法使用 ```c= int main(void) { int result = subtraction(3, 5); // Error 呼叫不到 subtraction 函數 } int subtraction(int a, int b) { return a - b; } ``` * 函數呼叫: ```c= // function.c #include <stdio.h> int add(int, int); // 函數可多次聲明 int add(int, int); int add(int, int); int main(void) { int result = add(3, 5); // 使用函數名呼叫 printf("result: %d\n", result); return 0; } int add(int a, int b) { // 但只能定義一次 return a + b; } ``` 編譯 ```shell= gcc function.c -o function.out ``` ### 函數 - 入棧 Stack * 函數的入棧是按照順序的,如下面程式的入棧順序就是 :::info * Stack 棧是由核心棧存器 `SP` 來管控,相關的核心棧存器還有 鏈結 `LR`、計數器 `PC` ::: 1. main 函數返回地址 2. main 函數 3. 遇到 subtraction 函數,保存 subtraction 函數完成後的返回地址 4. subtraction 函數入棧,**這裡又細分為,參數由右到左入棧** ```c= int subtraction(int a, int b) { return a - b; } int main(void) { int result = subtraction(3, 5); // Error 呼叫不到 subtraction 函數 } ``` Stack 概念圖如下 > ![](https://i.imgur.com/YIBLBdb.png) ## 更多的 C 語言相關文章 關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言 ### C 語言基礎 * **C 語言基礎**:有關於到 C 語言的「語言基礎、細節」 :::info * [**理解C語言中的位元操作:位元運算基礎與宏定義**](https://devtechascendancy.com/bitwise-operations-and-macros-in-c/) * [**C 語言解析:void 意義、NULL 意義 | main 函數調用、函數返回值意義 | 臨時變量的產生**](https://devtechascendancy.com/meaning_void_null_return-value_temp-vars/) * [**C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum**](https://devtechascendancy.com/c-struct_alignment_endianness_union_enum/) * [**C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性**](https://devtechascendancy.com/c-storage-scope-modifiers-lifecycle-linkage/) * [**指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節**](https://devtechascendancy.com/pointers-arrays-const-typedef-sizeof-null/) ::: ### 編譯器、系統開念 * **編譯器、系統開念**:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節 :::warning * [**理解電腦記憶體管理 | 深入瞭解記憶體 | C 語言程式與記憶體**](https://devtechascendancy.com/computer-memory_manager-c-explained/) * [**C 語言記憶體區塊規劃 | Segment 段 | 字符串特性**](https://devtechascendancy.com/c-memory-segmentation-string-properties/) * [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/) ::: ### C 語言與系統開發 * **C 語言與系統開發**:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用 :::danger * [**了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識**](https://devtechascendancy.com/understanding-c-library-static-dynamic/) * [**Linux 宏拓展 | offsetof、container_of 宏、鏈表 | 使用與分析**](https://devtechascendancy.com/linux-macro_offsetof_containerof_list/) ::: ## Appendix & FAQ :::info ::: ###### tags: `C`