首页 / 资讯中心 / 文章详情

段错误的根因:为什么解引用空指针通常会崩

段错误的根因:为什么解引用空指针通常会崩 ★ FEATURED ARTICLE
先猜输出。三段代码在 Linux x86-64 上各是什么下场int*pNULL;inta*p;/* 甲空指针解引用 */int*q;/* 未初始化栈上垃圾值 */intb*q;/* 乙野指针解引用 */charbuf[8];buf[1000000]x;/* 丙越界整整一百万字节 */甲几乎必崩乙崩不崩看运气丙越界一百万字节反而是死得最干脆的那个。三段代码在 C11 里全是未定义行为标准不给任何承诺可现实里它们的命运却呈现规律甲的段错误提示Segmentation fault是否附带(core dumped)取决于ulimit -c的设置乙常在头几条指令就死丙同样当场段错误。真正安静的越界另有其人它在堆里第一节结尾揭晓。这一篇把这些命运收拢到一条根因上崩溃不是printf报的不是 C 语言报的是硬件与操作系统联手判的。篇 16 埋的问题空指针为什么崩而不是读到 0在这里拆到底。一、崩的瞬间一条三层流水线以甲为例逐步回放int a *p的执行现场。第一层在 CPU 里。p是空指针值为 0MMU 拿着虚拟地址 0 查页表篇 16 讲过内核故意不映射地址 0 附近那一页这条翻译查不到任何有效映射MMU 当场报异常。注意这个异常与篇 16 的缺页异常是同一个异常demand paging 的补页、COW 的复制都靠它触发区别只在内核的裁决——查虚拟地址空间VMA发现这段地址没有任何合法映射无法补页定性为非法访问。第二层在内核里。内核确认这是一次进程无法自救的非法访问于是给进程发送SIGSEGV信号。信号的名字就是 segment violation 的缩写段错误的段字来自更古老的分段内存时代名字沿用至今含义早已变成页表裁决失败。第三层在进程里。进程对SIGSEGV的默认处置是终止并转储内存core dumped你在终端看到的那行Segmentation fault (core dumped)是 shell 在报告子进程的死因。所以谁杀了程序的完整答案MMU 发现异常内核定性判死信号执行判决main函数、C 运行库从头到尾不知情。图段错误的三层判决。MMU 报异常与 demand paging 是同一个异常内核查 VMA 定性可补页则续命无映射则发 SIGSEGV。乙和丙走的是同一条流水线只是入口不同。乙的q里是栈上残留的垃圾值若高位不是规范形态下一节细说MMU 直接拒绝死得极快碰巧是某个合法旧地址则读出一个错误的数不崩但答案错。丙走的是另一条路buf是main的栈上局部变量向高地址写一百万字节而main的栈帧紧挨着栈区顶端上面只剩 argv/环境变量的几 KB1MB 早早越过栈顶落进未映射区大概率当场 SIGSEGV。真正能安静越界的现场是另一幅图景在一大块malloc出来的堆内存内部越界只要不出这块的映射边界页表照常翻译连异常都不会有这正是篇 8 的结论越界不崩比崩更危险因为没人报警。二、规范位野指针死得快的硬件原因乙的看运气里藏着一条硬规则。x86-64 的 64 位地址只实现了 48 位有效位硬件要求高 16 位必须与第 47 位相同全 0用户态范围或全 1内核范围这样的地址叫规范地址canonical address。不满足呢MMU 连页表都不查直接判定地址非法抛出保护性异常。这一条解释了大量野指针死得快的现场未初始化的指针值是栈上旧数据只要不是恰好的合法地址大概率不满足规范形态栈上残留旧地址的高位常带垃圾形如0xdeadbeef0badf00d一次解引用立刻异常。顺带纠正一个直觉0xdeadbeef这类顺手的测试值在 64 位下高位全 0数值上反而是规范地址它不会被规范位检查拦下只会死于页表查无映射结局同样是崩。arm64 同理48/52 位之外的高位同样是规范位。所以野指针的典型命运不是悄悄写坏内存而是当场被抓——这是硬件在规范位上白送的检查不是 C 语言的功劳。三、为什么是通常三个不崩的剧本标题里的通常必须兑现。剧本一裸机没有这套防护网。Cortex-M 没有 MMU地址直接上总线地址 0 处映射的是 Flash 起始区放着中断向量表解引用空指针读到的不是异常是向量表里的数据若拿它当可写内存去写破坏的是向量表本身后果比崩溃隐蔽得多。篇 15 说过裸机上地址是真的这句话的另一面就是地址出错也没有裁判C 的崩是带 MMU 系统的奢侈品。剧本二优化器可能让崩变得不可预测。解引用空指针是 UBC11 §6.5.3.2编译器有权假设它不发生篇 8 的 UB 折叠在这里同样生效先解引用后判空的代码判空可能被整体删除崩溃位置可能漂移到看似无关的语句。所以在我机器上没崩不构成代码正确的证据只构成还没轮到它的观察。剧本三地址 0 并非永远不可映射。Linux 提供mmap固定地址映射的能力历史上 NULL 页可以被映射攻击者借此把空指针崩溃改造成空指针执行NULL dereference exploit。现代内核默认mmap_min_addr为 64KB普通进程再也摸不到第 0 页——空指针必崩是内核用一条配置守出来的不是天条。图三个例外剧本与两件工具。裸机无裁判、优化器折叠 UB、第 0 页可被映射ASan 与 core dump 是把崩用足的手段。四、崩是恩赐以及如何接住它换个视角SIGSEGV 是操作系统送的最快反馈。程序带着非法地址在内存里游荡没人报警时它污染的是别人的数据堆内大块越界的下场有人报警时它死在案发现场core dump 把现场整个封存。工程上要做的不是祈祷不崩而是把崩用足gcc-g-fsanitizeaddress demo.c./a.out# AddressSanitizer: SEGV on unknown address 0x000000000000# #0 0x40111a in main demo.c:6AddressSanitizer-fsanitizeaddress在每次访存处插检查把碰巧不崩的越界和悬空也抓出来并直接报出行号core dump 配gdb bt能还原崩溃时的完整调用链。再往前一步是预防指针初始化为 NULL、解引用前判空注意先解引用后判空等于白判、snprintf与长度校验拦住篇 49 的缓冲区溢出。最后一条红线给SIGSEGV挂自定义处理函数基本无救硬件异常类的信号处理函数返回后行为未定义现场早已不可恢复接住它的正确方式是让 core dump 落盘。写在最后段错误是三层流水线的判决MMU 翻译失败报异常、内核查 VMA 定性非法、SIGSEGV 终止进程它与 demand paging 是同一个异常的两种结局区别只在内核能否补页。空指针崩的前提是内核故意不映射第 0 页mmap_min_addr野指针死得快靠规范位x86-64 高 16 位须全 0/全 1。通常二字背后是硬件与内核的层层设卡。裸机没有这套防护网Cortex-M 地址 0 是向量表空指针读到的不是异常而是数据写它破坏的更关键。解引用空指针/野指针/越界全是 UB优化器折叠让没崩不可作为证据ASan 与 core dump 是把崩用足的两件工具。SIGSEGV处理函数返回后行为未定义现场不可恢复防御的位置在写代码时判空、限长、初始化。内存系列刚开头下一站是每个 C 程序员都该亲眼看过一次的现场《函数调用时栈里发生了什么栈帧与调用约定》把一次函数调用的入栈、传参、返回拆到寄存器和内存条上。同系列文章会陆续发布关注后即可收到更新。回复「交流」我很乐意分享我的学习经验和工作经验直接来聊。感谢阅读。转载请注明出处与作者。
阅读完成 · 觉得有帮助?
咨询建站