字符串常量到底存在哪里?
两种创建字符串的方式
c
char *s1 = "Hello"; // 指针,指向常量区
char s2[] = "Hello"; // 数组,复制到栈上
s1是指针,指向 只读存储区s2是数组,内容 复制到了栈上
修改 s1[0] = 'X' 会导致段错误(SIGSEGV)。
你以为的
字符串存在哪里无所谓,能用就行。
实际情况:.rodata 只读段
C 程序编译成 ELF 文件后,段分类如下:
| 段 | 内容 | 权限 |
|---|---|---|
.text |
代码(机器指令) | r-x |
.rodata |
字符串常量、const 变量、printf格式化串 | r-- |
.data |
已初始化的全局变量 | rw- |
.bss |
未初始化的全局变量 | rw- |
.rodata(read-only data)段本质上是只读内存,任何修改尝试都会触发段错误。
为什么 char *s = "Hello" 是只读的?
- C 标准规定修改字符串常量是未定义行为(UB)
- 链接器把字符串常量放在了
.rodata段 - 操作系统将
.rodata映射为只读页面(MMU 级别保护) - 写入时 CPU 触发页错误 → 内核发送 SIGSEGV → 进程崩溃
栈上的字符数组
c
char s2[] = "Hello";
s2[0] = 'X'; // 完全OK,因为 s2 在栈上
s2 是局部变量,内容从 .rodata 复制到了栈上,所以可以修改。
动手验证
保存为 string_lit.c:
c
#include <stdio.h>
#include <string.h>
char *global_str1 = "Hello";
char global_str2[] = "Hello";
const char *func_str() {
return "Constant string from function";
}
int main() {
const char *s1 = "Hello World";
char s2[] = "Hello World";
printf(" s1 = %p (指针指向 .rodata)\n", (void*)s1);
printf(" s2 = %p (数组在栈上)\n", (void*)s2);
printf(" \"Hello World\" 字面量 = %p\n", (void*)"Hello World");
printf("\n【修改测试】\n");
printf(" 尝试修改 s1[0] 会导致段错误!\n");
/* 如果你好奇段错误长什么样,取消下面这行注释再跑一次 */
/* s1[0] = 'X'; */ // ← 取消注释会崩溃
s2[0] = 'X';
printf(" s2 现在是: \"%s\"\n", s2);
printf("\n【比较字符串常量地址】\n");
const char *a = "Hello";
const char *b = "Hello";
const char *c = "World";
printf(" a = %p, b = %p\n", (void*)a, (void*)b);
printf(" a == b: %s\n", a == b ? "是" : "否");
printf(" a == c: %s\n", a == c ? "是" : "否");
printf("\n【全局变量中的字符串】\n");
printf(" global_str1 = %p (-> \"%s\")\n", (void*)global_str1, global_str1);
printf(" global_str2 = %p (-> \"%s\")\n", (void*)global_str2, global_str2);
printf("\n【作为函数返回值】\n");
const char *ret = func_str();
printf(" func_str() 返回: %p -> \"%s\"\n", (void*)ret, ret);
printf("\n【字符串大小】\n");
printf(" strlen(\"Hello\") = %zu\n", strlen("Hello"));
printf(" sizeof(\"Hello\") = %zu (含结尾的 \\0)\n", sizeof("Hello"));
return 0;
}
编译并运行:
bash
$ gcc -o string_lit string_lit.c
$ ./string_lit
命令解释:gcc -o string_lit string_lit.c 用 GCC 编译器将源文件编译链接成可执行文件 string_lit(-o 指定输出名称)。./string_lit 运行它。
运行结果:
s1 = 0x5bf23f735061 (指针指向 .rodata)
s2 = 0x7fff5664b72c (数组在栈上)
"Hello World" 字面量 = 0x5bf23f735061
【修改测试】
尝试修改 s1[0] 会导致段错误!
s2 现在是: "Xello World"
【比较字符串常量地址】
a = 0x5bf23f735008
b = 0x5bf23f735008
a == b: 是 (相同字面量,编译器会合并)
a == c: 否
【全局变量中的字符串】
global_str1 = 0x5bf23f735008 (-> "Hello")
global_str2 = 0x5bf23f737010 (-> "Hello")
global_str1 指向 .rodata(只读)
global_str2 在 .data(可写,但内容是副本)
【作为函数返回值】
func_str() 返回: 0x5bf23f73500e -> "Constant string from function"
【字符串大小】
strlen("Hello") = 5
sizeof("Hello") = 6 (含结尾的 \0)
用 objdump 直接查看 .rodata 段
想亲眼看到字符串常量在文件里的位置?用 objdump 反汇编 .rodata 段:
bash
$ objdump -s -j .rodata string_lit
命令解释:objdump 是反汇编工具,-s(display full contents)显示指定段的完整内容,-j .rodata(section)指定只显示 .rodata 段。
运行结果:
Contents of section .rodata:
2000 01000200 48656c6c 6f20576f 726c6400 ....Hello World.
2010 48656c6c 6f00436f 6e737461 6e742073 Hello.Constant s
2020 7472696e 67206672 6f6d2066 756e6374 tring from funct
2030 696f6e00
可以看到:
| 偏移 | 内容 | 说明 |
|---|---|---|
| 0x2008 | 48 65 6c 6c 6f 20 57 6f 72 6c 64 00 |
Hello World\0 |
| 0x2014 | 48 65 6c 6c 6f 00 |
Hello\0(注意"Hello"出现两次,但实际 .rodata 中"Hello World"里包含"Hello",编译器合并时复用了) |
对比 a 和 b 地址相同(0x5bf23f735008),说明编译器确实只存了一份。
主动触发段错误
修改代码取消注释 /* s1[0] = 'X'; */,重新编译运行:
bash
$ gcc -o string_lit_crash string_lit.c
$ ./string_lit_crash
Segmentation fault (core dumped)
这就是修改 .rodata 只读内存的下场。
关闭常量合并
默认情况下,相同的字符串常量会被合并(-fmerge-constants)。你也可以关闭它看看区别:
bash
$ gcc -fno-merge-constants -o string_lit_nomerge string_lit.c
$ ./string_lit_nomerge
命令解释:-fno-merge-constants 告诉 GCC "不要合并相同的常量"。这时 a 和 b 指向不同的 .rodata 地址。
用图示理解
内存布局:
低地址
┌──────────────────────┐
│ .text (代码段) │ ← r-x
├──────────────────────┤
│ .rodata (只读数据段) │ ← r--
│ "Hello World\0" │ 修改会崩溃
│ "Hello\0" │ 编译器会合并
│ "Constant string..."│
├──────────────────────┤
│ .data (已初始化数据段)│ ← rw-
│ global_str2 = "Hello"│ 这是 .data 中的副本
├──────────────────────┤
│ 栈 (.stack) │ ← rw-
│ s2 = "Xello World\0" │ 从 .rodata 复制过来,可写
└──────────────────────┘
高位地址
核心启示
- 字符串常量在 .rodata 段,内存只读
char *s = "..."是指针,指向只读区,不能修改char s[] = "..."是数组,从 .rodata 拷贝到栈上,可以修改- 相同字面量编译器会合并------指向同一地址,节省空间
- 函数返回的字符串常量也不能修改------同样指向 .rodata
- 用
objdump -s -j .rodata可以直观查看常量内容
下期预告
函数调用时,参数怎么传的?局部变量怎么分配的?返回地址存在哪?为什么递归太深会栈溢出?
下一期《函数调用时栈上发生了什么?》,我们进入第三篇章------函数调用。
更多内容详见专栏