谈谈C语言的字面字符串

阅读更多
摘要: 通过几段小程序深入分析了C语言中字面字符串(literal string)的特点以及正确的使用方式。

如果对C语言的字面字符串(literal string)缺乏足够的了解,编程时不注意它的特点,就可能会遇到一些略显奇怪的状况。本文对下面这段简单的代码加以几个简单的变形,再分别分析它们的输出,最后总结出字面字符串的特点和编程时需要注意的地方。

#include
int main() {
    printf("Hello!\n"); //Hello!
    return 0;
}
本文出现的所有代码的测试环境均为运行32-bit Debian Linux操作系统的Raspberry Pi 3

变形#1,声明一个局部字符类型指针指向字面字符串

#include
int main() {
    char *s = "Hello!\n";
    printf(s); //Hello!
    return 0;
}
依然输出Hello!, 符合预期。

变形#2, 修改字符串的第一个字符为'B'

#include
int main() {
    char *s = "Hello!\n";
    *s = 'B'; //crash here
    printf(s);
    return 0;
}
运行到*s = 'B'这句时进程异常退出, 错误信息为Segmentation fault,看上去有些奇怪,但我们先将这个问题放在一边,继续看后面几种变形。

变形#3, 在一个全部变量和一个局部变量中定义两个完全一样的字面字符串,观察这两个字符串所在的位置

#include
char *gs = "Hello!\n";
int main() {
    char *s = "Hello!\n";
    printf("%p,%p\n", gs, s); //0x104dc,0x104dc
    return 0;
}
这两个指针的所指向的位置是完全一样的!也就是说,即使代码中定义了多个相同的字面字符串,C编译器实际上也仅生成了一份拷贝。

变形#4, 考察字面字符串所在地址的内存访问权限。

#include
#include
char *gs = "Hello!\n";
int main() {
    char *s = "Hello!\n";
    printf("%p,%p\n", gs, s); //0x10518,0x10518
    sleep(100000);
    return 0;
}
先让代码#4打印出那两个相同的地址后长时间sleep,再趁它熟睡时通过ps命令查到该进程的pid为27612,然后查看/proc/27612/maps文件就获得了该进程的内存映射信息,其中第一行为

00010000-00011000 r-xp 00000000 b3:07 933808     /home/pi/a.out
这说明从地址0x10000开始的长度为4k的区域(恰好是一个页面的大小)是只读的,如果进程试图写入这块只读区域,就会触发操作系统的内存异常访问保护从而收到SIGSEGV信号并因此退出。

变形#5, 换一种方式来定义字符串。

#include
#include
char *gs = "Hello!\n";
int main() {
    char s[] = "Hello!\n";
    printf("%p,%p\n", gs, s); //0x10538,0x7e9d6360
    *s = 'B';
    printf(s); //Bello!
    sleep(100000);
    return 0;
}
将char *s改为char s[]后,编译器会在栈上分配一块和字符串"Hello!\n"同样大小的内存并它将复制进去。采用和变形#4同样的考察办法也能看出指针s的值0x7e9d6360是一个指向栈内存的地址,并且栈内存是可读写的:

7e9b6000-7e9d7000 rwxp 00000000 00:00 0          [stack]
于是,程序正常打印出"Bello!"。显然,还存在一种不使用栈空间而使用堆空间的变形,该变形的实现不在这里描述,留给读者作为练习。

变形#6, 改变内存访问权限。

#include                                                                                                                
#include                                                                                                             
char *gs = "Hello!\n";                                                                                                           
int main() {                                                                                                                     
  char *s = "Hello!\n";                                                                                                          
  //align to page boundary then make the page writable                                                                           
  void *page = (void *)((unsigned long)s & 0xffff1000);                                                                          
  if (mprotect(page, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC)) {                                                              
    perror("mprotect");                                                                                                          
  }                                                                                                                              
  *s = 'B';                                                                                                                      
  printf(s); //Bello!                                                                                                            
  printf(gs); //Bello!                                                                                                           
  return 0;                                                                                                                      
}                                                                                                                                
通过调用mprotect()函数将原本只读的内存页设为可写的,我们实现了对字面字符串的直接修改!但是,这种方式的副作用是巨大的,会令所有指向该字符串的指针都被影响,例如,在上面的代码中,通过指针s将'H'改为'B'后指针gs指向的内容也一起被改变了。由于这样的原因,在实际编程中极少会将一个原本只读的代码页改为可写的。相反,在调查某块不应被修改的内存区域被意外改写的bug时,可以将本来可写的内存页面设置为不可写,让有bug的代码由于触发内存访问异常而暴露出来。
http://click.aliyun.com/m/23316/

你可能感兴趣的:(编程,c)