替换列表和标识符列表都是将字符串 token 化以后的列表。区别在于标识符列表使用,作为不同参数之间的分割符。每一个参数都是一个 token 化的列表。在宏中空白符只起到分割 token 的作用,空白符的多少对于预处理器是没有意义的。
宏的一些奇技淫巧:https://gaomf.cn/2017/10/06/C_Macro/
以下是整理的一些linux kernel中的常见宏,由于不同体系架构,或者不同模块的宏定义不同,只挑选了其中容易看懂的宏作为记录,实现的功能大体一样。
Linux内核中do{...}while(0)意义:
"##"用于粘贴两个参数,"#"用于替换参数
条件为真,产生崩溃, 原理:未定义的异常 相对应的有 WARN_ON
!!(e) 对 e 的结果进行两次求非. 如果e为0,则结果为0; 如果 e 不为 0, 则结果为1。所以上述表达式的结果有两种:
检查表达式e是否为0为0编译通过且返回0;如果不为0,则编译不通过.
如果e为0,则该结构体拥有一个int型的数据域,并且规定它所占的位的个数为0。
如果e非0,结构体的int型数据域的位域将变为一个负数,产生语法的错误。
typeof获得x的变量类型,根据传入参数类型的不同,产生不同的行为,实现“编译时多态”。实际typeof是在预编译时处理,最后实际转化为数据类型被编译器处理。所以其中的表达式在运行时是不会被执行的,比如typeof(fun()),fun()函数是不会被执行的,typeof只是在编译时分析得到了fun()的返回值而已。typeof还有一些局限性,其中的变量是不能包含存储类说明符的,如static、extern这类都是不行的。
宏typecheck用于检查x是否为type类型,如果不是会抛出(warning: comparison of distinct pointer types lacks a cast),typecheck_fn用于检查函数function是否为type类型,不一致跑出(warning: initialization from incompatible pointer type)。
通过type进行隐式转换安全通过编译,否则会跑出warning
__UNIQUE_ID保证变量唯一
判断x是否为整数常量表达式
如果x是常量表达式,则(void )((long)(x) 0l)是一个空指针常量,就会使用第三个操作数即((int *)8)的类型。如果不是常量表达式,则会使用第二个操作数void类型。 所以会出现以下两种情况:
因为sizeof(void) = 1,所以如果x是整数常量表达式,则宏的结果为1,否则为0。 https://stackoverflow.com/questions/49481217/linux-kernels-is-constexpr-macro
描述:此函数为GNU扩展,用来判断两个类型是否相同,如果type_a与 type_b相同的话,就会返回1,否则的话,返回0。
同min 宏
返回一个能够整除y并且大于x,最接近x的值,向上取整,可用于地址的内存对齐
判断val是否在lo和hi的范围内,如果小于lo,返回lo,如果大于hi则返回hi,如果在lo和hi之间就返回val
取绝对值
利用typeof获取要交换变量的类型
根据一个结构体变量中的成员变量来获取整个结构体变量的指针。
把分支预测的信息提供给编译器,以降低因为指令跳转带来的分支下降
GCC的内建方法会判断 EXP == C 是否成立,成立则将if分支中的执行语句紧跟放在汇编跳转指令之后,否则将else分支中的执行语句紧跟汇编跳转指令之后。这样cache在预取数据时就可以将分支后的执行语句放在cache中,提高cache的命中率
http://www.169it.com/article/17243108930910839727.html
对齐是采用上对齐的方式,例如0x123以16对齐,结果是0x130,因为对齐常在分配内存时使用,所以分配的要比需要的大
获取未对齐的数据,主要是识别数据大小
编译器默认会对结构体采用字节对齐的方式,__packed关键字可以取消字节对齐,采用1字节对齐。 类似:
写入未对齐的数据
访问目标地址一次,先取得x的地址,然后把这个地址转换成一个指向这个地址类型的指针,然后再取得这个指针所指向的内容,达到了访问一次的目的。volatile表示不进行优化,强制访问一次。在一些并发的场景中对变量进行优化有可能导致错误,需要时刻得到变量的最新值,所以用volatile强制访问一次进行更新。 使用 ACCESS_ONCE() 的两个条件是:
https://blog.csdn.net/ganggexiongqi/article/details/24603363
CVE-2017-5123(waitid系统调用),检查指针是不是属于用户空间的,x86架构下ACCESS_OK宏的实现:
忙等待函数,在延迟过程中无法运行其他任务,会占用CPU时间,延迟时间是准确的。 msleep是休眠函数,它不涉及忙等待.用msleep(200)的时候实际上延迟的时间,大部分时候是要多于200ms,是个不定的时间值。
linux 内核中最常见的宏使用之一,系统调用
以open系统调用为例: SYSCALL_DEFINE 后面跟系统调用所带的参数个数n,第一个参数为系统调用的名字,然后接2*n个参数,每一对指明系统调用的参数类型及名字。
为什么要将系统调用定义成宏?CVE-2009-0029,CVE-2010-3301,Linux 2.6.28及以前版本的内核中,将系统调用中32位参数传入64位的寄存器时无法作符号扩展,可能导致系统崩溃或提权漏洞。 内核开发者通过将系统调用的所有输入参数都先转化成long类型(64位),再强制转化到相应的类型来规避这个漏洞。
time_after32(a, b)宏:返回true时,说明time a比b大,在后面
time_before32(b, a)宏:返回true时,说明time b在a前 只比较两个32位的数:
内存屏障,该语句不产生任何代码,但是执行后刷新寄存器对变量的分配。
执行该语句后cpu中的寄存器和cache中已缓存的数据将作废,重新读取内存中的数据。这就阻止了cpu将寄存器和cache中的数据用于去优化指令,而避免去访问内存。例如:
第三行中,GCC不会用存放b的寄存器给a赋值,而是invalidate b 的cache line,重新读取内存中的b值给a赋值。
另外的内存屏障宏定义:
内存对于缓存更新策略,要区分Write-Through和Write-Back两种策略。前者更新内容直接写内存并不同时更新Cache,但要置Cache失效,后者先更新Cache,随后异步更新内存。通常X86 CPU更新内存都使用Write-Back策略。
一些常量宏同时在汇编和C中使用,然而,我们不能像注释C的常量宏那样加一个“UL”或其他后缀。所以我们需要使用以下的宏解决这个问题。
例如调用:#define DEMO_MACRO _AT(1, UL):在C中会被解释为 #define DEMO_MACRO 1UL; 而在汇编中什么都不做,就是: #define DEMO_MACRO 1
判断是否支持大文件
linux 内核的一些错误码,以它们的负数来作为函数返回值,简单地使用大于等于-4095的虚拟地址来分别表示相应的错误码。在32位系统上,-4095转换成unsigned long类型的值为0xFFFFF001,也就是说地址区间[0xFFFFF001, 0xFFFFFFFF]被分别用来表示错误码从-4095到-1。
判断一个函数返回的指针到底是有效地址还是错误码
错误码与相应地址的互换
递归宏,颠倒字节
交换宏,不需要额外定义变量
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-11-28 21:25
被笔墨编辑
,原因: