本博文为原创,转载请注明出处 http://blog.csdn.net/lux_veritas/article/details/8227386
前段时间调bug,定位了一个字符串赋值的错误,偶然发现了一个有趣的现象,于是乎抽象出一个特定的场景,把问题扩展开来,分析了一个由memcpy函数引发的c常见指针问题和勿用,形成了本篇流水账。
问题描述:
main要打印一个字符串,其中调用copy_string函数,该函数完成的操作是将制定内存区中的字符串拷贝到一个临时字符串中,并返回给main用于赋值。源码如下:
#include <stdio.h> #include <string.h> char* copy_string(char *string) { char *temp; //memcpy(temp,string,10); // 1 //memcpy(*temp,*string,10); // 2 //memcpy(&temp,&string,10); // 3 return temp; } int main() { char *show,*result; show="haha show"; result="ca"; result = copy_string(show); printf("string : %s\n",result); return 0; }
做三次试验,每次取消// #的行首注释,编译执行,看看会发生什么有趣的现象
第一次编译取消//1行首注释,编译通过,无错误信息,执行时段错误。dmesg一下,error code 7
[guohongwei@gc13:c_test]$ gcc -o play string.c-g [guohongwei@gc13:c_test]$ ./play Segmentation fault [guohongwei@gc13:c_test]$ dmesg … … play[18381]: segfault at 0000003c2401bbc0rip 00000000004004ab rsp 00007fff061b2830 error 7
这里首先说明一下执行阶段错误码error code由三个位组成,从高到底分别为bit2 bit1 bit0,所以它的取值范围是0~7。
bit2:值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
bit1:值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
bit0:值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址。
7直观翻译为“用户态写一个没有权限的非法地址”
此处为非常典型的“悬垂指针”错误,变量temp为copy_string函数内部声明的局部变量(自动变量),分配在运行时栈空间上,随着函数返回栈被回收,随时可能在任何时候被覆盖,temp变量不再存在,temp指针不再指向任何区域,而是“悬垂”在程序地址空间的无效指针。悬垂指针的解决办法是将temp声明为静态变量,即添加static关键字,这样temp在程序编译的阶段即被分配给程序数据段,在程序的整个生命周期过程中中始终存在。如果读者以为这就搞定了,就大错特错了,重新编译链接加载执行:
play[28336]: segfault at 0000000000000000rip 00000000004004ae rsp 00007fff8bd8bc30 error 6
6为“用户态写一个无效地址”
此处为指针初始化错误。仅仅声明了*temp,却没有说明temp指向哪里,即仅作了声明却没有对其初始化。仅仅声明一个指针变量操作系统并不会为其在内存区中创建存储空间。解决办法就是进行指针初始化。有以下两种初始化方式:
1. static char* temp = “ca”; 2. char tmp[20]; temp = &tmp;
若采用第一种方式,抱歉,error 7。第二种方式,编译信息string.c:8: warning: initialization from incompatible pointer type,虽然有warning,但是毕竟执行成功了,来看一看原因。char* temp声明的其实是一个指向字符串常量的指针,编译时将其安排在输出文件的只读数据段中,对只读段的写操作肯定发生segfault。第二种方式产生warning的原因后文详述。所以,其实上面的代码其实是很丑陋的,为了运行,将char* temp变为static char temp[20]。所以,这即是memcpy()函数传参的注意事项,不能将字符串常量指针作为memcpy所要更改的内存区域起始地址。
这回取消//2的行首注释,来看一下编译信息、执行结果和dmesg:
[guohongwei@gc13:c_test]$ gcc -o play string.c-g string.c:11: warning: passing argument 1 of'memcpy' makes pointer from integer without a cast string.c:11: warning: passing argument 2 of'memcpy' makes pointer from integer without a cast [guohongwei@gc13:c_test]$ ./play segment fault [guohongwei@gc13:c_test]$ dmesg … … play[30880]: segfault at 0000000000000000rip 00000000004004b5 rsp 00007fff9d684330 error 4
4为“用户态读一个无效地址”
对参数string取值,*string即字符串常量“haha show”中的“h”,ASCII码为104,转化为地址0x00000068,该地址处根本没有映射内存页面,故为指向无效地址。
接下来进入本文的高潮部分,一个有意思的现象:
取消//3的行首注释,编译执行,查看log信息
[guohongwei@gc13:c_test]$ gcc -o playstring.c -g [guohongwei@gc13:c_test]$ ./play string : haha show [guohongwei@gc13:c_test]$
dmesg没有错误输出信息,并且打印结果也正确,但是我们知道程序的本意是想把从show开始的一段内存区内容copy给result,将result输出,//1的形式是正确的,//3的赋值形式肯定是错误的,那为什么会产生正确的结果呢?这就要从指向字符串的指针与指针数组的关系来说明。为方便,笔者画了下图:
初始情况声明了两个指向字符串的指针result和show,其中show已经初始化指向一个字符串常量,该常量在内存中某一区域。最初变量在内存中以数字形式存在,为了便于标识,于是用符号来为变量命名,指针变量本身也是一个符号,存储在内存中,只不过它的值是一个地址。所以如图result和show在内存中的某两个位置存放,show 和result 本身为两个地址值,指向某两块内存区域。&result,&show为二级指针,分别为result、show这两个指针变量所在内存区的地址。在本程序的调用关系中,根据memcpy函数的定义,是将从“&show”地址开始的一段内存区内容拷贝到“&result“地址开始的一段内存区中,所以实际是把show的值赋给了result,而result本身是一个指针,所以它便指向了show指向的区域。即result自身的指向发生了变化,而不是它所指内存区的内容变化了。
为了证实,将程序稍加改动,加几行地址输出:
#include <stdio.h> #include <string.h> char* copy_string(char *string) { //static char tmp[20]; static char *temp; memcpy(&temp,&string,10); //memcpy(temp,string,10); //memcpy(*temp,*string,10); return temp; } int main() { char *show,*result; show="haha show"; result="test"; printf("show point to address : %p\n",show); printf("result point to address : %p\n",result); result = copy_string(show); printf("***result string : %s\n",result); printf("show point to address : %p\n",show); printf("result point to address : %p\n",result); return 0; }
编译成功,输出结果:
[guohongwei@gc13:c_test]$ ./play show point to address : 0x400648 result point to address : 0x400652 ***result string : haha show show point to address : 0x400648 result point to address : 0x400648 [guohongwei@gc13:c_test]$
此处说明result和show指向了相同的地址。同时读者是否注意到,result本来已做了初始化,即指向只读的字符串常量,但是此处却没有产生任何错误,说明并没有对result原来指向的内存区域进行操作,否则必然产生“用户态写无权限地址”。
作为练习,如果把最后一段程序中static char *temp的static去掉,会有什么编译执行问题?给temp赋初值呢?