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

发布于:2022-12-05 ⋅ 阅读:(316) ⋅ 点赞:(0)

最近看到一篇不错的文章,其中介绍了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宏,上面最后的例子还不够完美,要处理更多的异常情况,就参考文章开头的文章,对比内核实际的定义,进一步学习吧!

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


网站公告

今日签到

点亮在社区的每一天
去签到