--- tags: Linux --- Linux 巨集 __is_constexpr 探索 === > 參考資料 > [[PATCH] linux/const.h: Explain how __is_constexpr() works](https://lore.kernel.org/linux-hardening/20220131204357.1133674-1-keescook@chromium.org/) > [Linux Kernel's __is_constexpr macro](https://stackoverflow.com/a/49481218/16257547) > [N1256](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) ## 介紹 在 C90 裡是沒有 Variable Length Array (VLA) 的,所以要配置一個陣列必須使用 constant expression 表示陣列大小,若當擴充使用,也還是有[危險](https://lkml.org/lkml/2018/3/7/621)的。為了能在編譯時間裡檢查是否為 constant expression ,於是 Martin Uecker 想到一個[方法](https://lkml.org/lkml/2018/3/20/805),也就是我們要探討的巨集 `__is_constexpr` ,其原理使用了 GNU C extension 、 conditional operator 的行為、 null pointer constant 的定義,了解後真的覺得這方法很漂亮。 ## Constant Expression 定義 那 constant expression 是什麼?根據 C99 是說,他是在編譯時期就可以被計算出值的一個表示。 > N1256 (6.6-2) > constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be. 然後有限制,其表示不可包含[指定敘述](https://en.wikipedia.org/wiki/Assignment_(computer_science))、增減值運算符、函數呼叫、[逗號運算符](https://en.wikipedia.org/wiki/Comma_operator),除了不需要計算的 subexpression(例如 `sizeof` )。 > N1256 (6.6-3) > Constant expressions shall not contain assignment, increment, decrement, function-call, or comma operators, except when they are contained within a subexpression that is not evaluated. Constant expression 也有不同種類,用來表示不同型態的常數,比方說 floating constant expression, integer constant expression, arithmetic constant expression 等。 ## `__is_constexpr` 運作原理 先來看看這個巨集長什麼樣子,可以看到有 `sizeof`, conditional operator, type casting ,最外層是兩邊的 size 比較,然後往裡面看,拿 $8$ 當 if-else 條件,那為何還需要 conditional operator 呢?接下來會分開解釋。 ```c #define __is_constexpr(x) \ (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8))) ``` ### Integer Constant Expression 這個巨集是輸入一個 `x` ,檢查 `x` 是否為一個 constant expression ,那有分歧應該也是從 `x` 開始,就從 `(void *)((long)(x) * 0l)` 開始解析。 從表面看最終究是變成一個 `(void *)` 型態的 0 ,有什麼差別?回到對 constant expression 的定義,他是可以在編譯時期就被計算出值的表示,所以重點不在他的值,而在他是不是一個 constant expression ,這也是 `__is_constexpr` 的最終目的,接下來做個實驗。 ```c // test_vla.c #define multiply_zero(x) ((long)(x) * 0l) int main(void) { const int a = 2; int arr1[multiply_zero(a)]; int arr2[multiply_zero(2)]; return 0; } ``` 打開編譯器的 `-Wvla` 來檢查是否有使用 VLA ,然後從結果來看,得知若 `x` 輸入的是一個變數 `a` ,他在編譯時期是無法被計算的,也就說 `multiply_zero(a)` 不是 constant expression 。相反的,下一行的 `multiply_zero(2)` 則是。 ```shell $ gcc -Wvla test_vla.c test_vla.c: In function ‘main’: test_vla.c:5:2: warning: ISO C90 forbids array ‘arr1’ whose size can’t be evaluated [-Wvla] 5 | int arr1[multiply_zero(a)]; | ^~~ ``` ::: spoiler 使用 `-Wvla` 問題例子 因為在 Linux kernel 裡有例外,舉 [fs/btrfs/tree-checker.c](https://github.com/torvalds/linux/blob/fb184c4af9b9f4563e7a126219389986a71d5b5b/fs/btrfs/tree-checker.c#L597) 為例,開發人員期望 `max(BTRFS_NAME_LEN, XATTR_NAME_MAX)` 可以被視為 $255$ ,其值也可以在編譯時期得出,不應該被視為 VLA ,但是依照 C 語言標準的定義,其中用了逗號運算符,外加要看 `max` 巨集是如何寫的,所以在使用 `-Wvla` 的情況下,會跳出警告。 ```c #define BTRFS_NAME_LEN 255 #define XATTR_NAME_MAX 255 char namebuf[max(BTRFS_NAME_LEN, XATTR_NAME_MAX)]; ``` 為了因應此情況,有的人往改良 `max` 的方向走,也有人是去找其他方法代替 `-Wvla` 。 ::: > N1256 (6.6-6) > 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` 為 constant expression ,則 `((long)(x) * 0l)` 會是 integer constant expression ,其值為 0 ,也被稱為 null pointer constant 。 > N1256 (6.3.2.3-3) > An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. 再來他會被轉成 `(void*)` 型態,該表示也是 null pointer constant ,這裡就告一段落了。 ### Conditional Operator 這裡是巨集的關鍵,從上一個解析裡知道 `(void *)((long)(x) * 0l)` 會根據 `x` 是否為 constant expression 而有所不同,這裡會繼續利用這個差異。 ```c 8 ? (void *)((long)(x) * 0l) : (int *)8 ``` > N1256 (6.5.15-6) > If both the second and third operands are pointers or one is a null pointer constant and the other is a pointer, the result type is a pointer to a type qualified with all the type qualifiers of the types pointed-to by both operands. Furthermore, if both operands are pointers to compatible types or to differently qualified versions of compatible types, the result type is a pointer to an appropriately qualified version of the composite type; __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. 從 C99 的定義,當 `(void *)((long)(x) * 0l)` 與 `(int *)8` 其中一個為 null pointer constant ,那麼這個 conditional operator 的比較結果會是 null pointer constant 另一邊指標的型態,也就會是 `(int*)` 型態。 整理一下,若 `x` 是 constant expression ,則 conditional operator 的結果會是 `(int*)` 的型態。若不是,則結果變為 `(void*)` 型態。 ### `Sizeof` 的差異 最後是 `sizeof` ,在這裡使用了 [GNU C extension](https://gcc.gnu.org/onlinedocs/gcc/Pointer-Arith.html) 的實作( feature ), `sizeof(void)` 回傳為 1 (在 C 語言標準 `sizeof(void)` 是無定義行為)。 :::spoiler C99 定義 > N1256 (6.3.2.2) > The (nonexistent) value of a void expression (an expression that has type void) shall not be used in any way, and implicit or explicit conversions (e xcept to void) shall not be applied to such an expression. If an expression of any other type is evaluated as a void expression, its value or designator is discarded. (A void expression is evaluated for its side effects.) > N1256 (6.5.3.4-1) > The sizeof operator shall not be applied to an expression that has function type or an incomplete type, to the parenthesized name of such a type, or to an expression that designates a bit-field member. ::: ```c #define __is_constexpr(x) \ (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8))) ``` 順著剛才的整理,當 `x` 為 constant expression ,則 conditional operator 的結果是 `(int*)` 型態,之後做了 dereference ,所以在右邊的 `sizeof` 裡是 `int` ,最後左右兩邊會相等得到 $1$ ,跟期望目標一致。再來換另一邊,若 `x` 不是 constant expression ,則 conditional operator 的結果變為 `(void *)` ,之後做 dereference ,所以右邊的 `sizeof` 裡會是 `void` ,其值為 $1$ ,最後左右兩邊不相等,得到 $0$ ,也跟期望目標一致。 ## 結語 `__is_constexpr` 探索就到這邊,其實還有些細節未提,像是為何使用 $8$ ,為何型態要轉成 `long` ,其實都跟記憶體對齊 alignment 和 CPU 架構有關。從過程中可以看到,所有行為幾乎都是符合 C99 標準的定義,也表示 C 語言標準的重要性與實用性。 順代一提,新的 Linux 版本 5.18 從原使用的標準 GNU89(C89) 換到 GNU11(C11) ,在新的標準中定義不少跟 atomic 有關的操作,對並行相關的程式將有不小的影響。 相關連結: [The Switch Has Been Made From C89 To C11/GNU11 With Linux 5.18](https://www.phoronix.com/scan.php?page=news_item&px=Linux-5.18-Does-C11) [Time to move to C11 atomics?](https://lwn.net/Articles/691128/)