反汇编分析赋值与自增自减

++(--)


后置的++(--)真的是先使用,后自增吗?


概念:前置++是先自增,后使用; 后置++是先使用,后自增.

下面从汇编角度看后置++的原理.如图:

image-20240502103655957

从上图来看,就是正常的先使用,后自增.图中的a++是有使用对象的,先把a的值给b,然后再自增.

但是如果只有单独的a++,并没有接收方呢? 看汇编:

image-20240502105312246

上面汇编说明,后置++是先使用,后自增的说法仅仅是表述方便.对编译器看来后置++先使用这一步骤,取决于有没有使用者.


后置++和前置++的差别

再看后置++与前置++:

image-20240502110116893

可以看到,如果在没有接收方的情况下, C语言的前置++和后置++是没有差别的.所以,习惯哪个就用哪个.

(在C++中,从++运算符重载的实现上看.后置++确实可能会慢一些)


复杂表达式


++i+++i+++i

分别使用vs和gcc计算++i+++i+++i的结果,如图:

image-20240502163539456

可以发现不同平台下这样的表达式结果可能是不一样的.为什么? 看看汇编下的它们长什么样:

image-20240502170103052

(tips:++i不加括号计算的结果也是一样的)

可以发现,两平台下的汇编代码的逻辑是有差别的,说明这种复杂表达式在不同平台的计算路径是不一样的,是因为编译器识别表达式的方式不一致/不确定导致的,可能是同时加载至寄存器,也可能是分批加载.最终导致应用层的表现不一致.

因此,类似这种复杂表达式,做不到统一规范的.我们一律不推荐使用.

还有一个问题,那就是编译器如何对这种复杂表达式完成变量与符号的识别和匹配的呢? 引入贪心规则,见下文.


贪心法

贪心法

C 语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。这个处理的策略被称为“贪心法”。需要注意的是,除了字符串与字符常量,符号的中间不能嵌有空白(空格、制表符、换行符等)。比如:==是单个符号,而= =是两个等号。解释:读到空格后,就需要判断是否是完整的操作符,一个=号能够独立,不需要再贪心组合;if和()中间可以有空格,如果读到的是if,就算读到空格,也不能算是独立的结构,还需要继续读下去,直到遇到()。

按照这个规则可能很轻松的判断 a+++b 表达式与 a++ +b 一致。
那++i+++i+++i;会被解析成什么样子呢?希望读者好好研究研究。另外还可以考虑一下这个表达式的意思:a+++++b;


编译器的贪心规则不是保证一定正确的规则

观察i+++++j的情况,如果使用编译器的自动对齐功能,对齐后的表达式是告警的

image-20240502173608784

如果我们手动调整,则没有告警

image-20240502173759840

这说明,编译器的贪心匹配规则只是尽可能去匹配,解释表达式,解释的结果不一定是对的.所以,读者们以后也要注意,不要绝对相信编译器的告警.编译器的告警也有可能会出错的.

另外,从这句代码中可以发现,C语言的空格并不是只让代码变得好看一些,还有起到操作符匹配划分的作用.

查看linux反汇编:

gcc -g test.c //生成debug版本可执行文件,便于查看源码位置,如果不带-g选项则不显示源码

linux反汇编命令:objdump -S a.out > a.s //S == source


赋值的原理

赋值是将赋值符号右边变量的值取出放到寄存器中,然后再通过寄存器赋值给赋值符号左边的变量.

image-20240526214709872

因此"右值"有常性,是因为右值被放到了寄存器中,寄存器有常性,右值就有常性.

posted @ 2024-09-18 19:00  HJfjfK  阅读(22)  评论(0编辑  收藏  举报