简介:面向 Windows Server 2012 R2 Standard 系统管理员与运维人员的 SxS 组件文件包,聚焦 .NET Framework 3.5 安装失败的典型场景。当系统在线更新组件受阻或安装介质缺少相应组件时,可通过指定该备用源路径补齐所需 side-by-side 程序集,减少反复排错与镜像查找成本。压缩包共 1568 个文件,约 85.45MB,以 dll 程序集、resx 资源、exe 可执行模块为主,并含 aspx、config、sql、browser 等配套文件,保留了较完整的组件目录层次,便于按模块定位与校验。资源已有 1808 人学习下载,适合需要离线修复 .NET 3.5 安装、维护 2012 R2 服务器环境的读者参考使用。目录中保留了较多程序集资源与配置清单,可用于核对组件版本、补齐缺失依赖,并作为后续系统维护的离线文件底稿,对具备一定 Windows 服务器维护基础的人员更为友好,也适合老旧服务器环境的维护与应急恢复。
1. 装不上 .NET 3.5 的那台 2012 R2,缺的其实是 sources\sxs
一台还在运行的 windows server 2012 r2 standard 业务机,某个老版进销存客户端硬要 .NET Framework 3.5,在服务器管理器里勾选「.NET Framework 3.5 功能」,进度条走到一半弹出 0x800F0906。这不是系统坏了,而是 2012 R2 把 NetFx3 的载荷从系统盘里摘了出去,只留一个空的功能项,真正要装的东西躺在安装介质的sources\sxs目录里。所谓「sxs 文件」,就是这份按需组件的源——一堆.cab包,配一条带/Source的 DISM 命令,离线、可校验、可复现。它适合三类人:维护存量 2012 R2 机器的运维,去内网交付现场做离线部署的实施,以及被 WSUS 策略卡住、连不上外网的环境。下面按「这东西为什么存在 → 怎么从镜像里抠出来 → 三条启用路径 → 五个翻车点」的顺序拆。
2. SxS 到底是什么:从 DLL 地狱到 NetFx3 的按需载荷
2.1 side-by-side 的由来,以及 WinSxS 为什么不能手删
sxs是 side-by-side 的缩写。早年 Windows 的共享库直接铺在System32里,谁后装谁覆盖,A 软件升级一个公共 DLL 就能把 B 软件干趴下,这堆烂账在圈内叫 DLL 地狱。后来的解法是允许同一个 DLL 的多个版本共存,由程序自带的 manifest 声明「我要 6.0.0.0 这一版」,加载器按 manifest 去一个专门的仓库里取。这个仓库就是C:\Windows\WinSxS,官方叫组件存储(component store)。
组件存储里放的不只是并行程序集,还有系统更新、功能载荷、驱动包、修复用的备份副本。关键在于:System32下面大量文件的实体数据其实就指向 WinSxS 里的那一份,用的是硬链接,不是复制。所以你在资源管理器里看到 WinSxS 体积巨大,但磁盘占用并不等于这个数字,删它不但省不出空间,还会把源砍掉。我见过有人为了「瘦身」直接删子目录,后面补丁装不上、sfc /scannow报错、功能启用一律 0x80073712,只能拿同版本介质做修复安装。
查看组件存储的真实状态,用 DISM 的分析开关,这是唯一被支持的方式:
# 分析组件存储:实际大小、可回收大小、备份与缓存的构成 dism.exe /Online /Cleanup-Image /AnalyzeComponentStore/AnalyzeComponentStore只读不写,输出里会列出「实际文件大小」「共享文件夹」「备份」「缓存」几项。如果「可回收的包」数量不为 0,可以跑/StartComponentCleanup清理旧版本组件;但 2012 R2 上加/ResetBase之后,已安装的更新就不能再卸载了,生产机上要掂量一下。
2.2 NetFx3 缺的到底是什么,为什么装了 4.5 也没用
从 Windows 8 / Server 2012 开始,.NET Framework 3.5 被改成了「按需功能」(Feature on Demand):功能项本身在系统里注册着,但二进制载荷不随镜像分发,安装时再去取源。这样镜像小、攻击面小,代价就是离线环境必须自己给源。2012 R2 自带的是 .NET 4.5,很多人以为「版本更高就向下兼容」,于是装完 4.5 发现老程序照样报错——因为 CLR 2.0 和 CLR 4.0 是并排运行的,不是升级关系。进程加载哪个 CLR,由程序集的supportedRuntime和编译目标框架决定,两者在机器上共存,互不替代。
同一个功能在不同入口下名字还不一样,这点最容易把人绕晕:
| 入口 | 标识 | 说明 |
|---|---|---|
| DISM | NetFx3 | 功能名,/FeatureName:NetFx3 |
| PowerShell(ServerManager) | NET-Framework-Core | 服务器功能名,Install-WindowsFeature用 |
| 服务器管理器 GUI | 「.NET Framework 3.5 功能」 | 底层还是调上面两个 |
| 客户端系统 | NetFx3(Optional Feature) | 语义相同,载荷不同,不能混用 |
勾选后实际落盘的位置是C:\Windows\Microsoft.NET\Framework\v2.0.50727(CLR 2.0 本体)与C:\Windows\Microsoft.NET\Framework\v3.5(3.5 增量,主要是 LINQ、WCF、WF 那批程序集),64 位系统还有对应的Framework64。这两个目录同时在,才算真的装全。
2.3 为什么不建议随手抓一个「NetFx3 离线包」
网上流传的所谓离线包,本质就是从某个安装介质里抠出来的 cab 再打成一个自解压 exe。问题有三层。第一层是来源不可控,包被替换、被塞私货,你从哈希上看不出来,因为压根没有可比对的基准。第二层是版本与语言错配,把客户端系统(比如某个 Win10 镜像)的 sxs 拿去喂 2012 R2,CBS 会直接判不匹配,报 0x800F081F;同一代但不同语言介质,也可能在语言相关包上失败。第三层是授权边界,安装介质的来源要合规,用自己手上的正版介质是最省心的做法。
我一般坚持的选型原则是:源必须来自与目标系统同版本、同架构、同语言(或至少语言中立)的安装介质,取完之后自己做一次哈希留档,以后同批次机器全部复用同一份源。这条原则在几十台机器的批量交付里能省掉大量「这台能装那台不能装」的玄学问题——听起来啰嗦,但它是最便宜的可复现性。
3. 从安装介质里把 sxs 取出来:挂载、定位与校验
3.1 挂载 ISO 并确认目录内容
先把安装镜像挂上。图形界面双击也行,但要做批量就得用命令,Mount-DiskImage不带-PassThru是拿不到盘符的,这是第一个小坑:
# 以管理员身份运行;把路径换成你自己的 2012 R2 Standard 安装镜像 $isoPath = "D:\ISO\server2012r2_standard_x64.iso" $img = Mount-DiskImage -ImagePath $isoPath -PassThru $vol = $img | Get-Volume $src = "$($vol.DriveLetter):\sources\sxs" Write-Host "已挂载到 $($vol.DriveLetter):,源目录:$src" Get-ChildItem -Path $src -Recurse -File | Select-Object FullName, Length | Sort-Object Length -Descending | Format-Table -AutoSize-PassThru让Mount-DiskImage返回对象,Get-Volume才能顺着拿到DriveLetter;没有这一步,脚本里就得靠Get-DiskImage | Get-Volume再查一次。最后的Get-ChildItem -Recurse -File是必须做的一步:不同批次、不同渠道拿到的 2012 R2 介质,sxs下的文件构成会有出入,载荷包常见名是microsoft-windows-netfx3-ondemand-package.cab,但我不建议你把文件名写死在脚本里,先列出来看一眼更稳。
3.2 只取需要的载荷,并按目录结构原样复制
确认有 cab 之后,把整个sxs目录复制到本地固定路径。注意是「原样复制」,不要把 cab 单独拎出来放进一个空目录——DISM 是按目录去扫的,目录层级变了有时候能过、有时候报找不到源,属于自己给自己制造不确定性:
# 目标路径建议短、纯英文、无空格,避开中文与超长路径 $dst = "C:\sxs-src\2012r2" New-Item -Path $dst -ItemType Directory -Force | Out-Null # /E 含空目录;/COPY:DAT 带上数据、属性、时间戳;/R:2 /W:5 控制重试 robocopy $src $dst /E /COPY:DAT /R:2 /W:5 /NP /LOG:C:\sxs-src\copy.log Write-Host "退出码:$LASTEXITCODE" # robocopy 0~7 都算成功,>=8 才是真失败robocopy的退出码和普通程序不一样,0 到 7 都表示「有复制、有跳过但没出错」,8 及以上才是失败,写脚本做判断的时候别用-ne 0。/NP关掉百分比进度,是为了让日志文件干净;/LOG保留一份复制记录,出问题时能回溯到底是哪个文件没拷全。
3.3 做一次哈希留档,然后卸载镜像
复制完要做校验,但校验的对象不是「跟网上某个哈希比对」,而是「跟原始介质比对」——同一批文件算两次,确认复制过程没有截断。这一步在内网交付里价值很高,一台机器装失败时,你能立刻排除「源不完整」这个变量:
# 原始介质与本地副本各算一次,逐项比对 $srcHash = Get-ChildItem $src -Filter *.cab -Recurse | Get-FileHash -Algorithm SHA256 $dstHash = Get-ChildItem $dst -Filter *.cab -Recurse | Get-FileHash -Algorithm SHA256 Compare-Object $srcHash.Hash $dstHash.Hash -SyncWindow 0 # 无输出 = 一致;有输出说明复制不完整,重跑 robocopy # 留档:把哈希写进文本,跟源放一起 $dstHash | Select-Object Path, Hash | Export-Csv "C:\sxs-src\2012r2-hash.csv" -NoTypeInformation -Encoding UTF8 # 收尾:卸载镜像 Dismount-DiskImage -ImagePath $isoPathCompare-Object这里比的是纯哈希字符串集合,-SyncWindow 0会做严格顺序比对,但我们真正关心的是「有没有多一个少一个」,所以更稳妥的写法是按文件名做键去 join。实际操作用Compare-Object $srcHash.Hash $dstHash.Hash也够,两边数量一致且无差异输出,基本可以判定复制完整。
注意:本地副本建议设为只读,或者干脆放在一个专用目录里不让人改。我遇到过源目录被别的同事顺手塞了个同名不同版本的 cab,DISM 选包时行为变得不可预测,排查了半天。
4. 三条启用路径:DISM 在线、PowerShell、离线镜像
4.1 DISM 在线启用,以及每个参数到底在干什么
最直接的一条命令,源指向刚准备好的目录,/LimitAccess是关键:
# 在线启用 NetFx3,源指向本地 sxs,禁止回落到 Windows Update dism.exe /Online /Enable-Feature /FeatureName:NetFx3 /All ` /Source:C:\sxs-src\2012r2 /LimitAccess逐条拆:/Online表示操作当前正在运行的系统,对应的是离线维护场景里的/Image;/Enable-Feature是动作;/FeatureName:NetFx3是功能标识,注意这里不是NET-Framework-Core,那是 PowerShell 的叫法;/All会连带启用该功能的父功能项,NetFx3 下面挂着若干子项,不加这个参数有时会出现「功能已启用但程序仍报错」的半吊子状态;/Source:给的是载荷目录,不是单个 cab 文件;/LimitAccess阻止 DISM 在源找不到时去连 Windows Update——这条在离线环境和被 WSUS 管控的环境里几乎是必加项,少了它你会看到命令卡住几分钟然后报 0x800F0906。
执行完立刻验证状态:
# 两个入口都查一遍,避免只看一个 dism.exe /Online /Get-FeatureInfo /FeatureName:NetFx3 Get-WindowsFeature -Name NET-Framework-Core | Select-Object Name, InstallStateGet-FeatureInfo的输出里找State : Enabled;Get-WindowsFeature的输出里InstallState应该是Installed。两个都对了,才算真装完。如果 DISM 显示 Enabled 但Get-WindowsFeature显示 Available,通常是服务器管理器的元数据还没刷新,重启一次ServerManager进程或者直接重开控制台即可。
4.2 PowerShell 等价写法与结果校验
习惯用 PowerShell 的话,ServerManager 模块那条更顺手,返回值是结构化对象,便于批量脚本判断:
# -Source 指向 sxs 目录;-Restart:$false 先不自动重启,便于收集日志 $r = Install-WindowsFeature -Name NET-Framework-Core ` -Source "C:\sxs-src\2012r2" -Restart:$false $r | Select-Object Success, RestartNeeded, ExitCode, FeatureResultSuccess为True、ExitCode为Success或NoChangeNeeded都算通过。RestartNeeded为True时要挑维护窗口重启,别在业务高峰动。这条命令底层走的还是 CBS,所以前面/LimitAccess的意义在这里同样成立——只是 ServerManager 没有暴露对应开关,它默认就不会去外网抓,行为上更「安分」一些。
装完再做一次落地校验,三个位置都确认,能挡掉大部分「装是装了但程序不认」的争议:
# 1) 目录是否存在(32 位与 64 位都要) Test-Path "$env:windir\Microsoft.NET\Framework\v3.5" Test-Path "$env:windir\Microsoft.NET\Framework64\v3.5" # 2) 注册表标记 Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5' ` -Name Install, Version -ErrorAction SilentlyContinue # 3) 最近的 CBS 日志尾巴,看有没有残留错误 Get-Content "$env:windir\Logs\CBS\CBS.log" -Tail 50 | Select-String -Pattern "NetFx3|error|failed"注册表里Install应该是1,Version是3.5.xxxxx这样的形式。CBS.log的尾巴主要用于事后复盘,DISM 的详细日志则在C:\Windows\Logs\DISM\dism.log,报错时两个都值得看。
4.3 离线镜像批量注入:挂载 WIM 再启用
做标准化交付时,更划算的做法是先把功能注进 WIM,装机时就带着,不用每台再跑一遍。这里第一步不是挂载,是看清楚索引号:
$wim = "D:\images\install.wim" # 先看清 Index 与 Edition 的对应关系,别凭记忆选 Get-WindowsImage -ImagePath $wim | Select-Object ImageIndex, ImageName New-Item -Path "C:\mount" -ItemType Directory -Force | Out-Null Mount-WindowsImage -ImagePath $wim -Index 2 -Path "C:\mount"Get-WindowsImage那一步非常重要。同一张 2012 R2 介质里通常有 Standard、Standard Core、Datacenter、Datacenter Core 等多个索引,序号在不同来源的镜像上并不保证一致,选错了就是把功能注进了另一个版本,白忙一场。挂载到C:\mount之后,用离线形式的 DISM 加功能:
# 注意是 /Image 而不是 /Online dism.exe /Image:C:\mount /Enable-Feature /FeatureName:NetFx3 /All ` /Source:C:\sxs-src\2012r2 /LimitAccess # 保存并卸载;-Save 才会把改动写回 wim Dismount-WindowsImage -Path "C:\mount" -Save/Image:后面跟的是挂载点,其余参数含义与在线模式一致。Dismount-WindowsImage不加-Save等于前功尽弃,这是新手最常犯的一个错。另外挂载期间 wim 文件被独占,别再拿它去做别的事;如果中途脚本崩了留下挂载点,用Dismount-WindowsImage -Path C:\mount -Discard清理干净再重来。
5. 避坑与排查:五条踩过的记录
5.1 0x800F0906 和 0x800F081F 提示很像,根因完全是两回事
现象:DISM 报错,一个是 0x800F0906「无法下载源文件」,一个是 0x800F081F「找不到源文件」,很多人当成同一个问题反复试。
原因:0906 多数是命令没带/LimitAccess,DISM 在源里没匹配上就转去连 Windows Update,离线环境下连不通,最终以「下载失败」收场;081F 则是源路径本身有问题——指向了sources根目录而不是sources\sxs、目录被清理过、或者介质版本/架构与目标系统对不上。
解决:先Get-ChildItem确认/Source指向的目录里确实有 cab,再检查命令里有没有/LimitAccess。两步都对了还报 081F,就换一份同版本介质重试,别在原地上反复怀疑人生。
5.2 0x800F0907 与 0x800F0954:组策略在中间拦了一刀
现象:域内机器上执行同样的命令,卡很久,然后报 0x800F0907 或 0x800F0954,本地测试机却一次成功。
原因:域里有针对「可选组件安装和组件修复」的策略,把按需功能与修复内容的来源指向了内部更新服务器,而该服务器上并没有放行 NetFx3 的按需载荷。命令带着/Source也未必能绕过去,因为策略层面先拦截了。
解决:临时在本地策略里允许「直接从 Windows Update 下载修复内容」不现实(内网本来就不通),正确做法是在域策略里把LocalSourcePath指向文件服务器上的 sxs 共享;或者现场用带/Source的离线路径并确认策略未强制拦截。改完策略不需要重启,但要把 DISM 会话关掉重开,否则读的还是旧配置。改注册表前先导出一份,事后还原,这个习惯能省掉很多返工。
5.3 手删 WinSxS 之后,系统更新全线报错
现象:为了腾空间删了C:\Windows\WinSxS下几个「看起来很大」的目录,之后补丁装不上,功能启用报 0x80073712,sfc /scannow提示有无法修复的损坏。
原因:前面说过,System32里大量文件是硬链接到 WinSxS 的,删掉的不是副本而是唯一实体。组件存储损坏后,CBS 无法定位到功能载荷的备份,任何依赖它的操作都会失败。
解决:组件存储只能通过受支持的命令回收,不能手删。已经删坏了的,用同版本介质做修复:
# 从同版本安装介质取修复源;install.wim 需先挂载或用 /Source:wim 语法 dism.exe /Online /Cleanup-Image /RestoreHealth ` /Source:C:\sxs-src\2012r2 /LimitAccess/RestoreHealth会扫描组件存储并把损坏的条目用源里的副本替换,/LimitAccess同样是为了不回落外网。这一步耗时较长,别在中途 Ctrl+C。修复完再跑一次/StartComponentCleanup收尾,但要不要加/ResetBase得看业务——加了之后已装更新不可卸载。
5.4 路径、权限与盘符:静默失败的三大来源
现象:/Source指向网络路径时,报 0x800F081F 或者 0x80070005(拒绝访问),但同一个路径在资源管理器里双击能进。
原因:三类。一是用了映射的网络驱动器盘符(比如Z:\sxs),DISM 以服务上下文运行,看不到当前用户的映射;二是权限给的是用户账号,而实际读取用的是计算机账号;三是路径里带空格、中文或者层级过深,某些老版本组件在解析时出问题。
解决:一律用 UNC 路径而不是盘符;共享权限和 NTFS 权限都要给「计算机账户」或相应的计算机组读权限;路径尽量短、纯英文。实在搞不定就先复制到本地盘,本地源是最没有悬念的方案。
5.5 功能显示已启用,老程序还是提示需要 .NET Framework 3.5
现象:DISM 和Get-WindowsFeature都显示装好了,目标程序启动仍然弹「需要 .NET Framework 3.5」。
原因:可能是 32 位程序在 64 位系统上通过 WOW64 加载 32 位 CLR,而Framework64\v3.5存在、Framework\v3.5缺失(或反过来),组件装了一半;也可能是程序的实际目标框架是 1.1 或某个特定 2.0 补丁级别,3.5 覆盖不到;还有一种是程序清单里没写supportedRuntime,运行时按默认策略选了 4.0 的 CLR 然后加载失败。
解决:先确认Framework与Framework64下的v3.5都在;再用配置文件强制指定运行时版本,在程序目录下建一个程序名.exe.config,写入启动版本声明;如果确认是 1.1 依赖,就要认清边界——2012 R2 本身不提供 1.1 运行时的受支持安装路径,这条路走不通就得回到应用侧改造成 3.5 或 4.x。
6. 进阶:把 sxs 做成可复用的本地源,配一份验证清单
单机装一次用不上「进阶」这两个字,真正省事的是把它变成域内所有 2012 R2 机器都能用的本地源。做法不复杂:把sxs放到文件服务器的共享目录,只读,然后通过策略把「可选组件安装和组件修复」的本地源路径指过去。策略位置在计算机配置的管理模板下、系统节点里,勾选后会自动写入注册表的HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Servicing下两个值,LocalSourcePath是路径,RepairContentServerSource控制修复内容来源。我不建议手抄数值,直接在策略编辑器里勾,写完用命令核对就行:
# 文件服务器侧:建只读共享,读权限给「计算机组」而不是具体用户 New-SmbShare -Name "sxs2012r2" -Path "D:\sxs\2012r2" ` -ReadAccess "DOMAIN\SrvComputers" # 替换为实际的计算机组 # 目标机侧:核对策略写入结果 $p = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Servicing' Get-ItemProperty $p | Select-Object LocalSourcePath, RepairContentServerSource共享权限给计算机组这一点,是前面 5.4 那个坑的正面解法:DISM 以系统身份读源,用户账号有没有权限根本不参与判断。策略生效后,机器上直接勾功能项就能从共享取载荷,不用每台都去挂 ISO。共享侧只读、路径纯英文、定期比对哈希,这三条守住,源就不会莫名其妙失效。
装完之后怎么判断「真的好了」,我固定跑一张清单:
| 检查点 | 命令或位置 | 期望结果 |
|---|---|---|
| DISM 功能状态 | dism /Online /Get-FeatureInfo /FeatureName:NetFx3 | State : Enabled |
| 服务器功能状态 | Get-WindowsFeature NET-Framework-Core | Installed |
| 32 位运行时目录 | %windir%\Microsoft.NET\Framework\v3.5 | 存在且有程序集 |
| 64 位运行时目录 | %windir%\Microsoft.NET\Framework64\v3.5 | 存在且有程序集 |
| 注册表标记 | HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5 | Install=1 |
| 安装日志 | C:\Windows\Logs\DISM\dism.log | 无error、failed收尾记录 |
| 应用侧验证 | 目标程序启动并跑一遍核心功能 | 无版本提示、无加载异常 |
这张表我一般会连同2012r2-hash.csv一起放进交付文档,谁接手都能自己复核,不用来问我「当时是怎么装的」。还有个小技巧值得单说:批量交付时把/Source指向共享、把上面那七行检查写成一段脚本,安装完自动跑一遍并把结果追加到一个总表里,几十台机器做完一看表就知道哪台需要回头查。多花二十分钟写脚本,能省掉后面几个小时的逐台登录。
血泪经验是有一年做机房迁移,一百多台机器里有一台的 sxs 是同事从另一张渠道不明的镜像里抠的,前面九十九台都过,就它报 0x800F081F,我盯着日志查了两个小时才想到去比对哈希。从那以后我每次准备源目录,都强制走一遍「复制 → 双份哈希比对 → 留档 csv」,源不确定的机器一律重新从正版介质取,不再图省事。希望帮到你。
本文还有配套的精品资源,点击获取