char*和char[]

一直就认为C中的char*和char几乎是一样的,要是说区别,那就是char[]在声明之后,地址值是只读的,而char*的值是可变的。

不过今天看到两个程序:

A:

char* get_str(void)
{
char str[] = {“abcdefghijk”};
return str;
}
int main(int argc, char* argv[])
{
char* p = get_str();
printf(“%sn”,p);
return 0;
}

B:

char* get_str(void)
{
char *str = {“abcdefghijk”};
return str;
}
int main(int argc, char* argv[])
{
char* p = get_str();
printf(“%sn”,p);
return 0;
}

理论上,两个程序好像都不可以,因为都是返回一个局部变量。可结果是,第一个有warning,运行的确什么都没有打印出来;而第二个一切正常,能打印出字母来。这是为什么呢?我的gcc版本是 version 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)

将c代码转化成汇编:

gcc-local-varibles.png
我们可以看出,两个程序只有在get_str 函数有区别,前者是将.LC0,即字符串重新复制了一遍,进行修改;而后者直接将字符串的地址赋给了局部变量。movl -4(%ebp), %eax 则是将这个地址返回给main。

看来gcc编译器不是像我们想象的那样,在函数中生成一个字符串,在进行操作;而是有一个全局的字符串,如果是char[],则会生成一个副本,如果是char*,则只是简单的生成一个引用。

可是问题没有结束,关键在于这个字符串是在 .section .rodata下面的,也就是只读的,有意思的是如果我们将B中的get_str改成:

char* get_str(void)
{
char *str = {“abcdefghijk”};
str[1]=’p’;
return str;
}

之后,运行会出现 Segmentation fault,而进行相同修改的A代码可以运行,但是打印出来的是无意义的字符串。

这个问题解决了,还有一些很有意思的东西。gcc 现在的版本为了加快运行速度,都将变量按照16字节边界存储。例如下面的代码:

void doit(void)
{
}
int main(int argc, char* argv[])
{
char a[10];
doit();
return 0;
}

生成的汇编代码main部分为:

main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $16, %esp
call doit
addl $16, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
我们可以发现main函数在初始化的时候 预留了16个字节的空间给局部变量。如果我们在main中再添加一个char b,结果还是16个字节,因为gcc会将所有变量总共需要的空间算出来,再转化为16的倍数。是不是所有的情况都是这样呢?看看下面的代码:

void doit(int n)
{
}
int main(int argc, char* argv[])
{
char a[120];
doit(9);
return 0;
}
这个时候相关汇编代码变成了:

subl $132, %esp
movl $9, (%esp)
call doit
movl $0, %eax
addl $132, %esp
为什么是132而不是128呢?因为main函数调用get_str的时候需要预先填入其所需的参数,然后才执行call填入IP,所以gcc就偷懒一起留了,因为 参数是一个int,为4个字节,所以总共需要留出128+4=132个字节。

具体的内存分布图参见下图

Leave a Comment

Your email address will not be published. Required fields are marked *