谈谈C语言的字面字符串

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

#include <stdio.h>
int main() {
    printf("Hello!\n"); //Hello!
    return 0;
}

本文出现的所有代码的测试环境均为运行32-bit Debian Linux操作系统的Raspberry Pi 3

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

#include <stdio.h>
int main() {
    char *s = "Hello!\n";
    printf(s); //Hello!
    return 0;
}

依然输出Hello!, 符合预期。

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

#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编译器实际上也仅生成了一份拷贝。

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

#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信号并因此退出。

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

#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;
}

将char *s改为char s[]后,编译器会在栈上分配一块和字符串"Hello!\n"同样大小的内存并它将复制进去。采用和变形#4同样的考察办法也能看出指针s的值0x7e9d6360是一个指向栈内存的地址,并且栈内存是可读写的:

7e9b6000-7e9d7000 rwxp 00000000 00:00 0          [stack]

于是,程序正常打印出"Bello!"。显然,还存在一种不使用栈空间而使用堆空间的变形,该变形的实现不在这里描述,留给读者作为练习。

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

#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;
}

通过调用mprotect()函数将原本只读的内存页设为可写的,我们实现了对字面字符串的直接修改!但是,这种方式的副作用是巨大的,会令所有指向该字符串的指针都被影响,例如,在上面的代码中,通过指针s将'H'改为'B'后指针gs指向的内容也一起被改变了。由于这样的原因,在实际编程中极少会将一个原本只读的代码页改为可写的。相反,在调查某块不应被修改的内存区域被意外改写的bug时,可以将本来可写的内存页面设置为不可写,让有bug的代码由于触发内存访问异常而暴露出来。

结论

事实上,对字面字符串的修改是C语言标准中一个未定义的行为[1], 但各大主流C编译器的实现都是对每个字面字符串仅保留一份只读拷贝,导致试图直接修改它们的代码都会遇到内存保护错误而异常退出。所以,千万不要试图直接修改一个字面字符串,如需要使用一个修改后的字面字符串,应先在栈上或堆上创建一份拷贝,再对这个拷贝进行修改。最后,当定义一个字符类型指针变量指向一个字面字符串时,最好总是给它加上const修饰符,以便编译器能在遇到试图修改一个字面字符串的代码时报错。



[1] https://www.securecoding.cert.org/confluence/display/c/CC.+Undefined+Behavior#CC.UndefinedBehavior-ub_33

时间: 2024-08-24 17:52:49

谈谈C语言的字面字符串的相关文章

Swift语言指南(十)--字符串与字符

原文:Swift语言指南(十)--字符串与字符 字符串是一段字符的有序集合,如"hellow,world"或"信天翁".Swift 中的字符串由 String 类型表示,对应着 Character 类型值的集合. Swift 中的 String 类型为你的编程提供了一个高速的,兼容 Unicode规范 的文本处理方式.Swift 创建和处理字符串的语法轻量可读,与 C 语言的字符串语法颇为相似.字符串的拼接非常简单,只需将两个字符串用 + 运算符相加.字符串的值是否

c语言-C语言指针,字符串复制过程的问题

问题描述 C语言指针,字符串复制过程的问题 下面是字符串复制的代码,str1[]如果限定大小为10,则会溢出,结果是s2正常,s1输出为 u? 请问为什么是这个结果呢? #include #include int main(){ char *s1; char *s2; char str[] = {""How are you?""}; char str1[10]={}; s1 = str; s2 = str1; while ((*s2 = *s1) != ''){ s

c语言-C语言课程设计字符串题目匹配单词,求大神~~~~~~~~~~~~~~~~~

问题描述 C语言课程设计字符串题目匹配单词,求大神~~~~~~~~~~~~~~~~~ 详细题目如下:设有n个单词的字典表(1<=n<=100),计算某单词在字典表中的4种匹配情况(字典中的单词和待匹配的单词长度上限为255):1)i :该单词在字典表中的序号:2)Ei:在字典表中仅有一个字符不匹配的单词不匹配的单词序号:3)Fi:在字典表中多(或少)一个字符(其余字符匹配)的单词序号:4)N:其他情况.当查找时有多个单词符合条件,仅要求第1个单词的序号即可.1)输入文件,文件格式为:n(字典表

c语言-C语言整数转字符串输出

问题描述 C语言整数转字符串输出 #include #include void to_str(n)int n;{ char s[10]; int i = 0; if(n { putchar('-'); n = -n; } do { s[i++] = n%10 +'0'; n/=10; } while(n>0); while(i--)putchar(s[i]);}main(){ int xy; scanf(""%d""&x); to_str(x);}这里

方法-C语言 指针指向字符串的问题

问题描述 C语言 指针指向字符串的问题 用指针指向字符串的方法 求出字符串中所有数字字符的和,我是初学者对这个很模糊,希望能仔细讲解一哈 解决方案 首先就是判断遍历这个字符串,如果该字符是数字字符,则统计. 判断是否为数字字符: ch <='0' && ch>='9' 也可以使用库函数isdigit() 求和的话: num += ch+'0' 解决方案二: 是这样么?也不知道LZ是不是这个意思 #include <cstring>#include <cstdi

VB语言一个关于字符串组合的程序

问题描述 VB语言一个关于字符串组合的程序 这样的数字构成的字符串在VB中怎么穷举? 12*5* 输出12051 12052 ... 12959 解决方案 for i = 0 to 9 for j = 0 to 9 print "12" & i & "5" & j next next

reverse-c语言中有关字符串转置的问题

问题描述 c语言中有关字符串转置的问题 #include int main() { void reverse(char *s); char *s = "abcdefg"; reverse(s); return 0; } void reverse(char *s) { char *end = s; char temp; while(*end){ end++; } end--; while(s < end) { temp = *s; *s++ = *end; *end-- = tem

c语言-关于C语言的问题字符串+1问题

问题描述 关于C语言的问题字符串+1问题 //给定一个字符串,把字符串内的字母转换成该字母的下一个字母,a换成b,z换成a,Z换成A, // 如aBf转换成bCg,字符串内的其他字符不改变,给定函数, //编写函数 void Stringchang(const char*input,char*output) 其中input是输入字符串,output是输出字符串 #include #include void stringchang(const char*input,char*output) { f

c语言-C语言程序在字符串中查找某字符

问题描述 C语言程序在字符串中查找某字符 #include int main(void) { int i, j; char ab[80]; char x; printf("Input a character: "); scanf("%c",&x); printf("Input a string: "); gets(ab); for(i=0;i<16;i++) if (ab[i]==x) j=i; printf("index