# 嵌入式工程師的 0x10 個 C 語言問題 > 原文:[A ‘C’ Test: The 0x10 Best Questions for Would-be Embedded Programmers](https://rmbconsulting.us/publications/a-c-test-the-0x10-best-questions-for-would-be-embedded-programmers/) > 紀錄閱讀這篇原文時做的筆記~ ## Preprocessor ### 1. 使用 `#define` 宣告一個常數,這個常數代表一年有多少秒(不用考慮閏年) ```cpp #define SECONDS_PER_YEAR (60UL * 60UL * 24UL * 365UL) ``` 要注意的地方: 1. `#define` 的基本語法,例如: - 不需要以分號 `;` 做結尾 - 使用 `( )` 包起來,以確保運算順序 2. Macro 的命名: - 良好的 macro 命名習慣是全部都大寫字母,且用 `_` 分隔不同單字 3. 使用 macro 的話,pre-processor 會在編譯前將 macro 的內容做展開,所以我們不必自己事先計算一年有多少秒 4. 以上計算的結果在 16-bit 的機器會造成 overflow,因此才用 `L`,告訴編譯器要將這個數字視為 `Long`。 - 16-bit unsigned integer 範圍:0 ~ 65,535 一年有 31,536,000 秒 5. 更細心一點的話,標示為 unsigned long `UL` 以避免 signed 和 unsigned 的陷阱 :::info 第五點原文: > As a bonus, if you modified the expression with a UL (indicating unsigned long), then you are off to a great start because you are showing that you are mindful of the perils of signed and unsigned types 我想作者的意思應該是說: **若一個 expression 內,同時有 signed 跟 unsigend,那 signed 其實會被 implicitly 解讀成 unsigned** ![image](https://hackmd.io/_uploads/SJFkUmyylx.png) ![image](https://hackmd.io/_uploads/SyWgLmJJgx.png) > 以上擷取自 CMU 15-213 「Bits, Bytes, and Integers」課程投影片 若一個 expression 同時有 signed 和 unsigned 的數字,則 signed 數字會被解讀成 unsigned(bit pattern 不會變,只是被解讀成 unsigned)。 例如以下程式碼: ```cpp #include <stdio.h> int my_arr[] = { 21, 22, 23, 24, 25, 26 }; #define MY_ARRAY_SIZE (sizeof(my_arr)/sizeof(my_arr[0])) int f(void) { int d = -1; int x = 0; if (d <= MY_ARRAY_SIZE) { x = my_arr[d + 2]; } return x; } int main(int argc, char *argv[]) { int x = f(); printf("%d\n", x); // 得到 0 return 0; } ``` 為什麼 return 的 `x` 是 0?因為問題出在 `if` 的判斷句: `d <= MY_ARRAY_SIZE` - `(sizeof(my_arr)/sizeof(my_arr[0]))` 得到的是無號的整數 `size_t`。 - 但是 `d` 是有號數! - 如果有號數和無號數在同一個 expression 內做比較,那有號數會被 implicitly 轉換為無號整數,然後進行比較。 - 因此 -1 若被看成無號數,那會是一個很大的整數,所以 `d` 就不可能小於等於 `MY_ARRAY_SIZE`,所以 `x` 就會一直是 0。 ::: :::info 補充:使用 macro 時為什麼要多善用 `( )` 考慮以下例子: ```cpp #include <stdio.h> #define SQUARE(X) X * X int main(int argc, char *argv[]) { int x = 10; printf("%d\n", SQUARE(x + 1)); // 以上寫法在 macro 展開後會變成 x + 1 * x + 1 // 結果就變成 2x + 1,而不是原本想要的 (x + 1) * (x + 1) int result = 100/SQUARE(5); // 預期應該要得到 4 // 但 macro 展開後會變成 100/5*5 // 所以實際得到的是 100 return 0; } ``` 所以在使用 macro 時,良好習慣就是用 `( )` 包起來,以確保運算順序是正確的 ::: :::info 補充:32-bit 和 64-bit 電腦 Data Type Size ![image](https://hackmd.io/_uploads/HyXDdXk1xx.png) > 以上擷取自 CMU 15-213 「Bits, Bytes, and Integers」課程投影片 ::: ### 2. 寫一個標準的 `MIN` macro,也就是給兩個參數,return 比較小的那個 ```cpp #define MIN(A, B) ((A) <= (B) ? (A) : (B)) ``` 要注意的地方: 1. 瞭解如何使用 `#define` 來定義 macro。Macro 可產生 inline code,而在嵌入式系統通常會滿常用到 inline code,因為可達到好的效能。 2. 瞭解使用 ternary conditional operator 的好處。Ternary conditional operator 可以讓編譯器產生比 `if-then-else` 更優化的程式碼。由於在嵌入式系統,「效能」是很重要的議題,因此懂得使用 ternary conditional operator 是很重要的。 3. 瞭解括號在使用 macro 的重要性。 4. 從這題可以討論到一個使用 macro 要很小心的點,像是以下的程式碼會發生什麼事? ```cpp least = MIN(*p++, b); ``` 以上寫法,在經由 pre-processor 展開後會變成以下: ```cpp least = ((*p++) <= (b) ? (*p++) : (b)); ``` 接下來,先來看 `*p++` 是在做什麼事~再看這樣的 macro 有可能會造成怎樣的問題。 首先,依據 [C Operator Precedence](https://en.cppreference.com/w/c/language/operator_precedence) 可以看到,postfix increment 的優先權是高於 `*` dereference operator: ![image](https://hackmd.io/_uploads/SJKvp5PAyg.png) 所以 `*p++` 其實是 `*(p++)`,實際執行的順序是: 1. 先將 `p` 的值(也就是某個記憶體位址)取出來,然後做 dereference 以取得儲存在該記憶體位址的 data 2. 然後 `p` 再遞增,指向下一個 data 所在的位址(也就是 `p++`,會讓 `p` 增加一個 `sizeof(<data_type>)` 單位) 而這項的 `MIN` macro 的寫法會造成的問題是,假如 `*p` 所取得的 data 的值是 ++小於等於 `b`++,那 `*p++` 在 macro 展開後 ++會重複出現兩次++,導致 ++`p++` 被執行兩次++: 1. 第一次是在 `(*p++) <= (b)` 2. 第二次則是在 `(*p++) : (b)` 執行 所以最終 `p` 指向的記憶體位址會是:原本的記憶體位址 + 兩個 data type 的 size。 如以下例子: ```cpp #include <stdio.h> #define MIN(A, B) ((A) <= (B) ? (A) : (B)) int main() { int nums[] = {100, 200, 300, 400}; int n = 1000; int *p = &nums[0]; printf("%p\n", p); // 0x16d0ef5e0 printf("%p\n", p + 1); // 0x16d0ef5e4 printf("%p\n", p + 2); // 0x16d0ef5e8 int least = MIN(*p++, n); printf("nums[0]: %d, n: %d, least: %d\n", nums[0], n, least); // nums[0]: 100, n: 1000, least: 200 printf("%d\n", *p); // 300 printf("%p\n", p); // 0x16d0ef5e8 return 0; } ``` 可以看到 `p` 最後指向的記憶體位址是 `0x16d0ef5e8`,也就是 `0x16d0ef5e0` 加上兩個 `sizeof(int)`。 :::info **補充:使用 `typeof` Operator** 若要解決前述傳入 `++x` 或 `x++` 而造成被執行兩次的問題,可以使用 `typeof` Operator 在 C23 標準([ISO/IEC 9899:2024 (en) — N3220 working draft](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf)),`typeof` 成為了 C 語言的一個 operator(在 C23 之前,`typeof` 是 GNU GCC 的 Extension,但 C23 就成為 C 語言的標準了~) `typeof` 更詳細的內容可以看 C23 標準 `6.7.3.6 Typeof specifiers` 此章節。 ```cpp /* macro.c */ #include <stdio.h> #define MIN(a,b) \ ({ typeof(a) _a = (a); \ typeof(b) _b = (b); \ _a <= _b ? _a : _b; }) int main() { int nums[] = {100, 200, 300, 400}; int n = 1000; int *p = &nums[0]; printf("%p\n", p); // 0x16f76f5e0 printf("%p\n", p + 1); // 0x16f76f5e4 printf("%p\n", p + 2); // 0x16f76f5e8 int least = MIN(*p++, n); printf("nums[0]: %d, n: %d, least: %d\n", nums[0], n, least); // nums[0]: 100, n: 1000, least: 100 printf("%d\n", *p); // 200 printf("%p\n", p); // 0x16f76f5e4 return 0; } ``` 編譯並執行: ```shell $ gcc macro.c -std=c23 -o macro && ./macro ``` PS. 若要使用 C23 標準,可以將 GNU GCC 更新到 `gcc-14`,並使用 `-std=c23` 這個 option 就能使用 C23 標準囉 若 GNU GCC 是版本是 `gcc-13`,那就使用 `-std=c2x` 這個 option。 從以上結果就可以看到,`*p++` 其實就只有被執行一次~ ::: :::info 補充:從組合語言來看 Ternary Conditional Operator 這篇原文作者有提到 ternary conditional operator 可以產生比 if-else 還好的程式碼,所以就觀察看看會產生怎樣的組合語言~ 有以下兩個 `.c` 檔: ```cpp /* if-else.c */ int main() { int n1 = 100; int n2 = 200; int n3 = 0; if (n1 > n2) { n3 = n1; } else { n3 = n2; } return 0; } ``` ```cpp /* ternary.c */ int main() { int n1 = 100; int n2 = 200; int n3 = 0; n3 = (n1 > n2) ? n1 : n2; return 0; } ``` 接下來,我在我的 ARM64 Ubuntu 24.04 虛擬機安裝 x86-64 的 cross-compiler: ```shell $ sudo apt install gcc-x86-64-linux-gnu ``` 用以下兩個指令產生 x86-64 的 intel 語法的 NASM 組合語言程式碼(因為我只有學過 NASM 所以才轉成 NASM 格式的 x86-64 組語 XD),且沒開編譯器最佳化(`-O0`): ```shell $ x86_64-linux-gnu-gcc -S -masm=intel -O0 -no-pie ternary.c -o ternary_nasm_asm $ x86_64-linux-gnu-gcc -S -masm=intel -O0 -no-pie if-else.c -o if-else_nasm_asm ``` 以下是 `ternary_nasm_asm` 的 `main`: ```nasm= main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov DWORD PTR -12[rbp], 100 ; n1 mov DWORD PTR -8[rbp], 200 ; n2 mov DWORD PTR -4[rbp], 0 ; n3 mov edx, DWORD PTR -8[rbp] ; move n2 到 edx register mov eax, DWORD PTR -12[rbp] ; move n1 到 eax register cmp edx, eax ; 做 n2 - n1,並將結果設置到 EFLAGS register 的 status flags cmovge eax, edx ; 依據前面 cmp 指令設置的 status flgs 做判斷 ; 若條件成立( SF == OF ),就將 edx (n2) 的值 mov 到 eax mov DWORD PTR -4[rbp], eax ; 將 eax 的值放到 n3 mov eax, 0 ; 清空 eax register pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: ``` 以下是 `if-else_nasm_asm` 的 `main`: ```asm= main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov DWORD PTR -12[rbp], 100 ; n1 mov DWORD PTR -8[rbp], 200 ; n2 mov DWORD PTR -4[rbp], 0 ; n3 mov eax, DWORD PTR -12[rbp] ; 將 n1 的值 move 到 eax register cmp eax, DWORD PTR -8[rbp] ; 做 n1 - n2,並將結果設置到 EFLAGS register 的 status flags jle .L2 ; 若 n1 <= n2,則跳到L2 mov eax, DWORD PTR -12[rbp] ; 接下來這裡是 n1 > n2 的 case ; 將 n1 的值 move 到 eax register mov DWORD PTR -4[rbp], eax ; 將 n1 的值 move 給 n3 jmp .L3 .L2: mov eax, DWORD PTR -8[rbp] ; 將 n2 的值 move 到 eax mov DWORD PTR -4[rbp], eax ; 將 n2 的值給 n3 .L3: mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: ``` 可以看到最主要的差異在於,這個例子的 ternary condition operator,使用了 x86-64 的 `cmovge`(conditional move if greater or equal)指令,這種 condition move(CMOVcc)指令是 branchless 的。它會依據前面 `cmp` 指令的結果決定要不要將 `edx` move 到 `eax`。這種做法就不用跳來跳去,比較有效率。 而 `if-else` 的方式則會產生 conditional jump 的組合語言。如果預測錯誤,那 CPU pipeline 就要 flush 然後重抓指令,這樣會比較沒效率。而 conditional move 的指令就能避免這種事情發生,所以會更有效率。 > 這篇有對 conditional jump 和 conditional move 的一些說明 > https://stackoverflow.com/questions/26154488/difference-between-conditional-instructions-cmov-and-jump-instructions --- 接下來來實際觀察看看執行時間的差異~ 以下兩個程式碼,為了要看出比較顯著的差異,改成讓 ternary conditional operator 和 if-else 都各執行 100000000 次。 此外,每一個 iteration 都變更 `n1` 和 `n2` 的值,增加 misprediction 的機率。 ```cpp /* ternary_time.c */ #include <stdio.h> #include <time.h> int main() { int n1 = 100; int n2 = 200; long long n3 = 0; clock_t start = clock(); for (int i = 0; i < 100000000; i++) { n3 += (n1 > n2) ? n1 : n2; n2 = n1; n1 = i % 300; } clock_t end = clock(); printf("n3: %lld\n", n3); printf("time: %f seconds\n", (double)(end - start) / CLOCKS_PER_SEC); return 0; } ``` ```cpp /* if-else_time.c */ #include <stdio.h> #include <time.h> int main() { int n1 = 100; int n2 = 200; long long n3 = 0; clock_t start = clock(); for (int i = 0; i < 100000000; i++) { if (n1 > n2) { n3 += n1; } else { n3 += n2; } n2 = n1; n1 = i % 300; } clock_t end = clock(); printf("n3: %lld\n", n3); printf("time: %f seconds\n", (double)(end - start) / CLOCKS_PER_SEC); return 0; } ``` 用以下指令編譯: ```shell $ gcc -O0 ternary_time.c -o ternary_time $ gcc -O0 if-else_time.c -o if-else_time ``` 結果如下: ![image](https://hackmd.io/_uploads/BkfI5MkJlg.png) 可以看到執行時間差了一倍多,有顯著的差異存在~ ::: ### 3. 使用 `#error` 這個 Pre-processor 指令的目的為何? > 原文所提供的參考資料:[In Praise of the `#error` Directive](https://rmbconsulting.us/Publications/ErrorDirective.pdf) > [Diagnostic directives - C](https://en.cppreference.com/w/c/preprocessor/error) > [Diagnostic directives - C++](https://en.cppreference.com/w/cpp/preprocessor/error) 在 C/C++,`#error` 會終止編譯過程,並產生指定的錯誤訊息。使用此指令可以讓 programmer 在編譯早期就發現某些錯誤條件,讓 programmer 能即時發現和處理問題。 `#error` 指令的語法如下: ```c #error "error_message" ``` 其中,`error_message` 是一個字串,表示要顯示的錯誤訊息。 ex. ```cpp /* error.c */ #include <stdio.h> #define MY_MACRO (1) #ifndef MY_MACRO #error "MY_MACRO is not defined at the first check point" #endif #undef MY_MACRO #ifndef MY_MACRO #error "MY_MACRO is not defined at the second check point" #endif int main() { printf("MY_MACRO: %d\n", MY_MACRO); return 0; } ``` ![image](https://hackmd.io/_uploads/S1_wrXkJgg.png) ## Infinite Loops ### 4. 嵌入式系統時常會用到 Infinite Loops,在 C 語言我們可以怎麼做出 Infinite Loops 呢? 有許多方法可以寫出無限迴圈,原文作者比較喜歡的是以下這個寫法: ```cpp while (1) { // do something } ``` 另一個寫法如下: ```cpp for (;;) { // do something } ``` 第三個寫法如下: ```cpp Loop: // do something goto Loop; ``` ## Data Declarations ### 5. 用變數 `a` 寫出以下 definitions 1. An integer 2. A pointer to an integer 3. A pointer to a pointer to an integer 4. An array of ten integers 5. An array of ten pointers to integers 6. A pointer to an array of ten integers 7. A pointer to a function that takes an integer as an argument and returns an integer 8. An array of ten pointers to functions that take an integer argument and return an integer 解答: ```cpp // 1. An integer int a; // 2. A pointer to an integer int *a; // 3. A pointer to a pointer to an integer int **a; // 4. An array of ten integers int a[10]; // 5. An array of ten pointers to integers int *a[10]; // 6. A pointer to an array of ten integers int (*a)[10]; // 7. A pointer to a function that takes an integer as an argument and returns an integer int (*a)(int); // 8. An array of ten pointers to functions that take an integer argument and return an integer int (*a[10])(int); ``` :::info 補充:CMU 15-213 課程提到如何解析 C 語言 Declaration 的技巧 這個解析的技巧就是: 1. 先找到變數名稱 2. 再來看變數名稱左右有沒有 operator,若兩側都有 operator 就看哪個優先權比較高,就優先用那個 operator 去解析 - 在變數宣告時,我們會遇到的 operator 基本上只有 `*`、`( )`、`[ ]` 這三種 operator ![image](https://hackmd.io/_uploads/SJKvp5PAyg.png) - 這三個 operator 優先權最高的是 `( )`,再來是 `[ ]`,最後是 `*` - `( )`:代表是 function - PS. 不要跟用來保護運算順序用的括號搞錯喔~這裡的 `( )` 指的是 function 傳入參數的 `( )` ㄛ - 像是若看到 `()`,就代表沒有指定這個 function 的參數 - 若看到 `(int, float)` 則代表這個 function 的第一個參數是 integer,第二個參數是 float - `[ ]`:代表是 array - `*`:代表是 pointer 3. 解析完最內層後,再往外一層解析,順序也是先看左右兩側有沒有 operator,然後看哪個 operator 優先權最高就先用該 operator 做解析 4. 後續依此類推 > 知道這個技巧後~再複雜再變態的 declaration 都有辦法解析了 >///< 開心 🥳 > 真的大推 CMU 15-213 這門課!!! --- ```cpp int *p; ``` ![001](https://hackmd.io/_uploads/rJSgeNaCkl.png) --- ```cpp int *p[13]; ``` ![002](https://hackmd.io/_uploads/SkDTGcydxg.png) --- ```cpp int *(p[13]); ``` ![003](https://hackmd.io/_uploads/SJNdg4p0yg.png) --- ```cpp int **p; ``` ![004](https://hackmd.io/_uploads/rJMqe4aCkl.png) --- ```cpp int (*p)[13]; ``` ![005](https://hackmd.io/_uploads/rJZTlEaA1g.png) --- ```cpp int *f(); ``` ![006](https://hackmd.io/_uploads/rJbPat11xg.png) --- ```cpp int (*f)(); ``` ![007](https://hackmd.io/_uploads/HJavRYykgl.png) --- ```cpp void (*f[10])(void *); ``` ![008](https://hackmd.io/_uploads/r1P4Z4aAye.png) --- ```cpp int (*(*f())[13])(); ``` ![009](https://hackmd.io/_uploads/Hy9keqk1eg.png) --- ```cpp int (*(*x[3])())[5]; ``` <!-- ![010](https://hackmd.io/_uploads/HylaWVaAyl.png) --> ![010](https://hackmd.io/_uploads/rJjmWq1Jge.png) --- ```cpp int (*(*f(int, float))(float))[3]; ``` ![011](https://hackmd.io/_uploads/rJm1MVpAkg.png) ::: :::warning 補充:關於 `f()` 和 `f(void)` 在 C 和 C++ 的差別 這是 [cppreference 關於 C++ Function declaration 的頁面](https://en.cppreference.com/w/cpp/language/function): ![cpp](https://hackmd.io/_uploads/ry4BtKyJlx.png) 可以看到在 C++,`f()` 和 `f(void)` 都是指 empty parameter list,就是指這個 function 沒有參數。 但在 [C 語言](https://en.cppreference.com/w/c/language/function_declaration): ![C](https://hackmd.io/_uploads/S1Br5K1Jle.png) 這兩種 function 的宣告方是是不一樣的意思,`f(void)` 代表這個 function 沒有參數,而 `f()` 則代表這個 function 的參數未定。 因為我是先學 C++,所以我的認知一直是 C++ 的版本,直到最近才知道原來 `f(void)` 和 `f()` 在 C 語言是不等價的 😱🤯😵‍💫😵 ++==**!!!更新!!!**==++ ![Picture1](https://hackmd.io/_uploads/BkwPcmPyee.png) 在 C23 有新的規範了。依據 [C23 標準](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf) § 6.7.7.4 (13),`f()` 這樣的宣告就相當於 `f(void)` 這樣的宣告,兩者的效力都是一樣的,都是指這個 function 是沒有參數的。 ::: ## Static > 關於 C/C++ `static` 的用法,我覺得這篇講的超清楚的:https://shengyu7697.github.io/cpp-static/ ### 6. `static` 這個 keyword 的作用是什麼? `static` 在 C 語言的三個主要用途: 1. ++用在 local variable++:該 local variable 的生命週期(lifetime)會變的跟整個程式一樣,也就是該變數的值會在 function 多次呼叫間保留下來。 2. ++用在 global variable++:將該 global variable 的作用範圍(scope)限制在該 `.c` 檔,只有該 `.c` 檔可以存取到該 global variable,其他 `.c` 檔無法存取到該 global variable 3. ++用在 function++:將該 function 的作用範圍限制在該 `.c` 檔,只有該 `.c` 檔可以存取到該 function,其他 `.c` 檔無法存取到該 function 所以 `static` 主要是可以延長 local variable 的生命週期,或是將 function/global variable 的作用範圍限制在該 `.c` 檔。 :::info 補充:C 語言的 Translation Unit、Scope、Static Storage Duration、Internal Linkage ++Translation Unit++ ![image](https://hackmd.io/_uploads/S1U5gfCAye.png) 依據 [C 語言標準](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf) § 5.1.1.1 (1),一個 translation unit 是指一個 `.c` 檔(source file)加上經由其 `#include` 所包含的所有檔案,且是經過 preprocessor 處理展開後的整體內容。 --- ++Scope 作用域++ ![image](https://hackmd.io/_uploads/rJpGzGRCJl.png) ![image](https://hackmd.io/_uploads/HygNMMAR1l.png) 依據 [C 語言標準](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf) § 6.2.1 (4): - ++File Scope++: - 被宣告在 block 或參數列表之外的 identifier(identifier 就是指變數名稱或 function 名稱),他的 scope 就是 file scope。 - 像是 global variable,以及被定義在 block 外部的 function(例如,一般的 function definition)。 - 具有 file scope 的 identifier,在自從他被宣告之後,在整個 translation unit 裡面都可以被看到和使用。 我覺得原文用 **terminaties at the end** of the translation unit,聽起來好像會有一種像是到某個時間點結束的概念,這樣好像會有點容易跟生命週期(lifetime)的概念搞混 >_\<,但這裡指的是作用域喔! - Scope 作用域:編譯期的概念,編譯器用來判斷在哪裡可以用這個 identifier - Lifetime 生命週期:執行期間的概念,也就是在程式執行時,這個 identifier 什麼時候會存在 - ++Block Scope++ - 如果一個 identifier 是在一個 block 內被宣告(像是 `if` block 或 `while` block),或是在 function definition 的參數列表中被宣告,那這樣的 identifier 就具有 block scope。 - 具有 block scope 的 identifier,在自從他被宣告後,在他所在的 block 內都可以看到和使用它。 - ++Function Prototype Scope++ - 被宣告在 function prototype 的參數列表內的 identifier。 --- ++The `static` Storage-Class Specifiers++ 依據 [cppreference](https://en.cppreference.com/w/c/language/storage_duration): > The `static` specifier specifies both **++static storage duration++** (unless combined with `_Thread_local`)(since C11) and **++internal linkage++** (unless used at block scope). > It can be used with ++functions at file scope++ and with ++variables at both file and block scope++, but NOT in function parameter lists. 使用 `static` 宣告的話,會被設定成 `static storage duration` 和 `internal linkage`。 - 但若還有合併 `_Thread_local` 關鍵字,那就會是 `thread storage duration`,而不是 `static storage duration`。 - 若是 block scope,那就不會有 linkeage,因為只會有 `static storage duration`。 `static` 可以用在: - File scope 的 function - File scope 跟 block scope 的變數 那什麼是 `static storage duration` 和 `internal linkage`? --- ++Static Storage Duration++ 每個物件都會有所謂的 `storage duration` 的特性,會用來決定物件的生命週期(lifetime)。C 語言有四種 storage duration,分別是 `automatic`、`static`、`thread`、以及 `allocated`。 依據 [cppreference](https://en.cppreference.com/w/c/language/storage_duration): > `Static storage duration`. The storage duration is ++the entire execution of the program++, and the value stored in the object is ++initialized only once++, ++prior to `main` function++. All objects declared `static` and all objects with either internal or external linkage that aren't declared `_Thread_local` (until C23) `thread_local` (since C23)(since C11) have this storage duration. - 擁有 `static storage duration` 的物件,其生命週期就是程式執行的整個期間。 - 這類物件的值只會被初始化一次,且是在 `main` function 被執行前就會被初始化。 - 怎樣的物件會有 `static storage duration`? - 用 `static` 關鍵字宣告的物件 - Internal linkage 的物件 - External linkage 且沒有被宣告成 `_Thread_local` 或 `thread_local` 的物件 --- ++Internal Linkage++ Linkage 是指一個 identifier(變數或 function)能否被其他 scope 存取到的能力。C 語言有三種 linkage,分別是 `no linkage`、`internal linkage`、以及 `external linkage`。 依據 [cppreference](https://en.cppreference.com/w/c/language/storage_duration): > `internal linkage`. The variable or function can be referred to from ++all scopes in the current translation unit++. > ++All **file scope** variables++ which are declared `static` or `constexpr`(since C23) have this linkage, and ++all **file scope** functions++ declared `static` > (`static` function declarations are only allowed at ++file scope++). - 具有 internal linkage 的變數或 function,只能被他所在的 translation unit 的所有 scope 存取。 - 那怎樣的變數和 function 會是 internal linkage? - 所有被宣告為 `static` 或 `constexpr` 的 file scope 變數 - 所有被宣告為 `static` 的 file scope functions - PS. `static` function 的宣告只允許發生在 file scope --- ++Static Storage Duration 物件的初始化++ ![image](https://hackmd.io/_uploads/SJokfc5Aye.png) ![image](https://hackmd.io/_uploads/HkAgGcq0Je.png) 依據 [C 語言標準](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf) § 6.7.11 (11),若 `static storage duration` 物件沒有被初始化,那他會被初始化成其 data type 的 0。 ::: :::info 補充:`static` 變數會被放在哪 ![image](https://hackmd.io/_uploads/SyJ47CFAJe.png) > 以上擷取自 CMU 15-213「Linking」課程的投影片 一個 Process 的 memory layout 包含 stack、heap、data section(data segment)、text section(text segment)。 其中 data section 又分成 `.data` section 和 `.bss` section。 - `.data` section 放的是有初始值的 global variable 和 `static` variable - `.bss` section 放的則是無初始值的 global variable 和 `static` variable,這些 variable 會被初始化成其 data type 的 0 假設有以下程式碼: ```cpp /* static.c */ #include <stdio.h> int global_var_initialized = 100; int global_var_uninitialized; static int static_global_var_initialized = 200; static int static_global_var_uninitialized; int main() { int local_var_initialized = 300; int local_var_uninitialized; static int static_local_var_initialized = 400; static int static_local_var_uninitialized; printf("global_var_uninitialized value: %d\n", global_var_uninitialized); printf("static_global_var_uninitialized value: %d\n", static_global_var_uninitialized); printf("local_var_uninitialized value: %d\n", local_var_uninitialized); printf("static_local_var_uninitialized value: %d\n", static_local_var_uninitialized); printf("address of global_var_initialized: %p\n", &global_var_initialized); printf("address of global_var_uninitialized: %p\n", &global_var_uninitialized); printf("address of static_global_var_initialized: %p\n", &static_global_var_initialized); printf("address of static_global_var_uninitialized: %p\n", &static_global_var_uninitialized); printf("address of local_var_initialized: %p\n", &local_var_initialized); printf("address of local_var_uninitialized: %p\n", &local_var_uninitialized); printf("address of static_local_var_initialized: %p\n", &static_local_var_initialized); printf("address of static_local_var_uninitialized: %p\n", &static_local_var_uninitialized); return 0; } ``` 用以下指令編譯: ```shell $ gcc -no-pie static.c -o static ``` 執行的結果: ![image](https://hackmd.io/_uploads/Sy6AfzcC1x.png) - 可以看到,沒有初始值的 global variable 和 `static` variable(無論是 global 或 local)都會被初始化成 0 記憶體位址重新整理後如下: | 記憶體位址 | 變數名稱 | | -------- | -------- | | `0xffffdebe7b04` | `local_var_uninitialized` | | `0xffffdebe7b00` | `local_var_initialized` | | `0x420050` | `static_local_var_uninitialized` | | `0x42004c` | `static_global_var_uninitialized` | | `0x420048` | `global_var_uninitialized` | | `0x420040` | `static_local_var_initialized` | | `0x42003c` | `static_global_var_initialized` | | `0x420038` | `global_var_initialized` | - 可以看到未初始化的 global 和 `static` variable 都被放在一起 - 有初始值的 global 和 `static` variable 也都被放在一起 ![image](https://hackmd.io/_uploads/r11-HCYA1e.png) - 使用 `objdump` 去查看可執行檔的 symbol table,可看到有初始值的 global 和 `static` 變數都被放在 `.data` section - 無初始值的 global 和 `static` 變數則都被放在 `.bss` section ::: ## Const ### 7. `const` 這個 keyword 的作用是什麼? 用 `const` 修飾的變數,在初始化後就不能再修改他的值。編譯器負責在編譯時期檢查 `const` 變數是否有被修改。 以下變數的宣告,分別代表什麼意思: ```cpp= const int a; int const a; const int *a; int * const a; int const * a const; ``` 前兩個都是 constant integer。 在這裡先提一下第五個 `int const * a const;`,第五個是原文裡面出現的,作者想表達的是 constant pointer to constant,但是 `const` 在 C 語言不能寫在變數名稱後面,這樣在編譯時期會出現 error 喔,如以下截圖: ![upload_fded058984a52cca91b3c7741ff6a072](https://hackmd.io/_uploads/HyQVD7TAJg.png) 那 `const` 跟 pointer 名稱,到底該怎麼放? :::info 補充:`const` pointer 的語法 跟據 [cppreference](https://en.cppreference.com/w/c/language/pointer) 的說明: ![upload_c3191704810ef92ca87a6d3975388e32](https://hackmd.io/_uploads/ry6SvQ60ye.png) 宣告 pointer 的語法是: ```perl * attr-spec-seq(optional) qualifiers(optional) declarator ``` - `qualifiers` 就是像 `const`、`volatile` 這類的修飾詞 - `declarator` 則是我要宣告的 pointer 的名稱 - 出現在 `*` 和 pointer 名稱之間的 `qualifiers`,是用來修飾該 pointer - ++所以 `* const <ptr_name>` 就代表這是一個 constant pointer++,也就是這個 pointer 只能指向固定的對象,不能讓他指向其他人 - 宣告這個 pointer 的時候就要初始化他,因為後續就不能更改 所以用來修飾 pointer 的 `qualifiers` 是出現在 `*` 和 pointer 名稱之間,且 `qualifiers` 不應該出現在 pointer 名稱的右側。 ::: 接下來~搭配前面在 Data declaration 提到的 CMU 15-213 課程提到的方法~ --- ```cpp const int *a; ``` ![pic01](https://hackmd.io/_uploads/BkDKDXpAyl.png) - Pointer to Constant - Pointer 所指向的 data 是**常數**,++不可以++修改這個 data 的值 - 但我們可以修改 pointer 要指向哪裡 ```cpp int n1 = 100; int n2 = 200; const int *ptr = &n1; *ptr = 999; // ERROR ptr = &n2; // OK ``` --- ```cpp int * const a; ``` ![pic02](https://hackmd.io/_uploads/rJvcwm6Akx.png) - Constant Pointer - 我們可以修改 pointer 所指向的 data - 但我們不可以修改 pointer 所指向的對象,所以在宣告 pointer 的時候,就要初始化說這個 pointer 要指向誰,因為後續就不能再作更改了 ```cpp int n1 = 100; int n2 = 200; int *const ptr = &n1; *ptr = 999; // OK ptr = &n2; // ERROR ``` --- ```cpp const int * const a; ``` ![pic03](https://hackmd.io/_uploads/HkdTPX60Jl.png) - Constant Pointer to Const - Pointer 所指向的 data 是**常數**,++不可以++修改這個 data 的值 - 我們不可以修改 pointer 所指向的對象,所以在宣告 pointer 的時候,就要初始化說這個 pointer 要指向誰,因為後續就不能再作更改了 ```cpp int n1 = 100; int n2 = 200; const int *const ptr = &n1; *ptr = 999; // ERROR ptr = &n2; // ERROR ``` --- ```cpp int * const * pcp = &cp; ``` ![pic04](https://hackmd.io/_uploads/S1VW_7p0ye.png) --- ```cpp int const * const * const a; ``` ![pic05](https://hackmd.io/_uploads/HJxQ_X6AJe.png) --- ```cpp int (*const *f)(void); ``` ![pic06](https://hackmd.io/_uploads/H1gn_XpRyx.png) :::info 補充:`const` 的一些資訊 ![S1PDC29Rke](https://hackmd.io/_uploads/H12gFQp0kx.png) 依據 [C 語言標準](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf) § 6.7.4.1 (7),如果嘗試用用一個 non-const lvalue 物件去修改一個 `const` 物件,這會是 undefined behavior。 --- ![B1cTA3q0yx](https://hackmd.io/_uploads/Hy6MF7a0Jg.png) 依據 [C 語言標準](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf) § 6.7.4.1,compiler 可以選擇把 `const`(且非 `volatile`)的變數放進 read-only 的記憶體區域(像 `.rodata`)。所以並不是 `const` 物件都一定會被放到 read only 記憶體區域。 此外,如果程式裡完全沒用到該變數的位址(`&obj`),那 compiler 甚至有可能不幫你分配記憶體空間(直接在指令裡用常數就好)。 --- https://en.cppreference.com/w/c/language/const ![HJcpvAoRye](https://hackmd.io/_uploads/HkEQ5760ke.png) 如以下例子: ```cpp #include <stdio.h> int main() { const int n = 100; printf("n: %d\n", n); int *ptr = &n; *ptr = 999; printf("n: %d\n", n); return 0; } ``` 用一個 pointer 去指向一個 `const` integer,然後再用這個 pointer 去修改它的值,這是 undefined behavior。雖然這段程式碼用 GNU GCC 可以編譯,但是會有 warning,且由於這是 undefined behavior,可能用不同編譯器的結果會不一樣。 ![image](https://hackmd.io/_uploads/HJTesTd0Jx.png) ::: ## Volatile ### 8. `volatile` 這個 keword 的作用是什麼?舉三個不同的例子說明 簡單來講~`volatile` 最主要的效果就是==禁止編譯器對變數的存取做最佳化的動作==,所以每次去存取 `volatile` 變數時都一定會去 memory 裡面存取該變數。 假設某個變數在程式碼裡面都沒被修改過,也沒有被宣告成 `volatile`,那編譯器可能就會假設該變數的值都不會被更改,那編譯器在做最佳化的時候可能就會直接省略對該變數的存取,甚至完全移除對該變數的讀取動作。 (這是合理但錯誤的假設,因為如果變數會被其他外部因素更改,像是被 ISR 或其他執行緒修改,那這樣就會有問題) 或是說假如我剛剛已經存取過某個變數的值了,這個變數的值被放在 register,那我接下來要用他的話,編譯器就假設這個變數在 memory 裡面的值都是不會被修改,所以就沒必要再跑去 memory 裡面抓他的值,直接用放在 register 裡面的值就好了。 那怎樣的變數會需要被宣告成 `volatile` 哩?基本上,只要是「可能在程式碼看不到的情況下會被修改」的變數(像是會被 ISR 或其他執行緒修改),就應該宣告成 `volatile`,這樣才能避免編譯器做出錯誤的最佳化。 需要設定成 `volatile` 的情境: 1. 周邊設備的硬體 register(像是 status register) - PS. 幾乎所有 memory-mapped I/O register(像是 UART、GPIO、SPI 等等),幾乎都會用 `volatile`,因為這些記憶體位址對應的是「硬體裝置」,他們的 value 可能會隨時變動,編譯器不應該把讀取或寫入的動作優化掉 2. 在 ISR 內被使用的非區域變數(原文是用 non-stack variable,就是指不是放在 stack 的變數) 3. 被多個執行緒共享的變數 原文作者還問了以下幾個更深入的問題: 1. 一個參數可以同時是 `const` 和 `volatile` 嗎?請解釋你的答案 2. Pointer 可以是 `volatile` 嗎?請解釋你的答案 3. 以下 function 有什麼錯誤? ```cpp int square(volatile int *ptr) { return *ptr * *ptr; } ``` 答案如下: 1. 可以。像是 read only status register,由於他會不預期地被更改,所以他是 `volatile`;且由於程式不應該試圖去修改它,所以他是 `const` 2. 可以,雖然不常見。其中一個例子就是當一個 ISR 修改了一個指向 buffer 的 pointer 3. 這個問題很缺德(wicked)!這個 function 是要 return 被 `*ptr` 指向的值的平方,但由於 `*ptr` 是 `volatile`,所以編譯器會產生類似以下的程式碼: ```cpp int square(volatile int *ptr) { int a,b; a = *ptr; b = *ptr; return a * b; } ``` 由於 `*ptr` 的值有可能會不預期地被改變,因此 `a` 和 `b` 的值有可能會是不一樣的,也因此有可能會 return 不是平方的數值。正確的寫法應該如下: ```cpp int square(volatile int *ptr) { int a; a = *ptr; return a * a; } ``` :::info 補充:從組合語言來觀察 `volatile` 的效果 ```cpp /* volatile.c */ int global_var = 0; volatile int global_volatile_var = 0; int main() { int a = global_var; int b = global_volatile_var; return 0; } ``` - 以上程式碼,若有開編譯器最佳化的話,由於 `a` 和 `b` 這兩個變數後續都不會被操作,編譯器可能會假設這兩個變數都不會被修改和存取,所以若有開編譯器最佳化的話,就沒必要去存取那兩個變數的值然後 assign 給 `a`/`b`;但由於 `global_volatile_var` 有被設定為 `volatile`,所以編譯器不會對這個變數做任何假設、也就不會做任何優化的動作,所以還是會去記憶體存取這個變數的值 - 所以若有開編譯器最佳化,預期並不會去記憶體存取 `global_var`,僅會去記憶體存取 `global_volatile_var` 接下來,我在我的 ARM64 Ubuntu 24.04 虛擬機安裝 x86-64 的 cross-compiler: ```shell $ sudo apt install gcc-x86-64-linux-gnu ``` 用以下兩個指令產生 intel 語法的 NASM 組合語言程式碼,且分別是將編譯器最佳化開到最大(`-O3`)和沒開編譯器最佳化(`-O0`): ```shell $ x86_64-linux-gnu-gcc -S -masm=intel -O0 -no-pie volatile.c -o volatile_intel_O0 $ x86_64-linux-gnu-gcc -S -masm=intel -O3 -no-pie volatile.c -o volatile_intel_O3 ``` 以下是沒開編譯器最佳化的 `main` 的內容: ```nasm= main: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov eax, DWORD PTR global_var[rip] mov DWORD PTR -8[rbp], eax mov eax, DWORD PTR global_volatile_var[rip] mov DWORD PTR -4[rbp], eax mov eax, 0 pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: ``` - 在 line 10 和 line 12 可以看到,都有確實去記憶體存取 `global_var` 和 `global_volatile_var` 的值 以下是有開編譯器最佳化的 `main` 的內容: ```nasm= main: .LFB0: .cfi_startproc endbr64 mov eax, DWORD PTR global_volatile_var[rip] xor eax, eax ret .cfi_endproc .LFE0: ``` - 可以看到 `main` 裡面只有去記憶體存取 `global_volatile_var` 的值(line 5) ::: ## Bit Manipulation ### 9. 嵌入式系統時常會需要操作 register 或變數的 bits 假設有個 integer 變數 `a`,請寫程式碼: 1. 將 `a` 的 bit 3 設為 `1` 2. 將 `a` 的 bit 3 設為 `0` 解法就是使用 `#define` 和 bit masks: ```cpp #define BIT3 (0x1 << 3) static int a; void set_bit3(void) { a |= BIT3; } void clear_bit3(void) { a &= ~BIT3; } ``` :::info 補充:其他 bit manipulation 考題 #### Toggle a Bit Toogle 第 k 個 bit,也就是若第 k 個 bit 是 1,那就把它變成 0;若是 0,則把它變成 1。 ```cpp int toggleBit(int n, int k) { return (n ^ (1 << k)); } void toggle_bit3() { a ^= BIT3; } ``` #### Find a Bit 找出第 k 個 bit 是 0 或 1 ```cpp int findBit(int n, int k) { return ((n >> k) & 0x1); } ``` #### Modify a Bit 將第 k 個 bit 設定為 `b` 1. 先將 1 左移 k 位,取得 mask ```cpp mask = (1 << k) ``` 2. 利用 `mask` 將第 k 個 bit 設定為 0 ```cpp n = (n & ~mask) ``` 3. 最後將 `b` 左移 k 位,然後和前述得到的 `n` 做 `|` operation ```cpp return ((n & ~mask) | (b << k)); ``` 完整程式碼: ```cpp int modifyBit(int n, int k, int b) { int mask = (1 << k); return ((n & ~mask) | (b << k); } ``` ::: :::danger 原文作者也有提到有的人可能會用 bit-field 來達到這樣的效果,但 bit-field 會是 ==NON-PORTABLE==,++不同編譯器的實作會不一樣喔++!所以不要用這種做法去做 bit manipulation ![image](https://hackmd.io/_uploads/r19qztdAke.png) 依據 [C 語言標準](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf) § 6.7.3.2 的內容,bit-field 是 high-order to low-order,或是 low-order to high-order,這是編譯器廠商決定的。 例如以下程式碼,我想用 bit-field 的方式將 `num` 的第三個 bit 設為 1: ```cpp /* test.c */ #include <stdio.h> typedef struct { unsigned int bit_0 : 1; unsigned int bit_1 : 1; unsigned int bit_2 : 1; unsigned int bit_3 : 1; unsigned int bit_others : 28; } MyBitField; int main() { int num = 0x0; MyBitField *ptr = (MyBitField *) &num; ptr->bit_3 = 1; printf("num: %b\n", num); return 0; } ``` 我在 ARM64 的 Ubuntu 24.04 虛擬機用 GNU GCC 編譯: ```shell $ gcc test.c -std=c2x -o test_arm64 ``` 接下來,測試用不同的編譯器是否會有差異,這裡用 PowerPC 編譯器來測試。首先要在 Ubuntu 安裝 PowerPC cross-compiler 和模擬器: ```shell $ sudo apt -y install gcc-powerpc-linux-gnu qemu-user ``` 用 PowerPC cross-compiler 編譯: ```shell $ powerpc-linux-gnu-gcc -static test.c -o test_ppc ``` 執行結果: ![image](https://hackmd.io/_uploads/S1AxDs_RJe.png) - 可以看到 `test_ppc` 是一個 32-bit 的 PowerPC 架構 ELF 可執行檔,且是靜態連結的版本。而 `MSB` 則是指 most significant byte first,也就是 Big Endian - 而 `test_arm64` 則是一個 64-bit 的 ARM 架構可執行檔,且是 Little Endian - 執行這兩個可執行檔,可看到結果是完全不一樣的!這樣的寫法就是 non-portable~ PS. `%b` 是 C23 新加入的 conversion specifier,在使用 GNU GCC 編譯時記得要設定 `-std=c2x`(for gcc-13)或 `-std=c23`(for gcc-14) ![image](https://hackmd.io/_uploads/SkJCriO0kl.png) ::: ## Accessing Fixed Memory Locations ### 10. 嵌入式系統時常會需要存取特定記憶體位址。請在記憶體位址 `0x67a9` 存入一個 integer `0xaa55` 有許多寫法可達到這效果,原文作者比較想看到的是類似以下的寫法: ```cpp int *ptr; ptr = (int *) 0x67a9; // 將 0x67a9 這個數字轉型成 pointer to integer // 並 assign 給 ptr // 這樣 ptr 就指向記憶體位址 0x67a9 *ptr = 0xaa55; ``` 另一種寫法如下: ```cpp *(int * const)(0x67a9) = 0xaa55; ``` :::success 原文作者建議在面試時寫第一個寫法比較好~ ::: ## Interrupts ### 11. Interrupts 在嵌入式系統扮演著重要的角色。因此,許多編譯器廠商會提供 extension 來 support interrupts 一般而言,編譯器廠商提供的 interrupt extension 的 keyword 最常見的會是 `__interrupt`。 以下程式碼使用 `__interrupt` 定義了一個 interrupt service routine(ISR),對以下程式碼有何看法呢? ```cpp __interrupt double compute_area(double radius) { double area = PI * radius * radius; printf(“nArea = %f”, area); return area; } ``` 以上程式碼有許多錯誤 1. ISR 不應該 return value 2. 不應該傳參數進去 ISR 3. 許多處理器/編譯器在處理浮點數運算不一定是 **re-entrant**。有些情況,需要 stack 額外的 registers,有的情況僅僅是不能在 ISR 內做浮點數運算。此外,ISR 本來就應該是短,不會花很多時間 4. 與第三點類似,`printf()` 通常會有 reentrancy 和 效能的問題。 :::info 補充:其他不該用在 ISR 的東西 Mutex 跟 Semaphore 也不建議用在 ISR 裡面,因為 ISR 要設計成執行時間很短、很快就結束。而 Mutex 跟 Semaphore 是 Blocking 機制,如果已經被鎖住,然後 ISR 又嘗試去鎖,那就會 block 了! 不過有些 RTOS 會提供可以在 ISR 裡面使用的版本,像是 FreeRTOS 就有 [`xSemaphoreGiveFromISR`](https://www.freertos.org/Documentation/02-Kernel/04-API-references/10-Semaphore-and-Mutexes/17-xSemaphoreGiveFromISR) 這個專門用在 ISR 的版本,這是在 ISR 裡面「釋放」semaphore 的操作。 而在 ISR 裡面釋放 semaphore 大多都是為了要把另一個 task 叫醒,讓他去處理跟 interrupt 有關、但可能較複雜、會需要花比較多時間的事情,這種用法叫做 [deferred interrupt handling](https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/11-Deferred-interrupt-handling)。 ::: :::info 補充:Interrupt Extension 原文用的 `__interrupt`,我只有查到好像是 `TMS320C6000 Optimizing Compiler` 這個編譯器有提供這個 extension。他們的 [user manual](https://downloads.ti.com/docs/esd/SPRUI04/the---interrupt-keyword-stdz0559860.html) 也有提到: > You can **only** use the `__interrupt` keyword with a function that is defined to ++return `void`++ and that ++has NO parameters++. The body of the interrupt function can have local variables and is free to use the stack or global variables. For example: > ```cpp > __interrupt void int_handler() { unsigned int flags; ... } > ``` 而 GNU GCC 用的是 ` __attribute__ ((interrupt ("IRQ")))`,使用方法是: ```cpp void f () __attribute__ ((interrupt ("IRQ"))); ``` > https://gcc.gnu.org/onlinedocs/gcc/ARM-Function-Attributes.html ::: :::info 補充:Thread Safety & Re-Entrancy ![image](https://hackmd.io/_uploads/B1CK6DCCye.png) ![image](https://hackmd.io/_uploads/HJgJRvA0Je.png) > 以上截自 CMU 15-213「Synchronization: Advanced」課程投影片 ::: ## Code Examples ### 12. 以下程式碼的 output 為何?以及為什麼? ```cpp void foo(void) { unsigned int a = 6; int b = -20; (a+b > 6) ? puts("> 6") : puts("<= 6"); } ``` 以上程式碼,`a` 是無號整數,`b` 是有號整數。 如前面所述,若一個 expression 同時有有號數和無號數,那有號數會被轉成無號數,所以 `a+b` 的 `b` 會是一個非常非常大的數,所以相加後一定是大於 6,所以會印出 `> 6`。 :::success 原文作者說這在嵌入式系統非常重要,並建議在嵌入式系統應多使用 unsigned data types(原文作者推薦的文章:[Efficient C Code for Eight-Bit MCUS](https://rmbconsulting.us/Publications/Efficient%20C%20Code.pdf))。 ::: ### 13. 對以下的 code 有何看法? ```cpp unsigned int zero = 0; unsigned int compzero = 0xFFFF; /* 1's complement of zero */ ``` 以上程式碼的 `compzero` 是想要取得 0 的 1 補數,但 `0xFFFF` 這寫法僅有在 `int` 是 16 bits 的系統才會得到正確的結果。 但現在大部分的 CPU,`int` 都是 32 bits,那以上寫法得到的 0 的 1 補數就會是錯誤的喔! 正確的寫法應該如下,這樣才能確保程式碼的可移植性: ```cpp unsigned int compzero = ~0; ``` :::success 原文作者說~好的嵌入式工程師是有需要對底層硬體有深入的瞭解和敏感度的! ::: :::info 在 64 bits 的電腦執行以下程式碼,可看到 `~zero` 才能得到正確的結果: ```cpp #include <stdio.h> int main() { unsigned int zero = 0; unsigned int compzero1 = 0xFFFF; printf("compzero1: %x\n", compzero1); // compzero1: ffff printf("compzero1: %b\n", compzero1); // compzero1: 1111111111111111 unsigned int compzero2 = ~zero; printf("compzero2: %x\n", compzero2); // compzero2: ffffffff printf("compzero2: %b\n", compzero2); // compzero2: 11111111111111111111111111111111 return 0; } ``` ::: ## Dynamic Memory Allocation ### 14. 雖然較少見,但有時在嵌入式系統還是會需要從 Heap 做 dynamic allocation 來配置記憶體空間。那在嵌入式系統做 dynamic memory allocation 會有什麼 issue 需要注意? 原文作者期望聽到的回答包括像是: - Memory fragmentation - 當 heap 經過多次的 allocation 和 free,有可能會造成嚴重的 fragmentation 問題,也就是 free memory block 加總起來的空間是夠用的,但我們找不到連續的 free memory block 來 allocate - Garbage collection 的問題 - 由於 C 語言並沒有 garbage collection 的機制,所以釋放記憶體資源這件事是由 programmer 負責~ - Variable execution time - `malloc` 的執行時間不是固定的,會根據 heap 的狀況而有差異。若 heap fragmentation 的狀況很嚴重,可能會需要找很久才能找到需要的 free memory block - 在 real-time system,我們就會希望執行的時間是 deterministic ㄛ! 以下程式碼的 output 為何?以及為什麼? ```cpp char *ptr; if ((ptr = (char *)malloc(0)) == NULL) { puts(“Got a null pointer”); } else { puts(“Got a valid pointer”); } ``` ![image](https://hackmd.io/_uploads/H1kc4e_R1l.png) 依據 [C 語言的標準](https://open-std.org/JTC1/SC22/WG14/www/docs/n3220.pdf),若做了 `malloc(0)` 這件事,那就是==依據各家編譯器廠商自己實作而定==,有可能會有兩種狀況: 1. 回傳 `NULL` 2. 或者是他的行為就像是分配了一個 size 非 0 的記憶體,但我們++不該用這個 pointer 去存取物件++ 而 GNU GCC 的實作是會回傳一個 non-NULL pointer。 > In the GNU C Library, a successful `malloc(0)` returns a non-null pointer to a newly allocated size-zero block > https://www.gnu.org/software/libc/manual/html_node/Malloc-Examples.html :::info 補充:有些嵌入式系統不會使用 std C lib 的 `malloc` 和 `free`,因為 - 很多小型的嵌入式系統沒有提供 std C library - Standard C lib 實作的 `malloc` 和 `free` 會佔用比較大的空間,在一些記憶體空間有限的嵌入式裝置並不適用 - 通常這些並不是 thread safe 的 dynamic allocation 機制 - 如前所述,執行的時間較不固定,每次呼叫這些 API 所需的時間可能差異頗大。但是在 real-time system 執行時間是 deterministic 是很重要的事 - 通常嵌入式系統的記憶體空間較有限,而 standard C library 的 dynamic allocation 可能會造成比較嚴重的 fragmentation ::: :::info 補充:FreeRTOS 的 Dynamic Allocation 機制 > https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/09-Memory-management/01-Memory-management FreeRTOS 提供五種 dynamic allocation 的機制,programmer 可以依照專案的需求選擇合適的方式。 1. [`heap_1`](https://github.com/FreeRTOS/FreeRTOS/blob/91659446648a00f24b0b8af6327230658cc65c07/FreeRTOS/Demo/CORTEX_LM3S811_KEIL/heap/heap_1.c#L4) - 最簡單的實作,不支援 free 的操作,所以一旦分配了就不能釋放 - 保證執行的時間都是 deterministic - `heap_1` 現在已經比較少用了,大多是用在記憶體配置這件事都在初始化階段就完成、不需要做釋放記憶體這個操作的系統 2. `heap_2` - 可以 allocate 也支援 free 的操作 - 相鄰的 free memory block 並不會被合併,因此有可能會造成 fragmentation - 用 best fit 去分配記憶體區塊。但因為 best fit,就必須要走完整個維護 free memory block 的 link list,才能找到 best fit 的 free memory block,所以執行時間是 non-deterministic 3. `heap_3` - 是 standard C library 的 `malloc` 和 `free` 的 wrapper,但是在呼叫 `malloc` 和 `free` 之前,會先暫停 scheduler,確保是 thread safe - 但仍有 standard C library 的幾個問題: - 執行時間是 non-deterministic - 佔的空間可能很大 4. `heap_4` - 跟 `heap_2` 很像,但是會將相鄰的 free memory block 做合併,以降低 fragmentation 的程度 - 使用 first fit 來分配記憶體區塊 - 由於使用 first fit,所以仍是 non-deterministic,但比 standard C library 的 `malloc` 還有效率 5. `heap_5` - 實作細節跟 `heap_4` 一樣,用 first fit,且會將相鄰的 free memory block 做合併 - 但差別在於,可以將多個 physical 記憶體設定成 logically 看起來像是一塊大記憶體 ++Heap 1 示意圖++ ![freertos_heap_1](https://hackmd.io/_uploads/SkyeucCC1g.png) ++Heap 2 示意圖++ ![freertos_heap_2_1](https://hackmd.io/_uploads/B1mEKqRRkg.png) Heap 2 是用 link list 去維護 free memory block,每個 free memory block 的起始位址都會放入 `BlockLink_t` 這個 struct。該 struct 的`pxNextFreeBlock` 是一個 pointer,會指向下一個 free memory block 的起始位址;然後 `xBlockSize` 是紀錄這個 free memory block 的大小。 `xStart` 是整個 link list 的開頭,`xEnd` 則是結尾,由這兩個串起維護 free memory block 的 link list,且這個 link list 會依照 free memory block 的 size 由小排到大。 ![freertos_heap_2_2](https://hackmd.io/_uploads/HJUb5q001g.png) 這是 heap 剛初始化完的樣子,現在整個 heap 都是 free 的,是一個大的 free memory block,所以在他的起始位址會被放入一個 `BlockLink_t`。 然後 `xStart` 的 `pxNextFreeBlock` 就會指向這個大的 free memory block 的起始位址;然後這個大的 free memory block 的 `pxNectFreeBlock` 就會指向 `xEnd`。 ![freertos_heap_2_3](https://hackmd.io/_uploads/S1Zfi9R0ke.png) 這是分配了第一個 task 的狀況,第一個 task 用掉了圖中藍色那部分的 heap。剩下的 heap 還是一整塊 free 的 memory block,所以在剩下的 free memory block 的起始位址也會放入一個 `BlockLink_t`,然後 `xStart` 的 `pxNextFreeBlock` 改成指向這個 free memory block 的起始位址,而這個 free memory block 的 `pxNextFreeBlock` 則指向 `xEnd`。 橘色的部分則是一開始的 free memory block 放 `BlockLink_t` 的地方。 ![freertos_heap_2_4](https://hackmd.io/_uploads/B1lu350A1e.png) 接下來又分配了第二個 task。 ![freertos_heap_2_5](https://hackmd.io/_uploads/HJ4n2cCRyl.png) 最後,假如釋放了那兩個 task 所佔用的記憶體區塊,由於 heap 2 沒有將相鄰 free memory block 合併的機制,所以會有一塊一塊的 free memory block,這樣就會有 fragmentation 的問題! 然後由於這個 free memory block list 會依據 free memory block 的大小由小排到大,所以可看到 `xStart` 的 `pxNextFreeBlock` 會先指向最小的那塊,而最小的那塊的 `pxNextFreeBlock` 則會指向第二小的那塊,第二小的那塊的 `pxNextFreeBlock` 則指向剩下最大的那塊,最大的那塊的 `pxNextFreeBlock` 就指向 `xEnd`。 ::: ## Typedef ### 15. 在 C 語言,`typedef` 是用來為既有的 data type 建立別名。我們也可以用 `#define` 來達到類似的效果,譬如以下程式碼: ```cpp #define dPS struct s * typedef struct s * tPS; ``` 以上兩種方式所宣告的 `dPS` 和 `tPS`,都是 pointer,且都是指向 `struct s` 的 pointer,那哪一種方式比較好勒?以及為什麼哩? 考慮以下狀況: ```cpp dPS p1, p2; tPS p3, p4; ``` 使用 `#define` 的作法,在經由 preprocessor 展開後會變成: ```cpp struct s * p1, p2; ``` 這樣的話,只有 `p1` 是 pointer,而 `p2` 則是一個 `struct s`。 所以++使用 `typedef` :+1: 會是比較好的做法++,才能避免這種狀況發生。 ## Obfuscated Syntax ### 16. 以下寫法在 C 語言是合法的嗎?若是合法,那這段程式碼會做什麼事? ```cpp int a = 5, b = 7, c; c = a+++b; ``` 這是合法的寫法,但關鍵在於編譯器該如何解析 `a+++b`。該解析成 `a++ + b`?或 `a + ++b` 勒? 編譯器在 Lexical analysis 階段,會依據 Maximum munch rule 來解析程式碼,也就是當編譯器遇到一串 characters 時,他會盡可能「吃掉」最多的 characters 來組成一個合法的 token。 所謂的「吃掉」就是指,盡量把相鄰的 characters 當成一個完整的語法單位,像是將 `++` 視為一個 token,而不是視為 `+` 和 `+` 兩個 tokens。 所以當編譯器看到 `a+++b` 時,會先盡量吃掉最多的 characters 來組成一個合法的 token。所以一開始先吃掉 `a++`,而這是合法的,剩下的 `+b` 也是合法的 token,那編譯器就會解析成 `(a++) + b`。 那 `c = (a++) + b` 實際做的事情就是: 1. 先將 `a` 的值取出來,然後跟 `b` 相加,並 assign 給 `c`,所以 `c` 是 12 2. 之後再做 `a++`,所以 `a` 的值變成 6 所以各變數最後的 value 會是: - `a`:6 - `b`:7 - `c`:12