• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-03-29 15:00 Aet 隐藏边栏 |   抢沙发  2 
文章评分 1 次,平均分 5.0

进程地址空间

概述

示例代码

图示

内核空间

  1. 内核空间是什么
    1. 内核空间是进程虚拟地址空间的高地址部分,在 64Linux 上位于 0xFFFF800000000000 以上,占 128TB
  2. 它和用户空间的根本区别不是"地址高低",而是页表属性:
    1. 内核空间的页表项 Supervisor 位为 1,CPURing 3(用户态)执行时硬件直接拒绝访问这些地址,触发缺页异常,OS 再转化为 SIGSEGV

  1. 里面住着什么

    1. 内核自己的代码和数据(调度器、内存管理、网络栈)住在这里,这和用户进程的 .text/.data 性质相同,只是权限更高
    2. 每个进程/线程各有一个内核栈(32-linux 8kb64-linux 16kb),进入内核态时 rsp 自动切换到这里,用户栈和内核栈完全独立
    3. 物理内存直接映射区是内核最重要的区域之一,全部物理 RAM 以固定偏移映射到这里,内核不需要额外的 ioremap 就能通过虚拟地址直接读写任意物理页
    4. vmalloc 区域用于内核动态分配的大块内存,以及加载内核模块的代码
  2. 进入内核的唯一合法途径:用户代码不能"跳进"内核,只能通过三条受控的门:

    1. 系统调用(主动,程序执行 syscall 指令请求内核服务)
    2. 硬件中断(被动,网卡/键盘等设备触发 IRQ
    3. 异常(被动,缺页、除零等 CPU 检测到的错误)
    4. 三条路都由硬件强制跳转到内核预先注册好的入口点,不存在自由跳转

  1. 栈是由 CPU 指令直接驱动的,无需操作系统参与,分配和释放都在纳秒级
  2. 特点
    1. 栈是后进先出(LIFO)结构,由 CPUrsp/rbp 寄存器直接管理,不需要操作系统参与
    2. 每次函数调用压入一个栈帧,函数返回时弹出,分配就是 sub rsp, N 一条指令,速度极快
  3. 重要的陷阱:返回局部变量的地址
    1. 函数一旦返回,那块栈帧就"销毁"了(实际上只是 rsp 移回去了,内容还在,但随时会被下一次调用覆盖),返回的指针就成了悬空指针
  4. 常见问题:无限递归
    1. 每层调用都要压栈,栈空间默认只有 8MB,递归太深直接踩到 guard page,触发 SIGSEGV,也就是 stack overflow

  1. 堆由用户态分配器(Linux 上是 ptmalloc)管理,分配器维护一条空闲链表,new 时先查链表有没有合适的空闲块,有就直接用,没有才通过 sbrk()mmap()OS 申请
    1. 超过 128KB 的大块直接走 mmap,不走 brk 指针,分配在独立的地址区域
  2. 堆内存的生命周期完全由程序员控制,这带来了灵活性,也带来了四种经典问题:
    1. 内存泄漏(new 没有对应 delete
    2. double freedelete 同一块两次,破坏分配器内部链表)
    3. 悬空指针(delete 之后继续访问)
    4. 堆溢出(越界写破坏相邻块的 header,导致下次 new/delete 时崩溃,而不是溢出的那一刻崩溃,这使得 bug 极难定位)

bss

  1. 存放没有初始值(或初始值为 0)的全局变量和静态变量
  2. 文件里只记录一个数字:这块区域有多大
    1. 程序加载时,OS 直接 mmap 一块全零的匿名内存,不需要从文件读任何东西
  3. 这就是为什么一个有 int arr[10000000] = {0} 的程序,编译出来的可执行文件很小,但运行时占 40MB 内存

data

  1. 存放有初始值的全局变量和静态变量
  2. 初始值必须存在可执行文件里,程序加载时由 OS 原样复制到内存
  3. 本质是:
    1. 初始值在文件里占了真实空间,加载时原样搬进内存,之后可读可写

rodata

  1. 只读数据段,和 .data 的本质区别在于"加了写保护"
    1. 核心就是"只读",OS 在页表层面加了写保护,所以 vtable 才能安全地被所有对象共享
  2. 主要内容:
    1. 字符串字面量 "hello" 存在这里,const char* p = "hello"p 这个指针变量本身在 .data,但它指向的字节序列 68 65 6C 6C 6F 00.rodata,所以 p[0] = 'H' 直接崩溃
    2. vtable 也存在这里,这也是为什么 vtable 不可能被运行时意外破坏,所有同类对象放心共享同一份表
    3. const 全局变量、编译器生成的 switch 跳转地址数组都在此

text

  1. 存放编译后的机器指令,权限是"可读+可执行,不可写"
  2. 主要内容:
    1. 每个函数的机器码顺序排列,函数名到地址的映射由符号表记录(调试用)
    2. 普通函数调用时编译器把地址硬编码进 call 指令
    3. 虚函数调用时地址存在 .rodatavtable 里,运行时再取出来 call rdx,这就是多态的底层代价,比直接调用多了两次内存读取
    4. __cxa_pure_virtual 也在 .text,它是运行时库注入的一个函数,纯虚函数槽位指向它,调用即 abort()

ELF header

  1. 可执行文件最开头的 64 字节(64位)
  2. 见下文

可执行文件

概述

图示

ELF header

概述

  1. ELFExecutable and Linkable Formatheader 是可执行文件最开头固定的 64 字节(64位),是整个文件的"目录页"
    1. OS 加载器只靠这 64 字节就能知道去哪里找一切

三组字段

  1. e_ident[16] 是身份信息,前 4 字节是魔数 7F 45 4C 46OS 加载时第一件事就是验证它,不匹配直接返回 ENOEXEC 拒绝执行
  2. 接下来一字节 EI_CLASS 决定是 32 位还是 64 位,这一个字节会影响后续所有地址字段的宽度
  3. 再下一字节 EI_DATA 是字节序,必须在解析任何其他字段前先读它
  4. e_typee_machine 描述文件种类和目标 CPU
    1. ET_EXEC 是普通可执行文件,ET_DYN 是共享库(.so),e_machine = 0x3E 表示 x86-64
  5. 最重要的三个导航字段是
    1. e_entry(程序入口地址,加载完成后跳到这里,对应 _start 函数)
    2. e_phoff(程序头表在文件中的偏移,OS 用它找到各个 LOAD 段并 mmap 进内存)
    3. e_shoff(节头表偏移,链接器和调试器用,运行时不需要,可以 strip 掉)

两张表的职责分工

  1. 程序头表(PHT)是运行时加载的蓝图,按"段"组织,一个 LOAD 段可能横跨 .text + .rodata 两个节,OS 按段权限统一 mmap
  2. 节头表(SHT)是编译链接的目录,按"节"组织,描述每个 .text.data.symtab 的名字、大小、类型,strip 命令删除的就是这张表,删完之后程序照样运行,只是没法用 gdb 查符号了

完整字段

ELF 文件整体结构

魔数的字节级细节

一些问题

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享