2026/10/28 2:34:21

2012 R2 离线安装 .NET 3.5:SXS 与 DISM 排错

2012 R2 离线安装 .NET 3.5:SXS 与 DISM 排错 简介面向 Windows Server 2012 R2 Standard 的系统运维、部署与排障人员这份 SxS 组件合集针对 .NET Framework 3.5 安装失败、指定备用源时缺少组件的问题整理而成适用于老旧业务系统兼容性恢复、离线服务器功能补齐等场景。压缩包共 1568 个文件整体约 85.45MB其中 dll 文件 720 个、resx 资源文件 180 个、exe 程序 84 个并包含 config、sql、browser、ascx、targets、manifest 等类型涉及运行库、配置项、安装组件和 Web 管理模块目录结构较完整便于按模块查找。已有 1808 人学习下载。对于需要恢复 .NET 3.5 功能、补齐离线系统组件的读者可借助包内文件快速定位缺失的 SxS 内容减少逐项搜集、版本比对和重复试错的时间也可为同类 Windows Server 环境的组件补充、兼容性验证与故障排查提供参考适合需要长期维护旧版服务器的管理员保留备用。1. 从一次源文件报错说起0x800f081f 是怎么出现的内网一台 Windows Server 2012 R2 Standard业务侧的需求只有一句装 .NET Framework 3.5。听起来像点几下鼠标就能完成。打开服务器管理器进入“添加角色和功能”向导走到“指定备用源路径”那一步填上挂载盘里的sources\sxs回车进度条走了两秒就变成红字0x800f081f源文件找不到。路径没错文件也在。问题出在手上那张 ISO它来自另一台机器版本看起来对得上但补丁级别差了一截语言也不一样。SXS 并不是随便找一份就能用的通用文件包其中的组件与这台机器的 build、SKU、语言相互绑定。比装不上更麻烦的是没装好有人反复重试最终把组件存储弄成 0x80073712有人为了绕过去卸掉了 .NET 4.5结果管理控制台全部失效。这套场景在 2012 R2 上极其常见——从 2012 开始.NET 3.5 的 payload 不再随系统默认加载必须手动指定来源。此文给手上有 2012 R2、要离线补 3.5 的人也给已经遇到过一次失败、想搞清楚 SXS 到底怎么对应的人。2. 环境先点清2012 R2 上 SXS 的版本、语言与权限前提SXSSide-by-Side在安装介质里就是sources\sxs这一层目录里面是一堆.cab组件载荷不是能双击运行的安装包。它的作用只有一个当 DISM 或Install-WindowsFeature需要某个功能的二进制文件、又不去联网取的时候从这个目录里按组件名和版本号精确取用。因此“源能不能用”这件事本质上是版本号比对而不是路径比对。我一般会在动任何命令之前先花三分钟把机器信息打出来。这不是流程仪式是因为后面每一个报错码的排查方向都由这三分钟决定版本不对就往镜像来源查语言不对就往语言包查补丁级别不对就往更新历史查。2.1 三条命令确认 SKU、安装类型与 buildPowerShell 用管理员身份起先看系统身份# 系统版本与架构2012 R2 的 Version 一定是 6.3.9600 Get-WmiObject Win32_OperatingSystem | Select-Object Caption, Version, OSArchitecture # EditionIDServerStandard / ServerDatacenter # InstallationTypeServer带 GUI/ Server Core无 GUI $cv HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion Get-ItemProperty $cv | Select-Object EditionID, InstallationType, BuildLabExBuildLabEx那一行长这样9600.17415.amd64fre.winblue_r4.141028-1500。前面9600是大版本中间17415是修订号。很多人只核对了9600就认为版本匹配但真正决定 SXS 能否被接受的是完整 build 与语言。介质来自同一次发布的同一语言版本成功率最高跨语言、跨修订号DISM 不会在开始前拦你它会跑完再告诉你找不到。语言也要单独确认因为带语言中性组件的功能对语言包有依赖# 系统区域与已安装语言 Get-WinSystemLocale | Select-Object Name, DisplayName dism /online /get-intl如果这台机器装过英文语言包、界面是中文、sxs又取自英文 ISO先别急着执行安装把语言这一项记下来后面对照日志时要用。2.2 三种 SXS 来源的取舍来源无外乎三种选哪种取决于你手上有什么而不是哪种更“先进”。来源适用场景优点主要限制ISO 挂载后的sources\sxs手上有对应版本的安装镜像最稳目录结构原样保留盘符重启后会变必须先拷到本地固定路径install.wim挂载后的Windows\WinSxS只有 wim、没有完整 ISO不依赖光驱或挂载工具索引选错会拿到别的 SKU挂载点占空间在线 Windows Update机器能出网且未被更新通道拦截一条命令不用准备介质内网、WSUS 环境基本走不通第一种是我默认的选择。把 ISO 挂上把sources\sxs整目录拷到C:\sxs\2012r2之后所有命令都指这个本地路径——盘符变化这种处理小问题栽过的人都懂。第二种是后备某些环境只给了install.wim那就挂载后用挂载点里的Windows\WinSxS当源路径写法不一样但 DISM 认。第三种只适合测试机生产内网不要指望。2.3 权限、服务与那条容易被忽略的组策略.NET 3.5 属于系统功能不是普通软件安装过程要写组件存储、要动WinSxS普通管理员令牌不够必须是提升后的会话。这条听着废话但远程执行脚本时最常翻车的就是这里脚本以计划任务身份跑看着是管理员实际是未提升的令牌报错还是 0x800f081f让你在源路径上白找半天。服务侧要确认 Windows Update 服务没被禁用。用/LimitAccess时理论上不联网但 DISM 在检查阶段仍会读更新相关配置服务停用状态下会多出一层无谓的失败。真正隐蔽的是组策略。路径在“计算机配置 → 管理模板 → 系统 → 指定可选组件安装和组件修复的设置”。这一项如果被设成“从不尝试从 Windows Update 下载负载”即使你给了/Source某些补丁层组件仍会被拒绝报 0x800f0907。内网批量镜像里这条被改过的概率不低gpresult /h C:\temp\gp.html导一份出来搜一下比事后猜省时间。3. 准备 SXS 备用源挂载镜像、导出组件、校验完整性准备阶段的目标很明确让C:\sxs\2012r2这个目录成为一个 DISM 认可、且重启后依然存在的源。中途用挂载盘符直接做源也能装上但只适用于一次性操作只要涉及批处理、重启、远程执行就必须落在本地固定路径。这一段的所有命令都在提升后的 PowerShell 里跑路径按自己的实际情况替换。3.1 挂载 ISO 并核对 sxs 目录# 挂载镜像并拿到盘符 $iso D:\iso\SERVER2012R2_STD.iso $img Mount-DiskImage -ImagePath $iso -PassThru $drive ($img | Get-Volume).DriveLetter 挂载盘符: $drive # 看 sxs 目录里到底有什么应该是一堆 cab没有 exe Get-ChildItem ${drive}:\sources\sxs -Filter *.cab | Measure-Object -Property Length -Sum | Select-Object Count, {nTotalMB;e{[math]::Round($_.Sum/1MB,1)}} # 顺手确认镜像版本别拿 2012 的镜像当 2012 R2 用 dism /Get-WimInfo /WimFile:${drive}:\sources\install.wim第三段输出是重点。sxs目录里的文件数量通常在几十个量级全是.cab。如果你看到的是一堆.msu或者干脆只有几个文件那这份镜像要么被裁剪过要么根本不是完整介质——这种源指过去DISM 会在解包阶段失败报错码和“路径错误”长得一模一样。Get-WimInfo那一步是给自己留证据确认镜像里确实有Windows Server 2012 R2 Standard的描述且版本行写着6.3.9600。别凭文件名判断文件名是最不可靠的信息源。3.2 从 install.wim 里导出组件手上只有 wim 的时候走这条路。核心是先看清索引再只读挂载最后把挂载点当源用。# 1. 列出所有索引看清每个索引对应哪个 SKU dism /Get-WimInfo /WimFile:E:\sources\install.wim # 2. 只读挂载到本地目录目录必须为空且已存在 New-Item -Path C:\mnt\wim -ItemType Directory -Force dism /Mount-Image /ImageFile:E:\sources\install.wim /Index:2 /MountDir:C:\mnt\wim /ReadOnly # 3. 确认挂载点里的组件存储确实存在 Test-Path C:\mnt\wim\Windows\WinSxS (Get-ChildItem C:\mnt\wim\Windows\WinSxS -Directory | Measure-Object).Count索引号一定要看第一步的输出不要凭记忆选。不同批次介质里 Standard 和 Datacenter、Core 和 GUI 的排列顺序并不固定选错索引虽然多数情况下组件仍能通用但在补丁级别有差异时会引入难以解释的失败。/ReadOnly是必须的只读挂载不会写回 wim卸载时用/Discard就行不会污染原始介质。用完记得卸载否则挂载点会一直占用下次再挂同一个 wim 会报“文件正在使用”dism /Unmount-Image /MountDir:C:\mnt\wim /Discard3.3 把源落到本地并做一次完整性校验# 拷贝到本地固定路径避开盘符变化 New-Item -Path C:\sxs\2012r2 -ItemType Directory -Force Copy-Item D:\sources\sxs\* -Destination C:\sxs\2012r2 -Recurse -Force # 记录一份清单出问题时用来比对两次拷贝是否一致 Get-ChildItem C:\sxs\2012r2 -Filter *.cab | Select-Object Name, Length | Sort-Object Name | Export-Csv C:\sxs\manifest.csv -NoTypeInformation -Encoding UTF8清单这一步看着多余实际很值。跨机器拷源、U 盘转存、压缩包解压都可能丢文件或截断。等 DISM 报“源文件找不到”时拿manifest.csv和目标目录一比对问题立刻定位到传输环节而不是又去怀疑版本。拷贝完成后卸载 ISO 释放盘符Dismount-DiskImage -ImagePath $iso到这里源就准备好了。判断标准很简单C:\sxs\2012r2里是几十个 cab、清单文件行数与源目录一致、路径不含中文和空格。4. 用 DISM 装 .NET 3.5一条命令里的五个参数命令行和图形界面走的是同一套组件服务接口区别在于命令行把每个决策点都摊开给你源在哪、要不要联网、失败了去哪看日志。图形界面失败只丢一个码命令行可以加/LogPath这是生产环境里唯一靠谱的“黑匣子”。4.1 在线与离线的差别到底在哪在线安装就是不带/Source让 DISM 直接去更新通道取组件。内网机器走这条路卡住的位置通常是组策略把可选组件下载指向了内部更新服务器而服务器上并没有这份载荷于是绕一圈回来报 0x800f0954 或 0x800f0906。很多人看到 0x800f0906 就去查网络实际上网络是通的只是通道指向不对。离线安装是把“取组件”这一步从网络上摘出来改成从本地目录读。这也是为什么 SXS 方案在内网几乎是唯一选择它把不确定性收敛到“源对不对”这一件事上。4.2 DISM 命令逐参数拆解:: 启用 .NET Framework 3.5指定本地备用源禁止联网取文件 dism /online /enable-feature /featurename:NetFx3 /all ^ /Source:C:\sxs\2012r2 /LimitAccess /NoRestart五个参数每个都有实际作用/online指向当前正在运行的系统。如果要对离线镜像操作换成/image:C:\mnt\wim那是另一个场景。/enable-feature和/featurename:NetFx3一起锁定目标功能。NetFx3是 DISM 侧的功能名和 PowerShell 里的NET-Framework-Core指同一个东西只是两套命名。/all会连带启用该功能的父项。.NET 3.5 下面挂着 WCF 激活等子项不加这个参数有时只装了一半业务侧调用 WCF 服务时才暴露。/Source:指向备用源目录。指到C:\sxs\2012r2即 sxs 目录本身和指到C:\sxs其父目录都能被识别我习惯指到 sxs 本身路径含义更明确。/LimitAccess是这条命令里最容易被省掉、也最不该省的一个。它的作用是阻止 DISM 在本地源缺件时回退去联系更新通道。省掉它源里少一个组件DISM 会先联网找一轮超时后再报错排查时间凭空翻倍。/NoRestart按需加。NetFx3 通常不强制重启但不加这个参数在某些补丁层场景下会自动重启跑批处理时很麻烦。执行完看最后几行输出。操作成功完成是期望结果如果是错误: 0x800f081f先别重跑去C:\Windows\Logs\DISM\dism.log搜最后一段CSI相关的行那里会写明它到底在找哪个组件名和哪个版本。4.3 PowerShell 版与无人值守写法服务器上我更愿意用 PowerShell 版因为输出是对象能直接判断状态也方便写进初始化脚本。# 装功能并把过程写进独立日志 $result Install-WindowsFeature -Name NET-Framework-Features -Source C:\sxs\2012r2 -LogPath C:\logs\netfx35.log -Restart:$false $result | Select-Object Success, RestartNeeded, ExitCode # 复核功能状态 Get-WindowsFeature NET-Framework-* | Select-Object Name, DisplayName, InstallState | Format-Table -AutoSize-Source要指向包含 sxs 内容的目录和 DISM 的/Source语义一致。-LogPath是 PowerShell 版比 DISM 顺手的地方不用去翻系统级的大日志每次安装单独一份出问题直接把这一个文件发给同事。InstallState应该是Installed。如果显示Available说明命令没真正落盘通常是会话未提升或源路径不可读。RestartNeeded为True时挑维护窗口重启别在业务时段直接重启。批处理里还要注意一点Install-WindowsFeature在部分 2012 R2 版本上需要先导入模块。Import-Module ServerManager -ErrorAction Stop写成一行Get-WindowsFeature通常也能触发自动加载但在受控环境里显式导入更稳。4.4 装完不等于完事补丁层怎么补.NET 3.5 装好之后它的组件版本还停留在介质打包时的水平。业务软件如果依赖某个修复还得把后续更新补上。内网做法一般是两条一是把 3.5 相关的离线更新包按顺序装上二是直接接入内部更新服务器让它自己收敛。前者可控后者省事。# 看当前系统里和 .NET 3.5 相关的已装更新 Get-HotFix | Where-Object { $_.HotFixID -match NET|Framework } | Select-Object HotFixID, Description, InstalledOn顺带提醒不要把 .NET 3.5 和 4.5 当成互斥的两套东西。2012 R2 自带 4.5.1两者并存、各自独立装 3.5 不需要动 4.x。真有人为了“腾空间”把 4.5 卸了后果是服务器管理器打不开恢复起来比重装系统还麻烦。5. 排查与避坑五类报错的现象、原因和处置这一章按报错码组织每条都按现象、原因、解决三层写。报错码本身不提供信息提供信息的是它后面那段日志下面每一条都告诉你日志该看哪儿。5.1 0x800f081f源里明明有文件却说找不到现象/Source指向的目录肉眼可见全是 cab命令执行到一半报 0x800f081f。原因绝大多数是版本或语言不匹配。SXS 里的组件带了完整版本号DISM 按名字找组件时要求精确命中介质与系统 build 差一个修订号就可能全部对不上。其次是拷贝不完整大文件被截断目录在、文件在、内容不全。第三种是路径问题UNC 路径没权限、路径里有中文或空格未加引号。解决先比对BuildLabEx与镜像版本再用 3.3 里生成的manifest.csv核对文件数量与大小最后把源拷到本地磁盘的纯英文短路径重试。三步走完还报同一个码去C:\Windows\Logs\DISM\dism.log搜0x800f081f上方的几行那里会写明它索要的具体组件版本。5.2 0x800f0906 与 0x800f0954被更新通道挡住现象不带/Source直接装或者带了源但没加/LimitAccess命令长时间无输出最后报这两个码之一。原因前者是更新通道取不到文件内网无外网、或更新服务不可达后者是组策略把可选组件安装指向了内部更新服务器而服务器上没有对应载荷客户端被明确禁止从 Windows Update 取。两者的共同点是跟你的 SXS 目录没关系。解决把/Source和/LimitAccess一起加上让命令彻底不碰更新通道。仍然失败时临时确认一下策略是否在拦截# 看更新源配置是否指向内部服务器 $wu HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU if (Test-Path $wu) { Get-ItemProperty $wu | Select-Object UseWUServer }UseWUServer为 1 且内网服务器没有载荷就是这条原因。改策略要走变更流程不要在生产机上直接改注册表。5.3 0x800f0907组策略那条“永远不要连更新”现象给了本地源也报 0x800f0907日志里出现“已阻止”字样。原因组策略“指定可选组件安装和组件修复的设置”里选了“从不尝试从 Windows Update 下载负载”且没有配置“备用源文件路径”。这条策略的本意是让机器只从指定位置取组件但很多人只改了前半句没填后半句。解决在策略里把备用源路径填成C:\sxs\2012r2或对应的共享路径让机器合法地使用本地源或者临时用本地策略覆盖一次装完再恢复。批量镜像里这条一定要在交付前检查。5.4 0x800f0922 与 0x80073712空间与组件存储现象0x800f0922 通常出现在安装接近结束时0x80073712 出现在任何阶段且往往伴随“组件存储损坏”。原因前者常见于系统保留分区或系统盘空间不足安装过程需要把组件展开再做一次事务提交空间不够就回滚后者是组件存储本身已经有损坏可能是之前的失败安装留下的半成品也可能是磁盘问题。解决先看空间Get-PSDrive C看剩余系统盘至少留出 810 GB 余量。组件存储问题先做一次健康检查别急着重装系统# 扫描组件存储健康状态只读不改动 dism /online /cleanup-image /scanhealth # 发现问题再尝试修复修复过程仍可指定本地源 dism /online /cleanup-image /restorehealth /Source:C:\sxs\2012r2 /LimitAccess/restorehealth会从源里取组件替换损坏项跑之前确认源完整否则等于白跑一次。5.5 重启后功能又变回“可用”的假象现象Get-WindowsFeature明明显示Installed重启后业务程序仍报缺少 .NET 3.5或者某次复查发现状态变回Available。原因一种是安装时用了/NoRestart事务没有最终提交状态是“已暂存”而非“已安装”另一种是系统被回滚到某个检查点或恢复过快照组件存储回到旧状态功能注册信息却没同步。解决装完不重启的情况下用Get-WindowsFeature看到InstallState为Installed只能算通过一半必须重启后再复核一次。快照和还原点环境里安装前先做快照装完验证通过再提交快照不要装到一半打快照。6. 验证跑通四层检查与一个真能跑的探针装完只看到一行“操作成功完成”就交付是我见过最常见的隐患。功能安装的“成功”分好几层功能注册成功、文件落到磁盘、注册表项写入、运行时能真正加载程序集。前两层命令能直接看后两层得实际跑点东西。我一般按四层走一遍全程不超过五分钟。检查层命令或位置期望结果功能状态Get-WindowsFeature NET-Framework-CoreInstallState为Installed磁盘目录C:\Windows\Microsoft.NET\Framework64\v3.5.30729目录存在且含csc.exe、mscorlib.dll注册表HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5Install为 1SP为 1运行时探针用 3.5 的编译器编译并执行一段 LINQ 代码输出结果正确运行时版本为 v2.0.50727前三层用命令就能确认Get-WindowsFeature NET-Framework-Core | Select-Object Name, InstallState Test-Path C:\Windows\Microsoft.NET\Framework64\v3.5.30729\csc.exe Get-ItemProperty HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5 | Select-Object Install, SP, Version第四层才是关键因为前三层都可能“看起来对但用不了”。用 3.5 自带的编译器编一段带 LINQ 的代码——LINQ 是 3.5 才有的东西2.0 的运行时编不过能编过并跑出结果说明程序集完整、运行时能加载# 写一个探针程序用到 LINQ3.5 特性并打印实际运行时版本 $src using System; using System.Linq; class Probe { static void Main() { var nums new[] { 1, 2, 3, 4, 5, 6 }; Console.WriteLine(sum_even nums.Where(n n % 2 0).Sum()); Console.WriteLine(runtime typeof(object).Assembly.ImageRuntimeVersion); } } New-Item -Path C:\temp -ItemType Directory -Force | Out-Null Set-Content -Path C:\temp\Probe.cs -Value $src -Encoding UTF8 # 用 3.5 的 csc 编译而不是系统里更新的那套 C:\Windows\Microsoft.NET\Framework64\v3.5.30729\csc.exe /nologo /out:C:\temp\Probe.exe C:\temp\Probe.cs # 运行并看结果 C:\temp\Probe.exe期望输出两行sum_even12和runtimev2.0.50727。第一行证明 LINQ 相关的程序集齐全第二行证明进程确实跑在 2.0 运行时上——如果打印出 v4.0.30319说明当前会话加载的是 4.x 运行时探针没验证到 3.5得换个干净的命令行窗口再跑一次。探针跑通之后顺手把这次安装的关键信息记进交付文档源目录路径、介质版本号、BuildLabEx、执行时间、四个检查层的结果。这些东西在半年后另一台机器复现时比任何口头交接都管用。从那以后我每次装完 .NET 3.5都会强制走一遍第四层——不管图形界面弹了多漂亮的对勾也不管InstallState显示得多干脆。被那个“已安装但跑不起来”的假象坑过一次就不太信状态字段了。希望这套流程帮到你。本文还有配套的精品资源点击获取