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

进程地址空间

概述

  1. Windows 进程地址空间和 Linux 一样采用虚拟地址空间,每个进程有独立的虚拟地址空间,由 Windows 内核和硬件 MMU 联合管理
  2. 核心区别在于:
    1. Windows 使用 PE(Portable Executable)格式代替 ELF
    2. 使用 HeapAlloc / VirtualAlloc 代替 ptmalloc / brk,且 32 位和 64 位的地址空间布局差异显著

32-Windows(4GB 总虚拟地址空间)

  1. 开启 /3GB 启动参数 + /LARGEADDRESSAWARE 链接选项后
    1. 用户空间可扩至 3GB(0x00000000 ~ 0xBFFFFFFF),内核压缩到 1GB
区域 地址范围 大小 说明
NULL 禁区 0x00000000~0x0000FFFF 64KB 不可访问,捕捉空指针解引用
用户空间 0x00010000~0x7FFEFFFF 2GB 用户程序可用
64KB 隔离带 0x7FFF0000~0x7FFFFFFF 64KB 用户/内核之间缓冲区
内核空间 0x80000000~0xFFFFFFFF 2GB Ring 0,用户态不可访问

64-Windows(理论 256TB 可用虚拟地址空间)

  1. 64-Windows 用户空间大小(128TB)和 64-Linux 相同,都源于 x86-64 CPU 的 48位虚拟地址限制
区域 地址范围 大小 说明
NULL 禁区 0x0000000000000000~0x000000000000FFFF 64KB 不可访问
用户空间 0x0000000000010000~0x00007FFFFFFFFFFF 128TB 用户程序可用
规范地址空洞 0x0000800000000000~0xFFFF7FFFFFFFFFFF 不可访问 x86-64 硬件限制,地址必须符合规范形式
内核空间 0xFFFF800000000000~0xFFFFFFFFFFFFFFFF 128TB Ring 0,用户态不可访问

内核空间

  1. 内核空间是什么
    1. Windows 内核空间是进程虚拟地址空间的高地址区域,用户态(Ring 3)无法直接访问。与 Linux 相同,隔离机制依赖页表中的 Supervisor 位:
    2. 内核页表项 Supervisor 位为 1,用户态访问触发访问违规(EXCEPTION_ACCESS_VIOLATION),最终表现为应用崩溃而非系统崩溃
  2. 里面住着什么
    1. Windows 内核(ntoskrnl.exe)的代码和数据:进程管理、内存管理器(Mm)、对象管理器、I/O 管理器
    2. HAL(Hardware Abstraction Layer,hal.dll):硬件抽象层,屏蔽不同硬件差异
    3. 内核驱动(*.sys):网卡、磁盘、文件系统驱动加载于此
    4. 每线程内核栈:线程进入内核态时使用,默认大小 12KB(32位)/ 24KB(64位),大幅高于 Linux 的 8KB/16KB
    5. 非分页池(Non-Paged Pool):必须常驻物理内存的内核数据,不可换出到页文件
    6. 分页池(Paged Pool):可换出到页文件的内核数据,类似 Linux 的 vmalloc 区域
    7. 系统 PTE 区域:内核动态映射用
  3. 进入内核的唯一合法途径
    1. 用户代码不能直接跳入内核,只能通过三条受控的门:
    2. 系统调用(syscall 指令,Win32 层通过 ntdll.dll 封装,最终调用 NtXxx 系列函数)
    3. 硬件中断(IRQ,网卡、键盘、定时器等设备触发)
    4. 异常(缺页、访问违规、除零等 CPU 检测到的错误)
    5. Windows 特有:
    6. 用户态通过 ntdll.dll → syscall 进入内核,中间多一层 Win32 API(kernel32.dll)→ Native API(ntdll.dll)的包装
  4. 内核崩溃 vs 用户崩溃
用户程序崩溃 内核/驱动崩溃
表现 弹出"程序已停止工作",WER 收集崩溃转储 蓝屏死机(BSOD),系统完全停机
影响范围 只杀死当前进程 所有进程死亡,系统重启
转储文件 AppCrashView 可查 %SystemRoot%\MEMORY.DMP

