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

对比 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 复制过来,可写
└──────────────────────┘
 高位地址

核心启示

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

下期预告

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

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


更多内容详见专栏

相关推荐
晓蛋4 天前
c语言指的是什么意思
c语言·编译器·编程开发·集成开发环境·程序实例
一隅论数智4 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
上海广测检测科技有限公司4 天前
智能平板出口巴西合规详解:Anatel射频、Inmetro电源/电池与LGPD数据合规叠加
经验分享
傲世仙尊4 天前
目录即文件-Ext文件系统收尾篇
linux·c语言
牵猫散步的鱼儿4 天前
重载、重写(覆盖)、重定义区别
c语言
phltxy4 天前
C 语言指针:从内存地址到灵活的数据访问
c语言
FLJwu4 天前
存储与固件量产实战01:运动相机TF卡量产翻车实录!V30≠4K可用,90%OEM踩中的隐形售后坑
经验分享·数码相机·相机
phltxy4 天前
C 语言中的数据存储:从类型到二进制位
c语言
xiaomu001234 天前
床垫选型的技术评估模型:结构、材料、卫生与检测四层口径
经验分享
汉风设计装饰4 天前
智慧办公弱电系统设计:门禁、监控与 IoT 传感器的集成方案
经验分享