栈堆静态区映射区

共 18 题
📑 题目列表 18 题
#
★★★

1. .bss 段为何在 ELF 中只占 size 而非数据?

为什么 .bss 段在 ELF 文件中只占用 size(大小)而不占用实际数据?

  • .bss 段存放未初始化/零初始化的全局变量
  • 存储空间与运行时空间的分离
  • 文件体积与加载效率

.bss 段存放未初始化的全局变量和静态变量(以及显式初始化为 0 的变量),这些变量在加载时统一清零。由于它们的值在文件中没有意义(全是 0),ELF 只需在节头中记录 .bss 的 size(大小),而不在文件里存放实际数据,从而减小文件体积。加载器在进程运行时按 size 分配一段清零的内存(通常映射到 ZERO_PAGE 或 memset 清零)。这是典型的"存储与运行时分离"优化。

全零数据无需存储,用 size 加"加载时清零"的语义即可复现,既省磁盘又省加载流量。这也是 .data 与 .bss 区分的关键:.data 存放有初始值的变量,必须存数据;.bss 存零值变量,只需 size。

#
★★★

2. ELF 中 .text / .data / .bss / .rodata 段的作用?

ELF 中 .text、.data、.bss、.rodata 段的作用分别是什么?

  • 各段的存放内容
  • 权限属性(读/写/执行)
  • 段与虚拟内存映射的关系

.text 存放编译后的可执行指令,权限为只读+可执行(RX),不可写。.data 存放已初始化的非只读全局/静态变量,权限为读写(RW)。.bss 存放未初始化或零初始化的全局/静态变量,只占 size、加载时清零,权限为读写。.rodata 存放只读数据(字符串常量、const 变量、跳转表等),权限为只读(R)。这些段按权限属性合并到不同的 LOAD segment,映射到进程虚拟地址空间。

段分工的本质是"按访问权限分区",使内核能以页粒度设置 R/W/X 权限,从而利用硬件防护(如 NX)提升安全性,并让 .bss 零初始化延迟到加载时。

#
★★★

3. Linux /proc/self/maps 中 [stack] 段的权限与范围?

Linux /proc/self/maps 中 [stack] 段的权限与范围是什么?

  • [stack] 的权限标志
  • 栈的地址范围与增长方向
  • 栈大小与 RLIMIT_STACK

[stack] 段映射进程主线程的栈,通常权限为 rw-p(读、写、无执行)。其地址范围从栈底(高地址)到当前栈顶,栈向下增长(向低地址)。栈的大小受 RLIMIT_STACK 限制(默认 8MB),mmap 得到的栈区域底部有 guard page。注意:每个线程有独立的栈,多线程时只有主线程的栈显示为 [stack],线程栈显示为单独的区域(或 [stack:TID])。

[stack] 是虚拟内存映射中的特殊标记,权限反映栈的读写特性;范围随栈增长动态变化,可通过 /proc/self/maps 观察到栈顶位置随函数调用深度变化。

#
★★★

4. init 与 fini 数组在 ELF 加载时如何触发 constructor/destructor?

init 与 fini 数组在 ELF 加载时如何触发 constructor(构造函数)和 destructor(析构函数)?

  • .init_array / .fini_array 段
  • 动态链接器对数组的遍历
  • 与 .init/.fini 函数段的区别

现代 ELF 使用 .init_array 和 .fini_array 段存放函数指针数组。动态加载/链接器(ld.so)在加载共享库或可执行文件时,按顺序遍历 .init_array 中的每个函数指针并调用,从而触发 C++ 的构造函数和 C 的 attribute((constructor)) 函数;在卸载或进程退出时遍历 .fini_array 触发析构函数。旧的 .init/.fini 段存放单个函数(由 _init/_fini 符号标记),为兼容保留。链接器负责在起始代码(crt1.o)中插入对这些数组的调用入口。

数组化设计的价值在于:可容纳任意数量的构造函数,且顺序可控制(按依赖),比早期单函数段更灵活。加载顺序:先 .init/_init,再 .init_array。

#
★★★

5. mmap 区在进程虚拟地址中的位置与共享库加载关系?

mmap 区在进程虚拟地址空间中的位置如何?它与共享库加载有什么关系?

  • mmap 区的位置(栈与堆之间)
  • 共享库通过 mmap 加载
  • 随机基址与 ASLR

在 Linux 进程的虚拟地址空间中,mmap 区位于堆(低地址)与栈(高地址)之间,是共享库、动态链接器、以及 mmap 创建的匿名/文件映射的存放区域。共享库(如 libc.so)和动态链接器(ld-linux.so)通过 mmap 映射到该区域,其地址空间向低地址增长(与堆方向相反)。该区域在现代内核中受 ASLR 随机化,基址随机,从而降低被利用风险。

