一个数据交换函数引发的思考
近日,在书中看到一个关于数据交换函数的源代码,发现挺有意思,具体代码如下: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(默认情况下是不进行优化的),我们也能够得到正确的答案。有兴趣的读者可以自己进行验证,笔者就不再赘述了。
页:
[1]