C语言程序中使用clang,gcc和clang编译器处理前置自增表达式的区别

博客分析了在Linux环境下,GCC和Clang编译器对同一C语言代码的不同处理,导致执行结果的差异。通过IDAPro逆向分析工具,解释了GCC生成的汇编代码逻辑导致输出196,而Clang的编译结果符合预期,输出186。建议使用Clang进行编译,因其转化方式更符合常人思维。
摘要由CSDN通过智能技术生成

今天刚好有学弟学妹来问我类似的问题,就借着这个问题回答一下:

基本环境:Linux下的gcc和clang(没看版本,应该是最新)

先附上源程序

#include

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

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

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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值