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 可以直观查看常量内容

下期预告

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

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


更多内容详见专栏

相关推荐
热心网友俣先生2 小时前
2026国赛C题:五年命题规律与预测
java·c语言·前端·数学建模
wuminyu2 小时前
Linux内核线程调度与虚拟线程调度机制系统级深度剖析
java·linux·c语言·jvm·c++
COHESION_20103 小时前
分级净化+选择性树脂工艺 在地下水轻微硝酸盐超标治理中的应用
经验分享·其他
jedi-knight3 小时前
Codex接入本地QWen3.8模型
人工智能·经验分享
神明不懂浪漫3 小时前
【第二章】库、表、增删改查操作
开发语言·数据库·经验分享·笔记
总线通信小百科3 小时前
挖掘机能联网吗?移动机械物联网数据采集实战指南
经验分享
牛马之星4 小时前
天然气计量用什么流量计?气体涡轮、罗茨流量计选型与厂家推荐
经验分享
ysu_03144 小时前
03-双链表与循环链表
c语言·数据结构·链表
BestHeaker4 小时前
SQL Server 存储过程实战:从破屏率统计看区间匹配与配置化设计(二)
经验分享·制造业·mes
是隼人5 小时前
buuctf-pwn pwnable_start(ret2shellcode)题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门