contributed by < freshLiver
>
GENMASK
(LEFT, RIGHT)這段程式碼是利用對 ~0UL
(64 個 1)進行位移來將重疊部份(第 l
到 h
位元)以外的部份皆設為 0,而需要設為 0 的區段有:
63 - h
個位元:因此 LEFT 應為 (63 - h)
。l
個位元:由於已經先右移 l
位元了,所以要再左移 l
位元才能使最低 l
位元皆為 0,因此 RIGHT 應為 l
。GENMASK
巨集的實作,闡述其額外的考量在看 Linux 核心中 GENMASK
的實作之前,要先來了解相關巨集以及 GNU C Extensions 的用途以及原理:
UL
UL
是一個定義在 include/vdso/const.h 中的巨集,展開後會變成 (((x##Y)))
,而其中的 ##
token concatenation,可以在前處理階段時將 x
、y
兩個 token 合併,因此這個巨集展該後相當於在 x
後加上一個 UL
後綴。
接著根據 n1570 的 6.4.4.1 Integer constants 可以知道 U
以及 L
分別代表了 unsigned-suffix 以及 long-suffix。而在 6.4.4.1 的第 2 項則說明了這個後綴是用來指定一整數常數的型別:
- An integer constant begins with a digit, but has no period or exponent part. It may have a prefix that specifies its base and a suffix that specifies its type.
因此可以知道這個巨集是用來表示整數常數 x
為 64 位元無號整數(LP64)。
BUILD_BUG_ON_ZERO
TODO : __CHECKER__
在哪被定義
這是一個定義在 include/linux/build_bug.h 的巨集,可以看到當 __CHECKER__
有被定義時,這個巨集會展開成:
這邊使用了 Bit-Fields 的技巧,
- 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.
而根據 6.5.3.3 Unary arithmetic operators 的第 5 項中則有明確規範一元運算子 !
的結果只會是 0
或 1
:
- The result of the logical negation operator ! is 0 if the value of its operand compares unequal to 0, 1 if the value of its operand compares equal to 0. The result has type int. The expression !E is equivalent to (0==E).
因此當 e
為非 0 時,這個 bit-field 的 width 會是 -1
,但由於 bit-field 的 width 必須為非負整數,所以會產生錯誤造成編譯失敗;相對的,當 __CHECKER__
沒定義或是 e
為 0 時則不會出現錯誤,而是會展開成常數 0 或是一個回傳 0 的表示式。
在 bit-field 一文中則有更詳細的說明。
__builtin_choose_expr(const_exp, expr1, expr2)
__builtin_choose_expr
是 GCC 的內建函式,根據文件中對這個函式的說明:
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 returns exp1 if const_exp, which is an integer constant expression, is nonzero. Otherwise it returns exp2.
This built-in function is analogous to the ‘? :’ operator in C, except that the expression returned has its type unaltered by promotion rules. Also, the built-in function does not evaluate the expression that is not chosen. For example, if const_exp evaluates to true, exp2 is not evaluated even if it has side effects.
This built-in function can return an lvalue if the chosen argument is an lvalue.
If exp1 is returned, the return type is the same as exp1’s type. Similarly, if exp2 is returned, its return type is the same as exp2.
可以知道,這個內建函式跟三元運算子 ?:;
相似,都是根據條件是否成立來決定要進行的操作,但 __builtin_choose_expr
這個函式要求條件 const_expr
必須為常數,否則會造成編譯失敗。
TODO : 與 ?:;
的用途、優缺點比較
__is_constexpr
這個巨集被定義在 include/linux/const.h 中,可以用來檢查 x
是否能夠在編譯時期被視為常數。
TODO : 原理
若是 x
能夠在編譯階段被視為常數的話,這個巨集就會回傳 1
,否則則會回傳 0。
在 include/linux/bits.h 中有定義 GENMASK
相關的巨集:
而 GENMASK
又會根據 __ASSEMBLY__
的定義與否展開成不同的內容:
可以看到若是 __ASSEMBLY__
有被定義的話,就會展開成
而從前面的說明可以知道這段可以應該 2 種情況討論:
(l) > (h)
能夠在編譯時期被確認是成立:
__is_constexpr
在編譯時期被相當於 1__builtin_choose_expr
會選擇 (l) > (h)
的表示式(l) > (h)
成立,所以 BUILD_BUG_ON_ZERO
會產生錯誤、造成編譯失敗(l) > (h)
在編譯時期被確認是不成立,或無法確認是否成立:
__is_constexpr
在編譯時期被相當於 0__builtin_choose_expr
會選擇 0
的表示式0
代表不成立,所以 BUILD_BUG_ON_ZERO
不會產生錯誤因此可以知道這段複雜的巨集用意是在避免使用常數作為 GENMASK
巨集的參數時誤將傳入數值比 h
大的 l
。
在 GCC 內建函式中有類似的函式 __builtin_constant_p(exp)
能夠作到與 __is_constexpr
相似的檢查,而原先的 GENMASK
巨集也是依賴這個內建函式進行檢查,但在 2018 年 3 月時被反應 __builtin_constant_p
在編譯時可能無法在正確的時間點檢查 exp
,因此會造成 __builtin_choose_expr
的 const_expr
部份被認為是非常數而導致編譯失敗。直到在 2021 年 3 月時才改用 __is_constexpr
來取代 __builtin_constant_p
進行判斷。
而在檢查完 h
、l
之後(或因 __ASSEMBLY__
未定義而不進行檢查),就會接著產生 mask 的部份:
因為 1
與 l
的字型非常相似,所以為了愛護眼睛,我將巨集的 h
以及 l
改用大寫字母 H
以及 L
替代。
可以看到這個巨集一樣是對 64 位元的無號整數 0 取一補數來產生 64 個 1 進行位移,而這邊將這個巨集分成兩部份討論:
((~UL(0)) - (UL(1) << (L)) + 1)
的部份:
(~UL(0))
:產生 64 個 1- (UL(1) << (L))
:將第 L
個位元值設成 0+ 1
:使用 + 1
進位的特性,將最低位的 0(也就是第 L
個位元)設成 1,而其它更低的位元(即第 0
至 L-1
位元)則會因進位被設成 0L-1
~ 0 位元皆為 0 的 mask(~UL(0) >> (BITS_PER_LONG - 1 - (H)))
的部份:
(~UL(0)
:產生 64 個 1(BITS_PER_LONG - 1 - (H))
:由於位元的編號是從 0 開始,所以最高位應為 63(即 BITS_PER_LONG - 1
),而 - (H)
則是找出應要右移的位數>> (BITS_PER_LONG - 1 - (H))
:右移讓最高的 63 - (H)
個位元為 063
~ H+1
位元皆為 0 的 mask最後再將兩個 mask 進行 AND 運算即可產生第 H
~ L
位元皆為 1 的 mask。
ALIGN_DOWN
(EXP1)這段程式碼是要將一地址 v
看作是結構體 foo
的起始位置,若地址 v
沒有以 4 位元組進行對齊的話,則需要將地址向下對齊,因此 EXP1 要先捨去地址的後 2 個位元,然後再轉型成結構體 foo
的指標,即 EXP1 應為 (struct foo *)(v & ~3)
。
在第九題的延伸問題中一併討論。
將 個位元以不同單位大小分割成偶數段,每次將奇數部份以及偶數部份透過位移以及 bitwise-or 互換,而因為 0xCC
與 0xAA
分別代表 0b11001100
與 0x10101010
,即分別為單位大小為 2 位元及 1 位元的偶數段部份,所以 EXP2 與 EXP3 應為對應單位大小的奇數段部份。因此 EXP2 應為 0x33
(0b00110011
),而 EXP3 則應為 0x55
(0b01010101
)
在 include/linux/bitrev.h 中包含了 1, 2, 4 位元組反轉的巨集實作,可以看到大致上分成 3 種實作方式:
bitwise 反轉(常數)
當要反轉的值屬於常數時,則會直接使用與測驗題相似的方式,應該是為了讓編譯器能夠將常數運算直接最佳化掉。
查表(非常數且無硬體指令)
在 lib/bitrev.c 中有透過陣列 byte_rev_table
列舉了 8 位元對應的反轉結果,因此當 CONFIG_HAVE_ARCH_BITREVERSE
沒有被定義時,可以不用以 4, 2, 1 位元作為單位大小進行反轉,直接透過查表找到該位元組對應的反轉結果即可。
反轉指令(非常數但有硬體指令)
在某些架構的指令集中可能有能夠反轉位元的指令,像是 ARM 的 RBIT
指令 就能夠直接反轉一個 32 位元的資料。因此在 CONFIG_HAVE_ARCH_BITREVERSE
有被定義時就能夠直接使用指令進行更有效率的反轉操作。
兩個 for each
巨集的實作概念都差不多,使用了 Variadic Macro 進行陣列宣告,並透過 for
迴圈以及 ,
運算子:
在規格書中的 6.5.17 Comma operator 中關於
,
這個運算子的說明:
- Syntax :
- The left operand of a comma operator is evaluated as a void expression; there is a sequence point between its evaluation and that of the right operand. Then the right operand is evaluated; the result has its type and value.
- EXAMPLE As indicated by the syntax, the comma operator (as described in this subclause) cannot appear in contexts where a comma is used to separate items in a list (such as arguments to functions or lists of initializers). On the other hand, it can be used within a parenthesized expression or within the second expression of a conditional operator in such contexts. In the function call
f(a, (t=3, t+2), c)
the function has three arguments, the second of which has the value 5.
因此在 for
迴圈的初始化部份會先執行 ,
左側,也就是先將陣列的第一個元素賦值給變數 i
,最後才會將 ,
右側的 0
,也就是陣列的初始 index 賦值給 _foreach_i
。
除了初始化用來儲存陣列的變數 i
以及陣列 index 的變數 _foreach_i
之外,每次迴圈結束還需要增加 _foreach_i
並將 i
移動到下一個元素,因此 _foreach_i
可以直接透過 ++_foreach_i
或 _foreach_i++
增加,但 i
由於並不是指標,所以必須重新初始化陣列,並將下一個元素透過 _foreach_i
賦值給 i
,因此 EXP4 與 EXP5 皆應為 ++_foreach_i
才能在每次迭代結束時將正確的元素重新賦值給 i
。
透過 $ grep -rP --include="*.[ch]" "\\[\\].*__VA_ARGS__.*"
尋找相關用法大多是用來初始化陣列或是檢查巨集參數的數量,其中與這題比較相近的是:
除了透過單純宣告成一個陣列之外,還能透過指定 index 來取得目標參數。
無乘法、無迴圈以外的 branch
ilog32
以二元搜尋的概念找出最高位元的 1 的位置,從中間開始,每次都將檢查範圍縮小一半,當最高位 1 位於高位元部份時,則可以右移掉低位元部份,讓兩種狀況能以相同的形式繼續進行檢查。若是以這個邏輯實作的話,只要重複三行程式碼:
可以簡單的知道 EXP10 應為 (v > 0x3U) << 1
,即用來檢查最高位元 1 是否在第 2 至 31 位元。
但這樣會因為每次都分成兩長度不為 0 的部份,所以最多只能檢查 31 個位元,造成當 v == 1
時會產生錯誤結果,因此最後還需要檢查 v
是否在第 0 個位元,即 v
是否為 0:
這邊要注意的是,由於前面都是透過 |
設定 ret
的第 0 至第 5 位元來紀錄所需位元數,因此最後需要改用 +
,才不會在某些情況(第 23 行條件成立時)產生錯誤結果。
在了解檢查機制之後,需要注意到在原程式碼的一開始宣告 int ret = v > 0;
時就已經先檢查 v > 0
的情況了,所以需要改用 +
的是第 23 至 25 行的部份:
而這邊又可以在進行簡化(左移 0
可以忽略、v
不須再右移、m
與 ret
的操作可以合併):
應此 EXP11 最精簡的表示法應為 ret += v > 1
。
研讀論文《Using de Bruijn Sequences to Index a 1 in a Computer Word》
運用〈你所不知道的 C 語言:前置處理器應用篇〉和〈你所不知道的 C 語言:數值系統篇〉提及的技巧
使用 indirect pointer 改寫簡化:
與實作四捨五入的概念(加上 0.5
再捨去小數點部份)有點相似,直接對地址加上對齊大小(這段程式碼為 16 位元組對齊),接著再捨去地址的最低 4 位元,因此 ~(NNN)
應為除最低 4 位元皆為 1 的數,即 NNN 應為 MAX_ALIGNMENT - 1
(15)。
但是直接加上對齊大小會讓已對齊的地址也被向上對齊,因此要先讓原地址 x
減 1 才能確保已對齊的地址向上對齊後保持不變,所以 MMM 應為 1。
跟第二題的概念相同,直接捨棄地址最後的 個位元。
md raid metadata
在 insmod
時,會使用到定義在 include/linux/module_decompress.c
中的 module_decompress
,而其中又會使用到 DIV_ROUND_UP
來確保核心模組解壓縮後仍有足夠空間能儲存資訊。