mmap 区是"动态映射大杂烩",承载共享库加载与 mmap 调用。它位于堆与栈之间,使地址空间按"代码段+数据段+堆→mmap→栈"的经典布局展开。

#
★★★

6. alloca/VLA 在栈上动态分配的原理与风险,为什么大尺寸 alloca 容易触发栈溢出,与 malloc 堆分配的本质差异如何?

alloca/VLA 在栈上动态分配的原理与风险是什么?为什么大尺寸 alloca 容易触发栈溢出?它与 malloc 堆分配的本质差异是什么?

  • alloca 调整栈指针分配
  • 栈大小的限制与溢出风险
  • 与 malloc 堆分配的生命周期/管理差异

alloca 和 VLA(变长数组)在栈上动态分配内存,原理是直接调整栈指针(如 RSP)向下移动,分配的内存随函数返回自动释放,无需手动 free。其风险在于:栈空间有限(RLIMIT_STACK 默认 8MB),大尺寸 alloca 会瞬间耗尽栈空间,导致栈溢出(stack overflow),且由于没有像 malloc 那样有明确的失败返回,可能直接破坏栈或触发 SIGSEGV/未定义行为。本质差异:malloc 从堆分配,受堆管理、可跨函数/线程使用、需显式释放;alloca 从栈分配,生命周期限于当前函数、自动释放、无堆管理器开销。

alloca 的优势是快(无堆锁)、自动释放;劣势是栈容量风险大且不可控。malloc 以堆管理器的开销换取灵活性和大容量。这是"栈=快速但受限,堆=灵活但需管理"的典型权衡。

#
★★

7. stack 与 heap 在进程虚拟地址空间的相对位置?

stack(栈)与 heap(堆)在进程虚拟地址空间的相对位置是什么?

  • 栈在高地址、堆在低地址
  • 两者增长方向相反
  • 经典进程布局

在典型的 Linux 进程虚拟地址空间中,栈位于高地址端(地址从高到低向下增长),堆位于低地址端(在数据段之后,向高地址增长)。两者增长方向相反:栈向下增长(压栈地址递减),堆向上增长(malloc 地址递增),中间隔着 mmap 区。这种相反的增长方向使两者都能向中间未使用区域扩展,最大化利用地址空间。

栈顶高地址、堆顶低地址、相向增长是进程地址空间的经典布局设计,兼顾了地址空间利用与 mmap 区的居中位置。

#
★★

8. 为何 C 全局数组未初始化值默认为 0(bss 段)?

为什么 C 语言中未初始化的全局数组其值默认为 0?

  • .bss 段的重置语义
  • 全局变量与局部变量的差异
  • 操作系统加载时的清零

C 全局变量和静态变量(包括未初始化的数组)存放在 .bss 段。加载器在进程启动时将该段映射到内存并清零,因此未初始化的全局数组默认为 0。这是 C 标准明确规定的语义(静态存储期变量零初始化)。而局部变量存放在栈上,未初始化时是栈上的残留垃圾值(有未定义行为),因为栈不会自动清零。.bss 清零由加载器完成,且因全零无需存储,节省了文件空间。

区分关键:静态存储期(全局/static)零初始化放在 .bss,加载清零;自动存储期(局部)不初始化,值是垃圾。这是 C 语言与许多高级语言(默认全零)的重要差异。

#
★★

9. Linux 中 stack 的 RLIMIT_STACK(默认 8MB)与动态扩展?

Linux 中栈的 RLIMIT_STACK(默认 8MB)与动态扩展机制是什么?

  • RLIMIT_STACK 的默认值与含义
  • 栈的动态扩展(按需缺页)
  • guard page 与溢出检测

RLIMIT_STACK 是资源限制,控制进程栈的最大虚拟地址范围,默认值通常为 8MB(ulimit -s 显示 8192 KB)。栈并非一次性分配 8MB 物理内存,而是采用动态扩展:栈顶向下增长时,访问新地址触发缺页,内核按需分配物理页,直到达到 RLIMIT_STACK 上限。栈底(顶部)下方有 guard page,当栈增长越过 guard page 时,内核触发 SIGSEGV 而非无限扩展,从而检测栈溢出。可用 setrlimit 调整限制。

栈是"按需扩展、上限受限"的虚拟区:动态增长保证小程序不浪费内存,RLIMIT_STACK 与 guard page 保证失控的递归不会无限吃掉内存而是以 SIGSEGV 终止。

#
★★

10. 为何 stack 由 OS 自动管理而 heap 由用户管理?

为什么 stack(栈)由 OS 自动管理而 heap(堆)由用户管理?

  • 栈的 LIFO 特性与自动分配
  • 堆的无序分配与用户介入
  • 两者在开销量上的差异

