---
# System prepended metadata

title: C 概念 & 編譯
tags: [C]

---

---
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`