首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >为什么有的软件一启动就报错,有的用了一个月才报错

为什么有的软件一启动就报错,有的用了一个月才报错

原创
作者头像
PC电脑医生
发布2026-09-22 14:31:56
发布2026-09-22 14:31:56
720
举报

装好一个软件,用了一个月,一切正常。

直到某天点了某个功能——报错,说缺少某个 DLL。

但软件明明能打开,主界面也正常。

为什么之前一直没事?

答案在于:程序和 DLL 的连接方式不止一种,而不同的方式,报错的时机完全不同。

先看程序怎么知道要加载哪些 DLL

exe 和 dll 都是 PE 格式的文件。

PE 文件里有一张表,叫导入表。

它记录着:这个程序需要调用哪些外部函数,这些函数分别来自哪个 DLL。

程序在磁盘上躺着的时候,它并不知道那些 DLL 会被加载到内存的什么位置。

所以它只记录函数名,不记地址。

真正填地址的工作,由系统在加载时完成。

加载时发生了什么

Windows 加载器的流程大致是:

  1. 读取 exe 的导入表
  2. 按表里列出的清单,逐个加载对应的 DLL
  3. 在每个 DLL 的导出表里查找需要的函数
  4. 把找到的实际地址,填进 exe 的导入地址表(IAT)的位置

填完之后,程序执行到调用语句时,取的就是已经填好的真实地址。

这套机制叫隐式链接——程序代码里看不到"加载 DLL"这个动作,因为它是系统在背后自动做的。

关键点在于第 2 步。

如果清单里某个 DLL 找不到,加载流程就走不下去了。

程序根本启动不了,弹窗报错。

三种连接方式,三种报错时机

上面说的是最常见的一种。

但还有另外两种方式,它们把"加载"这个动作推迟了。

  • 隐式链接(什么时候加载:程序启动时;缺失时的表现:立刻报错,打不开)
  • 延迟加载(什么时候加载:第一次调用它的函数时;缺失时的表现:用到那个功能才报错)
  • 动态加载(什么时候加载:代码里手动触发;缺失时的表现:由程序自己决定怎么处理)

这解释了你遇到的情况。

那个软件用的是延迟加载。

它启动时不需要那个 DLL,所以一切正常。

直到你点了那个功能——第一次真正调用该 DLL 里的函数——系统才去加载。

找不到,报错。

延迟加载是怎么实现的

这个机制挺巧妙,值得说一下。

编译的时候,链接器会给每个延迟加载的函数生成一个"替身"。

程序里原本调用真实函数的地方,被改成了调用这个替身。

程序启动时,替身什么都不做,所以 DLL 不会被加载。

等你第一次调用到它时,替身才开始干活:

  1. 调用一个系统提供的辅助函数
  2. 由它去执行 LoadLibrary,把真正的 DLL 加载进来
  3. 再用 GetProcAddress 找到目标函数的地址
  4. 把地址记下(后续调用就不用再找了)
  5. 跳转到真实函数执行

从第二次调用开始,替身直接被替换掉了,走的是正常路径。

所以延迟加载的代价只发生在第一次。

为什么要这么设计

主要原因有两个。

一是启动速度。

程序如果依赖几十个 DLL,全部在启动时加载,冷启动会很慢。

而其中很多 DLL 可能对应的是"偶尔才用一次"的功能——比如打印、导入导出、某些高级设置。

用延迟加载,不用这些功能的用户就永远不会加载它们。

二是兼容性。

有些 DLL 只在特定版本的 Windows 上存在。

如果用隐式链接,那在老系统上程序压根启动不了。

改成延迟加载,程序能正常跑——只有在调用到那个特定功能时才会失败,而这时程序可以自己处理。

第三种:完全由代码控制

动态加载是最灵活的一种。

代码里显式地写:

代码语言:cpp
复制
HMODULE h = LoadLibraryW(L"target.dll");
代码语言:cpp
复制
if (!h) {
代码语言:cpp
复制
    // 自己决定怎么办:提示、降级、或者退出
代码语言:cpp
复制
}

这种方式下,DLL 的加载时机完全由程序员决定。

好处是错误可以自己处理——LoadLibrary 返回空指针,程序可以弹个友好提示,或者换个方案继续。

而不是像隐式链接那样,直接崩在启动阶段。