栈遵循 LIFO(后进先出),分配和释放只在函数调用/返回时发生,操作规律简单(只需移动栈指针),因此 OS/编译器自动管理,无需用户干预,开销极低。堆的分配和释放顺序任意、大小不定,需要自由列表、碎片管理、并发控制等复杂机制,无法由硬件/编译器自动处理,必须由用户(或分配器)显式管理(malloc/free),因此开销量大且易出错。

本质是"访问模式决定了管理方式":栈的规律性使其可自动化,堆的任意性要求显式管理。这也是栈快、堆慢、栈无泄漏风险而堆有泄漏/悬空指针风险的根源。

#
★★

11. 进程地址空间布局,栈、堆、数据段、代码段的典型布局如何?

进程地址空间的典型布局是怎样的?栈、堆、数据段、代码段如何分布?

  • 经典布局从低到高:代码段、数据段、堆、mmap、栈
  • 各段的权限
  • 内核态区段

典型 Linux 进程虚拟地址空间从低地址到高地址依次为:代码段(.text,只读可执行)、数据段(.data/.bss,读写)、堆(向上增长)、mmap 区(共享库/动态映射)、栈(向下增长,高位)。64 位系统下地址空间远大于 32 位,栈和堆之间空间充足。最高地址保留给内核态(如 x86-64 从 0xffff800000000000 起),用户态与内核态通过地址空间划分隔离。随机化(ASLR)会让各段基址随机。

该布局是"权限分区 + 相向增长"的组合:低地址放代码/数据段,中间放堆与 mmap,高地址放栈,内核态独占最高位,兼顾了安全隔离与空间利用。

#
★★

12. 栈帧结构,局部变量、返回地址与参数传递如何组织?

栈帧(stack frame)的结构是怎样的?局部变量、返回地址与参数如何传递?

  • 栈帧的组成(参数、返回地址、局部变量、保存的寄存器)
  • 帧指针与栈指针
  • 调用约定(cdecl/SysV)

每次函数调用都会在栈上分配一个栈帧(stack frame),典型结构从高地址到低地址为:调用者传入的参数、调用者的返回地址(call 指令自动压入)、被保存的寄存器/帧指针、局部变量。栈顶由栈指针(RSP)指向栈帧底部,帧指针(RBP)用于访问返回地址和参数。不同调用约定决定参数传递方式:cdecl 用栈传参,SysV AMD64 用寄存器传参(rdi/rsi/rdx/rcx/r8/r9)。函数返回时恢复栈指针并跳转到返回地址。

栈帧是函数调用与局部存储的载体,通过 push/pop 和寄存器约定实现参数传递与返回。理解栈帧是理解栈溢出(覆盖返回地址)和栈回溯的基础。

#
★★

13. 线程栈与主线程栈的差异,pthread_create 默认栈大小、栈向下增长与 guard page,为什么线程栈不能无限增长?

线程栈与主线程栈的差异是什么?pthread_create 默认栈大小、栈向下增长与 guard page 如何工作?为什么线程栈不能无限增长?

  • pthread_create 默认栈大小(8MB)
  • 线程栈向下增长与 guard page
  • 线程栈受限的原因

主线程栈由内核在进程启动时分配,受 RLIMIT_STACK 约束;线程栈由 pthread_create 使用 mmap 显式分配,默认大小通常为 8MB(可调)。线程栈同样向下增长,底部有 guard page 用于检测溢出。线程栈不能无限增长,因为它是 mmap 分配的固定大小区域,不能像主线程栈那样动态扩展,且多个线程的栈需要彼此隔离,若无限增长会相互侵占或耗尽地址空间。栈溢出时 guard page 触发 SIGSEGV。

线程栈是 mmap 分配的受限固定区域,容量有限,guard page 提供越界检测。这是线程栈与主线程栈(可动态扩展)的关键差异,也解释了为何深递归/大栈并发会耗尽线程栈。

#
★★

14. Stack Clash 攻击的原理,栈向堆或其他映射区增长时为何能绕过 guard page,Linux 的 stack guard gap 如何防御?

Stack Clash 攻击的原理是什么?栈向堆或其他映射区增长时为何能绕过 guard page?Linux 的 stack guard gap 如何防御?

  • Stack Clash 的原理
  • 绕过 guard page 的方式
  • stack guard gap 防御

Stack Clash 是一类利用栈向其他映射区(堆、mmap)增长时"碰撞"的攻击。当栈溢出的增长越过了 stack guard page 且 guard page 恰好被其他映射(如 mmap 的堆)覆盖时,攻击者可通过交替分配大的栈变量和 mmap 映射,使栈悄悄扩进已映射的相邻区域,从而绕过 guard page 检测,篡改堆或其他映射中的数据。Linux 的防御是 stack guard gap:在栈与相邻映射之间保留一个较大的不可映射的真空区域(guard gap),使栈增长无法直接跨入其他映射,从而限制栈与其他区域的碰撞。

