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

下期预告

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

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


更多内容详见专栏

相关推荐
时间的拾荒人1 天前
C语言字符函数与字符串函数完全指南
c语言·开发语言
持力行1 天前
C/C++ 中的 char*:它标识数组吗?为什么能用下标访问?
c语言·c++
BomanLj1 天前
NSK W1202KA-3P-C3Z5 不锈钢精密滚珠丝杠技术规范
经验分享·规格说明书
小陈的代码之路1 天前
回文链表(LeetCode 234)C语言最佳解题思路
c语言·leetcode·链表
aaaameliaaa1 天前
计算斐波那契数(递归、迭代)(1,1,2,3,5.....)
c语言·开发语言·笔记·算法·排序算法
BomanGe41 天前
NSK百吨级超重载高速静音丝杠技术详解
经验分享·规格说明书
GMICLOUD1 天前
GMI Cloud 登陆 WAIC 2026 领航舞台,透传全栈全球化 AI 基建,联合媒体及多家企业发布AI出海白皮书
经验分享
Bomangedd1 天前
NSK滚珠丝杠RNFTL3232A3S技术手册
经验分享·规格说明书
郭泽斌之心1 天前
给 AI 交易助手做 LLM 网关:多通道负载均衡 + 静默失败自动切换
人工智能·经验分享·ea·mt5·fay数字人·easydeal
中云DDoS CC防护蔡蔡1 天前
短信验证码被攻击怎么办
运维·经验分享·http·网络安全·微信