简单聊聊我如何从Windows11平滑降级回Windows10
摘要
本文提出了一种方法,该方法借助Windows Setup的升级功能,允许Windows10以上版本用户在保留个人设置与资料的情况下,实现不同版本系统的平滑迁移而无需全新安装。该方法已在22458(Windows11)和20190(Windows10)下进行了测试,均已成功迁移回19044(21H2)。
前言
距离Windows10最早的预览版发布已经过去六七年了,而Windows11的第一个正式版本将于下个月发布,眼下我还停留在20190(Windows10)。这是一个令人尴尬的版本,内存泄漏、声卡爆破,每个月总要来这么几次的绿屏……老实说,如果不是因为解决了WSL2不能启动的问题,说啥我也不会来到这个版本。
我相信不少preview用户和我有着一样的遭遇:“升级了之后就回不去了”、“没有回滚就只能全新安装”。我停在20190版本可能已经快一年了(这个版本早在今年1月就已经过期),为的就是等到Release版本跑到20190前面,抓紧最后的升级机会。不过现在看来,微软已经打算全面推进Windows11,20000+版本号之后都会属于Windows11。
这里我就不得不提一嘴Win11,为了照顾Win10用户的使用习惯,微软甚至贴心地提供了10天的回滚机会,我的心里只有感恩了已经
好了,闲话不多说,我们直接进入实操环节。在接下来的章节中,我将介绍Windows的升级检测机制,简要阐述其检测原理以及绕过方法;此外,我还将给出在进行版本迁移后遇到的一些常见问题的解决方法。
从Setup入手,看看一般的升级步骤
在较新版本的Windows安装程序中,你能看到有选择要保留的内容
这一步,对于安装较新版本的Windows来说,你可以选择保留个人文件和应用
一项,这样升级之后个人资料和相关的环境就不会遭到破坏。
然而,当你想要安装较旧版本的系统时,这项就不起作用了,这也意味着只能全新安装。实际上,要想“保留个人文件和应用”,需要通过兼容性测试,其中的几个测试点有:
- 磁盘空间大小
- 目标系统是否兼容CompactOS
- 目标系统是否兼容Bitlocker
- 许可证正常
- 目标系统不是Staged Build版本
- 是否支持UEFI、安全启动等
- 是否从VHD启动
- 目标系统的版本是否符合要求
- ...
具体的兼容性检查项目不是我们讨论的重点。这些信息可以在
C:\$WINDOWS.~BT\Sources\Panther\CompatData*.xml
中查找到。
庆幸的是,我们只需要关注目标系统的版本是否符合要求,而微软暂时没有作过多的判断,我们可以相对容易地绕开这一点。
实操:绕过安装过程中的版本检测
实际上,微软目前的采取的版本检测方法也相当粗暴:直接检测两个版本号字符串,看目标系统的版本号是否大于当前系统的版本号。
方法一
这里需要用到反汇编工具,我用的是IDA,你也可以用别的。
加载镜像中的source\setupcompat.dll
文件,查找到ConX::Setup::Common::CWindowsVersion::IsLaterThan
方法(直接简单搜索IsLaterThan
也行):
这个函数就是判断版本号字符串的了,可以看到整个函数相对比较简单,在该函数的末尾有两个label,分别是返回0和返回1,这里直接全部返回0,把mov eax, 1
改成mov eax, 0
就行。
保存对DLL的修改,替换原本的source\setupcompat.dll
文件即可。
微软将setupcompat.dll的符号文件放在公共符号文件服务器上了,大大节省了我们解析相关代码的时间。
方法二
法一看着可能有些麻烦,如果不使用反汇编工具行不行?你也可以用WinHex等十六进制编辑工具直接进行修改,但是我不推荐采用这个方法。(我使用的是19044 x64版本的镜像)
打开编辑工具,直接搜索十六进制数据B801000000C3
。你可能会找到多个结果,在这些结果中,观察B801000000C3
出现的位置,如果在它之前的不远处出现33C0C3
的一串数据,那么就是我们想要的那个结果。 将B801000000C3
中的01
替换为00
,保存后替换原始的setupcompat.dll
即可。
现在,执行setup.exe
,你就可以从高版本“升级”到低版本了:
“升级”之后:常见问题解决
在升级之后,你有极大的概率遇到如下的问题:
- 登录黑屏
- 任务栏搜索没反应
- 开始菜单唤出速度慢
- 商店应用不能使用/微软商店应用不见了
这里我提供一种解决方法:
- 在PE下操作,否则容易蓝屏
- 删除
C:\ProgramData\Microsoft\Windows\AppRepository
目录下的所有StateRepository*
文件 - 重新进入系统,在powershell下运行
add-appxpackage -register "C:\Windows\SystemApps\*\AppxManifest.xml" -disabledevelopmentmode
。这一步的目的是恢复系统应用,命令出现的错误可以忽略。在执行完命令之后,按Ctrl+Alt+Del并注销用户,然后重新登录进桌面,这时候应该可以看到熟悉的欢迎界面,并且开始菜单等也可以正常使用了。 - 在powerhsell下运行
add-appxpackage -DisableDevelopmentMode -Register "C:\ProgramData\Microsoft\Windows\AppRepository\*\AppxManifest.xml" -verbose
。这一步的目的是恢复一些内置应用(如微软商店)。由于语言包的安装依赖于主体应用,因此你可能需要执行两遍。 - 在powerhsell下运行
add-appxpackage -DisableDevelopmentMode -Register "C:\Program Files\WindowsApps\*\AppxManifest.xml" -verbose
。这一步是恢复各类从商店下载的应用,同样因为语言的问题,需要执行两遍。
在你可能遇到一些比较无关紧要错误,例如:
- 资源正在使用中。可能是一些诸如VCLib之类的runtime包,这些可以不管。也有可能是一些在后台运行的应用(比如Omen控制中心),你可以关掉这些进程或者服务然后再执行。
- 版本达不到安装要求。这是因为还残留着一些降级之前系统的包,可以忽略。
上述过程本质上是在重建AppPackage的数据库。一般情况下,我们比较关心商店应用的数据。在删除StateRepository*
文件之前,可以先执行Get-AppPackage
获取已经安装的包。在输出中,SignatureKind
属性为Store
的就是用户安装的商店应用,而InstallLocation
属性即为其安装路径。删除StateRepository*
并不会对安装路径下原有文件造成影响。
如果你需要显式地恢复应用,或者不确定应用是否正确恢复了,你可以不用上面的通配符形式,而是使用精确的文件路径。
下面我以Windows Terminal应用为例,介绍如何精确地恢复某个应用:
- 首先获取
Get-AppPackage
,如图所示,其安装路径为C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.11.2921.0_x64__8wekyb3d8bbwe
- 下面开始恢复,执行命令
add-appxpackage -DisableDevelopmentMode -Register "C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.11.2921.0_x64__8wekyb3d8bbwe\AppxManifest.xml" -verbose
总结
我原本想写写自己是如何发现这个方法的,奈何探寻的过程枯燥而乏味,大多数读者对此不感兴趣,加之自身水平有限,没法进一步展开来讲,因此文中只取IsLaterThan
加以讨论,感兴趣的读者可以自行深入了解。
当然,上述的方法同样存在一些问题。我没有做很详尽的测试,但目前来看我的开发环境基本运行正常,这里我简要列举一些测试过的模块:
正常的功能模块
- WSL1
- WSL2
- Hyper-V / Docker(带有硬链接)
存在问题的功能模块
- 各类商店应用(均需要重新安装,可能是我的操作有问题)
- 颜色配置文件(降级之后我的色彩管理炸了,不过也有可能是显卡驱动的问题)
补充
- 有朋友提到可以采用较新版本的安装程序,并将里面的
install.wim
替换为旧版系统,以此来骗过安装程序。之前我试过这个方法,行不通。现在我用同样的办法在Windows11下操作,这里安装程序是较新的Win11,而install.wim
是旧版的win10,会出现以下的错误:
在重启后失败:
失败后进入系统的提示:
- 尝试通过修改注册表中的系统版本号,以此来欺骗安装程序的方法并不奏效。原因在于,安装程序获取版本号是通过
ntdll.dll
中的RtlGetVersion
方法,该方法会直接在内核中进行版本号的读取,而注册表项的填充也依赖于内核。 或许可以尝试修改ntdll.dll
等内核文件的相关内容来达到相同的目的,但是这种方法可能会遇到数字签名验证等问题。
- 如果有别的方法欢迎朋友们在楼下补充
勘误
勘误: 之前写的法二,直接通过修改hex的方式存在一些问题,不能精确地区分出我们想要的那个函数。这里附上正确的操作:
直接搜索B801000000C333C0C3
十六进制值,先去找到这一块中最近的填充边界(通过int3指令填充,直接找一连串的CC就行),在前面的边界起始的不远处,看看能否找到一串FF FF 00 00
的指令,如果能找到就是我们想要的那一块。这时候将B801
中的01改为00 即可。