• 一言不合就汇编--分析max宏的两种异常情况


    最近看到一篇不错的文章,其中介绍了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。我们来看一个例子。也就是本文开头提供的参考文章中的例子。

    1. #include "stdio.h"
    2. #define max(a,b) ((a) > (b))? (a): (b)
    3. int main(int argc, char** argv)
    4. {
    5. int x = 1;
    6. int y = 2;
    7. int max = max(x++, y++);
    8. printf("x = %d, y=%d, max=%d \n", x, y, max);
    9. return 0;
    10. }

    这里,我们使用了自增表达式。

    执行结果是
    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函数汇编如下:

    1. 000103f0 <main>:
    2. 103f0: e92d4800 push {fp, lr}
    3. 103f4: e28db004 add fp, sp, #4
    4. 103f8: e24dd018 sub sp, sp, #24
    5. 103fc: e50b0018 str r0, [fp, #-24] ; 0xffffffe8
    6. 10400: e50b101c str r1, [fp, #-28] ; 0xffffffe4
    7. 10404: e3a03001 mov r3, #1
    8. 10408: e50b3008 str r3, [fp, #-8]
    9. 1040c: e3a03002 mov r3, #2
    10. 10410: e50b300c str r3, [fp, #-12]
    11. 10414: e51b2008 ldr r2, [fp, #-8]
    12. 10418: e2823001 add r3, r2, #1
    13. 1041c: e50b3008 str r3, [fp, #-8]
    14. 10420: e51b300c ldr r3, [fp, #-12]
    15. 10424: e2831001 add r1, r3, #1
    16. 10428: e50b100c str r1, [fp, #-12]
    17. 1042c: e1520003 cmp r2, r3
    18. 10430: da000003 ble 10444 <main+0x54>
    19. 10434: e51b3008 ldr r3, [fp, #-8]
    20. 10438: e2832001 add r2, r3, #1
    21. 1043c: e50b2008 str r2, [fp, #-8]
    22. 10440: ea000002 b 10450 <main+0x60>
    23. 10444: e51b300c ldr r3, [fp, #-12]
    24. 10448: e2832001 add r2, r3, #1
    25. 1044c: e50b200c str r2, [fp, #-12]
    26. 10450: e50b3010 str r3, [fp, #-16]
    27. 10454: e51b3010 ldr r3, [fp, #-16]
    28. 10458: e51b200c ldr r2, [fp, #-12]
    29. 1045c: e51b1008 ldr r1, [fp, #-8]
    30. 10460: e59f0010 ldr r0, [pc, #16] ; 10478 <main+0x88>
    31. 10464: ebffff8b bl 10298 <printf@plt>
    32. 10468: e3a03000 mov r3, #0
    33. 1046c: e1a00003 mov r0, r3
    34. 10470: e24bd004 sub sp, fp, #4
    35. 10474: e8bd8800 pop {fp, pc}
    36. 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宏,上面最后的例子还不够完美,要处理更多的异常情况,就参考文章开头的文章,对比内核实际的定义,进一步学习吧!

    如果本文对您有所帮助,就点个赞吧!

  • 相关阅读:
    思腾云计算
    ElasticSearch7.3学习(九)----Mapping核心数据类型及dynamic mapping
    序列化--Serial
    Java设计模式 | 七大原则之迪米特法则
    Django配置静态文件
    ts 联合react 实现ajax的封装,refreshtoken的功能
    AliIAC 智能音频编解码器:在有限带宽条件下带来更高质量的音频通话体验
    <C++> 通讯录管理系统(纯手写含源码)
    Resolving the “address already in use“ Error in Server Deployment
    【JavaSE】之多态
  • 原文地址:https://blog.csdn.net/wwwyue1985/article/details/127092675