Aco*_*orn 11 c macros linux-kernel language-lawyer
__is_constexpr(x)Linux内核的宏如何工作?它的目的是什么?什么时候介绍?为什么要介绍?
/*
* This returns a constant expression while determining if an argument is
* a constant expression, most importantly without evaluating the argument.
* Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
*/
#define __is_constexpr(x) \
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
Run Code Online (Sandbox Code Playgroud)
有关解决相同问题的不同方法的讨论,请参阅:检测宏中的整数常量表达式
Aco*_*orn 16
__is_constexpr宏该__is_constexpr(x)宏可以在Linux内核的include/kernel/kernel.h中找到:
/*
* This returns a constant expression while determining if an argument is
* a constant expression, most importantly without evaluating the argument.
* Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
*/
#define __is_constexpr(x) \
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
Run Code Online (Sandbox Code Playgroud)
它是在Linux Kernel v4.17的合并窗口中引入的,在2018-04-05 提交3c8ba0d61d04 ; 虽然围绕它的讨论开始于一个月前.
该宏值得注意的是利用C标准的细微细节:条件运算符确定其返回类型的规则(6.5.15.6)和空指针常量的定义(6.3.2.3.3).
此外,它依赖于sizeof(void)允许(和不同于sizeof(int)),这是GNU C扩展.
宏的身体是:
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
Run Code Online (Sandbox Code Playgroud)
让我们关注这一部分:
((void *)((long)(x) * 0l))
Run Code Online (Sandbox Code Playgroud)
注意:(long)(x)强制转换旨在允许x具有指针类型并避免u64在32位平台上对类型发出警告.但是,这个细节对于理解宏的关键点并不重要.
如果x 是一个整数常量表达式(6.6.6),那么它遵循((long)(x) * 0l)是一个整数常量表达式的值0.因此,(void *)((long)(x) * 0l)是一个空指针常量(6.3.2.3.3):
值为0的整型常量表达式或类型为void*的表达式称为空指针常量
如果x是不一个整数常量表达式,则(void *)((long)(x) * 0l)不是一个空指针常数,不管其值的.
知道了,我们可以看到之后会发生什么:
8 ? ((void *)((long)(x) * 0l)) : (int *)8
Run Code Online (Sandbox Code Playgroud)
注意:第二个8文字旨在避免编译器警告有关创建指向未对齐地址的指针.第一个8文字可能只是1.但是,这些细节对于理解宏的关键点并不重要.
这里的关键是条件运算符返回一个不同的类型,具体取决于其中一个操作数是否为空指针常量(6.5.15.6):
[...]如果一个操作数是空指针常量,则结果具有另一个操作数的类型; 否则,一个操作数是一个指向无效或有资格版本的空隙,在这种情况下,结果类型是一个指向的一个适当资格版本空隙.
所以,如果x 是一个整数常量表达式,则第二操作数是空指针常量,因此表达式的类型在第三操作数,它是一个指向的类型int.
否则,第二个操作数是指针void,因此表达式的类型是指针void.
因此,我们最终有两种可能性:
sizeof(int) == sizeof(*((int *) (NULL))) // if `x` was an integer constant expression
sizeof(int) == sizeof(*((void *)(....))) // otherwise
Run Code Online (Sandbox Code Playgroud)
根据GNU C扩展,sizeof(void) == 1.因此,如果x是一个整数常量表达式,则宏的结果是1; 否则,0.
而且,由于我们只是比较两个sizeof表达式的相等性,结果本身就是另一个整数常量表达式(6.6.3,6.6.6):
常量表达式不应包含赋值,递增,递减,函数调用或逗号运算符,除非它们包含在未评估的子表达式中.
一个整数常量表达式应具有整数型和应仅具有是积分常数,枚举常数,字符常数,操作数的sizeof表达式,其结果是积分常数,和浮动常量是铸件的立即操作数.整数常量表达式中的转换运算符只能将算术类型转换为整数类型,除非作为sizeof运算符的操作数的一部分.
因此,总之,在__is_constexpr(x)宏返回一个整数常量表达式的值1,如果参数是一个整数常量表达式.否则,它返回一个整数常量表达式的值0.
在从Linux内核中删除所有可变长度数组(VLA)的过程中,宏出现了.
为了促进这一点,需要在内核范围内启用GCC的-Wvla警告 ; 以便编译器标记所有VLA实例.
当启用警告时,结果发现GCC报告了许多阵列是VLA的情况,而这些情况并非如此.例如在fs/btrfs/tree-checker.c中:
#define BTRFS_NAME_LEN 255
#define XATTR_NAME_MAX 255
char namebuf[max(BTRFS_NAME_LEN, XATTR_NAME_MAX)];
Run Code Online (Sandbox Code Playgroud)
开发人员可能期望max(BTRFS_NAME_LEN, XATTR_NAME_MAX)已解决255,因此应将其视为标准数组(即非VLA).但是,这取决于max(x, y)宏扩展到什么.
关键问题是,如果数组的大小不是C标准定义的(整数)常量表达式,GCC会生成VLA代码.例如:
#define not_really_constexpr ((void)0, 100)
int a[not_really_constexpr];
Run Code Online (Sandbox Code Playgroud)
根据C90的标准,((void)0, 100)是不一个常量表达式(6.6),由于逗号操作者正在使用(6.6.3).在这种情况下,GCC选择发出VLA代码,即使它知道大小是编译时常量.相比之下,Clang没有.
由于max(x, y)内核中的宏不是常量表达式,因此GCC触发了警告并生成了内核开发人员不想要的VLA代码.
因此,一些内核开发人员试图开发max其他宏的替代版本以避免警告和VLA代码.一些尝试试图利用GCC的__builtin_constant_p内置,但没有一种方法适用于当时内核支持的所有GCC版本(gcc >= 4.4).
在某些时候,Martin Uecker 提出了一种特别聪明的方法,它不使用内置函数(从glibc的tgmath.h中获取灵感):
#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
Run Code Online (Sandbox Code Playgroud)
虽然该方法使用了GCC扩展,但它仍然很受欢迎,并被用作__is_constexpr(x)宏之后的关键思想,它在与其他开发人员进行几次迭代后出现在内核中.然后使用该宏来实现max宏和其他宏,这些宏需要是常量表达式,以避免GCC生成VLA代码.