ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UEFI裸金属自检:手写21项硬件测试,从启动到一键报告的实现

UEFI裸金属自检:手写21项硬件测试,从启动到一键报告的实现 裸金属整机自检UEFI 下手写 21 项测试从硬启动到一键报告的实现过程先交代一下背景省得你误会我写了个什么大工程。我做的是服务器和测试机的硬件排障日常状态就是机器送过来点不亮或者能亮但进系统就死机、重启、报错。传统排查路径无非是换内存、换盘、换板子一套组合拳打下来运气好半小时定位运气不好折腾一整天。而且有个根本性痛点始终绕不过去——操作系统起不来的时候你手里那堆 Linux 下的诊断工具全是废的。裸金属服务器、无头工作站、机房角落里摸了几年灰的测试机这类场景下最缺的不是跑分工具而是一个能在系统之前启动、能覆盖绝大多数硬件、能让人一眼看懂哪坏了、能留证据给后续 RMA 的整机自检工具。我把能搜到的开源方案都试了一圈要么只测内存要么只测存储要么是命令行输出让人满屏找光标要么压根不支持 UEFI 启动。于是我心一横用 EDK2 写了这个 UEFI 引导的小工具21 项硬件测试全程图形化界面测完一键导出报告。这篇文章把我的完整设计思路、实现细节和踩过的坑都摊开讲清楚给同样被裸金属故障逼疯的人一个可复现的参考。这里说清楚我不是要重新发明 memtest86 或者 OCCT我要解决的是工程师带着 U 盘去现场插上启动全自动测试拔下 U 盘带走报告这个完整闭环。下面从为什么会有这个工具讲起再到 21 项测试的设计逻辑、可视化的实现路线、报告导出的细节最后是真实机器上遇到的一堆坑。全程没有一行废话。1. 裸金属排障的真空地带操作系统起不来时你还能靠什么1.1 现有工具的真实能力上限先说个反直觉的事实我们在日常运维里最依赖的故障定位手段在裸金属硬件出问题的那一刻基本全部失效。Linux 下的dmesg、smartctl、memtester、stress-ng这些工具很强大但前提是系统能起来。硬件故障的典型表现恰恰是系统起不来、起来就崩、跑一下就重启。你连 shell 都进不去上面这些工具就是镜中花。ipmitool这类带外管理工具倒是能绕过操作系统但它要求机器配了 BMC而且 BMC 本身的状态、固件版本、网络配置又是一层变量。小机房、实验室、个人工作站哪来那么多带外管理。U 盘启动的 memtest86 确实经典但它只测内存。硬件故障可不止内存一种PCIe 设备枚举失败、NVMe 盘掉盘、SATA 线接触不良、网卡固件异常、RTC 电池没电、USB 控制器数据异常……这些问题 memtest 一个都不管。再往上看商用方案有 PC-Doctor 这类企业级诊断工具功能全、服务好但按节点授权收费对小团队和独立工程师来说不是一个可随手带走的工具。所以现实是硬件级排障这个场景卡在一个很尴尬的真空地带。商用工具太贵开源工具太偏科系统工具在关键时刻偏偏用不上。我需要的是一个专门的 UEFI 应用——赶在操作系统加载之前直接把硬件底层按个遍。1.2 我为什么决定写一个 UEFI 级自检工具这个想法形成于一次惨痛经历。那天手上有三台同一批次的机器症状一模一样启动过程随机死机有时候 grub 菜单都出不来有时候能进系统跑十几分钟然后黑屏。由于是偶发问题常规替换法效率极低。我搬出 memtest86 打算先排除内存跑了一夜全过。又用两口锅互相换着测弄了一上午毫无进展。折腾到傍晚才发现是其中一台机器的 PCIe 转接卡在高温下间歇性接触不良而它恰好插着那张引起整个拓扑异常的 GPU。如果当时有一个工具能在 UEFI 阶段就遍历总线枚举、对每个 PCIe 设备做读写验证、跑完硬件压力测试后直观给出报告我根本不需要用半天时间去猜。那次之后我开始认真调研 UEFI 应用开发。EDK2 和 gnu-efi 都是成熟方案UEFI 规范本身提供了大量的底层协议接口——GOP 负责图形输出Block I/O 负责读块设备PCI I/O 负责总线访问SMI、RTC 等都有对应的协议入口。这意味着我可以在没有任何操作系统依赖的前提下直接跟硬件面对面。工具的核心目标就三条覆盖广、可视化、可留痕。覆盖广对应 21 项测试可视化对应图形界面和进度条可留痕对应一键导出的报告文件。这三条刚好补上整个排障链路上最缺的那一块。2. 21 项测试的设计逻辑不是把能测的测一遍而是把该测的测全2.1 测试项的分层与优先级设计测试清单时我给自己定了两个原则。第一每项测试必须能明确指向一类硬件资源不搞模糊的整体稳定性测试。第二测试要分优先级快速定位类测试在前耗时压力类测试在后避免用户为了排除最简单的故障被迫等待最久的测试。最终 21 项测试分成三个梯队梯队测试重点项目数单项目预计耗时定位目标基础枚举层CPU 信息、内存容量、PCIe 设备拓扑、存储设备识别、USB 控制器枚举6 项 5 秒/项硬件是否被 UEFI 正确识别功能验证层RTC 读写、串口回环、网卡寄存器读写、视频输出模式切换、风扇测速通道、电源状态读取7 项 30 秒/项硬件是否能完成基本功能操作压力可靠性层内存读写压力、存储读写校验、PCIe 带宽、USB 数据回环、CPU 长时间负载、系统复位循环8 项数分钟到数小时是否存在间歇性故障和过热降频先说基础枚举层的定位。这一层的目的不是判断硬件好不好而是判断硬件在不在。很多故障的第一现场其实是设备缺位——内存松了、转接卡松了、盘掉了。UEFI 固件在启动过程中已经做过一次设备扫描我们要做的是把这些信息完整地拉出来展示配合高亮标注让用户一眼看到哪个槽位缺设备。功能验证层解决的是在不一定好的问题。设备在枚举列表里好好躺着不代表它工作正常。典型例子是 RTC设备在但电池没电每次断电时间就重置。这类问题系统下看着像文件系统时间错乱实际根因在主板电池。再比如串口设备在但换个波特率就收不到数据这种偶发问题在系统下很难跟驱动隔离在 UEFI 阶段直接做回环测试反而一目了然。压力可靠性层是最耗时的部分也是这次设计中争议最大的部分。有人会问内存测试 memtest 不是做得更专业吗对所以我只在内存压力这一项上做了有限的自研重点做双通道对比读写和取反校验不追求替代 memtest 的覆盖深度。存储和 PCIe 的压力测试反而是自研价值最高的部分——能同时施加多路并发读写模拟真实负载下的带宽瓶颈还能在测试中持续监测温度传感器数据。2.2 关键测试项的检测思路挑几个关键测试展开说说具体实现思路。内存读写测试我用的是一个经典的退化模式。先对整段可用内存做连续写 0x55、读校验再做连续写 0xAA、读校验最后做随机地址读写对比。这个策略不追求穷尽所有数据模式那是 memtest 的活而是快速暴露数据线短路、地址线开路、颗粒热不稳定三类最常见故障。值得注意的是UEFI 环境下我们看到的可用内存只是物理内存的子集UEFI 固件自身和运行时服务会占用一部分访问被保留的区域会导致处理器异常。所以测试前必须先过 GetMemoryMap 拿到完整的内存布局对所有EfiConventionalMemory类型做标记其余区域一律跳过。存储读写测试我遇到过一个有意思的边界问题。UEFI 的 Block I/O 协议在读写时绕开了文件系统直接操作扇区。测试策略是对整块设备做读校验没有写是因为裸金属服务器有些盘可能存着重要数据对可安全写入的 U 盘对拷区做写读校验。读校验的重点是触发设备内部重试连续读同一扇区一百次每次校验 CRC如果出现偶发错误说明盘片或者接口信号链路存在不稳定因素。PCIe 验证这块我的做法是三层渐进。第一层通过 PCI I/O 协议遍历总线读取每个设备的配置空间检查 Vendor ID/Device ID 是否可读Class Code 是否与设备类型匹配。第二层对每个桥设备做寄存器读写往返测试确认配置空间可写。第三层是用 GPT 协议可不是磁盘分区表那个 GPT是 UEFI 的 Graphics Output Protocol触发一次显示设备模式切换然后在帧缓冲里画满测试图案再读回对比。这种渐进式检查可以让 PCIe 故障快速归类到链路层还是设备层。2.3 哪些测试看似高明实则鸡肋砍掉设计过程中有一份已砍名单写出来帮你少走弯路。首先是 CPU 温度压力测试砍了。UEFI 环境下没有标准化的温度读取接口必须依赖主板厂商的 SMM 变量或者 ACPI 表不同平台行为差异巨大。温度监测放到压力测试的伴随线程里可以做但作为独立测试项不具备跨平台可移植性。其次是网络连通性测试砍了。UEFI 的 network stack 虽然支持 TCP/IP但需要通过 SNP/MNP 协议栈加载而且 DHCP 依赖、PXE 环境依赖、交换机策略一堆外部变量测出的结果跟硬件本身故障混淆在一起不好归因。再次是复杂音频测试。裸金属服务器绝大多数没有板载声卡测试价值太低对于有声音故障诉求的桌面场景Windows 下已经有很多专业工具UEFI 阶段没必要重复造轮子。砍完这堆以后剩下的 21 项测试每一项都能独立产出一个明确结论通过 / 失败 / 警告而不是一坨含糊的检测可疑。3. 不装系统全程可视化的实现路线EDK2 GOP 自绘 UI3.1 UEFI 程序开发环境搭建开发 UEFI 应用主流方案有两个Intel 主导的 EDK2 和社区维护的 gnu-efi。两个我都试过最终选了 EDK2理由很简单EDK2 的封装层次更完整协议接口不用自己手工定义头文件标准库函数齐全调试符号和模拟器支持也更好。环境搭建其实没想象中复杂。一台 Linux 机器装好build-essential、nasm、uuid-dev、python3然后拉 EDK2 源码配置环境变量编译 OVMF这是虚拟机里用的 UEFI 固件用来在 QEMU 里快速验证最后交叉编译生成.efi文件放 U 盘启动。整体流程大约一小时就能跑通第一个 Hello World。核心代码结构是这样的#include Uefi.h #include Library/UefiLib.h #include Library/UefiBootServicesTableLib.h #include Library/UefiRuntimeServicesTableLib.h #include Protocol/GraphicsOutput.h #include Protocol/LoadedImage.h #include Protocol/SimpleFileSystem.h EFI_STATUS EFIAPI efi_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE *system_table) { // 初始化 // 启动测试调度器 // 进入 UI 主循环 return EFI_SUCCESS; }入口函数并不神秘EDK2 的 Build 系统会自动处理链接和重定位。真正需要认真设计的是软件的分层架构。我的做法是三层分离诊断引擎层只负责执行测试逻辑、产生结构化结果不关心任何界面表现UI 层只负责从诊断引擎拿状态、画界面、接收按键报告层在测试完成后统一从诊断引擎拉取所有结果记录格式化输出。每层通过简单的结构体接口互相通信后续给引擎加测试项不影响 UI给 UI 换皮肤也不影响引擎。3.2 用 GOP framebuffer 手写 UI 渲染作为 UEFI 应用我没有任何现成的 GUI 框架可用。EDK2 的 HII 框架Human Interface Infrastructure倒是支持表单式界面但那个是为 BIOS 设置页设计的画个进度条都费劲更别提实时更新的测试状态面板。我选择了更底层的方案直接操作 GOP 暴露的 framebuffer。GOP 是 UEFI 规范定义的图形输出协议核心接口是GOP-Mode-FrameBufferBase加一系列像素格式信息。拿到这块线性内存后一个像素一个像素地往里填值就能画图。在实际场景里我会优先选择 32 位像素格式这种格式每像素 4 字节用BGRA排列后缀可以直接当 32 位整数数组操作性能最好。我的 UI 层只实现三个基本图元填充矩形、位图字体、字符串。整个界面就是若干个矩形区域拼出来的标题栏、左侧测试列表、右侧日志区、底部进度条。说句实在话美观是谈不上的但一个屏幕之内用户立刻能看出三件事当前跑到第几项、前面几项是过是挂、有没有红色高亮的失败项。字体这块我自己处理了一个点阵字库16x16 像素的 ASCII 和常用汉字总共不到 200KB。之所以不用 UEFI 内置的字体接口HiiGetString之类的是因为那套接口在 GOP 模式下渲染效率太低而且字体样式不可控。自己维护点阵字库渲染时只是内存拷贝加掩码运算即便是老旧的嵌入式平台也能轻松维持 30 帧以上的刷新率。渲染循环并不复杂核心是脏矩形刷新技术。我维护一个标记位只有测试状态变化的那几个矩形区域需要重绘其余区域保持不动。完整重绘 1920x1080 的帧缓存虽然只是几百毫秒的事但局部刷新能让 CPU 占用更低也避免了长时间测试时风扇飙到全速时的噪音干扰。3.3 可视化交互的边界启动参数与键盘响应全程可视化不只是把结果画出来还要在测试进行中允许人工干预。UEFI 应用在启动后默认会接管所有输入我实现了三个控制键方向键上下选择测试模式回车启动/暂停测试ESC 中止当前轮次并跳到报告导出阶段。这里有个交互上的隐藏设计UEFI 的 STSystem Table里的ConIn在启动早期可能还没枚举完 USB 键盘。如果我在启动瞬间就进入输入监听循环有可能会出现按键完全无响应的情况。稳妥的做法是启动后先主动调用Stall延时 2 秒等 USB 控制器完成设备枚举再进入输入循环。这 2 秒在用户那里几乎无感知但对键盘兼容性的提升是决定性的。除了键盘交互我还读取了两个启动路径参数。用户可以在 U 盘的startup.nsh脚本里指定tool.efi -mode quick或-mode full这样同一份 U 盘可以做成快速排查和深度老化两用。参数解析是手写的不依赖 EDK2 的 ShellLib因为我要兼容直接从固件启动菜单选中.efi文件的场景。4. 一键出报告的完整链路从测试结果到可交付文件4.1 报告格式设计文本 JSON报告是整个工具的最终产品一份好的报告要满足三类读者的需求现场工程师需要快速定位看文本版、QA 和厂商需要结构化数据看 JSON 版、远程支持需要时间线记录看日志版。所以我的报告不是一个单一文件而是一个小型文件集合。文本报告走的是给人类看的风格头几行是机器完整信息主板型号、固件版本、CPU 型号和步进、内存总容量、启动时间、测试版本号。接下来按梯队展示每项测试的结果全部状态失败项打星号并把日志摘录附上最后是总判定全部通过、有错误、有警告三种状态。JSON 报告走的是给机器看的风格顶层是一个包含机器信息、测试开始结束时间、每项测试结构化结果的数组。每项测试的结果包含四个字段test_id、test_name、status、elapsed_ms、details其中details是自由格式 KV 列表可以记录具体参数比如内存测试的最大带宽、PCIe 设备的 BDF 号、存储设备的名字和扇区数。这种格式后续可以直接对接监控系统的 API或者当 debug 素材发给硬件厂商。4.2 FAT 分区写入实现与路径选择报告要写到哪这是个值得认真设计的问题。最直接的方案是写到当前启动的 U 盘里但这里有兼容性陷阱U 盘可能是 FAT32、可能是 exFAT也可能是 GPT 分区表下的 ESP 分区文件系统驱动不是都内置的。EDK2 官方自带 FAT 驱动但默认只编译 FAT16/32 支持FAT16/32是标配exFAT 在新版本 EDK2 里也有支持不过取决于编译选项。为了最大化兼容性我的处理是这样的优先找 FAT 分区找不到就尝试 4KB 扇区设备还是不行就退到串口输出。同时我把Shell环境下的文件系统路径抽象出来用户把 U 盘在 BIOS 里设置为第一启动设备时路径通常是FS0:但为了更稳靠我会扫描所有EFI_SIMPLE_FILE_SYSTEM_PROTOCOL实例找一个可写且容量最大的设备。写文件的实现本身不复杂调用FileHandleOpen、FileHandleWrite、FileHandleFlush三步走。但有个细节值得提醒UEFI 环境下的文件写入默认不会立即落盘U 盘颗粒又喜欢做 write buffering如果不显式调用Flush拔 U 盘的一瞬间数据可能还没物理写到位。测试完成后我会先对每个文件调用 Flush然后延时 3 秒再退出到固件确保控制器把缓冲数据写完。这个习惯帮我少丢了很多份报告。4.3 报告可靠性断电、异常终止的处理现场排障时最常见的场景不是跑完测试拿报告而是测试中途机器死机了我来找原因。如果整个报告体系只支持正常结束导出那这台机器的故障现场可能就丢了一半。我的处理思路是边测边存。诊断引擎每完成一项测试立刻把该结果追加写入一个临时日志文件runtime_log.txt如果测试因看门狗重启或断电中断下次启动时扫描到runtime_log.txt自动将其改名为report_interrupted_时间戳.txt并保留下来。这样一来即使测试只跑到第 7 项就死机我至少知道前 6 项都过了、第 7 项是在什么设备上挂的。这比盲人摸象式的重新测试要高效得多。另外还有个容易被忽略的问题测试完成后我要把结果展示在屏幕上并等待用户按键返回这个阶段 UEFI 的 watchdogs 通常已经不在了但如果在压力测试阶段看门狗超时可能会强制重启。这个话题我在下一节展开因为它是整个工具开发过程中让我印象最深的一个坑。5. 真实机器上的踩坑与打磨UEFI 自检远没有看起来简单5.1 看门狗重启问题我第一次在真实服务器上跑完整测试时发现个诡异现象内存压力测试跑到约 40 分钟的时候机器突然重启没有任何征兆也没有任何日志。重启后我查dmesg硬件都是好的整个系统看起来无比正常。当时差点以为是内存过热还特意开了机箱侧板对着吹风扇验证。后来翻 UEFI 规范才意识到UEFI 固件里有一个看门狗定时器Watchdog Timer默认超时一般是 5 分钟部分服务器固件会更长一些。它的设计初衷是防止固件在 POST 阶段卡死但一旦进入我的应用环境定时器依然在跑。正常的检测工具都会定期重置SetTimerResetWdt看门狗而我恰恰忘了这一茬。解决办法非常简单主循环里每次刷新界面时调用一次gBS-SetTimer(watchdog_timer, TimerCancel, 0); gBS-SetTimer(watchdog_timer, TimerPeriodic, EFI_TIMER_PERIOD_SECONDS(30));只要每 30 秒重置一次系统就永远不会被看门狗打断。这个坑让我意识到UEFI 应用虽然看起来像独立程序但本质上还是在固件管理的资源池里运行固件自己的后台线程一直在监视你。5.2 USB 键盘和存储兼容性第二个坑是在用不同品牌的 U 盘测试时踩到的。我的工具用 GOP 刷出界面后发现某些 U 盘上的报告写不进去报未找到可写设备。查了一圈发现这些 U 盘的分区表是 GPT exFAT而我的 FAT 驱动只认 MBR FAT32。U 盘容量越大厂商出厂格式越可能偏向 exFAT而 EDK2 的默认 FAT 驱动不加载 exFAT 支持。解决方案是在编译时额外加入 exFAT 驱动FatPkg的EXFAT_ENABLE标志同时在运行时做了降级策略如果 FAT32 和 exFAT 都找不到就尝试直接在整块 U 盘上创建一份 FAT32 文件系统调用 DiskIo 简单格式化逻辑再写入报告。这套混合方案跑下来兼容性从大约八成提到了接近满覆盖只剩少数加密 U 盘和特殊 CHS 布局的老盘还在手工处理清单里。USB 键盘的问题之前已经提过再补充一个易踩的坑有些主板上我延时 2 秒后依然收不到 USB 键盘输入。后来定位是这些固件在 UEFI 阶段把 XHCIUSB 3.x 控制器的所有端口初始化为仅 EHCI兼容模式而我的代码直接查 XHCI 协议自然就找不到设备。最终的代码里我在枚举控制器时会同时检查 EHCI 和 XHCI 两个协议拿到实际可用的那个控制器来注册输入回调。5.3 长工时测试的进度与容错压力测试耗时长UI 界面就不能只是干巴巴的进度条。我在存储测试和内存测试这两个大头里做了三层并发测试执行线程由 UEFI 事件驱动、UI 刷新线程、温度采样线程。温度采样线程每 5 秒读一次平台特定传感器如果有的话在界面角落画一个小折线图。这样长时间测试时用户能直观看到温度有没有异常爬升。容错方面我做了一个单测试失败不终止全局的策略。传统测试工具跑挂一个测试项就直接报错退出这在硬件排障上很浪费——用户可能既想测内存又想测存储结果存储那项挂在第一个坏扇区上后续测试全都白搭。我的调度器在单测试项失败后会询问用户立即终止、跳过该项继续、还是重试一次。这让现场工程师可以在非关键项失败时继续完成全部测试最终报告里完整记录每个失败项而不是只得到一个测试未完成的结论。还有一种在真实机器上特别常见的假失败场景某些老服务器在纯 UEFI 模式下启动后VGA 控制器不初始化导致 GOP 模式请求失败。这种情况下我做了个降级模式如果没有可用 GOP自动切到 ConOut 的文本模式在ConOut-OutputString里打印测试进度。虽然界面变丑了但测试链路完全不受影响。这个降级实现帮我兜住了不少老平台的风险。结尾这套工具的适用边界与下一步方向写了这么多最后说一下我对这套工具的边界理解。它解决的最大痛点是系统起不来时硬件状态不可见的信息断层把硬件排障的起点提前到了 UEFI 阶段。但我也得诚实说它不是万能的它测的是硬件能不能在固件层面正确响应不代表操作系统下驱动兼容性没问题它能覆盖绝大多数常见故障但保不住所有玄学级偶发问题。我在实际使用中最满意的一点是它的可复制性。制作一个启动 U 盘只需把一个 FAT32 分区设为可引导把.efi文件和startup.nsh丢进去。团队里的新手也能操作插上、开机、看界面、拔盘、提交报告不需要记任何命令。后续我准备把它扩展成 PXE 启动版本这样数据中心几百台机器可以用 iPXE 批量拉取同一个.efi文件测试结果统一回传 HTTP 服务等于有了一套私有的裸金属入网检测系统。当然这就是下一个项目的事了。
返回列表