最近看到一篇不错的文章,其中介绍了Linux内核max宏的演进(连接地址为:Linux内核中max()宏的奥妙何在?(一)),文中提到了下面这样一个问题:
如何实现求两个变量中的较大者的max宏?
如果是直截了当的干,那可能是下面的写法:
#define max(a, b) a > b ? a : b
学过C语言的开发者,大概都经过宏的常见错误训练,就是要考虑到隐含的运算符优先级问题。宏在预编译中处理,并不像函数那样直接生成目标代码,而是先进行替换,再参与正式的编译过程。所以,对于上面的实现,如果其中的a 和 b是一个表达式,那么替换后,就可能产生副作用,隐藏bug。比如,像下面这样使用上面的宏:
需要补充一下,上面这个例子其实不好想出来。
网上查了一下运算符的优先级,发现三目运算符 ?:居然是最低优先级的运算符
所以,上面直截了当的宏实现,反而在大部分情况下都可能是正常的。要想搞出一个异常的例子,感觉还不容易呢。不过,通过绞尽脑汁,最终还是让我想到了一个,就是上面代码里的写法。
上面的代码执行结果是什么呢?
按照本意,我们应该是要求max(x和y的较大者x , z和x的较大者z),所以结果应该是z的值3才对。但是,实际输出的是2。问题出在哪里呢?这就需要将上面的宏调用展开来分析了。
我们看看上面的宏展开后,变成了什么样:
x>y?x:y > z>x?z:x ? x>y?x:y : z>x?z:x
x>y?x:y > z>x?z:x ? x>y?x:y : z>x?z:x
上面两种背景分别代表了宏中的a和b。预编译阶段像上图那样展开后,编译阶段怎么处理的呢?这就涉及优先级和处理方向的问题了。
因为三目运算符?:优先级最低,又是右结合,所以,上面的展开式,最终是下面的运算关系:
【(x>y) ? x : 【((y>z) > x) ? z : 【 x ? 【(x>y) ? x: y 】 : 【(z > x)? z: x】】】】
首先计算红色的表达式,如果为真,则返回x,否则执行后面所有表达式的结果并返回。这实际已经跟代码里的意图不一致了。
那么,到底是不是这样呢,我们可以通过汇编来确认一下:
这里,我用机器自带的arm工具编译了上面的代码,然后再执行objdump -d xxx.out,查看汇编输出,main函数汇编如下:
开始一段是栈的操作指令。
第7行将2给寄存器r3,然后将r3保存到栈-8位置处,也就是变量x的值;
第9、10行变量y入栈
第11、12行变量z入栈
第13、14行读取变量x和y的值到寄存器r2 和 r3中
第15行比较两个寄存器值的大小,也就是x>y的条件表达式处理
如果x>y为真,则跳转到10594位置处,否则读取y和z的值进行比较,也就是前面展开式中绿色部分的开始 ((y>z) > x)
10594的代码读取栈中x变量的值,然后保存到栈-20的地方,也就是max变量在栈中的位置。因为上面的展开式中,x>y为真,所以直接将x的值给了max,其他后面的表达式都不需要执行,代码里的z并不对结果产生影响。
这就是优先级的变化,导致宏的行为意图发生了改变。
那么如何解决上面的问题呢?显然,只有在宏定义中补充上括号就可以了,代码修改如下:
再次执行代码,结果为
可见,此时,符合了预期。
第一个异常情况,这里就处理完了。
那么现在这个宏实现是不是就可以商用了呢?还不行,虽然现在的实现可能是大部分工程中的写法,但是仍然隐藏着bug。我们来看一个例子。也就是本文开头提供的参考文章中的例子。
- #include "stdio.h"
- #define max(a,b) ((a) > (b))? (a): (b)
-
- int main(int argc, char** argv)
- {
- int x = 1;
- int y = 2;
-
- int max = max(x++, y++);
- printf("x = %d, y=%d, max=%d \n", x, y, max);
- return 0;
- }
这里,我们使用了自增表达式。
执行结果是
x = 2, y=4, max=3
为啥是这个结果?
根据结果,可以猜想到:
宏进行了替换,a 和 b 被替换为了 x++ 和 y++
学过谭浩强老师C语言的都清楚,后加加是先用变量的值,然后变量再改变。因此,上面的例子,执行过程就是:
x 和 y的值先 进行比较,比较后,x和y都完成加1操作。后面的表达式是个条件选择,因为这里 a 不大于 b,所以冒号前面的a表达式执行不到,接着执行b表达式。
此时,会先用b的值作为表达式的值,返回,也就是max的值,也就是加1的y,为3,然后y再加1,变为4
这个分析对不对呢,我们有终极利器,那就是汇编。看汇编的流程,就知道编译器是怎么解释上面的代码了。
同之前,用机器自带的arm工具编译了上面的代码,然后再执行objdump -d xxx.out,查看汇编输出,main函数汇编如下:
- 000103f0 <main>:
- 103f0: e92d4800 push {fp, lr}
- 103f4: e28db004 add fp, sp, #4
- 103f8: e24dd018 sub sp, sp, #24
- 103fc: e50b0018 str r0, [fp, #-24] ; 0xffffffe8
- 10400: e50b101c str r1, [fp, #-28] ; 0xffffffe4
- 10404: e3a03001 mov r3, #1
- 10408: e50b3008 str r3, [fp, #-8]
- 1040c: e3a03002 mov r3, #2
- 10410: e50b300c str r3, [fp, #-12]
- 10414: e51b2008 ldr r2, [fp, #-8]
- 10418: e2823001 add r3, r2, #1
- 1041c: e50b3008 str r3, [fp, #-8]
- 10420: e51b300c ldr r3, [fp, #-12]
- 10424: e2831001 add r1, r3, #1
- 10428: e50b100c str r1, [fp, #-12]
- 1042c: e1520003 cmp r2, r3
- 10430: da000003 ble 10444 <main+0x54>
- 10434: e51b3008 ldr r3, [fp, #-8]
- 10438: e2832001 add r2, r3, #1
- 1043c: e50b2008 str r2, [fp, #-8]
- 10440: ea000002 b 10450 <main+0x60>
- 10444: e51b300c ldr r3, [fp, #-12]
- 10448: e2832001 add r2, r3, #1
- 1044c: e50b200c str r2, [fp, #-12]
- 10450: e50b3010 str r3, [fp, #-16]
- 10454: e51b3010 ldr r3, [fp, #-16]
- 10458: e51b200c ldr r2, [fp, #-12]
- 1045c: e51b1008 ldr r1, [fp, #-8]
- 10460: e59f0010 ldr r0, [pc, #16] ; 10478 <main+0x88>
- 10464: ebffff8b bl 10298 <printf@plt>
- 10468: e3a03000 mov r3, #0
- 1046c: e1a00003 mov r0, r3
- 10470: e24bd004 sub sp, fp, #4
- 10474: e8bd8800 pop {fp, pc}
- 10478: 000104ec .word 0x000104ec
从上面汇编的第六行看起,做的操作如下:
1(x)保存到r3
r3 x 保存到栈-8,变量x入栈
2(y)保存到r3
r3 y 保存到栈-12,变量y入栈
x 加载到 r2
x 加1保存到 r3
r3 写回到栈-8,也就是内存中的x变为2,完成++运算
y 加载到 r3
y 加1保存到 r1
r1 写回到栈-12,也就是内存中的y变为3,完成y的++运算
比较 r2 和 r3,注意,这里的r2 x和 r3 y是未增加前的值,也就是1 和2,而此时,栈中变量的值已经发生了变化。如此,就实现了用变量自增前的值参与运算,而自增后的值入栈内存。
对应到代码就是用自增前的值进行max比对
如果小的话,就跳转到10444位置
这个位置将内存栈中的y加载到r3寄存器
将y再加1,给r2寄存器
将r2寄存器写回栈,也就是内存中的y加1,完成y的第二次++操作
将增加前的值,也就是r3寄存器值写回到栈-16位置,是个新位置,max变量的栈内存位置。
然后将-16位置的栈内存给r3,也就是原来未增加的r3 (y)。即将未增加的y给max,作为表达式的返回值,而自身则加1了,因为y的栈位置-12保存的是增加后的值。
将栈-12位置的内存给r2,也就是最后加1的y
将栈-8位置的内存给r1,也就是比较前加1的x
将10478位置的内容给r0
之后调用printf
结合代码执行,不难理解,这里的r3是打印的max值,r2是y,r1是x
将r3 和 r0 寄存器清零
调整栈指针,函数返回
从上面汇编的过程可以看出,是符合前面的预期分析的。
至此,我们就完成了第二个异常的分析。对于这种情况,就需要在宏定义中重新创建两个新的变量作为变量x和y的副本,完成比较工作,而非直接使用x 和y本身。因此,宏定义修改如下:
通过typeof定义与输入一致类型的临时变量max1 max2,参与实际运算,从而避免参数的无意改变。实际执行结果为:
而之前为
x = 2, y=4, max=3
可见,此时跟期望就比较相符。
可见,打好基础很重要。
汇编和GDB可以帮助我们分析很多实际的问题。通过实际动手调试,你会得到比直接上网搜索更多的收获,比如,更好的编程感觉和更深的印象。
对于max宏,上面最后的例子还不够完美,要处理更多的异常情况,就参考文章开头的文章,对比内核实际的定义,进一步学习吧!
如果本文对您有所帮助,就点个赞吧!