栈

  1. 栈和 Linux 机制相同,由 CPU 的 rsp/rbp 寄存器管理,LIFO 结构,分配一条 sub rsp, N 指令完成,速度极快
  2. 关键区别:栈大小
    1. Linux 默认栈 8MB,进程启动时由 OS 保留
    2. Windows 默认栈 1MB(由 PE 头 SizeOfStackReserve 字段指定),可通过链接器 /STACK 选项或 CreateThread 参数修改
    3. Windows 栈采用按需提交机制:1MB 是"保留"大小,实际只提交少量页,通过 Guard Page 机制按需扩展;用完才触发 EXCEPTION_STACK_OVERFLOW
  3. Windows 特有的 Guard Page 机制:
    1. 栈底部维护一个 Guard Page
    2. 每次栈增长触碰 Guard Page 时,OS 自动将其提交为普通页,并在更低地址设置新的 Guard Page
    3. 当 Guard Page 被耗尽且无法扩展时,抛出 STATUS_STACK_OVERFLOW 异常
  4. 同样的陷阱:
    1. 返回局部变量的地址,函数返回后栈帧逻辑销毁(rsp 移回),指针变成悬空指针

堆

  1. Windows 堆由 堆管理器(ntdll.dll 中的 RtlHeap)管理,不存在 Linux 的 brk/sbrk 概念,全部通过 VirtualAlloc 向 OS 申请内存
  2. Windows 内存的三种状态(Linux 没有这个区分):
    1. Free:未分配,不占物理内存,也不占页文件
    2. Reserved:已预留虚拟地址范围,但不占物理内存(VirtualAlloc + MEM_RESERVE)
    3. Committed:已提交,建立了物理内存或页文件的对应关系(VirtualAlloc + MEM_COMMIT)
  3. 分配路径:

  1. 对应关系
Linux Windows
malloc/free HeapAlloc/HeapFree
new/delete new / delete(内部调用 HeapAlloc)
mmap(MAP_ANON) VirtualAlloc(MEM_COMMIT)
munmap VirtualFree(MEM_RELEASE)
brk/sbrk 无对应,Windows 不使用
  1. 同样的四种经典问题:
    1. 内存泄漏:new 没有对应 delete
    2. double free:delete 同一块两次
    3. 悬空指针:delete 后继续访问
    4. 堆溢出:越界写破坏相邻块头部,下次 HeapAlloc/HeapFree 时崩溃

bss段

  1. 和 Linux 相同:存放未初始化(或初始值为 0)的全局变量和静态变量
  2. Windows PE 格式的处理方式略有不同:
    1. PE 中 .bss 节通常被 合并进 .data 节,通过节头的 SizeOfRawData(文件中实际大小)和 VirtualSize(内存中大小)之差来表示未初始化部分
    2. VirtualSize > SizeOfRawData 的差额由 PE 加载器自动填零,效果和 Linux 的 .bss 完全相同
    3. 也可能单独存在 .bss 节,SizeOfRawData = 0,节头只记录 VirtualSize
  3. 同样的结论:
    1. int arr[1000000] 不会让可执行文件变大 4MB,运行时由加载器或 OS 填零

data段

  1. 存放有初始值的全局变量和静态变量,和 Linux 完全一致
  2. 初始值存在 PE 文件中,加载时由 PE 加载器原样复制到内存
  3. 页权限:可读 + 可写,不可执行

  1. 和 Linux 的唯一区别:
    1. PE 节名通常就叫 .data,与 Linux ELF 相同
    2. Windows 不区分 .data 和 .bss 节的写保护,两者都是 RW 权限

rdata段

  1. 等价于 Linux 的 .rodata
    1. Windows PE 格式中只读数据存放在 .rdata 节(Read-only DATA),Linux ELF 中叫 .rodata,概念完全相同
  2. OS 在页表层面设置写保护,写入触发 EXCEPTION_ACCESS_VIOLATION(Windows)vs SIGSEGV(Linux)
  3. 主要内容:
    1. 字符串字面量:const char* p = "hello" 中 "hello" 在 .rdata,p 本身在 .data,p[0] = 'H' 直接崩溃
    2. vtable:和 Linux 相同,所有同类对象共享,存放在 .rdata 防止被篡改
    3. const 全局变量
    4. switch 跳转表
    5. 导入表(Import Address Table,IAT)也可能存于此段
  4. Windows 特有内容
    1. 调试目录(Debug Directory):指向 .pdb 调试符号文件路径
    2. 异常表(Exception Directory):__try / __except 结构化异常处理(SEH)的 unwind 信息,x64 下存放 RUNTIME_FUNCTION 数组

