Windows 10依赖分析:Dependencies GUI排查DLL缺失
简介:面向 Windows 10 的依赖关系分析工具包,适合排查程序加载失败、缺少 DLL 或版本冲突的开发与运维人员。解压后得到 Releasex64 目录,直接运行其中的图形界面主程序,即可加载待分析的可执行文件,逐层查看它对 DLL、驱动程序及其他组件的引用情况,并显示依赖项的路径、版本等细节,较深层次的依赖链也能展开追溯。压缩包共 35 个文件,约 1.78MB,含 dll 运行库、pdb 调试符号、config 配置与 xml 说明文档等类型;符号文件保留调试信息,便于定位问题,配置文件保证工具在各环境下正常启动。目前已有 690 人学习下载。除故障诊断外,它也可作为了解程序依赖构成的教育工具,帮助开发者梳理打包与部署清单,减少跨环境迁移时的组件缺失;在复杂系统级调试中还可与其他分析工具配合使用。
1. 双击没反应的那个 exe,两秒就能问出它缺什么
把一个在开发机上跑得好好的 exe 拷到另一台 Windows 10 上,双击之后什么都没发生——没有报错框,事件查看器里只有一行看不出所以然的记录。这种时候最省时间的动作不是挂调试器,而是把文件拖进 depends 类工具里看一眼:依赖树一摊开,缺哪个 DLL、缺在第几层、是静态导入还是延迟加载,基本当场分明。这份 Windows 10 构建的 Dependencies GUI 包,解决的就是「程序起不来但说不清为什么」这类问题——它把 PE 文件的导入表、延迟加载、API Set 映射和模块搜索路径一次性摊在你面前。适合的人群很具体:做 Windows 桌面交付的、打安装包的、跑构建流水线的,以及被叫去现场「看看这台机器为啥装不上」的运维。新手照着点也能得出结论,熟手则更关心它的判定边界在哪。
2. 依赖树是怎么算出来的:导入表、延迟加载与 API Set 三层判读
很多人把这个工具当成「高级版文件属性」,其实它做的事相当机械:解析 PE 可选头里的数据目录,找到导入目录,逐个读描述符,再对每个被引用的模块递归下去。理解了这条链路,你才知道哪些红叉是真缺、哪些是工具在替你猜。这一章把三层判读讲透——磁盘上的导入表、运行期才触发的延迟加载、以及 Windows 10 上绕不开的 API Set 虚拟模块,顺序不能反。
2.1 导入目录里到底存了什么
PE 文件的可选头里有一张数据目录表,第 1 项(索引从 0 数)就是导入表。它指向一组IMAGE_IMPORT_DESCRIPTOR,每个描述符对应一个被依赖的 DLL,里面有两个关键字段:OriginalFirstThunk指向导入名称表(INT),FirstThunk指向导入地址表(IAT)。磁盘上的 IAT 还没被加载器填充,所以工具读的是 INT——那里存着「我要从你这个 DLL 里拿哪个函数」的符号名或序号。这就是为什么你看到的每一行都是kernel32.dll!CreateFileW这种形式,而不是一个裸地址。
名字导入和序号导入的差别,是后面所有误判的源头。名字导入写的是函数名字符串,跨版本相对安全;序号导入只写一个整数,一旦目标 DLL 的函数表顺序变了,调到的就是另一个函数,表现往往是「能启动但行为诡异」,而不是「启动失败」。工具会把序号导入单独标出来,我一般看到Ordinal 1234这类条目就会多留一个心眼,尤其是第三方 SDK 里的老 DLL。
延迟加载是第二层。它不在导入目录里,而在另一个数据目录项(延迟导入描述符)中,结构类似但语义完全不同:这些 DLL 在进程启动时不会被加载,只有第一次调用相关函数时才触发加载。所以延迟加载项标红,程序照样能跑到那行代码之前。这一点在排障时极易误导——你看到一棵树上挂着红叉,结果程序跑了一整天没事,因为那条路径根本没被走到。判读时要先分清哪些红叉属于「启动即失败」,哪些属于「用到才失败」。
转发导出是第三层,容易被忽略。有些 DLL 本身不实现函数,只是把调用转发给另一个模块,PE 里表现为导出表里的一个字符串OTHERDLL.FuncName。依赖树展开时,转发目标会被继续往下追,于是你看到的深度可能比你预期的大很多。这不是工具算错了,是模块本身就这么设计的。
| 依赖类型 | 在 PE 里的位置 | 工具里的典型表现 | 常见误判 |
|---|---|---|---|
| 静态导入(按名) | 导入目录 / INT | 列出模块!函数名 | 基本可信 |
| 静态导入(按序号) | 导入目录 / INT | 只显示序号 | 误以为同名函数可用 |
| 延迟加载 | 延迟导入描述符 | 单独分组,常标红 | 把「用到才缺」当成「启动就缺」 |
| 转发导出 | 导出表字符串 | 会继续向下展开 | 以为多了一层真实依赖 |
| API Set 虚拟模块 | 导入表里是api-ms-win-* | 解析后映射到实体模块 | 老工具直接标红 |
| 程序集引用(.NET) | CLI 元数据,不在导入表 | 导入表里只有mscoree.dll | 误判「没有依赖」 |
这张表建议打印出来贴在工位上,遇到红叉先对一遍类型,能省掉一半的无效排查。
提示:序号导入在系统 DLL 上很少见,但在某些第三方组件和旧版运行库里非常普遍,看到序号不要直接判定「不可移植」,先确认目标机器上的同名 DLL 是不是同一构建。
2.2 为什么老工具在 Windows 10 上会看走眼
十年前大家用的是另一个老牌依赖查看器,那套工具在 Windows 7 时代够用,到了 Windows 10 就开始出玄学结论。原因不复杂:Windows 10 引入了 API Set 机制,大量api-ms-win-core-*.dll、api-ms-win-crt-*.dll这样的名字出现在导入表里,但磁盘上根本不存在对应的文件。加载器在运行时通过一套 schema 把这些虚拟名字映射到kernelbase.dll、kernel32.dll、ucrtbase.dll之类的实体模块。老工具不认识 schema,只会在磁盘上找文件,找不到就标红——于是你的程序「缺了三十个 DLL」,实际上一个都不缺。
第二个坑是并行程序集(SxS)。应用程序清单里可以声明dependentAssembly,指定要加载某个特定版本的 CRT 或公共控件。这些依赖根本不出现在导入表里,只看导入表会得出「干净得可疑」的结论。判读时要顺手看一眼 exe 同目录有没有.manifest,或者用工具的资源视图确认清单内容。
第三个是 .NET。一个纯托管程序的导入表里往往只有mscoree.dll一个条目,因为真正的程序集引用存在 CLI 元数据里,走的是另一套加载逻辑。如果你拿它当原生程序分析,结论会是「这程序没有依赖」——显然荒谬。这类文件要换到程序集视图去看,或者干脆用ildasm一类的工具补一刀。
第四个数位数。32 位进程和 64 位进程加载的模块是两套,System32和SysWOW64的文件系统重定向也让人头晕。拿 32 位构建的工具去展开 64 位目标,或者反过来,都可能得到「模块找不到」的假象。确认工具位数和目标位数一致,是判读前的第一件事,比看任何红叉都优先。
选这个 GUI 版本的理由也就在这几条上:它在 Windows 10 上对 API Set 有解析、对延迟加载分组清晰、对清单和 .NET 有单独视图。换句话说,它替你处理掉了最容易产生假阳性的那部分工作,剩下的判断才轮到你自己做。
2.3 一次完整的判读顺序:从根模块到叶子
拿到一个起不来的 exe,我一般按固定顺序走,不跳步,因为跳步最容易得出错误结论。
第一步,确认根模块本身。位数、子系统(GUI 还是控制台)、是否带清单、是不是 .NET 程序集。这四项决定了后面用哪套判读逻辑。位数在工具里通常直接显示,清单和 .NET 需要看资源或切换视图。
第二步,只看第一层直接依赖。不要一上来就全展开,几百个节点铺满屏幕,人眼根本抓不住重点。第一层的红叉才是「启动必挂」的高危项,深层的红叉大概率是某些可选功能路径上的模块。
第三步,逐个展开第一层里的可疑项。判断标准是:这个模块是不是系统模块?系统模块标红,八成是位数或 API Set 的问题;第三方模块标红,才是真的缺文件。
第四步,单独看延迟加载分组。这一组里的红叉只代表「用到时才会缺」,优先级排在后面,但如果业务逻辑会走到,同样要补。
第五步,检查模块搜索路径。工具默认按 Windows 加载器的那套顺序找文件:exe 所在目录、系统目录、当前目录、PATH。如果你的 DLL 放在别的子目录里靠代码手动LoadLibrary加载,工具是看不到的,会显示成缺失。这种情况要在工具的搜索路径里加上那个目录,再看一遍。
第六步,给结论。结论必须落到一句话上:「因为 X 目录缺少 Y.dll(或 Y.dll 的 Z 版本),导致 W 功能不可用」。说不出这句话,说明还没判读完成,回去继续。
3. 在 Windows 10 上跑通:从拖进去到读出一棵能下结论的树
工具拿到手的第一件事不是双击打开,而是把目录结构摸清楚。这类构建包通常带一个 GUI 主程序、一个命令行版本,外加一批运行库文件。运行库必须和主程序放在同一目录,随便挪动其中一个就可能出现「主程序打不开」——这本身就是一个自举的依赖问题,挺讽刺,但也正好是练手素材。
3.1 两个入口:GUI 和命令行,先都确认一遍
先用一条命令把目录里的可执行文件列出来,确认这版给了哪些入口,别凭猜。这个过程只需要十秒,却能避免后面写脚本时把名字写错。
:: 切到解压后的目录,先看有哪些 exe cd /d D:\tools\dependencies-win10 dir /b *.exe :: 如果目录里还有子文件夹,一并扫一层 dir /b /s *.exe | moredir /b是精简输出,只列文件名不带时间大小;/s递归子目录。加| more是为了在文件多的时候分屏,避免刷屏。这一步的产出是一份入口清单:哪个是 GUI、哪个是命令行、有没有附带的小工具。
确认命令行入口之后,立刻打印帮助,把这一版支持的开关记下来。不同构建的开关名字会有出入,照着网上抄参数是最容易翻车的地方。
:: 用命令行版本打印帮助,把输出重定向到文件方便对照 Dependencies.exe -h > help.txt 2>&1 notepad help.txt> help.txt把标准输出写进文件,2>&1把错误输出也合并进去——有些工具把帮助打到 stderr,不合并会看到一个空文件。看完帮助,你手上就有三样东西:可执行文件名、看导入的开关、导出结构化结果的开关(如果有)。
GUI 的启动没什么讲究,双击主程序即可。如果双击没反应,先怀疑运行库:把整个目录原封不动放在一起,别只拷一个 exe 出来。
3.2 界面参数:递归深度、搜索路径、视图过滤
GUI 打开之后,把目标文件拖进窗口,或者走菜单里的打开。默认会展开第一层依赖,右侧是模块列表,底部是标签页(导入、导出、模块、清单之类)。真正决定你看到什么的,是下面这几个设置。
| 设置项 | 作用 | 我一般怎么设 |
|---|---|---|
| 递归深度 | 控制自动展开到第几层 | 先设 1,逐个手动展开 |
| 搜索路径 | 额外目录,用于找不在系统路径里的 DLL | 把发布目录和其bin子目录都加上 |
| 视图过滤 | 隐藏系统模块或只看缺失项 | 排查阶段开「只显示未解析」 |
| 位数匹配 | 决定用哪套加载逻辑解析 | 必须和目标文件一致 |
| 导出格式 | 文本 / CSV / JSON | 脚本消费一律用 JSON |
递归深度是最容易用错的一个。设成 5 看起来信息量大,实际上把系统 DLL 的整棵树都拉出来了,几千个节点里找那一个红叉,效率极低。我的习惯是先设 1,看清楚直接依赖,再对可疑的那一支单独展开。这样屏幕上永远只有十几行,眼睛跟得上。
搜索路径要设对。默认情况下工具只按系统加载器的规则找文件,如果你的程序把 DLL 放在plugins\或lib\子目录里靠代码动态加载,工具会判成缺失。把那些目录加进搜索路径再刷新,红叉会消失一大片——这属于典型的假阳性,不是真问题。
视图过滤是排障效率的关键。开会话第一件事就是勾上「只显示未解析/缺失项」,屏幕上立刻只剩几行,全是需要你处理的。等问题清完了再关掉过滤,通览一遍结构。
注意:搜索路径加得太多也会骗自己。把开发机上的全局 PATH 一股脑加进去,工具能找到的 DLL 客户机器上未必有。判断「客户能不能跑」时,搜索路径只应该包含发布目录本身,不要加开发环境的东西。
3.3 读树的三个动作:展开、定位、对比
树读起来像文件管理器,但三个动作的用法和文件管理器完全不同。
展开要「沿着怀疑走」。看到第一层里有个第三方 DLL 标红,先展开它,看它自己又依赖了什么。很多时候红叉只是表象——真正缺的是它依赖的某个运行库,而那个运行库在更深一层。沿着一条链路走到叶子,比横向扫一百个节点有用。
定位要善用搜索框。几百个节点里找一个vcruntime140.dll,眼睛会瞎。搜索框支持模块名和函数名,输入关键字直接跳到匹配节点。我的习惯是先搜.dll之外的关键字,比如api-ms-win,一次把所有 API Set 条目挑出来,确认它们都被正确解析了;然后再搜ordinal,检查有没有序号导入。
对比是熟手才会用的动作。同一个程序,在开发机和客户机上各导出一份依赖清单,然后做差集——客户机上多出来的「未解析」项,就是差异所在。这一步用 GUI 做不了,得靠命令行导出,第 4 章会给出具体脚本。手工对比也行,但节点一多必然出错,我不推荐。
最后提醒一个习惯:结论要当场记下来。工具的界面不提供「备注」功能,看到什么就随手记在排查单上,模块名、层级、判定结果三列。等排查结束再回头复述,八成会记混。
4. 命令行批量出清单:把 depends 检查接进打包收尾
GUI 适合现场排障,不适合重复劳动。一个安装包里有几十个 exe 和 DLL,逐个拖进窗口是不现实的,而且人眼比对两份清单必然漏。这一步要交给命令行版本,让它在打包脚本的最后跑一遍,产出一份可以 diff 的清单。
4.1 单文件导出:先跑通一个,再谈批量
不要一上来就写循环。先拿一个文件跑通单次导出,确认输出格式、确认退出码、确认编码,这三样都会在后面咬你一口。
:: 单个文件导出,先确认开关名(以 -h 的实际输出为准) Dependencies.exe -imports -json "D:\build\app.exe" > app.imports.json :: 检查退出码,0 表示跑通 echo %ERRORLEVEL% :: 看一眼前 40 行,确认不是错误信息混在 JSON 里 powershell -NoProfile -Command "Get-Content app.imports.json -TotalCount 40"-imports是看导入的开关,-json让输出走结构化格式,具体的开关拼写要以你这版-h的输出为准,不同构建会有出入。重定向用>而不是管道,是为了留下文件作为后续 diff 的基准。%ERRORLEVEL%在批处理里必须紧跟在命令后面读,中间插一条别的命令就会被覆盖,这是个很老的坑。
看前 40 行的目的是防止「导出失败但错误信息写进了输出文件」。如果文件开头是Error:或者一堆栈信息,说明参数不对,后面所有解析都是白做。这一步不能省。
4.2 批量扫描一个发布目录:控制并发和超时
发布目录里通常混着 exe、DLL、配置文件、资源。全都扫一遍没有意义——DLL 本身就是被依赖方,扫它的导入得到的是一棵更深的树,噪声大于信息。我的做法是只扫 exe,以及少数明确作为插件加载的 DLL。
# 批量导出:只扫 exe,结果按文件名落到 out 目录 $tool = "D:\tools\dependencies-win10\Dependencies.exe" $root = "D:\build\publish" $outDir = "D:\build\dep-reports" New-Item -ItemType Directory -Force -Path $outDir | Out-Null Get-ChildItem -Path $root -Filter *.exe -Recurse | ForEach-Object { $name = $_.BaseName + ".json" $dest = Join-Path $outDir $name # -NoNewWindow 避免每跑一次弹一个黑框 $p = Start-Process -FilePath $tool ` -ArgumentList @("-imports", "-json", "`"$($_.FullName)`"") ` -NoNewWindow -Wait -PassThru ` -RedirectStandardOutput $dest if ($p.ExitCode -ne 0) { Write-Warning ("失败: {0} (exit {1})" -f $_.Name, $p.ExitCode) } }这段脚本的关键点在三个地方。-Recurse递归子目录,因为发布包通常是有层级的;Start-Process加-Wait保证每个文件跑完再跑下一个,避免同时开几十个进程把机器拖死;-RedirectStandardOutput直接写文件,比在 PowerShell 里捕获字符串再落盘更稳,中文路径也不容易乱码。
路径里带空格是常态,所以ArgumentList里给文件路径手动加了双引号。这一点不做,遇到D:\My Build\app.exe这种路径必然失败,而且报错信息往往指向工具本身,让人误以为是工具的问题。
超时也要考虑。有些 exe 会触发工具去做较深的递归,或者文件本身很大,单个跑几十秒是有的。真要上 CI,给每个进程加一个超时上限,超了就 kill 并记一条警告,不要让整条流水线卡在一个文件上。
4.3 解析输出并做差集:落到一份能交付的清单
导出的 JSON 结构因版本而异,但核心信息稳定:模块名 + 是否解析成功 + 解析到的完整路径。写解析脚本时不要把字段名写死,先探测一遍。
import json, glob, os, sys def flatten(path): """把一个导出文件压成 {模块名: 是否已解析} 的字典""" with open(path, encoding="utf-8", errors="replace") as f: data = json.load(f) result = {} # 不同版本的字段名可能是 modules / imports / dependencies nodes = data.get("modules") or data.get("imports") or data.get("dependencies") or [] for node in nodes: name = node.get("name") or node.get("module") or node.get("fileName") if not name: continue # resolved 字段可能叫 resolved / found / isResolved ok = node.get("resolved", node.get("found", node.get("isResolved", True))) result[name.lower()] = bool(ok) return result def load_dir(d): out = {} for fp in glob.glob(os.path.join(d, "*.json")): try: out[os.path.basename(fp)] = flatten(fp) except json.JSONDecodeError as e: print(f"[skip] {fp}: {e}", file=sys.stderr) return out if __name__ == "__main__": dev = load_dir(sys.argv[1]) # 开发机导出的目录 prod = load_dir(sys.argv[2]) # 客户机/发布包导出的目录 for exe in sorted(set(prod) - set(dev)): print(f"[新增] {exe}") for exe in sorted(set(prod) & set(dev)): miss = [m for m, ok in prod[exe].items() if not ok] if miss: print(f"[缺失] {exe}: {', '.join(sorted(miss))}")flatten里用了多层or兜底,是因为字段名在不同构建之间确实会变,写死一个键名,换个版本就 KeyError。errors="replace"是防导出文件里混进非 UTF-8 字节,直接崩掉比读出来乱码更浪费时间。
差集部分的逻辑是:只关心「发布包里有、开发机上没有」的文件,以及「发布包里解析失败」的模块。前者说明这次交付新增了组件,后者说明新增的组件缺依赖。两个列表都为空,才能签收。
load_dir的异常处理只捕JSONDecodeError,其他异常照常抛出——文件不存在、权限不足这类问题应该立刻暴露,而不是被静默跳过。这个取舍在写一次性脚本时很重要,过度兜底会让你在「为什么报告是空的」上浪费半小时。
5. 避坑与排查:五条血泪记录,每条都对应一个具体现象
以下五条都是实际踩出来的,顺序按出现频率排。每条都按「看到什么 → 为什么 → 怎么办」写,对着现象查就行。
5.1 满屏红叉,但程序跑得好好的
现象:拖进去之后几十个api-ms-win-*标红,看起来像是缺了一整套运行库,但同一台机器上程序正常启动。
原因:这是搜索路径没配对,工具没法解析 API Set schema,于是在磁盘上硬找这些虚拟文件名,当然找不到。另一种可能是这版构建对 API Set 的支持不完整。
解决:先确认工具的位数和目标一致,再检查有没有「解析 API Set」这类选项并打开。如果换了版本还是满屏红,换一个角度验证——把目标文件拷到一台干净的同版本 Windows 10 上直接运行,能跑就说明红叉是假的。判读时把 API Set 条目整体折叠掉,别让它们污染视线。
5.2 32 位工具看 64 位文件,结论全乱
现象:一个 64 位的 exe,展开后系统 DLL 全部标红,路径指向SysWOW64下面,而且文件确实存在。
原因:工具本身是 32 位构建,走了 WOW64 重定向,访问System32时被系统悄悄转到SysWOW64,拿到的是 32 位版本的 DLL,和目标文件的位数不匹配。
解决:位数对齐,这是硬规矩。要么换 64 位构建的工具,要么至少在分析 64 位目标时确认工具进程是 64 位。判断方法很土但有效:随便找一个系统 DLL,看工具报出的路径里是System32还是SysWOW64,一眼就能分辨。
5.3 延迟加载报缺失,但程序一整天都没事
现象:延迟加载分组里挂着红叉,业务跑了整天没出问题。
原因:延迟加载的语义就是「用到才加载」,那些函数所在的代码路径根本没被执行。工具只是诚实地告诉你「如果走到这里就会失败」。
解决:先确认业务会不会走到那条路径,走不到就不是缺陷,最多记一条待办。要走得到,就得把这个 DLL 补进安装包。判断路径是否可达,靠的是对业务的理解,工具给不了答案——这也是为什么依赖分析永远替代不了读代码。
5.4 扫到一半卡死,或者整台机器变慢
现象:批量扫一个发布目录,跑到某个 exe 就没动静了,CPU 不高,内存缓慢上涨。
原因:大概率是递归深度太大,或者目标文件引用了网络路径上的模块。加载器在 UNC 路径上找文件会走网络超时,几十秒到几分钟不等。
解决:给每个进程设超时,超了就 kill 并记一条「未完成」。同时把搜索路径限制在本地发布目录,不要让 PATH 里的网络映射盘参与解析。发布目录里如果有指向网络共享的符号链接,直接跳过——那类文件本来就不该出现在交付物里。
5.5 .NET 程序导入表干干净净,被误判成「无依赖」
现象:一个托管 exe,导入表里只有mscoree.dll,结论是「这程序没什么依赖」,结果在客户机上因为缺某个程序集起不来。
原因:托管程序集的引用存在 CLI 元数据里,不在 PE 导入表里。工具如果只按原生逻辑解析,看到的就是一片空白。
解决:切到程序集/托管视图再看一遍,或者用专门的程序集检查工具补一刀。判读托管程序时,导入表只能用来确认运行时的存在,真正的依赖清单要从元数据里取。这个坑我踩过一次之后,凡是看到导入表里只有一个mscoree.dll的文件,一律先按托管程序处理。
6. 把依赖树变成发布自检:三类假阳性与一份差集清单
依赖分析最容易犯的错不是「没看出来」,而是「看出来一堆假问题」。所以收尾这一章不讲怎么看树,讲怎么排除噪声。
三类假阳性要先过一遍:API Set 虚拟模块(磁盘上本来就没有文件)、搜索路径外的动态加载模块(代码里LoadLibrary加载的)、延迟加载项(用到才缺)。这三类在报告里都应该被显式排除,而不是靠人肉记住「这些红叉不用管」。排除的方式很土但管用——在导出的 JSON 上做一层过滤,把模块名匹配api-ms-win-的条目直接剔除,把延迟加载分组单独标记一个deferred: true,剩下的才是需要人工确认的。
然后是差集。开发机和发布包各导一份,跑第 4 章那个脚本,输出的两个列表就是发布前的检查项。我现在的习惯是把它做成一个固定动作:
| 阶段 | 动作 | 通过标准 |
|---|---|---|
| 打包前 | 对发布目录批量导出导入清单 | 全部文件导出成功,无超时 |
| 打包后 | 与上一次基线做差集 | 新增文件清单已人工确认 |
| 交付前 | 在干净虚拟机上再导一次 | 与打包结果一致,无新增缺失 |
| 交付后 | 保留两份报告归档 | 出问题时能回溯是哪次引入的 |
「在干净虚拟机上再导一次」这一步别省。开发机上装着各种 SDK 和运行库,很多依赖被环境掩盖了;虚拟机是检验发布包自洽性的唯一可靠手段。我吃过一次亏,开发机上一切正常,客户机上缺一个 VC 运行库,追了两天才定位到是打包脚本漏拷了 redist 目录。
最后说一个具体技巧:把每次的导出结果按「发布版本号 + 日期」命名归档,不要覆盖。依赖问题是会反复的,同一个 DLL 可能在某个版本里被临时替换过,出了问题能翻出历史快照比对,比重新复现快得多。从那以后我每次出安装包,跑完构建脚本的最后一步都强制走一遍依赖导出和差集,报告不进版本库,但一定留档。这个习惯救过我两次,希望帮到你。
本文还有配套的精品资源,点击获取