我们先看第一段代码
用gcc编译执行后,报“段错误”(Segment fault)。如果你在windows下执行的话,那就会弹出一个非法操作的对话框了。
第二段代码
用gcc编译执行后一切正常。
那么char *a="abcdefg"和char a[]="abcdefg"究竟有啥区别呢?为啥第一段代码会出段错误呢。
我们把第一段代码反汇编来看一下(foo函数),反汇编的命令是objdump -d exec_file_name
08048394 <foo>:
8048394:
55
push %ebp
8048395:
89 e5
mov %esp,%ebp
8048397:
83 ec 10
sub $0x10,%esp
804839a:
c7 45 fc 80 84 04 08
movl $0x8048480,-0x4(%ebp)
80483a1:
8b 45 fc
mov -0x4(%ebp),%eax
80483a4:
83 c0 02
add $0x2,%eax
80483a7:
c6 00 78
movb $0x78,(%eax)
80483aa:
c9
leave
80483ab:
c3
ret
08048394 <foo>:
8048394:
55
push %ebp
8048395:
89 e5
mov %esp,%ebp
8048397:
83 ec 10
sub $0x10,%esp
804839a: c7 45 fc 80 84 04 08 movl $0x8048480,-0x4(%ebp)
80483a1: 8b 45 fc mov -0x4(%ebp),%eax
80483a4: 83 c0 02 add $0x2,%eax
80483a7: c6 00 78 movb $0x78,(%eax)
80483aa:
c9
leave
80483ab:
c3
ret
我们可以看到,对于char *a="abcdefg",代码中会把地址0x8048480赋值给a,然后给a[2]赋值。那么这个0x8048480地址,我们猜一下也知道,肯定是"abcdefg"所在的位置了。那段错误是如何产生的呢?
原因在于,"abcdefg"被编译器放到了.rodata段,这个段的属性是readonly的,因此当我们要给a[2]赋值时,就会看到段错误了。
我们使用objdump -s exec_file_name命令可以看到可执行文件中的所有段,下面是.rodata的内容,可以看到"abcdefg"就在里面(0x8048478表示这一行的地址,61正好在0x8048478+8=0x8048480位置)。
Contents of section .rodata:
8048478 03000000 01000200
61626364 65666700 ........abcdefg.
那么对于char a[]="abcdefg"又是什么情况呢?
我们也来反汇编一下。
080483e4 <foo>:
80483e4:
55
push %ebp
80483e5:
89 e5
mov %esp,%ebp
80483e7:
83 ec 18
sub $0x18,%esp
80483ea:
65 a1 14 00 00 00
mov %gs:0x14,%eax
80483f0:
89 45 f4
mov %eax,-0xc(%ebp)
80483f3:
31 c0
xor %eax,%eax
80483f5: a1 00 85 04 08 mov 0x8048500,%eax
80483fa: 8b 15 04 85 04 08 mov 0x8048504,%edx
8048400: 89 45 ec mov %eax,-0x14(%ebp)
8048403: 89 55 f0 mov %edx,-0x10(%ebp)
8048406: c6 45 ee 78 movb $0x78,-0x12(%ebp)
804840a:
8b 45 f4
mov -0xc(%ebp),%eax
804840d:
65 33 05 14 00 00 00
xor %gs:0x14,%eax
8048414:
74 05
je 804841b <foo+0x37>
8048416:
e8 fd fe ff ff
call 8048318 <__stack_chk_fail@plt>
804841b:
c9
leave
804841c:
c3
ret
这里有两个地址0x8048500和0x8048504,如果看不懂汇编也关系,事实上这几行的意思是从这两个地址把数据拷贝给数组a。显然a的内存空间在栈上,那么这当然是可以写的啦,所以就不会出错了。也就是说,对于char a[]="abcdefg",编译器会生成一段代码,把数据从.rodata这个只读的段,拷贝到a的数组中。这里附上.rodata的内容,大家可以算算地址。
Contents of section .rodata:
80484f8 03000000 01000200 61626364 65666700 ........abcdefg.
还有点东西,如果你把char a[]="abcdefg"改成char a[]="testtest",反汇编出来的又会是不一样的结果。
080483e4 <foo>:
80483e4:
55
push %ebp
80483e5:
89 e5
mov %esp,%ebp
80483e7:
83 ec 18
sub $0x18,%esp
80483ea:
65 a1 14 00 00 00
mov %gs:0x14,%eax
80483f0:
89 45 f4
mov %eax,-0xc(%ebp)
80483f3:
31 c0
xor %eax,%eax
80483f5: c7 45 eb 74 65 73 74 movl $0x74736574,-0x15(%ebp)
80483fc: c7 45 ef 74 65 73 74 movl $0x74736574,-0x11(%ebp)
8048403:
c6 45 f3 00
movb $0x0,-0xd(%ebp)
8048407:
c6 45 ed 78
movb $0x78,-0x13(%ebp)
804840b:
8b 45 f4
mov -0xc(%ebp),%eax
804840e:
65 33 05 14 00 00 00
xor %gs:0x14,%eax
8048415:
74 05
je 804841c <foo+0x38>
8048417:
e8 fc fe ff ff
call 8048318 <__stack_chk_fail@plt>
804841c:
c9
leave
804841d:
c3
ret
这里直接将两个常量0x74736574赋值给数组a了,0x74736574翻译成ascii正好是test(注意字节序,这里是little endian)。
其实可能还有其他的初始化数组a的情况,也不必去细究细节。
关键是要明白一点,写char *a=“abcdefg”的话,a指针指向的事实上是只读的区域,因此,更准确的写法是const char *a="abcdefg"。
而char a[]=“abcdefg”,编译器会生成一段初始化数组的代码,但是数组a的内容是可以修改的。如果不需要修改数组a的内容,写成const char *a="abcdefg"会有效率上的提高。
最后感慨一下,C/C++是一个大坑,没有多年的编程积累和对操作系统及编译器等底层的了解,是很难真正掌握的。