6. 代码区:只读、函数代码存储
说到代码区,我脑子里第一个蹦出来的印象就是「只读」两个字。你想想看,程序跑起来之后,函数代码要是能被随便改,那不乱套了?
代码区,也叫文本段(Text Segment),是进程虚拟地址空间中一块非常特殊的区域。它存放的就是你写的那些函数指令——编译后的机器码。这块区域在程序加载时就被固定下来,运行期间只读,不可写。
为什么必须是只读的?
这个问题我当年刚学的时候也困惑过。代码放在内存里,为什么不让改?
其实原因很直接:安全。如果代码区可写,恶意程序就能注入代码,改写你的函数逻辑。比如你调用 check_password(),结果它被改成了永远返回 true——那还谈什么安全?
另外,共享也是一个重要原因。同一个程序启动多个进程,它们的代码区可以共享同一块物理内存。如果代码区可写,共享就没法做了——你改了我怎么办?
核心要点:代码区只读,是为了安全 + 共享。这是现代操作系统的基本设计原则。
代码区里到底放了什么?
说白了,就是你的函数。包括:
- 你自己写的普通函数、成员函数
- 静态函数(包括全局静态函数和类静态成员函数)
- 编译器生成的隐含函数(比如构造函数、析构函数、虚函数表的一部分)
- 一些只读的常量数据(比如字符串字面量,在某些平台上)
嗯,这里要注意:字符串字面量在有些编译器里放在代码区,有些放在只读数据区(.rodata)。但不管放哪,都是只读的。我曾经见过新手写 char *p = "hello"; p[0] = 'H';,然后程序崩溃——就是因为试图修改只读内存。
代码区的生命周期
代码区的生命周期和进程一样长。从程序加载开始,到进程退出结束。它不像堆和栈那样动态变化,也不存在「分配」和「释放」的概念。
我个人习惯把内存区域分成三类:
| 区域 | 读写属性 | 生命周期 | 典型内容 |
|---|---|---|---|
| 代码区 | 只读 | 进程生命周期 | 函数指令、部分常量 |
| 栈 | 读写 | 函数调用期间 | 局部变量、函数参数 |
| 堆 | 读写 | 手动管理 | 动态分配的对象 |
代码区与函数指针
函数指针指向的就是代码区的地址。你写 void (*fp)() = &func;,这个 fp 存的就是 func 在代码区中的入口地址。
我在项目中遇到过一个问题:有人试图通过函数指针修改函数行为,结果段错误。原因就是函数指针指向只读区域,写操作不被允许。
小技巧:如果你需要动态替换函数行为,别想着改代码区。用虚函数、函数对象、或者 std::function 来实现多态,这才是正道。
代码区的大小
代码区的大小在编译时就确定了。你写的代码越多,代码区就越大。但要注意:代码区大小不等于可执行文件大小。可执行文件里还包含了符号表、调试信息、资源数据等,这些不会全部加载到代码区。
我曾经优化过一个嵌入式项目,代码区快满了。那时候我一个个函数看,把没用的调试日志、死代码删掉,硬是腾出了 30% 的空间。嗯,代码区虽小,但寸土寸金。
代码区与多线程
多线程环境下,所有线程共享同一个代码区。这意味着:
- 函数代码只有一份,所有线程都能执行
- 不需要为每个线程复制代码
- 代码区不需要加锁——因为是只读的
你想想看,如果每个线程都复制一份代码,内存开销得多大?共享代码区是操作系统做的一个非常聪明的设计。
避坑指南
我曾经踩过一个坑:在嵌入式系统里,把一个大数组定义成局部变量,结果栈溢出了。后来我把数组改成 static const,它就被放到了代码区(或者只读数据区),栈的压力瞬间没了。
所以,如果你有只读的大块数据,比如查找表、配置数据,不妨考虑用 static const 定义。这样它们会进入只读区域,既安全又节省栈空间。
警告:不要试图用 const_cast 去掉 const 属性后修改只读区域的数据。这是未定义行为,程序可能崩溃,也可能悄无声息地出错——比崩溃更可怕。
代码区的内存布局
为了让你更直观地理解,我画了一张图:
从这张图可以看得很清楚:代码区在最底部(低地址),向上依次是数据区、堆区、栈区。栈区在高地址,向下增长。堆区向上增长。这样设计是为了让堆和栈能灵活使用中间的地址空间。
总结
代码区是程序运行的基础。它只读、共享、生命周期与进程一致。理解代码区,能帮你更好地理解函数指针、多线程、内存安全这些概念。
我个人觉得,搞懂代码区是理解整个内存模型的起点。你想想看,如果连代码放哪、能不能改都不清楚,后面谈堆栈管理、智能指针,根基就不牢。
嗯,这一章就到这里。代码区虽然简单,但它是整个程序的「骨架」。骨架稳了,后面的血肉才好往上长。
一句话记住:代码区只读,函数代码安家。别想着改它,用多态才是正道。