首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >文件都在却在某一步崩了:内存权限里「可写」不等于「可执行」

文件都在却在某一步崩了:内存权限里「可写」不等于「可执行」

原创
作者头像
PC电脑医生
发布于 2026-09-24 14:23:20
发布于 2026-09-24 14:23:20
1010
举报

有一类崩溃很特别:文件一个不少、依赖完整、版本也匹配,程序却在运行到某一步时直接崩掉。 它也是用 DLL修复工具 扫不出任何异常的典型情况。

崩溃的位置往往很怪——不在某个模块里,而是指向一片说不上来的地址。

这一类的根因不在依赖,而在内存权限:代码能执行的地方,比你以为的窄得多。

一、内存不是"一整块",而是按页分管权限

现代系统的内存被划成一个个页,每页有独立的权限:可读、可写、可执行。

关键在于:这三项是分开授予的。

模块被映射进来时,它的不同部分会被赋予不同权限:

  • 代码部分:可执行、通常只读
  • 数据部分(全局变量、各段):可读可写,不可执行
  • 栈与堆:可读可写,不可执行

所以"这块内存我能写"和"这块内存我能执行"是两件独立的事。

二、默认拒绝:数据区不能当代码跑

这条规则的反面就是它要防的东西:把一段机器码写进数据里,然后跳过去执行。

按老习惯,这是可行的——写进去,跳过去,代码就跑起来了。而在现在的系统上,跳过去的那一刻会被直接拒绝:那一页的权限里没有"可执行"。

表现就是一次访问冲突,而崩溃地址落在一片栈或堆的范围内——看起来跟任何模块都不沾边。

这解释了一类很有特点的故障:

  • 在老系统上正常、换到新系统就崩,且崩得很早或很随机
  • 崩溃位置指向栈或堆,而不是代码段
  • 用工具扫描文件,一切正常

因为它要的不是文件,是权限。

三、它是双刃剑:两类正常需求也被影响

这条默认规则防住了"把数据当代码"这类手法,但它也切到了两类合理场景:

第一类,需要动态生成代码的运行时。

脚本引擎、虚拟机、以及各种即时编译机制,它们的正常工作方式就是在运行时产生机器码再执行。

它们不能靠"关掉全局保护"来解决——正确做法是显式地把那片内存标记为可执行,而不是让整个进程都放开。

第二类,老程序与保护方案。

早年的程序和保护壳往往默认"写进去就能执行"。系统策略一变,它们就崩——而这类程序通常已经不再更新,只能靠系统侧给它开例外。

这个取舍值得记住:默认拒绝的代价是"一些老程序跑不了",换来的是"一大类攻击手法直接失效"。这是一次有意为之的交换。

四、怎么判断自己遇到的是这一类

三个线索:

线索一,看崩溃地址落在哪。

  • 落在某个模块的范围内 → 该模块内部的问题
  • 落在栈或堆的范围 → 往"数据当代码执行"这个方向想

线索二,看它对系统的敏感程度。

同一份程序,老系统正常、新系统崩——这类"随系统版本变化"的特征是重要提示。

线索三,看这个程序是否需要动态生成代码。

如果它自带脚本、插件编译、表达式求值这类能力,那么它可能存在需要显式声明的部分。

五、为什么"补文件"在这里没有用

这一层的问题和依赖完全无关(所以 DLL修复工具 在这里没有用武之地):

  • 文件齐备、版本正确、签名正常
  • 报错也不指向任何缺失的模块
  • 它指向的是一次被拒绝的操作

所以按名称查找、补齐文件的思路在这里不管用——DLL修复工具 这类工具扫描的结果通常是"一切正常",而这个"正常"说的是文件层面,与"能不能执行"无关。

判断这个区别的意义在于:它能把"反复补文件"这条死路提前终止,直接转到正确的方向上。

六、处理方向

第一条,优先为个别程序设置例外,而不是全局关闭。

这是最小代价原则。关掉全局保护等于把整台机器的防护一起降下来,而需要配合的通常只有那一个程序。

第二条,如果程序是自己维护的,就改代码。

需要动态生成代码的地方,显式声明那片内存的用途;不需要的地方,就不要指望数据区能执行。

第三条,注意声明范围。

声明可执行的范围要尽量小,用完能撤销就撤销。"整块放开、一直放开"是最省事也最危险的写法。

第四条,别把"可写"当成"可执行"。

这两件事在权限上是分开的,不要依赖任何"写了就能跑"的隐含假设。

七、按现象定位

  • 崩在栈或堆地址上(原因方向:数据区执行被拒;处理方向:检查是否需要显式声明可执行)
  • 老系统正常、新系统崩(原因方向:权限策略差异;处理方向:为该程序设置例外,而非全局关闭)
  • 带了脚本/表达式引擎的程序启动即崩(原因方向:动态代码未显式声明;处理方向:在代码里声明那片内存的用途)
  • 崩在某个模块范围内(原因方向:模块内部问题;处理方向:属另一类,与本节无关)
  • 扫描文件一切正常却崩(原因方向:与文件无关;处理方向:换方向:查权限与执行流)
  • 越是被加固过的程序越容易崩(原因方向:早期执行阶段介入;处理方向:参考启动早期那一层)

八、小结

关于"文件都在却在某一步崩了",记住四条:

  • 内存权限是逐页分开授予的:可写不代表可执行
  • 数据区默认不可执行,所以"把代码写进数据再跳过去执行"会被直接拒绝,表现是崩在栈或堆地址上
  • 两类正常需求会受影响:需要动态生成代码的运行时(应显式声明)、以及老程序与保护方案(需要系统侧例外)
  • 这类问题与文件完整性无关,所以按名称补文件的思路找不到出路

这里的迁移经验是一个设计取向:默认拒绝 %2B 显式声明。它让"不该被执行的代码"根本没有机会执行,代价是兼容性——但兼容性问题的正解是为个别对象显式声明,而不是把整套默认规则关掉。这个思路在很多安全策略上都是通用的。

https://www.ijinshan.com/functions/repairdll.html?channel=4086

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、内存不是"一整块",而是按页分管权限
  • 二、默认拒绝:数据区不能当代码跑
  • 三、它是双刃剑:两类正常需求也被影响
  • 四、怎么判断自己遇到的是这一类
  • 五、为什么"补文件"在这里没有用
  • 六、处理方向
  • 七、按现象定位
  • 八、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档