摘要: 通过几段小程序深入分析了C语言中字面字符串(literal string)的特点以及正确的使用方式。
#include <stdio.h>
int main() {
printf("Hello!\n"); //Hello!
return 0;
}
本文出现的所有代码的测试环境均为运行32-bit Debian Linux操作系统的Raspberry Pi 3
#include <stdio.h>
int main() {
char *s = "Hello!\n";
printf(s); //Hello!
return 0;
}
#include <stdio.h>
int main() {
char *s = "Hello!\n";
*s = 'B'; //crash here
printf(s);
return 0;
}
运行到*s = 'B'这句时进程异常退出, 错误信息为Segmentation fault,看上去有些奇怪,但我们先将这个问题放在一边,继续看后面几种变形。
变形#3, 在一个全部变量和一个局部变量中定义两个完全一样的字面字符串,观察这两个字符串所在的位置
#include <stdio.h>
char *gs = "Hello!\n";
int main() {
char *s = "Hello!\n";
printf("%p,%p\n", gs, s); //0x104dc,0x104dc
return 0;
}
这两个指针的所指向的位置是完全一样的!也就是说,即使代码中定义了多个相同的字面字符串,C编译器实际上也仅生成了一份拷贝。
#include <stdio.h>
#include <unistd.h>
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信号并因此退出。
#include <stdio.h>
#include <unistd.h>
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;
}
7e9b6000-7e9d7000 rwxp 00000000 00:00 0 [stack]
于是,程序正常打印出"Bello!"。显然,还存在一种不使用栈空间而使用堆空间的变形,该变形的实现不在这里描述,留给读者作为练习。
#include <stdio.h>
#include <sys/mman.h>
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;
}
2125

被折叠的 条评论
为什么被折叠?



