gcc和clang编译器处理前置自增表达式的区别
本文最早发布于我的知乎回答:https://www.zhihu.com/questio...
今天刚好有学弟学妹来问我类似的问题,就借着这个问题回答一下:
基本环境:Linux下的gcc和clang(没看版本,应该是最新)
先附上源程序
#include <stdio.h> int main(int argc, char *argv[]) { int sum,i=2; sum=(++i)+(++i)+(++i)+(++i); printf("%d %d\n",sum,i); return 0; }
是和题主一样的问题,使用gcc编译该程序:
gcc -g -o test-gcc test.c
得到可执行程序test-gcc,执行后输出
19 6
使用clang编译该程序:
clang -g -o test-clang test.c
clang提示警告:
test.c:4:7: warning: multiple unsequenced modifications to 'i' [-Wunsequenced] sum=(++i)+(++i)+(++i)+(++i); ^ ~~ 1 warning generated.
得到可执行程序test-clang,执行后输出
18 6
看完了现象,那么本质原因如何呢?我们借助IDA逆向分析工具来观察test-gcc和test-clang这两个可执行程序,我使用的是IDA Pro 7.0的macOS版本,还不知道IDA Pro是什么的同学可以百度搜一搜,会得到答案。
我们首先分析“结果正常”的test-clang,将test-clang导入进IDA Pro 7.0 64-bit,定位到关键汇编代码(我添加了注释):
;子程序开始(主函数开始) main proc near ;定义了四个双字变量,因为是64位系统,所以这些变量都是8个字节的 var_10= dword ptr -10h var_C= dword ptr -0Ch var_8= dword ptr -8 var_4= dword ptr -4 ;程序初始化 push rbp mov rbp,rsp sub rsp,10h ;进行printf的格式化参数初始化,可以忽略 mov rdi,offset format ; "%d %d\n" ;将var_4变量赋值为0,var_C变量赋值为2 mov [rbp+var_4],0 mov [rbp+var_C],2 ;将var_C加1,这里要借助寄存器eax来加 ;eax中的值现在是3,var_C=3 mov eax,[rbp+var_C] add eax,1 mov [rbp+var_C],eax ;将var_C再加1,借助了另一个寄存器ecx来加 ;ecx中的值现在是4,var_C=4 mov ecx,[rbp+var_C] add ecx,1 mov [rbp+var_C],ecx ;eax和ecx现在相加了,结果送入eax ;eax中的值为3+4=7 add eax,ecx ;将var_C再加1,借助了寄存器ecx来加 ;ecx中的值现在为5,var_C=5 mov ecx,[rbp+var_C] add ecx,1 mov [rbp+var_C],ecx ;再将上面用到的eax加上了ecx ;现在eax中的值为7+5=12 add eax,ecx ;将var_C再加1,借助了寄存器ecx来加 ;ecx中的值现在为6,var_C=6 mov ecx,[rbp+var_C] add ecx,1 mov [rbp+var_C],ecx ;再将上面用到的eax加上了ecx ;现在eax中的值为12+6=18 add eax,ecx ;将上面eax中的18送入变量var_8 mov [rbp+var_8],eax ;输出var_8和var_C,分别为18 6 mov esi,[rbp+var_8] mov edx,[rbp+var_C] mov al,0 call _printf ;用于函数返回 xor ecx,ecx mov [rbp+var_10],eax mov eax,ecx add rsp,10h pop rbp retn main endp
这里面clang把我们的C语言代码按题主手算的方法来编译为了汇编代码,我这里所说的变量var_C等,实际访问的时候是使用的[rbp+var_C],这个是汇编中的寻址方式(基址寄存器+偏移量),如果不懂的话可以略过,就理解为变量var_C即可。
我们将这段汇编代码,按照汇编流程的思维,转化为C语言代码:
#include <stdio.h> int main(int argc, char *argv[]) { int var_4=0,var_C=2,var_8,eax,ecx; eax=++var_C; ecx=++var_C; eax+=ecx; ecx=++var_C; eax+=ecx; ecx=++var_C; eax+=ecx; var_8=eax; printf("%d %d",var_8,var_C); return 0; }
结果显然是
18 6
分析完了test-clang,我们再按照同样的方式分析一下test-gcc,就会发现情况有所不同:
;子程序开始(主函数开始) main proc near ;定义两个变量,因为使用了-g附加调试信息 ;所以IDA分析出就是我们源程序中的sum和i变量 sum= dword ptr -8 i= dword ptr -4 ;程序初始化 push rbp mov rbp,rsp sub rsp,10h ;设置变量i的值为2 mov [rbp+i],2 ;变量i连续加了两次1,执行后i=4 add [rbp+i],1 add [rbp+i],1 ;将变量i的值送入eax,eax为4 mov eax,[rbp+i] ;将eax+eax的所在内存内容的地址送入edx ;这句话就相当于edx=(eax+eax),edx=8 ;只是编译之后变成了复杂的写法 lea edx,[eax+eax] ;变量i继续加1,执行后i=5 add [rbp+i],1 ;将变量i的值送入eax,eax=5 mov eax,[rbp+i] ;eax+edx的值放入edx,edx=8+5=13 add edx,eax ;变量i继续加1,执行后i=6 add [rbp+i],1 ;将变量i的值送入eax,eax=6 mov eax,[rbp+i] ;eax+edx的值放入eax,eax=6+13=19 add eax,edx ;将eax的值送入变量sum mov [rbp+sum],eax ;调用printf输出sum和i,分别为19 6 mov edx,[rbp+i] mov eax,[rbp+sum] mov esi,eax mov edi,offset format ; "%d %d\n" mov eax,0 call _printf ;程序返回 mov eax,0 leave retn main endp
可以看出来gcc的编译思路比较清奇,所以才导致了意想不到的结果,我们同样将汇编语言的代码,按照汇编语言的思维,转换为C语言代码:
#include <stdio.h> int main(int argc, char *argv[]) { int sum,i,eax,edx; i=2; ++i; ++i; eax=i; edx=eax+eax; eax=++i; edx+=eax; eax=++i; eax+=edx; sum=eax; printf("%d %d",sum,i); return 0; }
结果显然是:
19 6
至此我们找到了不同编译器运行结果不同的原因,是因为gcc和clang在编译这同一段C语言代码的时候,把他们按照不同的思路转化为了汇编代码,所以执行结果才不同,显然clang的转化方式更能符合正常人的思维,所以现在更推荐使用clang,用clang来代替gcc。