text段

  1. 存放编译后的机器指令,和 Linux 完全相同,权限"可读 + 可执行,不可写"
  2. 主要内容:
    1. 每个函数的机器码顺序排列
    2. 普通函数调用:编译器将目标地址硬编码进 call 指令(或通过 IAT 间接调用 DLL 函数)
    3. 虚函数调用:和 Linux 相同,通过 vptr → vtable(.rdata)→ 函数地址(.text),多了两次内存读取
    4. 纯虚函数槽位指向 _purecall()(MSVC 运行时),调用即 abort(),等价于 Linux 的 __cxa_pure_virtual
  3. Windows 特有:DLL 调用的间接跳转
    1. 调用 MessageBoxA 等 DLL 函数时,.text 中生成 call [IAT_slot]
    2. IAT_slot 在 .rdata 或 .idata 中,存放 DLL 函数真实地址
    3. DLL 加载时由加载器填入实际地址,运行时通过这个间接表调用,这就是 Windows DLL 调用比直接调用多一次内存读取的原因

可执行文件(PE 格式)

概述

  1. Windows 使用 PE(Portable Executable) 格式,对应 Linux 的 ELF 格式
  2. PE 文件包括 .exe、.dll、.sys(驱动)等

图示

PE Header

概述

  1. PE Header 是可执行文件最开头的元数据区域,是整个文件的"目录",Windows 加载器(ntdll!LdrLoadDll)靠它完成加载
  2. 与 ELF header 的对应关系:
ELF PE 作用
Magic 7F 45 4C 46 Magic 4D 5A(MZ)+ 50 45 00 00(PE) 文件类型识别
e_machine Machine(COFF Header) 目标架构
e_entry AddressOfEntryPoint(Optional Header) 程序入口
e_phoff 节头表中 LOAD 段信息 加载信息
e_shoff 节头表偏移(PointerToSectionHeaders) 节描述
ELF Class(32/64位) Magic[4] = PE32 / PE32+ 位宽

DOS Header(历史遗留,20字节关键字段)

  1. OS 读取前 2 字节验证 MZ 魔数,然后跳到 e_lfanew 指向的位置读取 PE 签名

COFF Header(20字节)

字段 大小 含义
Machine 2字节 0x8664 = x86-64,0x014C = x86-32,0xAA64 = ARM64
NumberOfSections 2字节 节的数量
TimeDateStamp 4字节 编译时间戳(Unix 时间)
SizeOfOptionalHeader 2字节 Optional Header 大小
Characteristics 2字节 文件属性标志(是否 DLL、是否可重定位等)

Optional Header(最重要的导航字段)

字段 含义
Magic 0x010B = PE32(32位),0x020B = PE32+(64位)
AddressOfEntryPoint 程序入口点 RVA(相对虚拟地址),指向 mainCRTStartup
ImageBase 加载基址,默认 .exe = 0x400000,.dll = 0x10000000;ASLR 开启后运行时随机化
SectionAlignment 内存中节的对齐粒度,通常 0x1000(4KB,一页)
FileAlignment 文件中节的对齐粒度,通常 0x200(512字节)
SizeOfImage 加载到内存后的总大小
SizeOfHeaders 所有头部(DOS + PE + 节头表)的总大小
SizeOfStackReserve 主线程栈预留大小,默认 1MB(0x100000)
SizeOfStackCommit 主线程栈初始提交大小,默认 4KB(0x1000)
SizeOfHeapReserve 默认堆预留大小,默认 1MB
DataDirectory[16] 16个数据目录,指向导入表、导出表、异常表、重定位表等

关键 DataDirectory 条目

索引 名称 对应 Linux ELF
0 Export Directory .dynsym(导出符号)
1 Import Directory NEEDED + PLT/GOT(导入依赖)
2 Resource Directory 无直接对应(嵌入资源)
3 Exception Directory .eh_frame(异常处理)
5 Relocation Directory .rel.text(重定位)
12 IAT Directory PLT/GOT(函数地址表)

两张表的职责分工

  1. 节头表(Section Table)
    1. 描述每个节在文件中的偏移、在内存中的 RVA、大小、权限(类似 ELF 的节头表 SHT + 程序头表 PHT 的结合)
  2. DataDirectory:
    1. 指向各种特殊数据(导入、导出、异常表等),是 PE 特有的机制,ELF 用专用节名(如 .plt、.got)来承担类似职责

PE 加载过程(对应 ELF 加载)

  1. Linux 的 ASLR 修正在动态链接器 ld.so 中完成,Windows 在 ntdll!LdrLoadDll 中完成,流程相似

一些问题

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

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2026-07-28
Everything will be better.

发表评论

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