你知道
char a[] = "hello";
和
char *a = "hello";
两种写法的区别吗?
让我们看一下下面的程序:
#include <stdio.h> int main(int argc, char *argv[]) { char *a = "aaaaa"; char b[] = "55"; b[0] = 'f'; a[0] = 'f'; printf("a=%s, b=%s
", a ,b); return 0; }
编译执行:
# gcc -o mytest mytest.c -Wall -g # ./mytest Segmentation fault (core dumped)
直接segmentation fault了。我们看一下是哪一步触发的segfault:
# gdb mytest core.2125.11.18446744073709551615.1.2125 ... Core was generated by `./mytest'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x000000000040114f in main (argc=1, argv=0x7fff9b3e4a08) at mytest.c:9 9 a[0] = 'f'; (gdb)
不用看更多了,gdb直接就告诉了我们之前我们运行的./mytest那个进程因为触发了Segmentation fault,已经被终止运行了。问题行出在main函数里的
a[0] = 'f';
这行。
那么问题就来了,为什么这步会触发非法地址访问呢?而它前面那句:
b[0] = 'f';
就没事?
我相信很多人都看过各种解释和说法,但是你看到的那些“解释”是怎么具体体现在可执行程序里的呢?
这就是最开始我们问的问题,
char *a = "aaaaa"; char b[] = "55";
有什么区别?我们先看一下编译后的程序是什么样的
# objdump -DS mytest int main(int argc, char *argv[]) { 401126: 55 push %rbp 401127: 48 89 e5 mov %rsp,%rbp 40112a: 48 83 ec 20 sub $0x20,%rsp 40112e: 89 7d ec mov %edi,-0x14(%rbp) 401131: 48 89 75 e0 mov %rsi,-0x20(%rbp) char *a = "aaaaa"; 401135: 48 c7 45 f8 10 20 40 movq $0x402010,-0x8(%rbp) 40113c: 00 char b[] = "55"; 40113d: 66 c7 45 f5 35 35 movw $0x3535,-0xb(%rbp) 401143: c6 45 f7 00 movb $0x0,-0x9(%rbp) b[0] = 'f'; 401147: c6 45 f5 66 movb $0x66,-0xb(%rbp) a[0] = 'f'; 40114b: 48 8b 45 f8 mov -0x8(%rbp),%rax 40114f: c6 00 66 movb $0x66,(%rax) …… ……
这就很显而易见了:
首先main函数初始化栈,分了一些内存空间作为main函数的栈。关于函数调用栈是怎么回事,参考下文:
以及相关系列文章。我这里就不再解释函数栈的原理了。我们通过汇编语言可以看到char *a和char b[]现在位于main函数栈的下列位置:
+--------+ | a | <--- -0x8(%rbp) +--------+ | b[2] | <--- -0x9(%rbp) +--------+ | b[1] | <--- -0xa(%rbp) +--------+ | b[0] | <--- -0xb(%rbp) +--------+ 注: b[0~2]每个占一个字节,但变量a会占更多字节(我只是没画出来),因为a是一个指针变量,占用的字节数和体系结构有关。
我们先看char b[] = "55";
char b[] = "55"; 40113d: 66 c7 45 f5 35 35 movw $0x3535,-0xb(%rbp) 401143: c6 45 f7 00 movb $0x0,-0x9(%rbp)
0x35就是字符'5',所以0x3535就是"55",所以“movw $0x3535,-0xb(%rbp)”这句就是给main函数栈的-0xb(%rbp)起始的位置存两个字节的"55",也就是b[0]='5'; b[1]='5';
然后下面一句就很简单了,编译器为字符串自动补 ,所以“movb $0x0,-0x9(%rbp)”就相当于b[2]=' ';这样char b[] = "55";就出来了。
接着我们再看char *a = "aaaaa";
char *a = "aaaaa"; 401135: 48 c7 45 f8 10 20 40 movq $0x402010,-0x8(%rbp)
为什么它只有一句“movq $0x402010,-0x8(%rbp)”? 上面我们说0x3535是"55",但是这个"0x402010"是什么?它显然不等于"aaaaa",而且我们也没看出程序有把"aaaaa"存放进a的过程。
因为这个“$0x402010”是一个地址,这个地址指向的是"aaaaa"这个字符串实际存储的位置,那这个位置在哪呢?
# objdump -sj .rodata mytest mytest: file format elf64-x86-64 Contents of section .rodata: 402000 01000200 00000000 00000000 00000000 ................ 402010 61616161 6100613d 25732c20 623d2573 aaaaa.a=%s, b=%s 402020 0a00
看到“402010: 616161616100 .....”了么?0x61就是ascii的'a',0x00就是' '。所以0x402010就是[aaaaa ]的地址。这段内容属于只读的数据段,注意“只读”两个字,代表它不可写。
接写来看赋值部分b[0] = 'f':
b[0] = 'f'; 401147: c6 45 f5 66 movb $0x66,-0xb(%rbp)
参考上面我给出的main函数栈中b数组的位置,-0xb(%rbp)就是b[0],0x66就是'f'。所以这句很明显就是b[0] = 'f';
再看a[0] = 'f':
a[0] = 'f'; 40114b: 48 8b 45 f8 mov -0x8(%rbp),%rax 40114f: c6 00 66 movb $0x66,(%rax)
“-0x8(%rbp)”存储的是*a,也就是上面我们说的地址0x402010,我们先把这个地址放在AX寄存器中。然后寻址AX寄存器的内容为地址的地址空间,也就是0x402010对应的地址空间,也就是第一个"a"的位置,程序尝试向这个位置写入0x66(也就是'f')。上面我们已经说了,0x402010属于只读数据段,必然不允许写入,所以这这个写入操作就是一个非法的访问。所以直接触发了Segmentation fault。
所以char *a = "hello"; 和char a[] = "hello";的区别就是,char *a = "hello";得到的是一个指向"hello"所在只读空间的指针变量。而char a[] = "hello"得到的是一个栈上的数组。
说到这里我觉得你应该懂了,strcpy(a, b)就是读取字符串b的内容,写入a,当你的a对应的是只读空间时,必然不允许写入。
评论区 Kevin Yang 同学问道:“那函数的形参写char* str和char str[]有区别吗[好奇]”
我觉得这是一个好的引申问题。对于C语言来说(我没说别的语言),当我们声明一个函数的参数是一个数组的时候,我们实际上得到的是一个指针。C语言没有传递数组的方式,通常都是以指针的形式传递。所以char *str和char str[]作为函数形参是没有区别的。
如下面的例子:
#include <stdio.h> void func1(char *a) { a++; printf("func1: %s
", a); } void func2(char a[]) { a++; printf("func2: %s
", a); } int main(int argc, char *argv[]) { char *a = "abcdef"; func1(a); func2(a); return 0; }
编译执行:
# gcc -o mytest mytest.c -Wall -g # ./mytest func1: bcdef func2: bcdef
在来看看参数是怎么传递和存储的:
# objdump -DS mytest ... void func1(char *a) { 401126: 55 push %rbp 401127: 48 89 e5 mov %rsp,%rbp 40112a: 48 83 ec 10 sub $0x10,%rsp 40112e: 48 89 7d f8 mov %rdi,-0x8(%rbp) a++; 401132: 48 83 45 f8 01 addq $0x1,-0x8(%rbp) printf("func1: %s
", a); ... ... void func2(char a[]) { 401150: 55 push %rbp 401151: 48 89 e5 mov %rsp,%rbp 401154: 48 83 ec 10 sub $0x10,%rsp 401158: 48 89 7d f8 mov %rdi,-0x8(%rbp) a++; 40115c: 48 83 45 f8 01 addq $0x1,-0x8(%rbp) printf("func2: %s
", a); ... ... int main(int argc, char *argv[]) { 40117a: 55 push %rbp 40117b: 48 89 e5 mov %rsp,%rbp 40117e: 48 83 ec 20 sub $0x20,%rsp 401182: 89 7d ec mov %edi,-0x14(%rbp) 401185: 48 89 75 e0 mov %rsi,-0x20(%rbp) char *a = "abcdef"; 401189: 48 c7 45 f8 26 20 40 movq $0x402026,-0x8(%rbp) 401190: 00 func1(a); 401191: 48 8b 45 f8 mov -0x8(%rbp),%rax 401195: 48 89 c7 mov %rax,%rdi 401198: e8 89 ff ff ff callq 401126 <func1> func2(a); 40119d: 48 8b 45 f8 mov -0x8(%rbp),%rax 4011a1: 48 89 c7 mov %rax,%rdi 4011a4: e8 a7 ff ff ff callq 401150 <func2> return 0; 4011a9: b8 00 00 00 00 mov $0x0,%eax } 4011ae: c9 leaveq 4011af: c3 retq
首先字符串的首地址是放在main函数栈的-0x8(%rbp)这个位置的:
char *a = "abcdef"; 401189: 48 c7 45 f8 26 20 40 movq $0x402026,-0x8(%rbp)
然后在调用func1(a)和func2(a)前,程序将这个指针保存在%rdi寄存器里(我没有开优化,这里多绕了一下,但是最好还是在rdi寄存器里。因为rax寄存器通常用于保存返回值。):
40119d: 48 8b 45 f8 mov -0x8(%rbp),%rax 4011a1: 48 89 c7 mov %rax,%rdi
然后就是调用func1()和func2()。进入func1或func2时先是一顿栈操作,预留了func1/2的栈空间。
401126: 55 push %rbp 401127: 48 89 e5 mov %rsp,%rbp 40112a: 48 83 ec 10 sub $0x10,%rsp
然后我们上面说了我们把字符串的地址保存在了rdi寄存器里,接着func1/2就把这个地址从rdi寄存器里取出来,保存到func1和func2各自的栈的-0x8(%rbp)位置。
401158: 48 89 7d f8 mov %rdi,-0x8(%rbp)
注意这里两个函数的-0x8(%rbp)都是相对于各自的栈来说的,是两个不一样的位置,而且和main函数的-0x8(%rbp)也是不一样的。欲了解函数调用和返回过程,请参考:
(进阶可参考: 醉卧沙场:递归函数的堆栈操作)
更多内容可参考:醉卧沙场:README - 专业性文章及回答总索引
我在这里不多叙述函数调用的原理了,接着说这个问题。
上面看到func1和func2都把地址参数保存在了各自的栈的临时变量里,然后对各自的变量进行a++操作:
a++; 40115c: 48 83 45 f8 01 addq $0x1,-0x8(%rbp)
最后都打印出来。
可以看到不管是将参数写成char *a还是写成char a[],编译出来的都没有区别。当然不同的编译器以及不同的编译选项都可能造成不同的编译结果(特别是在栈的操作上),但是总体原理是一样的。