UEFI开机图标替换实战:HackBGRT原理、配置与回滚避坑
简介:这是一份围绕 Windows 10 开机 LOGO 个性化定制的小型工具包,面向希望替换系统启动画面、且具备基本动手能力的用户与开发者。压缩包共 20 个文件,约 73KB,主体由 C# 与 C 源码(cs、c、h)构成,并附带 Makefile、Markdown 说明、示例 BMP 图、配置文本及许可证等,目录按源码、配置、文档和示例素材分区,便于了解 BGRT 修改工具的组织方式与编译线索。已有 4783 人学习下载,说明该主题在系统美化和启动界面定制群体中具有一定关注度。包内源码与说明可帮助读者理解开机画面替换的实现思路、配置项含义及图片规格要求(如 480×270 像素 PNG),也可作为研究 UEFI/BIOS 启动图形显示与二次开发的参考资料;示例图与文档便于对照验证效果,适合想深入原理而非仅套用现成工具的中高级用户。
1. 开机图标这事,小到没人写文档,却卡住过不少人
装过几十台机器之后你会发现,客户第一眼看的东西不是配置单,是开机那三秒钟的图标。开机图标这个需求小到没人愿意为它单独写一份说明,却实实在在卡住过一批人:改注册表没用,换主题没用,把设置翻遍了也找不到这一项,最后只能去问“是不是要刷 BIOS”。HackBGRT-1.5.1 这个压缩包就是冲这一件事来的,它把 Windows 引导管理器画的那个 logo 换成你自己的图片,不装驱动、不常驻后台、不改系统主题。它适合做展厅机、工控面板、批量装机的从业者,也适合只想把开机画面换成自己作品的人。前提是机器走 UEFI 引导,而且你愿意动手碰一次引导分区。
2. HackBGRT 的工作机制:BGRT 表、bootmgfw.efi 与被改掉的那一刀
在动手之前,得先搞清楚这一刀切在哪儿。很多人以为开机图标是系统起来之后画的,所以去改登录界面、改锁屏、改 OEM 信息,全都白费力气。真相是:那个画面出现在内核加载之前,画它的是引导管理器,跟系统里的任何设置项都没有关系。搞不清这一层,后面每一次失败你都不知道该往哪个方向查。
2.1 开机那一秒的画面,究竟是谁画的
一台 UEFI 机器从上电到进桌面的链路大致是:固件自检 → 读 EFI 系统分区(ESP)里的引导程序 →bootmgfw.efi(Windows 引导管理器)→winload.efi→ 内核。厂商的 logo 由固件画,Windows 的那个窗口图标由bootmgfw.efi画,位置则由 ACPI 里的 BGRT 表(Boot Graphics Resource Table)告诉系统——固件把自己画 logo 的坐标和尺寸登记在这张表里,引导管理器接着在同一个位置画自己的东西,所以你看到的画面切换才不会跳一下。
这就解释了为什么改系统设置永远不会有反应:那个画面出生的时候,系统还没加载完成,注册表还没被读取。想让开机图标变成你想要的东西,只能去改引导阶段执行的那个二进制。HackBGRT 的名字本身就来自这张表,它做的事说穿了很简单——在bootmgfw.efi被固件调用之前,先插一段自己的代码进去,把画面画完,再把控制权交回原来的引导流程。
理解了这条链路,后面所有的“为什么必须关某个开关”“为什么改完要重启两次”“为什么升级系统之后又变回去了”,答案都能自己推出来。
2.2 替换式注入:bootmgfw.efi 被动了哪一刀
HackBGRT 采用的是替换式注入,具体动作分三步。第一步,把 ESP 分区上原有的\EFI\Microsoft\Boot\bootmgfw.efi改名备份,通常是加个后缀放在同一目录,这一步是整套流程的保底措施,也是后面能不能把机器救回来的关键。第二步,把自带的 loader 放到同一个路径、同一个文件名上,因为固件只认这个路径,不认文件内容。第三步,这个 loader 启动后先按配置画出你给的 BMP,再链式加载刚才备份的原版引导文件,让系统正常起来。
这个设计带来两个必须接受的约束。其一,由于替换后的二进制没有经过签名,Secure Boot 打开时固件会拒绝执行它,所以这一项必须关闭;这不是让你绕过什么,而是这类引导期定制绕不开的前提,接受不了就别做。其二,所有风险都集中在引导分区上:改坏了的结果不是蓝屏,是根本进不去系统,连安全模式都摸不到。反过来说,只要备份还在,用任意一个能读 ESP 的环境把文件改回来,机器就能恢复,这也是我一直强调“先确认备份”的原因。
需要说清楚边界:这套改动只发生在引导阶段,不进内核、不加驱动、不 hook 系统调用,进系统之后它就不存在了。所以它不会影响性能,也不会跟杀毒软件打架。
2.3 包内文件的分工与它们的角色
解压之后不要急着双击,先认清每个文件是干什么的,这比看十篇教程都管用。
| 文件 | 作用 | 我的处理方式 |
|---|---|---|
setup.exe | 挂载 ESP、备份原引导文件、写入 loader | 只在确认过分区和 Secure Boot 状态后,以管理员身份运行 |
config.txt | 指定图片路径、背景色、显示超时等 | 先复制一份原版留档,再改 |
splash.bmp | 默认图片,被config.txt引用 | 直接用自己的图覆盖同名文件最省事 |
| 引导相关文件 | 注入到 ESP 的 loader 本体 | 不手工动,交给 setup 处理 |
| 源码/说明 | 供二次开发和排错 | 出问题时对照配置项含义用 |
这里有个经验:新手最容易犯的错是绕过setup.exe手工往 ESP 里拷文件。手工拷不是不行,但你会漏掉备份这一步,而备份恰恰是唯一的后悔药。至于config.txt的具体键名和取值范围,不同小版本的注释写法会有出入,我一般直接打开包内那份原文件读注释,比在网上翻半天靠谱——文件里的注释永远和你手上的这个版本对得上。
3. 动手替换:从环境盘点到最后一次重启
流程本身并不长,十分钟能走完,但每一步都有前置条件。我习惯把顺序固定成“先验环境、再做图、最后写盘”,因为一旦写盘失败,前面两步的成果还能留到下次用;反过来先写盘再发现图片尺寸不对,就得重来一遍。
3.1 动手前的环境盘点
有三件事必须在改之前确认:固件是 UEFI 而不是 Legacy、系统盘是 GPT、Secure Boot 的当前状态。三条里有一条不符,后面的步骤大概率白做。用管理员权限开 PowerShell,逐条跑:
# 1) 固件类型,期望输出 UEFI;如果是 Bios 就到此为止 (Get-ComputerInfo).BiosFirmwareType # 2) 系统盘分区表格式,期望是 GPT Get-Disk | Select-Object Number, PartitionStyle, FriendlyName, Size # 3) Secure Boot 状态,True 表示开着,需要进固件设置关掉 Confirm-SecureBootUEFI # 4) 找到 EFI 系统分区,确认它在哪块盘上 Get-Partition | Where-Object GptType -eq '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' | Select-Object DiskNumber, PartitionNumber, Size第 1 条的含义不用多解释。第 2 条之所以要单独查,是因为不少人装系统时用了第三方工具,把 GPT 盘转成了 MBR,这种情况下 ESP 根本不存在,工具自然无从下手。第 3 条的输出如果是 True,就重启进固件设置把 Secure Boot 关掉,同时留意一件事:关闭 Secure Boot 有可能触发 BitLocker 要求输入恢复密钥,如果你开着系统盘加密,先把 48 位恢复密钥找出来抄在纸上,这一步省不得。
第 4 条在多硬盘机器上尤其重要。我曾经在一台双硬盘的机器上折腾了半小时,图标就是不变,最后发现setup.exe把文件写到了另一块盘的 ESP 上——固件启动的是第一块盘的引导项。所以看到多块盘都有 ESP 时,先确认 BIOS 里第一启动项是哪块盘。
提示:三条检查里任何一条不满足,都不要抱着“试试看”的心态往下走。引导阶段的问题没有中间态,要么正常开机,要么开不了机。
3.2 图片预处理:分辨率、色深与体积的取舍
引导阶段能读的图片格式非常有限,实践上就是未压缩的 BMP。PNG、JPG 直接改后缀是不行的,文件头对不上,loader 读不出来,结果就是开机一片黑或者干脆跳回原图标。所以必须做一次真正的格式转换。
分辨率上有个取舍。用屏幕原生分辨率最清晰,但 BMP 是位图,体积随像素线性膨胀:1920×1080 的 24 位图大概 6 MB,1280×720 约 2.7 MB,1024×768 约 2.3 MB。而 ESP 分区通常只有 100 MB 左右,还要留给引导文件和其他工具,所以我一般把成品压在 2 MB 以内,宁可稍微降一点分辨率,也不要让图片把分区撑爆。
from PIL import Image import os TARGET_W, TARGET_H = 1920, 1080 # 目标分辨率,尽量贴合面板原生值 MAX_BYTES = 2 * 1024 * 1024 # 自定的体积上限 src = Image.open("logo_src.png").convert("RGB") # 强制 24 位,丢掉 alpha # 等比缩放,避免直接拉伸把人像或文字拉变形 scale = min(TARGET_W / src.width, TARGET_H / src.height) new = src.resize((int(src.width * scale), int(src.height * scale)), Image.LANCZOS) # 居中贴到纯黑画布上,四周留黑比拉伸铺满好看得多 canvas = Image.new("RGB", (TARGET_W, TARGET_H), (0, 0, 0)) canvas.paste(new, ((TARGET_W - new.width) // 2, (TARGET_H - new.height) // 2)) canvas.save("splash.bmp", format="BMP") print("bytes:", os.path.getsize("splash.bmp"))这段代码有三个关键点。convert("RGB")是为了去掉 alpha 通道,32 位带透明度的 BMP 在引导阶段很可能被当成花屏处理。等比缩放加居中贴图,是为了避免非原生比例的画面被拉伸——开机图标变形是最常见的返工原因。最后打印体积,是为了在写盘前就发现超限,而不是等到写完才发现分区满。
生成之后再做一次头部校验,比肉眼确认可靠得多:
import struct with open("splash.bmp", "rb") as f: header = f.read(54) # BMP 文件头 + 信息头共 54 字节 magic, = struct.unpack("<H", header[0:2]) # 必须是 'BM' width, = struct.unpack("<i", header[18:22]) height, = struct.unpack("<i", header[22:26]) bpp, = struct.unpack("<H", header[28:30]) compression, = struct.unpack("<I", header[30:34]) # 0 表示不压缩 print(f"magic={magic:#x} {width}x{abs(height)} {bpp}bpp compression={compression}") assert magic == 0x4D42, "不是 BMP 文件" assert compression == 0, "必须是未压缩位图" assert bpp in (24, 32), "色深不支持"这四行断言覆盖了绝大多数“图放了但没显示”的情况。很多转图工具默认输出 RLE 压缩的 BMP,肉眼看着完全正常,loader 却读不了,校验能提前把这类问题挡掉。
3.3 写入引导分区与第一次重启
把生成好的splash.bmp覆盖到解压目录里的同名文件,然后打开config.txt确认图片路径指向它。配置项大致是这样组织的(键名和取值范围以包内注释为准):
# 只列最常用的几项,剩下的保持默认即可 image = splash.bmp background = 000000 timeout = 3image是相对路径,一般相对于工具目录本身,改成绝对路径也能用,但换机器时会失效,我倾向保持相对。background是画布底色,用六位十六进制不带#,和你的图片边缘颜色对齐能减少突兀感。timeout是画面停留时间,设太小会一闪而过,设太大又会拖慢开机,3 秒上下是个比较稳的值。
配置改完,右键以管理员身份运行setup.exe。它会挂载 ESP、备份原引导文件、把 loader 写进去,全程有文字输出。这里不要急着关窗口,把输出读完,重点确认两件事:一是它操作的确实是系统盘那块 ESP,二是备份动作确实执行了。确认完重启,第一次开机就是验证时刻。
3.4 验证改动是否真的生效
重启后如果看到了自己的图,说明写入成功。但“能看到”不等于“全都对”,还要确认系统能正常进桌面、能正常重启和关机各一次。有些机器第一次能进,第二次就卡住,通常是原引导文件的链式加载写得不对,或者 ESP 上有多个引导项互相干扰。
想更确定一点,可以重新挂载 ESP 看一眼文件状态:
# 把 ESP 挂到一个空闲盘符,S 被占用就换别的 mountvol S: /S # 看备份在不在,以及在不在同一个目录下 Get-ChildItem S:\EFI\Microsoft\Boot\ | Where-Object Name -like "bootmgfw*" | Select-Object Name, Length, LastWriteTime # 检查完记得卸载,长期挂着会影响后续系统更新 mountvol S: /D备份文件存在,且修改时间早于你运行setup.exe的时间,说明整个流程是干净的。做完这一步再卸载盘符,别图省事一直挂着——系统更新时会对 ESP 做操作,多一个挂载点容易出岔子。
4. 配置文件详解:分辨率、超时与回滚路径
配置文件不复杂,但每一项都对应一个具体的视觉结果,改错一项就得重启一次看效果,代价不低。与其反复试,不如先把每项的作用和边界弄清楚。这一章按“参数—效果—坑”的顺序拆,最后一节专门讲怎么把机器还原成没动过的样子。
4.1 常用参数逐项拆解
不同小版本的注释文案会有差异,但核心就那么几项,功能是稳定的:
| 参数 | 作用 | 取值建议 | 常见误用 |
|---|---|---|---|
image | 指定要显示的图片 | 相对路径,指向同目录的 BMP | 写成 PNG 路径,或路径含中文和空格 |
background | 画布底色 | 六位十六进制,如000000 | 带#前缀导致解析失败 |
timeout | 画面停留秒数 | 2~5 之间 | 设成 0 或过大,前者一闪而过后者拖慢开机 |
| 多图相关项 | 轮换显示多张图 | 按注释格式追加条目 | 条目数超限或格式不统一 |
路径这一项值得单独说。中文路径和带空格的路径在引导阶段解析时容易出问题,我一般把整个工具目录放到一个纯英文、无空格的短路径下,比如C:\bgr\tool\,改完再运行。这个习惯让我少排查过至少三四次“图明明在那儿就是不显示”的问题。
还有一点,把config.txt保存为 UTF-8 无 BOM。带 BOM 的文件,第一个键名前面会多出不可见字符,解析器把它当成未知项跳过,表现为“整份配置只有第一行不生效”——这种问题靠读配置是看不出来的,只能靠保存格式规避。
4.2 多图、超时与背景色的配合
想显示多张图做轮换,需要在配置里追加多条图片条目,每一条指向一个 BMP。这里有个容易被忽略的约束:所有图片的尺寸最好保持一致,因为画布大小是固定的,混用不同分辨率会出现某一张被裁切或者缩放到角落的情况。
背景色和图片边缘的配合也值得花两分钟。如果你的图是深色底,背景色设成000000就融为一体,看不出边界;如果图是浅色底,背景设黑反而会显出一个明显的方框。我通常的做法是先看图的主色调,取边缘像素的颜色填进去,这样在低亮度屏幕上过渡最自然。
超时这一项,装机场景和展厅场景的取值完全不同。批量装机追求效率,2 秒足够,用户还没反应过来就进系统了;展厅机希望画面多停留一会儿让人看清品牌,可以给到 4~5 秒,但不要超过 5 秒,否则每天开关机几十次,累积起来的时间很可观。
4.3 回滚:把机器还原成没动过的样子
回滚的优先级是:先用工具自带的恢复功能,再考虑手工操作。setup.exe重新运行一次,通常会检测到已经注入过并给出恢复选项,能走这条路就走这条,因为它会把备份原样改回来,不需要你知道文件名是什么。
工具走不通时,手工恢复也不难,核心就是“把备份改回原名”:
mountvol S: /S # 先看清楚备份的实际文件名,不同版本命名可能是 .bak 或 .orig Get-ChildItem S:\EFI\Microsoft\Boot\ | Where-Object Name -like "bootmgfw*" # 假设备份名是 bootmgfw.efi.bak,先把注入的挪开再改回来 Rename-Item S:\EFI\Microsoft\Boot\bootmgfw.efi bootmgfw.efi.injected Rename-Item S:\EFI\Microsoft\Boot\bootmgfw.efi.bak bootmgfw.efi mountvol S: /D命令里的文件名一定要按你机器上实际看到的改,不要照抄。操作完重启,图标回到原样就说明恢复成功。如果连备份都找不到了,还有最后一招:用系统安装介质启动到修复环境,执行bcdboot C:\Windows /s S: /f UEFI重建引导文件,这条命令能让机器重新开得了机,但开机图标会回到系统默认,属于保命而不是复原。
注意:回滚操作同样在动引导分区,做之前把该存的东西存好,并且在操作期间别断电。
5. 避坑与排查:五个让人想重装系统的瞬间
下面五条都是我在真机上遇到过的,按“现象—原因—解决”写,遇到对应症状直接对号入座,不用从头排查。
现象一:改完重启,图标纹丝不动,系统一切正常。原因通常是写错了分区。多硬盘机器上setup.exe可能把文件写到了另一块盘的 ESP,而固件启动的是第一块盘。解决方式:进固件设置确认第一启动项对应的物理盘,用Get-Partition比对磁盘编号,只保留目标盘的数据线再跑一次,或者临时在固件里把启动顺序调整到正确的那块盘。
现象二:开机黑屏,只有一行白字或者直接跳进固件设置。这说明引导文件被替换但加载失败,常见于替换过程中断、备份丢失,或者 loader 版本与固件不兼容。解决方式:用任意能读 ESP 的环境(系统安装介质、PE 环境)把备份改名回bootmgfw.efi;备份也没了就用bcdboot重建引导。这类问题九成出在“没等setup.exe跑完就关窗口”上。
现象三:图标显示了,但变形、只显示左上角一块,或者四周被裁掉。原因是图片分辨率和面板原生分辨率不匹配,且没有做居中贴图处理。解决方式:回第 3.2 节,用等比缩放加居中贴到目标尺寸画布的方式重新生成,别直接拉伸原图。顺带检查一下 BMP 的头里宽度高度是不是你以为的那个值。
现象四:画面花屏、颜色明显不对、像老电视的雪花。大概率是图片不是标准 24 位未压缩 BMP,或者带了 alpha 通道。解决方式:用第 3.2 节末尾的四行断言校验一遍,compression必须是 0,bpp必须是 24。有些在线转换工具输出的是 RLE 压缩格式,视觉上完全正常,但 loader 读出来就是乱的。
现象五:用了一年都好好的,某次系统大版本更新后图标变回默认。原因是累积更新重写了\EFI\Microsoft\Boot\bootmgfw.efi,你注入的那份被覆盖掉了,备份文件可能还在但已经不起作用。解决方式:重新跑一遍setup.exe即可,配置文件和图片都还在,不用重做。建议把工具目录连同配置一起放在一个固定的地方,别用完就删。
另外补一条不算坑但很致命的:开着 BitLocker 的机器,关 Secure Boot 之后重启会要求输入 48 位恢复密钥。密钥在你第一次启用加密时被要求备份过,通常在账户里或者打印出来了。如果实在找不到,先别关机,把密钥找出来再继续,否则只能走数据恢复流程。
6. 进阶:把开机图标做成一条能复用的流水线
单台机器改一次,十分钟的事,按上面流程走就行。但如果手上是几十台同型号的展厅机、工控面板要批量处理,逐台点setup.exe就不可接受了,而且人一多,漏掉备份、图片尺寸不统一这类问题必然出现。我后来把它整理成了一条固定流水线:素材统一进一个目录,一个脚本批量出图并校验,一个检查脚本在每次系统更新后跑一遍,判断需不需要重新注入。
批量出图的部分直接复用第 3.2 节的逻辑,加一层目录遍历就行,关键是把断言放在循环里,任何一张图不合格就整批停下,而不是带着一张坏图去写盘:
import os, struct from PIL import Image SRC_DIR, OUT_DIR = "src_logos", "out_bmp" TARGET = (1920, 1080) os.makedirs(OUT_DIR, exist_ok=True) def to_bmp(src_path, dst_path): img = Image.open(src_path).convert("RGB") scale = min(TARGET[0] / img.width, TARGET[1] / img.height) img = img.resize((int(img.width * scale), int(img.height * scale)), Image.LANCZOS) canvas = Image.new("RGB", TARGET, (0, 0, 0)) canvas.paste(img, ((TARGET[0] - img.width) // 2, (TARGET[1] - img.height) // 2)) canvas.save(dst_path, format="BMP") def check_bmp(path): with open(path, "rb") as f: h = f.read(54) assert struct.unpack("<H", h[0:2])[0] == 0x4D42, f"{path} 不是 BMP" assert struct.unpack("<I", h[30:34])[0] == 0, f"{path} 被压缩了" assert struct.unpack("<H", h[28:30])[0] == 24, f"{path} 不是 24 位" for name in os.listdir(SRC_DIR): base = os.path.splitext(name)[0] out = os.path.join(OUT_DIR, base + ".bmp") to_bmp(os.path.join(SRC_DIR, name), out) check_bmp(out) # 任何一张不合格,整个循环抛出异常 print(base, os.path.getsize(out), "bytes")循环里那句check_bmp是整个脚本的价值所在。它把“图看着没问题”这种主观判断换成了三条硬断言,出问题时直接告诉你哪一张、错在哪一项,不用靠开机重启去试。
第二个脚本用来做更新后的自检。系统大版本更新会覆盖引导文件,但更新完你不会立刻知道,往往是某天开机发现图标没了才想起来。我习惯在每次更新后跑一次:
mountvol S: /S $boot = "S:\EFI\Microsoft\Boot" $cur = Get-Item "$boot\bootmgfw.efi" -ErrorAction SilentlyContinue $bak = Get-ChildItem $boot -Filter "bootmgfw*.bak" -ErrorAction SilentlyContinue if ($bak) { "备份存在,注入文件时间: " + $cur.LastWriteTime if ($cur.LastWriteTime -gt $bak.LastWriteTime) { "疑似被更新覆盖,需要重新注入" } } else { "没有备份,说明当前是原厂引导或已回滚" } mountvol S: /D判断依据很朴素:如果当前引导文件的修改时间比备份还新,说明它被别的东西重写过,注入已经失效。这个判据不依赖版本号,也不会误伤,是我用得最顺手的一条。
有个细节值得提一句:批量场景下我建议给每台机器留一份配置快照,把config.txt和成品 BMP 一起归档到带机器编号的目录里。原因是半年后你可能会被问“这台机器当时用的是哪张图”,而机器上的文件早被覆盖过好几轮了。归档这件事花不了几分钟,能省掉后面一堆对不上账的麻烦。
说到底,这类引导期改动的坑都不在技术难度上,而在“没备份就动手”和“没验证就写盘”上。从那以后我每次碰 ESP 之前,都会先挂载、先看备份、先把恢复命令在记事本里写好,再执行注入——哪怕这台机器我已经改过十次。希望帮到你。
本文还有配套的精品资源,点击获取