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

下期预告

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

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


更多内容详见专栏

相关推荐
fofantasy9 分钟前
NSK LH12AN 微型导轨技术手册
运维·网络·数据库·经验分享·规格说明书
黑科技iOS上架40 分钟前
ios应用被封号后再次上架很难么?
经验分享·ios
x138702859572 小时前
c语言中srtlen(指针使用计算字符长度)、传值和传址调用
c语言·开发语言·算法·visual studio
iCxhust2 小时前
C#进程管理程序
开发语言·汇编·stm32·单片机·c#·微机原理
卡梅德生物科技小能手2 小时前
卡梅德生物科普CD124(IL-4Rα):2型免疫炎症的核心调控靶点
人工智能·经验分享·深度学习
LaughingZhu3 小时前
Product Hunt 每日热榜 | 2026-06-12
人工智能·经验分享·深度学习·神经网络·产品运营
LaughingZhu4 小时前
Product Hunt 每日热榜 | 2026-06-11
人工智能·经验分享·神经网络·html·产品运营
hhcgchpspk4 小时前
汇编语言传递数据和地址的误区
汇编·笔记·nasm·masm
智者知已应修善业4 小时前
【51单片机2个外部中断显示中断历时,初始化8左移3位共阳数码管】2024-6-6
c++·经验分享·笔记·算法·51单片机
BomanGe24 小时前
NSK双滑块定位承载装置技术手册
经验分享·规格说明书