简介:面向软件部署人员、开发调试者与普通用户的 Visual C++ 2008 SP1 运行库合集,主要解决用 C/C++ 编写的程序因缺少动态链接库而无法启动、频繁提示 dll 丢失等问题。压缩包 9.2MB,内含 3 个文件:两个 exe 分别为 32 位(x86)与 64 位(x64)安装程序,另附一份 htm 中文说明文档,方便按系统平台选择安装并确认组件构成。安装后即可获得 CRT、标准 C++、ATL、MFC、OpenMP 与 MSDIA 等类库的运行时支持,在支持并行部署的系统上组件会写入 WinSxS 本机程序集缓存,用户无需安装 Visual Studio 2008 便能运行对应的 32 位或 64 位应用。其体积远小于 .NET 运行库,部署轻便,兼容 Windows 2000 SP4、XP、Vista 与 Server 2003 等环境,常用于旧版软件、行业工具的兼容性修复。已有 789 人学习下载。
1. 一个 2008 年的运行库,为什么还在给新机器收拾问题
前段时间,我帮人看一台刚换的工控机:上面装着一个用了十几年的设备配置工具,双击后只弹出一句“应用程序无法启动,因为应用程序的并行配置不正确”。程序本身没有损坏,问题在于它编译时依赖的 VC 2008 运行库没有安装在这台新机器上。系统再新、自带运行库再多,也不会替 2008 年的老程序补上 MSVCR90.dll 这一环。VC 2008 运行库 x86/x64 SP1 官方正式版就是解决这一点:两个安装包,一个补 32 位,一个补 64 位,把 9.0.30729 这一代的 C 运行时、C++ 标准库、MFC 和 ATL 注册到系统里。它适合仍在维护老系统的运维和二次开发者、要交付桌面客户端的开发者,也适合被 0xc000007b 和“并行配置不正确”困扰过的人。
2. 拆开这个包:MSVCR90、MSVCP90、MFC90 分别负责什么
很多同事以为“装运行库”就是装一个 exe,装完即可。实际上,这个包里是两套彼此独立的 Win32 程序集,装进系统后还要能被程序的清单文件正确找到,任意一步不对都白装。先看清文件构成,后面的安装和排错才有参照。
2.1 x86 和 x64 是两套互不相干的运行库
64 位系统上跑 32 位程序,走的是 WOW64 这层转换。程序加载器会去C:\Windows\SysWOW64找 32 位的msvcr90.dll;真正的 64 位程序则去C:\Windows\System32找 64 位版本。对应的 WinSxS 目录也分成两支:x86_Microsoft.VC90.CRT_...和amd64_Microsoft.VC90.CRT_...,互不覆盖。
所以“我只装了 x64 包,为什么 32 位老程序还报错”是最高频的误解。64 位包只服务 64 位进程,32 位进程一个字节都用不上。反过来也一样。我一般把 x86 和 x64 两个都装,成本只是几十兆磁盘。
包内文件职责大致如下,排错时按“报错的是哪一类函数”对着看:
| 文件 | 位数 | 主要职责 | 缺失时的典型症状 |
|---|---|---|---|
| msvcr90.dll | 跟随包 | C 运行时,内存分配、字符串、线程、文件 IO | 启动即报找不到 DLL,或并行配置错误 |
| msvcp90.dll | 跟随包 | C++ 标准库,std::string、iostream、异常 | 用到 STL 的模块加载失败 |
| mfc90.dll / mfc90u.dll | 跟随包 | MFC 框架本体,u 表示 Unicode 版 | 界面程序白屏或直接退出 |
| mfcm90.dll / mfcm90u.dll | 跟随包 | MFC 的托管扩展 | 混合模式程序集加载失败 |
| atl90.dll | 跟随包 | ATL 模板库 | 老 COM 组件初始化失败 |
| msvcm90.dll | 跟随包 | 托管 C++ 运行时 | C++/CLI 程序集加载失败 |
| Microsoft.VC90.CRT.manifest | 跟随包 | 程序集清单,声明版本与公钥 | 私有部署时清单对不上,直接报并行配置错误 |
表中“跟随包”意思是:安装包的位数决定它装进去的是 32 位还是 64 位文件,两套包里的同名文件不能互换。
2.2 SP1、RTM 与 Debug 版的版本矩阵
这份包是 SP1,对应版本号前缀 9.0.30729。VC 2008 的版本线不止这一条,而 Windows 的并行程序集机制是按“主版本.次版本”匹配、再按发布者配置做向上重定向的。搞不清这几条线,就会出现“明明装了却还报并行配置错误”的异常情况。
| 版本线 | 版本号前缀 | 来源 | 能否与 SP1 混用 |
|---|---|---|---|
| RTM | 9.0.21022 | 最初版 VS 2008 自带 | 清单写死 9.0.21022 的老程序,通常会被重定向到 9.0.30729 运行 |
| SP1 | 9.0.30729 | 就是这份包 | 基线版本,最常用的目标 |
| SP1 加后续安全更新 | 9.0.30729.x | 系统更新、新版可再发行包 | 小版本更高,向下兼容,清单不必改 |
| Debug 版 | 9.0.30729.x(带 d 后缀) | 只随开发环境安装 | 不可分发,目标机永远装不上 |
两个关键点。第一,清单里version的最后一位一般不用跟着安全更新改,发布者配置会把你请求的 9.0.30729.1 重定向到机器上实际存在的 9.0.30729.6161 之类的更高小版本;但主次版本号必须落在 9.0 这条线上,写成 9.0.21022 或 8.0.x 都可能解析失败。第二,msvcr90d.dll这类带 d 的调试版运行时只授权开发机使用,随产品发布属于明确的授权违规,而且目标机上根本不会存在。我在客户现场见过“开发机正常,客户机 0xc000007b”的案例,追下去就是某个第三方静态库是 Debug 编译的。
2.3 静默安装:参数、顺序与退出码
图形界面双击只适合单机救急,批量场景一律静默。这套安装包支持/q(完全静默)和/qb(带进度条),两个包建议先 x86 后 x64,虽然互不依赖,但顺序固定便于脚本判断。
:: 以管理员身份执行;/q 无界面,/qb 显示进度条 :: 先装 32 位,再装 64 位,两个都要装 vcredist_x86.exe /q echo x86 返回码: %ERRORLEVEL% vcredist_x64.exe /q echo x64 返回码: %ERRORLEVEL%%ERRORLEVEL%要单独接住,别被下一条命令覆盖,这是批处理脚本里最常见的疏漏。返回码含义如下:
0:安装成功,无需重启3010:安装成功,但需要重启才能生效1638:机器上已存在同产品的更高版本,本次跳过,这不算失败5或0x80070005:权限不足,静默安装同样要管理员令牌1603:安装过程致命错误,多半是包损坏或被安全软件拦截
装完不要急着结束,用 WinSxS 里的清单文件确认结果比看安装向导可靠得多:
# 查看 WinSxS 中已注册的 VC90 程序集清单,x86 与 amd64 会分开列出 Get-ChildItem "$env:windir\WinSxS\Manifests" -Filter "*Microsoft.VC90*" | Select-Object -ExpandProperty Name # 再交叉验证一遍卸载入口里的版本信息 Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like '*Visual C++ 2008*' } | Select-Object DisplayName, DisplayVersionGet-ChildItem那段要看的是清单名里带不带x86_和amd64_两个前缀,只有其中一个就说明另一个包没装成功。第二个命令的-ErrorAction SilentlyContinue是必须的,因为 32 位系统上不存在 WOW6432Node 这个分支,不加会抛错中断脚本。DisplayVersion显示的版本号能直接告诉你这台机器是停在了 SP1 基线,还是已经被后续安全更新推高了。
3. 三条部署路径:整机安装、私有目录、打进安装包
运行库怎么送到目标机器上,比装什么更考验判断。三种路径各有适用边界:选错了,轻则多要一次管理员密码,重则整个交付流程卡在客户 IT 部门那里。
3.1 整机安装:终端机器最省事的做法
终端用户机器、自有设备、内部工控机,优先走整机安装。装一次,机器上所有依赖 VC90 的程序都受益,后续安全更新也能通过系统更新自动覆盖,这是维护成本最低的路径。
操作步骤:先用管理员账户运行 x86 包,再运行 x64 包,最后跑一遍上面那段 WinSxS 检查。注册表里的卸载入口、WinSxS 里的清单、System32/SysWOW64 下的 DLL 三处一致,才算干净。
要注意的是权限模型。这套安装包注册的是全局并行程序集,必须写 WinSxS,普通用户权限下一定会失败,而且失败得很难看:图形界面会提示“安装失败”,静默安装则只返回 5。域环境里我一般直接用组策略的启动脚本推,脚本里判断一下机器架构再决定跑哪个包:
:: 注意:32 位安装器在 64 位系统上,PROCESSOR_ARCHITECTURE 仍是 x86 :: 真正的位数要看 PROCESSOR_ARCHITEW6432 set "IS64=" if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" set "IS64=1" if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" set "IS64=1" if defined IS64 ( start /wait "" "%~dp0vcredist_x64.exe" /q ) start /wait "" "%~dp0vcredist_x86.exe" /q这段脚本最关键的是PROCESSOR_ARCHITEW6432这个变量。32 位安装器或 32 位脚本宿主在 64 位系统上运行时,PROCESSOR_ARCHITECTURE会诚实地报告x86,只有PROCESSOR_ARCHITEW6432才暴露真实架构。我曾经因为这个判断失误,给一批 64 位机器只发了 32 位包,事后排查了整整半天。start /wait也不能省,否则两个安装器会并发跑,相互抢注册表锁。
3.2 私有部署:没有管理员权限时的可行方案
客户方 IT 不给管理员权限、或者你只是交付一个绿色版小工具,这时走应用本地私有部署。原理是把 CRT 的 DLL 和清单文件放进 exe 同目录,加载器先在应用目录找,找到就用,不再去 WinSxS 解析全局程序集。
文件从开发机的 redist 目录取,VS 2008 的路径由VS90COMNTOOLS环境变量可以推出来:
# VS90COMNTOOLS 指向 Common7\Tools,往上两级才是 VC 目录 $vc = Join-Path $env:VS90COMNTOOLS "..\..\VC\redist" $crt = Join-Path $vc "x86\Microsoft.VC90.CRT" # 64 位对应 amd64\Microsoft.VC90.CRT $dest = ".\dist\x86" # 目标输出目录,和 exe 同级 New-Item -ItemType Directory -Force -Path $dest | Out-Null Copy-Item "$crt\msvcr90.dll", "$crt\msvcp90.dll", "$crt\Microsoft.VC90.CRT.manifest" ` -Destination $dest -Force Get-ChildItem $dest | Select-Object Name, LengthNew-Item -Force保证目录不存在时自动创建、存在时不报错,适合放进构建脚本反复执行。amd64这个目录名容易被写成x64,实际 redist 下就是amd64,写错会直接复制失败。
光复制文件还不够,宿主程序必须在自己的清单里声明这是一次私有程序集引用,否则加载器还是会去全局找。清单可以直接嵌进 exe,也可以放一个程序名.exe.manifest在同目录:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.30729.1" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b" language="*"/> </dependentAssembly> </dependency> </assembly>name、publicKeyToken、processorArchitecture三个字段必须和同目录下的Microsoft.VC90.CRT.manifest完全一致,version可以低于实际文件版本,但不能高于。processorArchitecture在 64 位版本里写amd64,这里如果照抄成x86,64 位进程会在启动瞬间失败,报的还是并行配置错误,很有迷惑性。
私有部署的代价要提前说清楚:这份 CRT 从此不受系统更新管辖。以后出了 CRT 层面的安全补丁,全局安装的机器自动修好,你的私有副本还是旧的,得重新发一次版本。用 MFC 的程序还要额外把mfc90u.dll、mfcm90u.dll和Microsoft.VC90.MFC.manifest一起带上,很多程序只复制了 CRT 三个文件,结果界面一弹就崩,就是漏了这一步。
3.3 打进安装包:把自己当分发方时的注意点
用安装器封装时,把两个 vcredist 作为资源带上,安装阶段调用。要点是:只在真正缺少时安装、正确接住 3010、卸载时不回退别人的运行库。
; 安装器脚本片段(Inno Setup 风格):按位数决定跑哪个包 [Run] Filename: "{app}\redist\vcredist_x64.exe"; Parameters: "/q"; \ StatusMsg: "正在安装 64 位运行库..."; Check: IsWin64; Flags: waituntilterminated Filename: "{app}\redist\vcredist_x86.exe"; Parameters: "/q"; \ StatusMsg: "正在安装 32 位运行库..."; Flags: waituntilterminatedFlags: waituntilterminated是必须的,去掉它安装器会立刻进入下一步,用户还没看到进度条,主程序就启动了,随后报错。Check: IsWin64控制 64 位包只在 64 位系统上跑,32 位系统上强行安装会返回 1603,虽然无害但会污染日志。
卸载时不要在卸载脚本里动运行库。同一台机器上可能还有别的软件依赖它,卸载时删了别人的运行库是典型的连带故障,而且用户根本想不到是卸载你造成的。
4. 排错:先分清“缺 DLL”和“并行配置不正确”
VC 2008 相关的报错表面看都是“跑不起来”,底下却是两套完全不同的机制。分不清这两类,就会在错误的方向上耗费大量时间。
4.1 两种报错背后的两套加载机制
“找不到 msvcr90.dll”属于常规 DLL 搜索失败:加载器按应用目录、系统目录、PATH 的顺序找,找不到就报错。这类问题通常是文件确实不在、或者位数不对。
“应用程序的并行配置不正确”属于 SideBySide 解析失败:文件明明在 WinSxS 里,但清单里声明的程序集版本、公钥、架构三项对不上,激活上下文创建失败,进程根本走不到加载 DLL 那一步。这类问题的证据不在弹窗里,在事件查看器里:应用程序日志中来源为 SideBySide 的事件,描述会直接写出它请求的是哪个程序集、哪个版本、哪个架构。
还有一种0xc000007b,翻译过来是镜像格式无效,九成是位数混搭:64 位宿主加载了 32 位 DLL,或者反过来。这个错误码和运行库缺失长得很像,但排查方向完全不同,先看位数。
4.2 用 dumpbin 把导入表读出来
别靠猜。开发机上用 dumpbin 把目标程序的导入表和目标架构打出来,比对着报错弹窗看快十倍。
:: 列出依赖的 DLL,重点看有没有 MSVCR90/MSVCP90/MFC90 系列 dumpbin /dependents MyApp.exe :: 再看目标架构,注意 machine 一栏是 x86 还是 x64 dumpbin /headers MyApp.exe | findstr /i "machine"/dependents输出了什么,就要求目标机上必须有对应的程序集,缺一不可。如果输出里出现MSVCR90D.dll这种带 d 的名字,问题就明确了:是个调试版模块混进了发布包,装多少遍运行库都没用,得回去重新编译。
/headers的 machine 字段要和目标机的进程位数对齐。我在一个项目里遇到过主程序是 x64、某个插件 DLL 是 Win32 的情况,现象是主程序能启动但一加载插件就整个进程消失,dumpbin 一跑就定位了。
机器上没装开发环境也不要紧,进程监视器能替代一半的活。做法是:打开进程监视器,过滤器设成“进程名是目标程序”,再加一条“路径包含 .dll”,然后启动程序。看结果列表里Result为NAME NOT FOUND的那几行,那就是加载器实际找过但没找到的路径,搜索顺序一目了然。这个方法的好处是能看到私有部署有没有生效:如果加载器先去应用目录找、并且成功了,说明清单配置正确。
4.3 用 sxstrace 把并行配置错误还原成文本
遇到“并行配置不正确”,最有效的工具是系统自带的 sxstrace。它把 SideBySide 的解析过程完整记录下来,最后落成一个可读文本。
:: 复现问题前后各执行一次,中间启动出错的程序 sxstrace trace -logfile:sxs.etl :: 启动程序,看到报错后回到命令行按回车结束跟踪 sxstrace parse -logfile:sxs.etl -outfile:sxs.txt生成的sxs.txt里搜ERROR,通常能看到类似“无法解析依赖项 Microsoft.VC90.CRT,版本 9.0.30729.1,架构 x86”这样的行。到这里问题就具体了:要么机器上只有 amd64 那一支、缺 x86;要么请求的版本主次号越界;要么应用目录里放了一份同名清单把全局的挡住了。
sxstrace需要管理员权限,跟踪期间的日志文件会持续增长,复现完记得按回车结束,别把它挂在后台。这套流程我一般只在 dumpbin 和事件日志都没给出结论时才上,因为它的输出很长,读起来费神但有黑匣子的效果,失败原因一行不落。
5. 避坑:五条血泪记录
以下五条都是我自己的教训或现场见过的情况,按“现象、原因、解决”记下来,遇到时可以直接对照。
5.1 安装与位数相关的三个坑
只装了 64 位包,32 位程序照样报错。现象是 x64 包安装成功、在 WinSxS 里也能看到 amd64 分支,但那个老配置工具双击还是提示找不到 msvcr90.dll。原因是 32 位进程走 WOW64,加载的是 SysWOW64 下的 32 位运行库,和 64 位包毫无关系。解决办法就是两个包都装,别做选择题;批量分发时按 3.1 里的脚本判断架构,两个都跑。
32 位安装器里判断位数判错,误以为机器是 32 位。现象是脚本在 64 位机器上跳过了 x64 包的安装,事后才发现一半机器只装了一半。原因就是PROCESSOR_ARCHITECTURE在 32 位宿主里返回x86,真实架构藏在PROCESSOR_ARCHITEW6432。解决办法是同时读两个变量,任一为 AMD64 就按 64 位处理。
静默安装返回 3010,被安装器当成失败直接回滚。现象是功能一切正常,但安装器日志里写着“运行库安装失败,回滚”,整套部署白做。原因在于 3010 表示安装成功、待重启,属于成功码的一种,很多脚本只认 0。解决办法是把 0 和 3010 都判成功,其余才判失败;同时把“建议重启”写进安装完成提示,别让用户以为已经全部生效。
5.2 版本与清单相关的两个坑
装完重启,程序仍报并行配置错误。现象是 WinSxS 里 x86 和 amd64 都在,注册表也有记录,但只有这一个程序报错,别的老程序正常。原因通常是程序目录里躺着一份旧版msvcr90.dll或Microsoft.VC90.CRT.manifest,应用目录优先级高于全局,加载器拿到的是那份旧清单,版本或公钥对不上就解析失败。解决办法是清掉应用目录里的私有副本,改走全局安装;确实需要私有部署的,就按 3.2 把清单和文件版本对齐,别新旧混放。
另一种同表现的情况是 ATL 相关的安全更新把atl90.dll的小版本推高了,某些老程序的清单把版本卡得很死,请求的架构又写错,照样解析失败。这时还是用 sxstrace,看它到底请求了哪个版本、哪个架构,再决定是补装还是改清单。
开发机一切正常,客户机报 0xc000007b。现象是同一个安装包,测试机跑得好好的,到了客户现场双击就退出。原因八成是发布包里混了 Debug 编译的模块,依赖msvcr90d.dll,或者某个第三方 DLL 的位数和主程序不一致。解决办法是用 dumpbin 把发布包内所有 exe 和 dll 过一遍,凡是导入表里出现带 d 后缀的运行时,打回重新出 Release 包;再把每个 DLL 的 machine 字段和主程序对齐。
补充一条容易被忽略的:有些团队为了省事直接用/MT静态链接 CRT,确实不依赖运行库了,但如果同一个进程里有多个模块各自静态链接了 CRT,跨模块传递FILE*、或者在一个模块里分配、在另一个模块里释放,堆不一致会导致随机崩溃,比缺 DLL 还难查。要静态链接就整个产品线统一静态,别一半一半。
6. 把运行库检查写进发版流程:一份自检脚本和一张核对表
这些坑的共同点是:发现得太晚。等用户装了包报错,再回头查是哪个模块带了 Debug CRT,成本是发布前的十倍。我现在的做法是把它变成构建流水线上的一道强制检查,出包就扫,扫到就直接失败。
6.1 交付前跑一遍的依赖自检脚本
思路很朴素:把产物目录里所有 exe 和 dll 扫一遍,看有没有引用 VC90 系列的运行时,有就说明这个包离不开运行库,必须在交付物里带上,或者确认目标机已装。
# scan_vc90.py:粗扫产物目录里的 VC90 依赖,命中则退出码 1 import pathlib import re import sys # 抓 PE 导入表里的 DLL 名字符串,带 d 后缀的调试版也一并纳入 PATTERN = re.compile(rb"(msvcr90d?|msvcp90d?|mfc90u?d?|mfcm90u?d?|atl90|msvcm90)\.dll", re.I) def scan(path: pathlib.Path): data = path.read_bytes() return sorted({m.group(0).decode("ascii", "ignore").lower() for m in PATTERN.finditer(data)}) def main(root_dir: str) -> int: hits = {} for item in pathlib.Path(root_dir).rglob("*"): if item.suffix.lower() in {".exe", ".dll"}: found = scan(item) if found: hits[str(item)] = found for name, deps in hits.items(): print(f"{name}: {', '.join(deps)}") # 退出码 1 让流水线直接红掉,别让问题流到客户现场 return 1 if hits else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))逻辑说明:rglob("*")递归遍历整个产物目录,只处理 exe 和 dll;read_bytes读原始字节,用正则匹配导入表里的 DLL 名称字符串,速度很快,几百兆的包几秒跑完。参数root_dir就是产物目录,不传则用当前目录。退出码约定为 0 表示干净、1 表示命中,方便直接挂在构建脚本的最后一步。
这个方法是字节级粗扫,会有误报,比如版本资源或字符串常量里正好出现了msvcr90.dll这几个字。所以它适合当“提醒器”而不是“判决器”:命中之后,再对可疑文件单独跑一次dumpbin /dependents精确定位。误报的代价只是多看一眼,漏报的代价是客户现场白屏,这个交换比很划算。
6.2 交付前的一张核对表
脚本管自动化,人工再补几条它看不出来的:
| 检查项 | 判定标准 | 漏掉的后果 |
|---|---|---|
| 目标机是否已装 VC90 | WinSxS 清单里 x86 与 amd64 都在,或注册表版本号可用 | 启动即报找不到 DLL |
| x86 与 x64 是否都覆盖 | 32 位和 64 位产物各自能找到对应运行库 | 一半用户正常、一半报错 |
| 是否混入 Debug 运行时 | 导入表里无 msvcr90d.dll 等带 d 名称 | 客户机 0xc000007b |
| 私有部署清单是否对齐 | 应用清单与同目录 CRT 清单的版本、公钥、架构一致 | 并行配置错误 |
| 静默安装返回码处理 | 0 与 3010 均判成功 | 安装器误回滚 |
| 卸载流程是否回避运行库 | 卸载脚本不删全局程序集 | 卸载一个软件搞坏其他软件 |
那张表我贴在发布检查单的第二页,出包前逐条划一遍,五分钟,比事后救火便宜太多。从那以后,我每次发版都强制走一遍“扫依赖、看位数、验清单”这三步,不管包多小、客户多急。绕过去省下的十分钟,通常会在某个没有开发环境的现场以三倍时间还回来。希望帮到你。
本文还有配套的精品资源,点击获取