---
tags: linux2022
---
# 2022q1 Homework3 (quiz3)
contributed by < [`jj97181818`](https://github.com/jj97181818) >
## 測驗 1
```c
#define GENMASK(h, l) \
(((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT)))
```
1. 因為這裡的 unsigned long 為 8 bytes,所以 `~0UL` 有 64 bit 的 1。
2. 先討論 `((~0UL) >> (LEFT))`,為了讓連續的 bitmask 最高位到 `h` 停止,又因 LSB 從第 0 位開始算起,MSB 是第 63 位,所以需要讓 64 bit 的 1 向右移 `63 - h` 位。
3. 再來看 `((~0UL) >> (l) << (RIGHT))`,先將 64 bit 的 1 向右移 `l` 位,為了讓連續的 bitmask 最低位從第 `l` 位開始,就再左移 `l` 位,其餘會補 0,就實現從第 `l` 位開始。
4. 將 `((~0UL) >> (LEFT))` 和 `((~0UL) >> (l) << (RIGHT))` 做 AND 運算,即可同時滿足最高位到 `h`、最低位從 `l` 開始,即 GENMASK 巨集。
因此 `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};
}
```
1. `to_fd` 要將指定的位址 v 當作 fd 的起始位址,且要確保以 4 bytes 向下對齊,而向下對齊的巨集:
```c
#define align_down(x) ((x) & ~(MAX_ALIGNMENT - 1))
```
`v & ~(4 - 1)` = `v & ~3` = `v & 1100`
透過將位址 v 的最後兩位 clear 掉,來保證該位址一定為 4 的倍數,做到向下對齊。
2. 因為該位址是 fd 的起始位址,會有個指向 fd 第一個成員 struct foo 的指標,所以會需要在位址前加上 `(struct foo *)`。
因此 `EXP1` = `(struct foo *)(v & ~3)`
## 測驗 3
```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;
}
```
目標是從 12345678 變成 87654321(這裡的 1~8 都是代表著 0 或 1 的值,只是用 1~8 來表示比較能清楚看出 reverse 的過程):
1. `x = (x >> 4) | (x << 4);`
```
(12345678 >> 4) | (12345678 << 4)
= 00001234 | 56780000
= 56781234
```
這行可以看出從 8 bit 的中間切成兩半,並互換。
2. `x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2);`
```
((56781234 & 11001100) >> 2) | ((56781234 & 00110011) << 2)
= (56001200 >> 2) | (00780034 << 2)
= 00560012 | 78003400
= 78563412
```
這行接著上一行切兩半的方式,先用遮罩 0xCC 取得原本位在 1, 2, 5, 6 的值,那 `EXP2` 的位置一定需要保留 3, 4, 7, 8 的位置,才能讓 x 保留原本 1~8 位置的所有值,因此這裡選用 0x33 當作遮罩,並左移兩位,在與 `((x & 0xCC) >> 2)` 做 OR 運算時,即可保留所有資訊,並切從 4 bit 中間切成兩半做互換。
3. `x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);`
```
((78563412 & 10101010) >> 1) | ((78563412 & 01010101) << 1)
= (70503010 >> 1) | (08060402 << 1)
= 07050301 | 80604020
= 87654321
```
這行與上一行同樣的概念,只是改為每兩個 bit 中間切兩半做互換,可以看到這裡用的遮罩為 0xAA,並右移一位,而另外一個遮罩可以推得是 `0x55`,同樣要記得左移一位,就完成互換的動作了。從最初的 12345678 變成 87654321。
因此 `EXP2` = `(x & 0x33) << 2`, `EXP3` = `(x & 0x55) << 1`
## 測驗 4
```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__})))
```
逗號運算子:`(expression1, expression2)`
首先計算表達式 1,然後計算表達式 2,並為整個表達式返回表達式 2 的值。
1. `foreach_int` macro 中,有個 for 迴圈:
+ 初始條件:`unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0)`
+ `((int[]){__VA_ARGS__})[0]` 為整數陣列,其中 `__VA_ARGS__` 會填入 `foreach_int` 從第二個以後的參數,當作整數陣列的值,並取得整數陣列的第 0 個值,然後將值 assign 給 `i`
+ 在逗號運算子的第一個表達式計算完成後,再將第二個表達式的值 0 回傳給 `_foreach_i`
+ 繼續執行的條件:`_foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int)`
+ `sizeof((int[]){__VA_ARGS__}) / sizeof(int)` 會得到整數陣列的元素數量
+ 在 `_foreach_i` 小於元素數量時繼續執行迴圈
+ 遞增:`(i) = ((int[]){__VA_ARGS__, 0})[EXP4]`
+ 將 `_foreach_i` 先加 1,然後就可以讓 i 取得下一個整數陣列的元素
+ EXP4 為 `++_foreach_i`
2. `foreach_ptr` macro 中,也有個 for 迴圈:
+ 初始條件:`unsigned _foreach_i = (((i) = (void *)((typeof(i)[]){__VA_ARGS__})[0]), 0)`
+ `((typeof(i)[]){__VA_ARGS__})[0]` 是一個型別為 `typeof(i)` 的陣列,然後取出陣列第 0 個位置的值,這個值可以預期傳入的是位址,因為這個巨集的名稱是 `foreach_ptr`
+ `((i) = (void *)((typeof(i)[]){__VA_ARGS__})[0])` 將值強轉型為指向 void 的指標,然後 assign 給 `i`。
+ 然後 0 assign 給 `_foreach_i`
+ 繼續執行的條件:`(i)`
+ 只要 `i` 不為 0 就會繼續執行迴圈
+ 遞增:
+ 和 EXP4 一樣的概念,EXP5 為 `++_foreach_i`
```c
(i) = (void *) ((typeof(i)[]){__VA_ARGS__, NULL})[EXP5],
_foreach_no_nullval(_foreach_i, i, ((const void *[]){__VA_ARGS__}))
```
因此 `EXP4` = `++_foreach_i`, `EXP5` = `++_foreach_i`
## 測驗 5
```c
#include <limits.h>
int divide(int dividend, int divisor)
{
int signal = 1;
unsigned int dvd = dividend;
if (dividend < 0) {
signal *= -1;
dvd = ~dvd + 1;
}
unsigned int dvs = divisor;
if (divisor < 0) {
signal *= -1;
dvs = ~dvs + 1;
}
int shift = 0;
while (dvd > (EXP6))
shift++;
unsigned int res = 0;
while (dvd >= dvs) {
while (dvd < (dvs << shift))
shift--;
res |= (unsigned int) 1 << shift;
EXP7;
}
if (signal == 1 && res >= INT_MAX)
return INT_MAX;
return res * signal;
}
```
1. 以 `signal` 來紀錄相除結果的正負號,然後如果除數或被除數為負整數,一律改為正整數。
2. `shift` 從 0 開始,當 `dvd > (EXP6)` 時,shift 加 1,因為這題目的是要做除法,這裡的 EXP6 一定會和除數 `dvs` 有關。這個迴圈會在 `shift` 愈來愈大之後在某個條件下停止,可想到的是將 dvs 左移 shift 位,當 shift 愈大,`dvs` 也愈大,直到超過或等於 `dvd` 的大小。這裡 `shift` 的意義是類似商的概念,看除數乘以多大的倍數(左移多少位)能夠比被除數大或至少相等。
3. 在被除數 `dvd` 仍大於等於除數 `dvs` 時,表示可以繼續進行除法。當 `dvs` 左移 `shift` 位會比 `dvd` 大時,就將 `shift` 減 1。`1 << shift;` 的想法是將小於 `dvd` 並左移過後的 `dvs` 只取其最高位元,用 1 左移 `shift` 位來表達。
4. 因為迴圈要 `dvd` 小於 `dvs` 才會停止,EXP7 要更新 `dvd`,而 `dvd` 減去 `dvs` 左移 `shift` 位,就是更新被除數,讓它減去除數乘以一個倍數。
5. 當被除數 `dvd` 不再大於等於除數 `dvs` 時,相除的結果如果超出能表達的最大值,就直接回傳 INT_MAX。其餘則回傳除完的結果。
因此 `EXP6` = `dvs << shift`, `EXP7` = `dvd -= dvs << shift`
## 測驗 6
## 測驗 7
```c
int ilog32(uint32_t v)
{
int ret = v > 0; // 1, 0000 0001
int m = (v > 0xFFFFU) << 4; // 16 , 0001 0000
v >>= m;
ret |= m; // 0001 0001
m = (v > 0xFFU) << 3; // 8, 0000 1000
v >>= m;
ret |= m; // 0001 1001
m = (v > 0xFU) << 2; // 4, 0000 0100
v >>= m;
ret |= m; // 0001 1101
m = EXP10; // EXP10 = (v > 0x3U) << 1 2, 0000 0010
v >>= m;
ret |= m; // 0001 1111
EXP11;
return ret;
}
```
限制:
+ EXP10 和 EXP11 皆包含 > 運算子
ilog32 與 ffs 的實作很像,只是 ffs 是從 LSB 開始找第一個為 1 的 bit,這裡是從 MSB 找第一個為 1 的 bit。
1. 最初的 `ret = v > 0` 是要記錄當 v > 0 時,最高位的 1 也需要一個位元儲存。
2. 程式主體是做以下的操作 4 次。
+ 更新 m
+ 將 v 右移 m 位
+ 更新 ret。
3. 第一次先操作將 32 位元切半來看,比較高位元的 16 bit 中有沒有 1,有的話代表至少要儲存 16 bit(`m` = 16),然後將 v 右移 16 位,為的是方便繼續觀察原本較高位元的 16 bit 中還需要多少 bit 儲存。接著 ret 會記錄到目前為止共需要 16 bit 來存這個 v。
4. 第二次就是將剩下 16 位元切半來看,看比較高位元的 8 bit 中有沒有 1,有的話代表至少要再多存 8 bit(`m` = 8),然後將 v 右移 8 位,方便繼續觀察原本較高位元的 8 bit 中還需要多少 bit 儲存。然後 ret 記錄到目前為止共需要 16 + 8 bit 來存這個 v。
5. 接著第三次和第四次分別是看 8 位元中較高位元的 4 bit,和 4 位元中較高位元的 2 bit,因此 EXP10 為 `(v > 0x3) << 1` 才能查看較高位元的 2 bit 中是否有 1。
6. 最後到 EXP11,只剩下最後兩位還沒判斷,如果 v > 1,那麼就代表最後兩位都需要儲存,而最高位元已經在最初的 `ret` = 1 就有加上一位了,所以只需要再加一位,EXP11 為 v > 1 時 `ret` 加上 1。
因此 `EXP10` = `(v > 0x3) << 1`, `EXP11` = `ret += v > 1`
## 測驗 8
## 測驗 9
### ROUND_UP 巨集
向上對齊:
```c
/* maximum alignment needed for any type on this platform, rounded up to a
power of two */
#define MAX_ALIGNMENT 16
/* Given a size, round up to the next multiple of sizeof(void *) */
#define ROUND_UP_TO_ALIGNMENT_SIZE(x) \
(((x) + MAX_ALIGNMENT - MMM) & ~(NNN))
```
1. 向上對齊是利用位運算的方法,計算記憶體對齊後的大小,這裡的 MAX_ALIGNMENT 是 16 bytes,向上對齊會以 16 的倍數往上取。
例如:原本大小(這裡的 x)為 1~16 的變數,在向上對齊之後大小都會變成 16,17~32 會變成 32,33~48 會變成 48。
2. 左段的程式 `((x) + MAX_ALIGNMENT - MMM)` 是將大小加上 alignment 的大小再減去 MMM,要讓原本大小不足 16 的都補到 16。MMM 應該為 1,因為當 x = 16 時,x + MAX_ALIGNMENT = 16 + 16 = 32,會讓 16 在對齊後變成 32。
3. 右段的程式 `~(NNN)` 是要去除不足 16 的餘數,因為最後算出來的值應該是 16 的倍數。
當 `NNN` = `MAX_ALIGNMENT - 1`時:
~(MX_ALIGNMENT - 1)
= ~(15)
= 11110000
4. `((x) + MAX_ALIGNMENT - 1) & ~(MAX_ALIGNMENT - 1)`
= (x + 16 - 1) & 11110000
= (x + 00001111) & 11110000
因此 `MMM` = `1`, `NNN` = `MAX_ALIGNMENT - 1`
### ROUND_DOWN 巨集
向下對齊:
```c
/* maximum alignment needed for any type on this platform, rounded up to a
power of two */
#define MAX_ALIGNMENT 16
/* Given a size, round up to the next multiple of sizeof(void *) */
#define ROUND_DOWN_TO_ALIGNMENT_SIZE(x) \
((x) & ~(MAX_ALIGNMENT - 1))
```
因為向下對齊是將大小為 1~15 對齊成 0,16~31 對齊成 16,所以不需要加上 MX_ALIGNMENT 來補足不到 16 的餘數,不足的都直接不要了。
### 在 Linux 核心找出類似的巨集和程式碼
> 並說明 round-up/down 的應用場合
在 `linux/include/linux/netfilter_bridge/ebtables.h` 的[第 107 行](https://github.com/torvalds/linux/blob/fc02cb2b37fe2cbf1d3334b9f0f0eab9431766c4/include/linux/netfilter_bridge/ebtables.h#L107) :
```c
#define EBT_ALIGN(s) (((s) + (__alignof__(struct _xt_align)-1)) & \
~(__alignof__(struct _xt_align)-1))
```
EBT_ALIGN 是做向上對齊。
## 測驗 10
```c
#define DIVIDE_ROUND_CLOSEST(x, divisor) \
({ \
typeof(x) __x = x; \
typeof(divisor) __d = divisor; \
(((typeof(x)) -1) > 0 || ((typeof(divisor)) -1) > 0 || \
(((__x) > 0) == ((__d) > 0))) \
? ((RRR) / (__d)) \
: ((SSS) / (__d)); \
})
```
限制:
+ RRR 和 SSS 為表示式,且都包含 (__x) 和 (__d) (注意小括號)
+ RRR 和 SSS 限用 +, -, >>, << 這幾個運算子,可搭配小括號,並以符合 C99 的最精簡形式撰寫
+ 變數在 RRR 和 SSS 出現的順序 (從左到右),必定是 __x 先於 __d
1. 先看三元運算子的條件
+ `((typeof(x)) -1) > 0` 在 x 的型別為 unsigned 的時候才會為 true
+ `((typeof(divisor)) -1) > 0` 在 divisor 的型別為 unsigned 的時候才會為 true
+ `(((__x) > 0) == ((__d) > 0))` 在兩個值皆大於 0 或小於 0 的時候,才為 true,也就是兩者同號為 true
2. 當上述三元運算子的任一條件為 true,運算結果會是 `((RRR) / (__d))`,即被除數或除數其中一個為 unsigned,或是兩者皆為有號數,但是同號。
3. 當上述三元運算子的條件皆為 false,運算結果會是 `((SSS) / (__d))`,即兩者皆為有號數,且不同號。
4. 所以要找最接近該數字的整數做為商的話,最初的想法是將除完的小數加上 0.5,然後加完之後只取整數的部分。如果加上 0.5 能夠讓讓小數部分進位到整數的話,就代表原本小數後第一位是大於等於 0.5 的,也就是原本就比較靠近該數字取 ceil,即往上找最接近的整數。反之,該數字會比較靠近它取 floor,即往下找最接近的整數。
5. 用題目給的例子來看:
+ DIVIDE_ROUND_CLOSEST(7, 3) = 2
+ 7 除以 3 大約等於 2.3...,2.3 + 0.5 = 2.8
+ 小數點後第一位並沒有進位到整數位,因此 2.3 往下找最接近的整數,即 2。或是 2.8 往下取最近整數。
+ DIVIDE_ROUND_CLOSEST(5, 3) = 2
+ 5 除以 3 大約等於 1.6...,1.6 + 0.5 = 2.1
+ 小數點後第一位有進位到整數位(整數從 1 變為 2),因此 1.6 往上找最接近的整數,即 2。或是 2.1 往下取最近整數。
6. 接著要思考 `RRR`, `SSS` 各自要怎麼表達才能做到上面的想法。因為 `((RRR) / (__d))` 中我能改變的只有 RRR 的部分,所以我做了一些調整,變成:
+ RRR 的部分
```c
以下四行的 / 先表示除到小數點後第一位
floor((__x) / (__d) + 0.5)
= floor((__x) / (__d) + (0.5 * (__d)) / (__d))
= floor(((__x) + 0.5 * (__d)) / (__d))
因為 C 語言中的 / 本來就是無條件捨去小數點第一位,所以可以刪除 floor:
= ((__x) + 0.5 * (__d)) / (__d)
然後 0.5 * (__d) 可改成 (__d) >> 1
= ((__x) + (__d) >> 1) / (__d)
```
+ SSS 的部分
只有被除數和除數為異號時,才會執行到這。經過除法運算之後,因為值為負的,所以原本加 0.5,這裡要換成減 0.5,才能達到讓小數後第一位進位到整數的效果。
```c
以下四行的 / 先表示除到小數點後第一位
floor((__x) / (__d) - 0.5)
= floor((__x) / (__d) + ((-0.5) * (__d)) / (__d))
= floor(((__x) + (-0.5) * (__d)) / (__d))
因為 C 語言中的 / 本來就是無條件捨去小數點第一位,所以可以刪除 floor:
= ((__x) - 0.5 * (__d)) / (__d)
然後 (-0.5) * (__d) 可改成 -(__d) >> 1
= ((__x) - (__d) >> 1) / (__d)
```
因此 `RRR` = `(__x) + (__d) >> 1`, `SSS` = `(__x) - (__d) >> 1`
## 測驗 11
fls 是找最高位 1 的位置。
```c
static inline unsigned long fls(unsigned long word)
{
int num = 64 - 1;
if (!(word & (~0ul << 32))) {
num -= 32;
word <<= 32;
}
if (!(word & (~0ul << (64 - 16)))) {
num -= 16;
word <<= 16;
}
if (!(word & (~0ul << (64 - 8)))) {
num -= 8;
word <<= 8;
}
if (!(word & (~0ul << (64 - 4)))) {
num -= 4;
word <<= 4;
}
if (!(word & (~0ul << (64 - 2)))) {
num -= 2;
word <<= 2;
}
if (!(word & (~0ul << (64 - 1))))
num -= 1;
return num;
}
```
參考 [Digit-by-digit calculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation)
假設今天要求 $N^2$ 的平方根 N,
定義 $N^2 = (a_n + ... + a_0)^2$,其中 $a_m$ 會是 $2^m$ 或 $0$(用二進制的概念),
要從 $P_n$ 一直迭代算到 $P_0$,慢慢逼近要求的 N($P_i$ 會愈來愈靠近 N):
$\quad\ P_n = a_n$
$\rightarrow P_{n-1} = a_n + a_{n-1}$
$\rightarrow \ ...$
$\rightarrow P_{m+1} = a_n + a_{n-1} + ... + a_{m+1}$
$\rightarrow P_m = a_n + a_{n-1} + ... + a_{m+1} + a_m$
$\rightarrow \ ...$
$\rightarrow P_0 = a_n + a_{n-1} + ... + a_{m+1} + a_m + ... + a_0$
由上可知:$P_m = P_{m+1} + a_m$,而 $a_m$ 會是 $2^m$ 或 $0$。
如果 $P_m^2 = (P_{m+1} + 2^m)^2 \leq N^2$(加上了 $2^m$ 也沒有超過 $N^2$),則 $a_m = 2^m$,且 $P_m = P_{m+1} + 2^m$;反之,則 $a_m = 0$,且 $P_m = P_{m+1}$。
為了避免在每一輪算 $P_m$ 的時候都要用 $P_m$ 的平方去決定 $a_m$ 是 $2^m$ 或 $0$,改用 $X_m = N^2 - P_m^2$(該輪 $P_m^2$ 和原本的 $N^2$ 還差多少,愈小代表 $P_m$ 愈靠近 $N^2$ 的平方根),
然後 $X_m = X_{m+1} - Y_m$(因為 $P_m$ 可能會比 $P_{m+1}$ 來的更靠近 N,所以在與 $N^2$ 的差異上,$X_m$ 可能會比 $X_{m+1}$ 小,其中 $Y_m$ 是 $X_m$ 和 $X_{m+1}$ 的差,是大於等於 0 的),
而 $Y_m = X_{m+1} - X_m = (N^2 - P_{m+1}^2) - (N^2 - P_{m}^2) = P_m^2 - P_{m+1}^2 = 2P_{m+1}a_m + a_m^2$。
上面有寫到會從 $P_n$ 一直迭代算到 $P_0$,首先是 $P_n = a_n$,為了確認 $a_n$ 是 $2^n$ 還是 $0$,將其代入 $P_m^2 = (P_{m+1} + 2^m)^2 \leq N^2 \rightarrow P_n^2 = (0 + 2^m)^2 \leq N^2$,所以確認 $a_n = 2^n$。
前面有提到 $Y_m = 2P_{m+1}a_m + a_m^2$,將其分成 $c_m$ 和 $d_m$ 兩部分:
(在確定 $a_m$ 為 $2^m$ 而不是 $0$ 後,$a_m$ 代入 $2^m$)
$c_m = 2P_{m+1}a_m = 2P_{m+1}2^m = P_{m+1}2^{m+1} \\
d_m = a_m^2 = (2^m)^2 \\
Y_m = \begin{cases}
c_m + d_m & \text{if } a_{m} = 2^{m} \\
0 & \text {if } a_{m} = 0
\end{cases}$
更新 $c_m$, $d_m$:
$c_{m-1} = P_m2^m = (P_{m+1}+a_m)2^m = P_{m+1}2^m + a_m2^{m} = \begin{cases}
c_m/2 + d_m & \text{if } a_{m} = 2^{m} \\
c_m/2 & \text {if } a_{m} = 0
\end{cases} \\
d_{m-1} = a_{m-1}^2 = (2^{m-1})^2 = \frac{d_m}{4}$
當 $c_{-1} = P_02^0 = P_0 = N$ 此時 c 就是要求的平方根。
```c
unsigned long i_sqrt(unsigned long x)
{
unsigned long b, m, y = 0;
if (x <= 1)
return x;
m = 1UL << (fls(x) & ~1UL);
while (m) {
b = y + m; // Y_m = c_m + d_m
y >>= 1; // c_m / 2
if (x >= b) { // X_m >= Y_m
x -= b; // X_{m-1} = X_m - Y_m
y += m; // c_{m-1} = c_m + d_m
}
m >>= 2; // dm / 4
}
return y;
}
```
將程式中的 `b` 看成上述的 $Y_m$,`m` 看成上述的 $d_m$,`x` 看成上述的 $X_m$,`y` 看成上述的 $c_m$,程式就是在迴圈不斷地更新 $c_m$ 和 $d_m$。
因此 `XXX` = `y >>= 1`, `YYY` = `x -= b`, `ZZZ` = `m >>= 2`