关于相同代码在vs2017和Dev-c++中结果不同的原因初探索:UB(undefined behaviour)
今天一个学长拉着我研究了好一会儿一个简单的代码,因为它居然在不同软件中运行结果不同。这让我们有些费解,于是做了一些探究。
下面先附上代码(有问题)。
main()
{
int i = 1;
printf("%d,%d,%d\n",i = 3,i + 1, i);
}
下面分别是在Dev-c++和vs2017中运行的结果
虽然创建的是cpp文件,但gcc环境并不影响c程序的运行。
那么问题来了,是不同软件编译器的问题吗?(优化全部关了)
于是先去查看两者的反汇编:
先是Dev-c++的
0x0000000000401530 <+0>: push rbp
0x0000000000401531 <+1>: mov rbp,rsp
0x0000000000401534 <+4>: sub rsp,0x30
0x0000000000401538 <+8>: call 0x402100 <__main>
0x000000000040153d <+13>: mov DWORD PTR [rbp-0x4],0x1
=> 0x0000000000401544 <+20>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000401547 <+23>: lea ecx,[rax+0x1]
0x000000000040154a <+26>: mov DWORD PTR [rbp-0x4],0x3
0x0000000000401551 <+33>: mov edx,DWORD PTR [rbp-0x4]
0x0000000000401554 <+36>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000401557 <+39>: mov r9d,edx
0x000000000040155a <+42>: mov r8d,ecx
0x000000000040155d <+45>: mov edx,eax
0x000000000040155f <+47>: lea rcx,[rip+0x2a9a] # 0x404000
0x0000000000401566 <+54>: call 0x402b18 <printf>
0x000000000040156b <+59>: mov eax,0x0
0x0000000000401570 <+64>: add rsp,0x30
0x0000000000401574 <+68>: pop rbp
0x0000000000401575 <+69>: ret
然后是vs2017的
int main()
{
00007FF74E9A1820 push rbp
00007FF74E9A1822 push rdi
00007FF74E9A1823 sub rsp,108h
00007FF74E9A182A lea rbp,[rsp+20h]
00007FF74E9A182F mov rdi,rsp
00007FF74E9A1832 mov ecx,42h
00007FF74E9A1837 mov eax,0CCCCCCCCh
00007FF74E9A183C rep stos dword ptr [rdi]
00007FF74E9A183E lea rcx,[__3C9E9496_test.cpp (07FF74E9B1003h)]
00007FF74E9A1845 call __CheckForDebuggerJustMyCode (07FF74E9A1082h)
int i = 1;
00007FF74E9A184A mov dword ptr [i],1
printf("%d,%d,%d\n", i = 3, i + 1, i);
00007FF74E9A1851 mov dword ptr [i],3
00007FF74E9A1858 mov eax,dword ptr [i]
***//dword 意为双字,即四字节,ptr即为指针,整行的意思为将内存地址i中的dword(32位)数据赋给寄存器eax***
00007FF74E9A185B inc eax
00007FF74E9A185D mov r9d,dword ptr [i]
00007FF74E9A1861 mov r8d,eax
00007FF74E9A1864 mov edx,dword ptr [i]
00007FF74E9A1867 lea rcx,[string "%d,%d,%d\n" (07FF74E9A9C28h)]
00007FF74E9A186E call printf (07FF74E9A11D1h)
}
00007FF74E9A1873 xor eax,eax
}
00007FF74E9A1875 lea rsp,[rbp+0E8h]
00007FF74E9A187C pop rdi
00007FF74E9A187D pop rbp
00007FF74E9A187E ret
本人电子信息工程二年级生,还未接触过汇编语言。跟着学长大佬的脚步一起查找了相关汇编指令,勉强理解了反汇编码的大意并做了对比,发现两者的调用顺序并不一样。
Dev-c++中,遵循了c语言从右至左执行的原则。
在遇到不需要运算的表达式时,给出了相关变量的地址(即 i ),而遇到需要运算的表达式时,先运算,并开辟一个储存计算结果的临时内存,给出临时内存的地址(即 i + 1)。所以先执行i,把i的地址存入寄存器;然后计算 i + 1,得出2,把新的地址存在另一个寄存器中;再计算 i = 3,使i变为3,把i的地址存入寄存器;最后输出从左至右便为3,2,3.
而在vs2017中,却没有从右至左执行。
从反汇编中可看出,在vs中,是先执行了 i = 3,把i的地址存入寄存器;再执行了 i + 1,把新的地址存在另一个寄存器中;然后执行了i;最后一起输出:3,4,3.
那为什么执行顺序出现了不同呢,通过查询了相关资料和学长指导后了解了一个概念————UB(undefined behaviour),即未定义行为。
以下仅为个人理解
那什么是未定义行为呢?
包含多个不确定的副作用的代码的行为总是被认为未定义。编译器对于未定义行为可能不会报错,但是这些行为编译器会自行处理。
这里附上一个比较全面生动的阐述c语言的未定义行为(undefined behaviour)
这里又涉及到了另外两个知识点:序列点和副作用。
1.在c语言中存在一些符号会产生序列点,例如“ ,” “&& 和 || ”等。这些序列点会对左右的表达式运算存在影响,而影响主要指的是副作用的生效顺序。
2.如果一个表达式不仅算出一个值,还修改了环境(数据对象或者文件),就说这个表达式有副作用(因为它多做了额外的事)。比如:a++ 。
3.可参考C语言中的序列点和副作用(文章里说:“甚至都不要试图探究这些东西在你的编译器中是如何实现的,K&R 明智地指出,”如果你不知道它们在不同的机器上如何实现, 这样的无知可能恰恰会有助于保护你。” 结果我正好去研究了。。)
#于是我大致得出了如下结论#
由于printf函数中存在带着副作用的表达式,导致计算机在执行printf过程中判定其为未定义行为,于是不同的编译器对相同的一段代码产生了自己的编译行为,导致了执行顺序不同,最后输出结果也不同了。
Final,在了解以上知识点外,深刻认识到了一点————在写程序的时候绝不能按照自己想当然的想法去写代码,这会是极大的隐患,所以我们应该尽量避免这种情况的发生,而是应该写出让所有编译器只会存在一种处理方式的代码。