什么时候会用这种方式

典型的场景是插件系统。

主程序启动时并不知道用户装了什么插件。

它扫描插件目录,对每个找到的文件调用 LoadLibrary——加载成功就用,失败就跳过。

这个过程完全动态,和静态的导入表没有关系。

那怎么判断自己遇到的是哪种

有几个观察角度。

  • 双击图标就报错,界面从没出现过:隐式链接
  • 软件能正常打开,用某功能时才报错:延迟加载
  • 报错信息里提到了插件或模块名:动态加载
  • 换台电脑就好了:环境差异(缺的文件在那台机器上有)

第二种最容易被误解。

因为用户会觉得"之前一直好好的,怎么突然坏了"。

实际不是突然坏了,是那个 DLL 从来就没被加载过——直到你第一次用到它。

它可能从安装的第一天起就是缺的。

一个相关的排查思路

知道这个机制之后,排查方向会清楚很多。

如果是启动就报错

说明这个 DLL 是核心依赖。

程序压根跑不起来,所以没法用"逐个功能试"的办法定位。

这时候用工具看加载过程最直接。

有个系统自带的工具能记录所有文件访问——过滤出目标进程,然后找那些"尝试加载但没成功"的记录。

注意一个细节:加载器会按顺序在多个目录里找同一个文件。

前面几次找不到是正常的"寻路",只有全线失败的那一个才是真正缺的。

如果是用到才报错

这一类反而好定位。

因为你能明确知道"我点了什么功能才出问题"。

那个功能对应的模块,就是需要检查的方向。

多数情况下是运行库缺失——比如某个组件依赖特定版本的 VC%2B%2B 运行库。

关于 DLL修复工具

这类工具处理的是运行库这一类组件——它们有明确的版本对应关系,也有固定的部署位置。

比如报错的文件名是 msvcp140.dll 或 vcruntime140.dll,那就是 VC%2B%2B 运行库的问题。

数字后缀直接对应版本:140 是 2015 至 2022 这一套,120 是 2013,100 是 2010。

工具能自动判断当前系统需要哪个版本、该装 x86 还是 x64,然后部署到位。

而这类问题,正好是 DLL修复工具擅长处理的部分:运行库有明确的版本对应关系,也有固定的部署位置,工具可以自动判断并补齐。

但如果是软件自己的私有 DLL 缺失,工具就无能为力了——那种情况得重装那个软件。

区分方法很简单:报错的文件名如果是常见的系统组件名,多半是运行库问题;如果名字很怪、明显属于某个特定程序,那就是私有组件。

顺带说个容易忽略的点

延迟加载有个副作用值得知道。

因为它把加载推迟到了运行时,所以原本能在启动阶段就发现的问题,被推迟到了使用阶段。

对开发者来说,这意味着测试不充分时,某些缺陷会漏到用户那里。

因为开发机上往往装了各种运行库,而用户的机器可能没有。

所以测试环境最好是干净的——新装的系统,什么都没额外装过。

这样才容易暴露出那些"依赖了某个碰巧存在的东西"的问题。

而对用户来说,这也解释了一件事:为什么同样的软件,在别人电脑上好好的,在你这里就报错。

不是软件坏了,是两台机器的组件环境不同。

一句总结

DLL 缺失的报错时机,取决于程序和 DLL 的连接方式。

隐式链接在启动时就要求所有依赖齐备,所以缺一个就打不开。

延迟加载把加载推到了第一次调用时,所以能启动、但用到才报错。

动态加载则完全由代码控制,程序可以自己决定失败后怎么办。

搞清楚属于哪一种,就知道该往哪个方向查——也能判断出该不该用 DLL修复工具介入:它处理的是运行库这类有标准版本关系的组件,处理不了软件私有的模块。

安装包地址:

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

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

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

目录
  • 先看程序怎么知道要加载哪些 DLL
    • 加载时发生了什么
  • 三种连接方式,三种报错时机
  • 延迟加载是怎么实现的
    • 为什么要这么设计
  • 第三种:完全由代码控制
    • 什么时候会用这种方式
  • 那怎么判断自己遇到的是哪种
  • 一个相关的排查思路
    • 如果是启动就报错
    • 如果是用到才报错
    • 关于 DLL修复工具
  • 顺带说个容易忽略的点
  • 一句总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档