contributed by < Risheng1128
>
1
延伸問題:
GENMASK
巨集的實作,闡述其額外的考量GENMASK
巨集和 include/linux/bitfield.h 的應用案例討論以下的程式碼,目的為產生一組從 l
bit 到 h
bit 為 1
的 mask
這邊題目考慮 64
位元的系統,最直觀的方式就是將 0xFFFFFFFFFFFFFFFFUL
分別右移和左移後進行 AND
運算,即可得到 mask ,以下採用題目 8
位元的範例作簡單解釋
從以上範例可以推論出右移的部份為 7 - 6 = 1
, 左移的部份為 4
,把結果套成 64
位元的系統,可以得到解答
LEFT = 63 - h
RIGHT = l
不過這邊有個問題沒有釐清,還不確定為什麼題目要 >> l
GENMASK
在 github 中搜尋 GENMASK
後發現有兩處定義,分別位於 include/linux/bits.h
和 tools/power/x86/intel-speed-select/isst.h
include/linux/bits.h
以下為 GENMASK
實作原始碼
這邊的巨集函式 GENMASK
由兩個不同的巨集函式 GENMASK_INPUT_CHECK
及 __GENMASK
所組成,首先討論 GENMASK_INPUT_CHECK
的部份,由先前的 commit 可以得知 GENMASK_INPUT_CHECK
的功能是為了保持輸入參數的正確性,也就是確保 first argument 為 high bit 且 second argument 為 low bit ,同時如果在編譯時期就得知兩個 arguments 的數值,則 high bit 要大於 low bit
GENMASK() and GENMASK_ULL() are supposed to be called with the high bit as the first argument and the low bit as the second argument. Mixing them will return a mask with zero bits set.
To prevent such mistakes from appearing again, add compile time sanity checking to the arguments of GENMASK() and GENMASK_ULL(). If both arguments are known at compile time, and the low bit is higher than the high bit, break the build to detect the mistake immediately.
由上述的原始碼可以清楚得知 GENMASK_INPUT_CHECK
有兩種實作,由巨集 __ASSEMBLY__
決定要使用哪一種 (line 6
) ,根據 include/uapi/linux/const.h
裡面的註解及原始碼可以清楚得知巨集 __ASSEMBLY__
是用來區分常數巨集在 assembler 和 C code 的實作
因此我們可以得知 GENMASK_INPUT_CHECK
會根據使用在 assembler 或是 C code 有不同的實作
GENMASK_INPUT_CHECK
為 0
GENMASK_INPUT_CHECK
為 (BUILD_BUG_ON_ZERO(__builtin_choose_expr(__is_constexpr((l) > (h)), (l) > (h), 0)))
接著可以討論在 C code 的情況,從巨集 BUILD_BUG_ON_ZERO
開始,從 include/linux/build_bug.h 可以找到原始碼
參考 sparse type checking ,可以清楚知道巨集 __CHECKER__
是要判斷程式的語義 (semantics) ,接著分析 BUILD_BUG_ON_ZERO(e)
,從上述註解可以得知當 BUILD_BUG_ON_ZERO(e)
的條件 e
為 true
時,會報出編譯器錯誤
接著參考 bit-field 得到上述原始碼的詳細分析,首先分析 (-!!(e))
!!(e)
: 對 e
做兩次 NOT
,確保結果一定是 0
或 1
-!!(e)
: 對 !!(e)
乘上 -1
,因此結果會是 0
或 -1
可以得到 -!!(e)
的結果一定是 0
或 -1
,即當 e
不合法時 -!!(e)
為 -1
,反之 e
合法時 -!!(e)
為 0
,搭配上前面的 struct { int:
可以得到以下結論
e
為合法時,宣告一個 structure 中有一個 int
,其中有 0 bit 的 bit-fielde
為不合法時,宣告一個 structure 中有一個 int
,其中有 -1 bit 的 bit-field參考 C99 標準,可以得知 bit-field 的 width
不能為負數,因此會產生編譯器錯誤,且沒有變數名稱的 bit-field (稱為 unnamed bit-field
) 其 width
可以為 0
,且不會使用到任何空間
6.7.2.1
The expression that specifies the width of a bit-field shall be an integer constant expression with a nonnegative value that does not exceed the width of an object of the type that would be specified were the colon and expression omitted. If the value is zero, the declaration shall have no declarator.
A bit-field declaration with no declarator, but only a colon and a width, indicates an unnamed bit-field. As a special case, a bit-field structure member with a width of 0 indicates that no further bit-field is to be packed into the unit in which the previous bit-field, if any, was placed.
接著分析 __builtin_choose_expr
, __builtin_choose_expr
為 GNU extension ,可以參考 Other Built-in Functions Provided by GCC ,以下為 prototype 以及說明
type __builtin_choose_expr (const_exp, exp1, exp2)
You can use the built-in function
__builtin_choose_expr
to evaluate code depending on the value of a constant expression. This built-in function returnsexp1
ifconst_exp
, which is an integer constant expression, is nonzero. Otherwise it returnsexp2
.
從以上的說明可以得知 __builtin_choose_expr
的目的是用來判斷 const_exp
是否為非 0
的 constant expression ,如果是則回傳 exp1
,不是則回傳 exp2
,至於什麼是 constant expression ? 可以參考 C99 標準,如下所示
6.6.6
A constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be.
最後分析巨集函式 __is_constexpr()
,可以在 include/linux/const.h
中找到,以下為原始碼
首先根據以前的 commit 可以得知使用 __is_constexpr(x)
的原因
However, it turns out that
__builtin_constant_p()
does not always return a compile time constant [0]. It was thought this problem was fixed withgcc 4.9
[1], but apparently this is not the case [2].Switch to use
__is_constexpr()
instead which always returns a compile time constant, regardless of its inputs.
可以得知原本使用 __builtin_constant_p()
,由於其不總是回傳 compile time constant ,才會換成巨集函式 __is_constexpr()
接著可以開始分析 __is_constexpr()
的實作,這邊參考 Linux Kernel's __is_constexpr
Macro 的解說
考慮 ((void *)((long)(x) * 0l))
,首先 (long)(x)
是為了避免 x
可能為指標型態的情況,接著參考以下 C99 規格,定義了 integer constant expression 以及 null pointer constant ,並考慮兩種情況
6.3.2.3 Pointers
An integer constant expression with the value 0
, or such an expression cast to type void *
, is called a null pointer constant.
6.6 Constant expressions
An integer constant expression shall have integer type and shall only have operands that are integer constants, enumeration constants, character constants, sizeof
expressions whose results are integer constants, and floating constants that are the immediate operands of casts. Cast operators in an integer constant expression shall only convert arithmetic types to integer types, except as part of an operand to the sizeof
operator.
x
是 integer constant expression ,根據上述 C99 規格 6.3.2.3
小節,可以得知 ((long)(x) * 0l)
為值為 0
的 integer constant expression ,且 (void *)((long)(x) * 0l)
為 null pointer constantx
不是 integer constant expression ,則 (void *)((long)(x) * 0l)
就不是 null pointer constant得知了這樣的差異,可以繼續考慮 8 ? ((void *)((long)(x) * 0l)) : (int *)8
,這邊的第一個 8
可以視為永遠都是 true
,因此都會執行 expression
,接著參考 C99 標準,我們可以得知為什麼這邊要使用三元運算子,且 conditional-expression
為何是 (int *)8
6.5.15 Conditional operator
… if one operand is a null pointer constant, the result has the type of the other operand; otherwise, one operand is a pointer to void
or a qualified version of void
, in which case the result type is a pointer to an appropriately qualified version of void
void *
或是 void
的 qualified versions (含有 const
或 volatile
等等的 qualifiers) ,則結果會回傳適當的 void
的 qualified version而 (int *)8
中的 8
是為了要對齊地址,不要讓編譯器產生 warning ,最後可以得到兩個最終結論
x
是 integer constant expression ,則 sizeof(int) == sizeof(*((int *) (NULL)))
成立,回傳 1
x
不是 integer constant expression ,則 sizeof(int) == sizeof(*((void *)(....)))
不成立,回傳 0
最後總結 GENMASK_INPUT_CHECK
的實作
(l) > (h)
是 integer constant expression , __is_constexpr
回傳 1
,且 __builtin_choose_expr
回傳 (l) > (h)
,這時有兩種情況
(l) > (h)
成立,則 BUILD_BUG_ON_ZERO
的條件為 true
,會發起編譯器錯誤(l) > (h)
不成立,則 BUILD_BUG_ON_ZERO
的條件為 false
,因此編譯通過且回傳 0
(l) > (h)
不是 integer constant expression , __is_constexpr
回傳 0
,且 __builtin_choose_expr
回傳 0
,此時 BUILD_BUG_ON_ZERO
的條件為 false
,因此編譯通過且回傳 0
接著分析 __GENMASK
的部份,以下為實作程式碼
和小考的實作相似,由於 GENMASK_INPUT_CHECK
通過編譯會回傳 0
,因此不會影響到 __GENMASK
的結果,所以原始碼才將兩者的結果加起來
tools/power/x86/intel-speed-select/isst.h
以下為 GENMASK
實作原始碼
這邊的實作和我們題目的實作幾乎一樣,只差在這邊的 GENMASK
會根據 long
的大小改變 MSB 的位置
2
延伸問題:
考慮以下程式碼
首先,根據題目敘述
填入一個 fd 結構體,並確保第一個成員的地址得以對 4 個位元組進行向下對齊
可以得知 fd 的第一個成員地址必須向下對齊 4 個位元組,因此可以使用以下的方式,如此一來可以將成員地址的最低 2 位元都清為 0 ,即為 4 的倍數
接著觀察結構 fd
的宣告,可以看到抑一個成員的型態為 struct foo *
只要將 v & -3
轉型成 struct foo *
即可,因此最後的答案為
EXP1 = (struct foo *) (v & -3)
3
延伸問題:
這題有點難想,參考了 課前測驗參考解答: Q1 裡提到 32-bit 的位元反轉實作,搭配程式碼測試寫出了以下程式
將上述程式碼 line 3 (x << 2) & 0xCC)
和 line 4
((x << 1) & 0xAA)
的分別改寫則可得到表單答案 (原本是先 <<
再 &
改成先 &
再 <<
)
EXP1
=(x & 0x33) << 2
EXP2
=(x & 0x55) << 1
接著可以開始討論到底做了什麼,這邊使用 x = 0x87
做測試,一共有三個步驟
計算過程
從結果可以看出第一個步驟是將低 4 位元和高 4 位元互換位置
計算過程
將 8-bit 看成兩個部份 (4, 4)
(x & 0xCC) >> 2
: 分別將兩部份中的高 2 位元取出,並且移動到低 2 位元的位置
(x & 0x33) << 2
: 分別將兩部份中的低 2 位元取出,並且移動到高 2 位元的位置
結論: 分別將兩部份的高 2 位元和低 2 位元交換
計算過程
將 8-bit 看成兩個部份 (4, 4)
(x & 0xAA) >> 1
: 分別將兩部份中的奇數位元取出,並且移動到偶數位元的位置
(x & 0x55) << 1
: 分別將兩部份中的偶數位元取出,並且移動到奇數位元的位置
結論: 分別將兩部份的奇數和偶數位元交換
由以上分析可以發現,主要可以看成先切一半接著交換位置,再對一半的一半做一樣的事,直到不能再分割為止
搜尋 reverse bit
後,在 drivers/input/joystick/psxpad-spi.c
發現相似的巨集
To Do: 理解該程式碼功能
4
延伸問題:
考慮以下程式碼,可以看到這兩個巨集函式主要是使用 for
迴圈進行迭代
為了可以更清楚看到巨集展開的樣貌,這邊針對測試觀察
首先 foreach_int
的部份,巨集展開如下所示
展開後就很明顯 EXP4
主要是用來控制下一個資料,因此可以推論 EXP4 = ++_foreach_i
接著換 foreach_ptr
的部份,巨集展開如下所示
和 EXP4
相同, EXP5
也是用來控制下一個資料,因此我們可以得知 EXP5 = ++_foreach_i
5
延伸問題:
在解釋程式之前,先將 EXP6
和 EXP7
解出來,參考以下程式碼
首先 EXP6
的部份 (位於上述程式的 line 2
) 。先參考 line 7
的程式碼,對應之後可以推論 EXP6 = dvs << shift
,後續會解釋程式邏輯
接著換 EXP7
(位於上述程式的 line 10
) 。首先發現如果沒有 EXP7
,則 while
會變成無窮迴圈,因此可以推論 EXP7
是一個 dvd
變小或是 dvs
變大的運算,接著看了上述的程式碼後,可以得知 dvd
要減去 dvs << shift
,因此 EXP7 = dvd -= dvs << shift
舉個簡單的例子,假設 dividend = 255
且 divisor = 3
首先討論上述程式碼 line 32
的位置,以下展示迴圈的執行
接著討論 line 36
的 while
所執行的情況,以下為執行步驟
最後因為 line 50
回傳 res * signal
,即 0b01010101
6
延伸問題:
考慮以下程式碼
首先討論 EXP8
的部份,可以得知函式 can_insert()
是用來判斷節點是否可以加到 hash table 上
ToDo: 理解程式碼中…
7
延伸問題:
考慮以下程式碼
觀察程式碼後,從上述程式碼的 line 4 7 10
可以找到規律, v 根據條件分別右移 16
8
及 4
因此可以推論 EXP10
為右移 2
的情況,故 EXP10 = (v > 0x3U) << 1
EXP11
的部份就有點難想,我是測試才發現如果沒有 EXP11
的話回傳值在某些測資會小於正確答案 1
,因此這格的答案算是 try and error
出來的,EXP11 = ret += v > 1
,後續會講解
根據上述程式碼,舉個簡單的例子,假設 v = 0x7FFFFF
稍微研究 line 53
的程式碼 ret += v > 1
可以得知是為了要找 v
的 MSB 是否會在第 3 7 11 15 ... 31
的位置,如果有的話必須再加 1
8
延伸問題:
9
延伸問題:
ROUND_DOWN
巨集這邊參考別人的 Github ,比對後可以得到答案 MMM = 1u
且 NNN = MAX_ALIGNMENT - 1u
根據以下程式碼,可以得知主要邏輯就是將 x
的低 4
位元分別加上 1
,即 0b1111
,接著將低 4
位元的部份利用 &
清掉
以下提供一些測試結果
ROUND_DOWN
理解向上對齊後,向下對齊就沒有那麼的難想了,兩者的差別只在於向上對齊需要進位後再清除,而向下對齊可以直接清除即可。以下為實作程式碼
以下提供一些測試結果
10
延伸問題:
考慮以下程式碼
其中最重要的部份在於 line 5 6
的判斷式,以下提供例子判斷 (divisor 不為負數)
情況 1
(x
屬於 unsigned
或 divisor
屬於 unsigned
)
RRR
的程式碼情況 2
(x
和 divisor
屬於 signed
)
x
和 divisor
同號( x
和 divisor
皆為正),執行 RRR
的程式碼x
和 divisor
異號( x
為負 divisor
為正),執行 MMM
的程式碼從上述的兩種情況可以得到一個結論
x
為正時執行 RRR
程式碼;為負時執行 MMM
程式碼
因此得到答案
不過這邊有找到一個有問題的情況,如果 x
為負且 divisor
屬於 unsigned
,使用以下程式測試
產生以下結果
11
延伸問題:
考慮以下程式碼,目的是要計算整數開平方
在解題之前可以先討論實作演算法,參考 Digit-by-digit calculation ,
To Do: 理解演算法中