近日,在书中看到一个关于数据交换函数的源代码,发现挺有意思,具体代码如下:
1 void swap(int* a, int* b) 2 { 3 *a ^= *b ^= *a ^= *b; 4 }
根据 C 语言异或赋值操作符(^=)的计算规则和异或运算符(^)的运算法则,应按照从右到左的顺序进行计算,具体计算过程演示如下:
1 *a = *a ^ *b; 2 *b = *b ^ *a = *b ^ ( *a ^ *b ) = *a; //将式1代入 3 *a = *a ^ *b = ( *a ^ *b ) ^ *a = *b; //将式1和式2代入
从计算过程可以看出,a 和 b 的值的确进行了交换,那我们通过具体程序来进行验算一下:
1 #include <stdio.h> 2 3 void swap(int* a, int* b) 4 { 5 *a ^= *b ^= *a ^= *b; 6 } 7 8 int main(int argc, char** argv) 9 { 10 int a = 13, b = 68; 11 12 printf("Before exchange: a = %d, b = %d\n", a, b); 13 swap(&a, &b); 14 printf("After exchange: a = %d, b = %d\n", a, b); 15 16 return 0; 17 }
笔者的计算环境是:Linuxmint 17.3 + gcc 4.8.4/clang 3.5.0。先使用 gcc 进行编译,看看结果如何:
$ gcc -o swap_gcc swap.c $ ./swap_gcc Before exchange: a = 13, b = 68 After exchange: a = 0, b = 13
结果非常令人诧异,只有 b 的值进行了交换,而 a 的值却是 0,为什么会是 0 ?我们待会再来分析,现在我们再用 clang 进行编译,看看结果又是如何:
$ clang -o swap_clang swap.c $ ./swap_clang Before exchange: a = 13, b = 68 After exchange: a = 68, b = 13
结果还是令人欢欣鼓舞的,那为什么会出现两种不同的结果呢?直观的感觉肯定是与编译器有关的,为了证实这一想法,我们通过反汇编之后来看看其中的差异之处:
$ objdump -d swap_gcc
截取其中与 swap 函数有关的段落如下:
1 000000000040052d <swap>: 2 40052d: 55 push %rbp 3 40052e: 48 89 e5 mov %rsp,%rbp 4 400531: 48 89 7d f8 mov %rdi,-0x8(%rbp) //保存 a 的值 5 400535: 48 89 75 f0 mov %rsi,-0x10(%rbp) //保存 b 的值 6 400539: 48 8b 45 f8 mov -0x8(%rbp),%rax 7 40053d: 8b 10 mov (%rax),%edx //取 a 的值并存入寄存器 edx 8 40053f: 48 8b 45 f0 mov -0x10(%rbp),%rax 9 400543: 8b 08 mov (%rax),%ecx //取 b 的值并存入寄存器 ecx 10 400545: 48 8b 45 f8 mov -0x8(%rbp),%rax 11 400549: 8b 30 mov (%rax),%esi //取 a 的值并存入寄存器 esi 12 40054b: 48 8b 45 f0 mov -0x10(%rbp),%rax 13 40054f: 8b 00 mov (%rax),%eax //取 b 的值并存入寄存器 eax 14 400551: 31 c6 xor %eax,%esi //将寄存器 eax 与 esi 中的值进行异或运算后存入寄存器 esi 中,即 *a = *a ^ *b 15 400553: 48 8b 45 f8 mov -0x8(%rbp),%rax 16 400557: 89 30 mov %esi,(%rax) //将寄存器 esi 中的值写入原先存放 a 的值的地址处,至此,完成了最后一个异或赋值表达式 *a ^= *b 的计算 17 400559: 48 8b 45 f8 mov -0x8(%rbp),%rax 18 40055d: 8b 00 mov (%rax),%eax //取 a 的新值(即 *a ^ *b)并存入寄存器 eax,注意此时寄存器 eax 中原先保存的值被覆盖了 19 40055f: 31 c1 xor %eax,%ecx //将寄存器 eax 与 ecx 中的值进行异或运算后存入寄存器 ecx 中,即 *b = *b ^ (*a ^ *b) = *a 20 400561: 48 8b 45 f0 mov -0x10(%rbp),%rax 21 400565: 89 08 mov %ecx,(%rax) //将寄存器 ecx 中的值写入原先存放 b 的值的地址处,至此,完成了中间那个异或赋值表达式 *b ^= *a 的计算 22 400567: 48 8b 45 f0 mov -0x10(%rbp),%rax 23 40056b: 8b 00 mov (%rax),%eax //取 b 的新值(即 *a)并存入寄存器 eax,注意此时寄存器 eax 中原先保存的值再次被覆盖了 24 40056d: 31 c2 xor %eax,%edx //将寄存器 eax 与 edx 中的值进行异或运算后存入寄存器 edx 中,发现问题了吗??? 25 40056f: 48 8b 45 f8 mov -0x8(%rbp),%rax 26 400573: 89 10 mov %edx,(%rax) //将寄存器 edx 中的值写入原先存放 a 的值的地址处 27 400575: 5d pop %rbp 28 400576: c3 retq
通过以上汇编代码和简要的分析,大家发现问题了吗?很显然,第24行的计算过程出现了问题,因为此时寄存器 edx 中存放的是最初始的 a 的值,而寄存器 eax 中存放的是 b 的新值(也就是 a 的初始值),因此,计算后寄存器 edx 的值就是 0 了(即*a ^ *a)。知道了原因,那如何修正这一问题呢?显然,只需要在第 23 行和第 24 行之间插入如下代码即可:
mov -0x8(%rbp),%rax mov (%rax),%edx
即通过以上代码重新取得 a 的新值(即 *a ^ *b)即可。接下来,我们再看看另外一种反汇编的情况:
$ objdmup -d swap_clang
同样,截取其中与 swap 函数有关的段落如下:
1 00000000004004e0 <swap>: 2 4004e0: 55 push %rbp 3 4004e1: 48 89 e5 mov %rsp,%rbp 4 4004e4: 48 89 7d f8 mov %rdi,-0x8(%rbp) 5 4004e8: 48 89 75 f0 mov %rsi,-0x10(%rbp) 6 4004ec: 48 8b 75 f0 mov -0x10(%rbp),%rsi 7 4004f0: 8b 06 mov (%rsi),%eax 8 4004f2: 48 8b 75 f8 mov -0x8(%rbp),%rsi 9 4004f6: 8b 0e mov (%rsi),%ecx 10 4004f8: 31 c1 xor %eax,%ecx 11 4004fa: 89 0e mov %ecx,(%rsi) 12 4004fc: 48 8b 75 f0 mov -0x10(%rbp),%rsi 13 400500: 8b 06 mov (%rsi),%eax 14 400502: 31 c8 xor %ecx,%eax 15 400504: 89 06 mov %eax,(%rsi) 16 400506: 48 8b 75 f8 mov -0x8(%rbp),%rsi 17 40050a: 8b 0e mov (%rsi),%ecx 18 40050c: 31 c1 xor %eax,%ecx 19 40050e: 89 0e mov %ecx,(%rsi) 20 400510: 5d pop %rbp 21 400511: c3 retq
对以上代码的分析过程可以参考前一部分,也是比较清晰易懂的。从运算过程可以看出,在进行异或运算之前,都是取出 a 或 b 的最新的值,即保证了计算过程严格按照文章开始时演算的步骤进行,也就能够得出正确的值。
由此可见,我们的猜测是正确的,原因的确与编译器有关。如果希望 swap 函数能够在不同的编译环境下正常工作,我们可以将原异或赋值表达式拆分成以下 3 个异或赋值表达式即可。
1 *a ^= *b; 2 *b ^= *a; 3 *a ^= *b;
另外,在使用 gcc 编译时,如果加上优化选项 -O1/-O2/-O3/-Os(默认情况下是不进行优化的),我们也能够得到正确的答案。有兴趣的读者可以自己进行验证,笔者就不再赘述了。