10 字符串常量到底存在哪里?

字符串常量到底存在哪里?

两种创建字符串的方式

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",编译器合并时复用了)

对比 ab 地址相同(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 "不要合并相同的常量"。这时 ab 指向不同的 .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 复制过来,可写
└──────────────────────┘
 高位地址

核心启示

  1. 字符串常量在 .rodata 段,内存只读
  2. char *s = "..." 是指针,指向只读区,不能修改
  3. char s[] = "..." 是数组,从 .rodata 拷贝到栈上,可以修改
  4. 相同字面量编译器会合并------指向同一地址,节省空间
  5. 函数返回的字符串常量也不能修改------同样指向 .rodata
  6. objdump -s -j .rodata 可以直观查看常量内容

下期预告

函数调用时,参数怎么传的?局部变量怎么分配的?返回地址存在哪?为什么递归太深会栈溢出?

下一期《函数调用时栈上发生了什么?》,我们进入第三篇章------函数调用。


更多内容详见专栏

相关推荐
05候补工程师2 小时前
【编译原理】自顶向下语法分析深度解析:从 LL(1) 文法判定、改写到预测分析表
经验分享·笔记·考研·自然语言处理
优化控制仿真模型2 小时前
2026年初中英语考纲词汇表(1600词)PDF电子版
经验分享·pdf
Aurorar0rua2 小时前
CS50 x 2024 Notes C - 09
c语言·开发语言·学习方法
相醉为友3 小时前
040 Linux/裸机/RTOS 项目开发的跨平台兼容性——C语言静态接口抽象底层原理分析
linux·c语言·mcu
草履虫君3 小时前
windows系统装机,小白win10装机教程wepe模式,包括系统盘怎么制作,bios怎么设置
windows·经验分享
weixin_421725264 小时前
2026年C/C++/C#全解析:底层语言的进化与场景抉择,选错直接掉队
c语言·c++·c·编程语言·技术选择
bucenggaibian5 小时前
Nearoh:9年开发者从零造语言,Python的简洁+C的性能
c语言·python·开发者·编程语言·nearoh
水饺编程5 小时前
第5章,[标签 Win32] :设备的尺寸(三)
c语言·c++·windows·visual studio
智者知已应修善业5 小时前
【51单片机从奇数始再转偶数逐一点亮并循环】2023-9-8
c++·经验分享·笔记·算法·51单片机