百科问答小站 logo
百科问答小站 font logo



为什么char *a="xxxxx", *b="xxx"; strcpy(a, b);的用法不行? 第1页

  

user avatar   zorrolang 网友的相关建议: 
      

你知道

       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对应的是只读空间时,必然不允许写入。


char *a和char 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[],编译出来的都没有区别。当然不同的编译器以及不同的编译选项都可能造成不同的编译结果(特别是在栈的操作上),但是总体原理是一样的。




  

相关话题

  编程的时候 命名 方法或变量 词穷了怎么办? 
  未来会不会出现这样的编程语言? 
  为什么很多新型编程语言都抛弃了 C 语言风格的 for 语句? 
  C#中的typeof()是一个函数吗? 
  在元宇宙世界中,我要怎么证明「我是我」? 
  中国计算机专业的大学生相比于美国差在哪里? 
  为什么C语言能长盛不衰? 
  为什么计算机对人类的冲击要远大于其他科学? 
  如何看待 WannaCrypt0r 还没有结束? 
  计算机专业到底是不是“围城”? 

前一个讨论
十六岁,小说写成这个水平可以出版吗?
下一个讨论
有哪些类似「一期一会」的深刻而高雅的,用来刻章送人的词?





© 2024-12-24 - tinynew.org. All Rights Reserved.
© 2024-12-24 - tinynew.org. 保留所有权利