# 2022q1 第 3 週測驗題 contributed by < `rickywu0421` > [題目連結](https://hackmd.io/@sysprog/linux2022-quiz3) ## 測驗 `1` ### 題目 在 Linux 核心原始程式碼,[include/linux/bitfield.h](https://github.com/torvalds/linux/blob/master/include/linux/bitfield.h) 提及一個巨集 `GENMASK`,其作用是依據給定的範圍,產生連續的 [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)),例如: - `GENMASK(6, 4)` 產生 011100002$_2$ - `GENMASK(39, 21)` 產生 0x000000ffffe00000$_2$ (64 位元) 已知我們使用的微處理器架構為 64 位元,且 `unsigned long` 為 8 位元組寬度 (符合 [LP64](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models) 資料模型),以下是可能的 `GENMASK` 巨集的定義: ```c #define GENMASK(h, l) \ (((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT))) ``` ### 解題 我們以 `GENMASK(6, 4)` 為例, 其可以由以下 bitwise and 操作生成: ```c 01111111 (A) & 11110000 (B) ------------- 01110000 ``` 其想法就是:生成一個 bitmask A, 其在 bit 6 以下都是 1, 其他為 0; 再生成一個 bitmask B, 其在 bit 4 以上都是 1, 其他為 0。最後將 `A & B` 即可圍出想要的 bitmask。 故 `LEFT = 63 - h`, `RIGHT = l`。 ## 測驗 `2` ### 題目 考慮以下程式碼: ```c struct foo; struct fd { struct foo *foo; unsigned int flags; }; enum { FOO_DEFAULT = 0, FOO_ACTION, FOO_UNLOCK, } FOO_FLAGS; static inline struct fd to_fd(unsigned long v) { return (struct fd){EXP1, v & 3}; } ``` 函式 fo_fd 接受指定的地址 v,填入一個 fd 結構體,並確保第一個成員的地址得以依據〈[你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory)〉描述,對 4 個位元組進行向下對齊 (即 C++ Boost 描述的 align_down)。其中 `struct foo` 運用〈[你所不知道的 C 語言:指標篇](https://hackmd.io/@sysprog/c-pointer)〉提及的 forward declaration 技巧,可在實作階段才提供具體 `struct foo` 的定義。 ## 解題 `(struct fd){EXP1, v & 3}` 用到 [compound literal](https://gcc.gnu.org/onlinedocs/gcc/Compound-Literals.html) 的技巧, 其可作為 lvalue (因為其作為一個 object)。 若要判斷一個 bit stream 是否對 4 個位元組對齊, 則最後兩個 bit 必須都被 clear。因此向下對齊 4 個位元組也就是把 bit stream 的最後兩個 bit clear 掉。舉例來說, `0b11001001` 沒有對 4 個位元組對齊, 我們可以透過以下操作達成向下對齊: ```c 11001001 (original) & 11111100 (bit mask) ----------------- 11001000 ``` 而 `0b11111100` 也就是 `0b00000011(0x3)` 做位元反轉得到的結果。 故 `EXP1 = (struct foo *)(v & ~3)`。 另外, 我們也可以看出, `flag` 為 `v & 3` 的目的為儲存原本的第一個元素相對向下對齊的位址的偏移量, 故 `(char *)fd.foo + fd.flags` 可以存取到原本的第一個元素。 ## 測驗 `3` ### 題目 考慮以下程式碼,能將輸入的 8 位元無號整數逐一位元反轉,如同 [LeetCode 190. Reverse Bits](https://leetcode.com/problems/reverse-bits/) 的描述。 ```c #include <stdint.h> uint8_t rev8(uint8_t x) { x = (x >> 4) | (x << 4); x = ((x & 0xCC) >> 2) | (EXP2); x = ((x & 0xAA) >> 1) | (EXP3); return x; } ``` ### 解題 反轉 8 位元無號整數的方法就是不斷的將隔板的左右反轉並逐漸縮小範圍, 需要反轉的次數為 $log_28 = 3$, 如以下舉例 (以 `0b11010100` 為例) 1. 第一次反轉, 隔板間隔為 `4` 個 bit: ```c 1101|0100 0100|1101 ``` 此操作對應得程式碼片段如下: ```c x = (x >> 4) | (x << 4); ``` 2. 第二次反轉, 隔板間隔為 `2` 個 bit: ```c 01|00 11|01 00|01 01|11 ``` 此操作對應得程式碼片段如下: ```c x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2); ``` 故 `EXP2 = (x & 0x33) << 2`。 3. 第三次反轉, 隔板間隔為 `1` 個 bit: ```c 0|0 0|1 0|1 1|1 0|0 1|0 1|0 1|1 ``` 此操作對應得程式碼片段如下: ```c x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1); ``` 故 `EXP3 = (x & 0x55) << 1`。 ## 測驗 `4` ### 題目 延伸〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉,我們嘗試將 [foreach 巨集](https://en.wikipedia.org/wiki/Foreach_loop) 予以擴充,使得支援以下寫法: ```c int i; foreach_int(i, 0, -1, 100, 0, -99) { printf("i is %i\n", i); } const char *p; foreach_ptr(p, "Hello", "world") { printf("p is %s\n", p); } ``` 預期輸出如下: ```c i is 0 i is -1 i is 100 i is 0 i is -99 p is Hello p is world ``` 對應的巨集定義: ```c #include <assert.h> #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) #define foreach_int(i, ...) \ for (unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0); \ _foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int); \ (i) = ((int[]){__VA_ARGS__, 0})[EXP4]) #define foreach_ptr(i, ...) \ for (unsigned _foreach_i = \ (((i) = (void *) ((typeof(i)[]){__VA_ARGS__})[0]), 0); \ (i); (i) = (void *) ((typeof(i)[]){__VA_ARGS__, \ NULL})[EXP5], \ _foreach_no_nullval(_foreach_i, i, \ ((const void *[]){__VA_ARGS__}))) ``` ### 解題 #### foreach_int for loop 可分為: #### 1. 起始條件 ```c unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0) ``` 將 `i` 賦值為陣列索引值 `0` 的元素, 並將 `_foreach_i` 賦值為 `0`。 #### 2. 測試條件 ```c _foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int) ``` 判斷索引值 `_foreach_i` 是否小於陣列的元素個數。 #### 3. 更新條件 ```c (i) = ((int[]){__VA_ARGS__, 0})[EXP4]) ``` 將 `i` 更新為下一個陣列的元素。故 `EXP4 = ++_foreach_i`。 #### foreach_int #### 1. 起始條件 ```c unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0) ``` 將 `i` 賦值為陣列索引值 `0` 的元素, 並將 `_foreach_i` 賦值為 `0`。 #### 2. 測試條件 ```c (i) ``` 判斷 `i` 是否不為 `NULL`。 #### 3. 更新條件 ```c (i) = (void *) ((typeof(i)[]){__VA_ARGS__, \ NULL})[EXP5], \ _foreach_no_nullval(_foreach_i, i, \ ((const void *[]){__VA_ARGS__}))) ``` 將 `i` 更新為下一個陣列的元素。故 `EXP5 = ++_foreach_i`。並透過 `_foreach_no_nullval` 巨集確認 `i` 是否為 NULL: #### _foreach_no_nullval ```c #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) ``` 這是為了檢查當 `_foreach_i < (陣列元素個數)`時 `p` 是否不等於 `NULL`。