攻击本质是"栈增长越界 + guard page 被占用/绕过"。防御的关键是拉开栈与相邻映射的距离(guard gap),并确保 guard page 不被映射覆盖,让栈溢出在到达可写映射前就被拦截。

#

15. 为何 C++ 静态对象初始化在 .data 而 .bss 仅 POD?

为什么 C++ 静态对象初始化存放在 .data 段而 .bss 段仅存放 POD?

  • .data 存放已初始化数据
  • .bss 仅存零初始化 POD
  • 非 POD 的构造函数与初始化顺序

C++ 静态对象若是指针/值语义的简单变量(POD)且零初始化,放在 .bss;若静态对象有非平凡构造函数(非 POD),其初始化代码在运行时执行(通过 .init_array 调用构造函数),因此不能像 .bss 那样只在加载时清零,而需要运行时构造。已初始化的静态数据(含字符串字面量、const 等)放在 .data/.rodata。.bss 只存放"零初始化且无需构造"的 POD,因为它的语义是加载时清零即可。

关键差异是"初始化是否需要运行时代码"。POD 零初始化只需清零,可放 .bss;非 POD 对象需要构造函数,必须在运行时完成,故属于 .init_array 驱动或 .data 的范畴。这是 C++ 静态对象与 C 变量在 .bss 使用上的本质区别。

#

16. 栈的增长方向与栈溢出,为什么栈向下增长而堆向上?

栈的增长方向是什么?为什么栈向下增长而堆向上?

  • 栈向下(高地址向低地址)
  • 堆向上(低地址向高地址)
  • 历史与架构原因

在 x86 等主流架构上,栈向下增长(高地址向低地址,push 使 SP 递减),堆向上增长(低地址向高地址)。这种相向布局是历史与硬件设计共同决定的:栈使用 push/pop 指令和递减的栈指针,向下增长便于硬件实现;堆以递增地址分配,与栈相向而行使两者共享中间地址空间,避免相互冲突并最大化利用。注意,栈方向随架构而定(如某些 RISC 架构可向上),但主流 x86/ARM 均为向下。

栈向下增长是 CPU 指令集设计的既有约定(push 递减 SP),堆向上是内存分配的自然选择,两者相向保证地址空间高效利用。

#

17. 栈溢出与堆溢出的区别与防护?

栈溢出与堆溢出的区别是什么?各自的防护手段是什么?

  • 栈溢出(覆盖返回地址)
  • 堆溢出(覆盖相邻堆块)
  • 防护:栈金丝雀、NX、ASLR、堆检查

栈溢出发生在栈上(如 recv 到栈缓冲区),通过覆盖返回地址或帧指针劫持控制流,攻击面是函数返回;堆溢出发生在堆上(如溢出堆缓冲区),通过覆盖相邻堆块的头/元数据或数据,实现任意写或控制流劫持。防护手段:栈常用 Stack Canary(金丝雀)、NX、ASLR、SSP;堆常用 heap 元数据完整性检查、隔离、ASLR 以及各种加固(如 glibc 的 tcache 检查)。两者都可能被 W^X、ASLR 等通用防护缓解。

区别在于攻击目标:栈溢出的核心是返回地址,堆溢出的核心是堆块元数据/相邻对象。防护本质都是"加随机性 + 加完整性校验 + 禁止执行",从不同层面阻断利用。

#

18. 内存映射(mmap)与文件 IO 的对比?

内存映射(mmap)与文件 IO 的对比是怎样的?各自的优缺点是什么?

  • mmap 将文件映射到虚拟地址空间
  • 与 read/write 系统调用的差异
  • 缺页与缓存行为

mmap 将文件区域映射到进程虚拟地址空间,应用通过指针直接读写,无需显式 read/write,数据通过缺页按需加载,且复用页缓存,避免了数据在用户态与内核态之间的拷贝。传统文件 IO(read/write)需要系统调用并在用户缓冲与内核缓冲之间复制数据。mmap 的优点:减少拷贝、可随机访问、共享内存(MAP_SHARED);缺点:缺页开销、映射管理复杂度、共享写需要同步、对小文件/频繁改写可能不如 read/write。现代 io_uring 等也降低了传统 IO 的开销。

mmap 用"虚拟内存+缺页"替代系统调用式的数据传输,适合大文件、随机访问、共享场景;传统 IO 适合小数据、顺序读写。选择取决于访问模式与拷贝/缺页开销的权衡。