ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UEFI内存测试工具advmemtest:从原理到颗粒级错误定位

UEFI内存测试工具advmemtest:从原理到颗粒级错误定位 1. 为什么我要自己写一个内存测试工具内存这东西平时不坏的时候你完全感觉不到它的存在一旦出问题那真是能把人逼疯。蓝屏、随机重启、编译到一半报段错误、游戏突然闪退、文件复制完校验不通过——这些症状你拿去搜十有八九会有人告诉你先测内存。问题是市面上能用的内存测试工具要么是闭源黑盒要么免费版限制轮数要么跑一晚上只告诉你有错误却不告诉你错在哪。我自己就踩过这个坑。一台工作站跑编译偶尔挂MemTest86 跑了两轮没报错我差点以为是编译器的问题。后来换了参数跑了一整夜才在第四轮抓到一个 bit 翻转。问题是它只告诉我地址 0x1A3F5C00 出错我拿着这个地址完全不知道是哪根内存条、哪个颗粒的问题。八根条子难道一根一根拔下来试那个下午我拔插了十六次内存腰都快断了。所以 advmemtest 这个项目就是冲着这几个痛点去的图形界面让操作门槛降下来免费版不限轮数让你可以放心跑通宵Pro 版把错误定位到颗粒让你知道到底该换哪一根。它跑在 UEFI 环境下不依赖操作系统直接对物理内存做读写校验这一点和 MemTest86 是同一个路子但交互和错误报告做得更细。这篇文章我会把整个项目的设计思路、UEFI 下的内存测试原理、LVGL 图形界面的移植要点、错误定位到颗粒的实现逻辑以及我在开发和实测过程中踩过的坑全部摊开讲一遍。不管你是想直接用这个工具还是想自己动手写一个类似的东西应该都能拿到点实在的东西。2. 内存测试工具的核心设计思路拆解2.1 为什么选择 UEFI 而不是操作系统内运行在操作系统里跑内存测试最大的问题是你测的内存不完全是你的。操作系统已经把物理内存做了映射你申请到的是一段虚拟地址背后对应哪块物理页你控制不了。更麻烦的是操作系统自己、各种驱动、后台进程都在占用内存你没法对全部物理内存做完整的读写校验。而且一旦内存出错导致系统崩溃测试也就中断了。UEFI 环境就不一样了。UEFI 固件在操作系统加载之前运行这个时候整个物理内存基本是空闲的你可以直接对物理地址做读写。没有操作系统调度没有虚拟内存映射你写进去什么、读出来什么中间没有第三方干扰。这也是为什么 MemTest86 这类工具都选择在 UEFI 或 BIOS 层面运行。具体到 advmemtest它编译成一个 UEFI 应用.efi 文件你可以把它放到 U 盘上从 UEFI 启动菜单直接引导。整个过程不需要硬盘上有操作系统也不需要安装任何东西。这一点对维修场景特别友好——一台开不了机的电脑你插上 U 盘就能测内存不用先想办法把系统跑起来。注意UEFI 应用和传统的 BIOS 引导是两回事。如果你的机器是纯 Legacy BIOS 模式或者 CSM 兼容模式这个工具跑不起来。需要在 BIOS 里确认启动模式是 UEFI并且关闭 CSM。2.2 图形界面为什么选 LVGLUEFI 本身提供了一套图形输出协议GOPGraphics Output Protocol你可以直接往帧缓冲里写像素。但如果你要从零画按钮、画进度条、画表格那工作量就大了。LVGL 是一个轻量级嵌入式图形库C 语言写的资源占用小控件丰富而且有成熟的移植方案。选 LVGL 的理由很直接它能在 UEFI 环境下跑起来而且开发效率高。你不需要自己实现按钮的点击检测、进度条的动画、列表的滚动这些 LVGL 都帮你做好了。我只需要把 LVGL 的显示驱动对接上 GOP 的帧缓冲把输入驱动对接上 UEFI 的 Simple Text Input 协议剩下的界面逻辑用 LVGL 的 API 写就行。这里有个细节值得说一下。LVGL 默认是给嵌入式 RTOS 环境设计的通常配合 FreeRTOS 这类系统使用。但在 UEFI 下没有 RTOS我是在主循环里手动调用lv_timer_handler()来驱动 LVGL 的定时任务。这个调用频率要控制好太频繁浪费 CPU太慢界面会卡。实测下来每 5ms 调用一次比较合适既保证界面流畅又不会明显拖慢内存测试的进度。2.3 免费版不限轮数的商业逻辑很多内存测试工具的免费版会限制轮数比如只能跑两轮想跑更多轮就得买 Pro。这个限制其实挺恶心的因为内存错误往往是间歇性的两轮跑不出问题不代表内存没问题。我自己就遇到过第四轮才报错的情况。advmemtest 免费版不限轮数这个决定是故意的。内存测试的核心价值在于能跑通宵限制轮数等于阉割了核心功能。那 Pro 版卖什么卖的是错误定位到颗粒的能力。免费版告诉你有错误地址是 0xXXXXPro 版告诉你错误发生在第 3 根内存条的第 7 个颗粒上。这个信息对普通用户可能无所谓但对维修人员、对批量测试场景、对需要精确更换硬件的用户来说价值就大了。这个思路其实和很多工具软件一样基础功能免费高级诊断收费。免费版足够你判断内存有没有问题Pro 版帮你判断哪根内存有问题。2.4 错误定位到颗粒的技术前提定位到颗粒这句话说起来简单做起来需要几个前提条件。第一你得知道内存条的物理地址映射。一根内存条插在哪个插槽上它的物理地址范围是多少这个信息在 UEFI 里可以通过 SMBIOS 表拿到。SMBIOS Type 17 结构体里记录了每个内存设备的信息包括它在哪个插槽、容量多大、厂商是谁。第二你得知道内存条上颗粒的排列方式。一根内存条通常有 8 个或 16 个颗粒每个颗粒负责一部分地址线。具体哪个地址对应哪个颗粒这取决于内存条的设计。一般来说地址线是交错分布的连续地址会轮流落在不同颗粒上。要精确定位需要知道内存条的 rank、bank、row、column 的组织方式。第三你得能控制测试模式让错误更容易暴露。不同的数据模式全 0、全 1、0x55、0xAA、随机数、行走位对不同类型的硬件缺陷敏感度不一样。比如相邻地址线短路用行走位模式最容易测出来而数据线保持问题用棋盘模式更有效。Pro 版的做法是先通过 SMBIOS 拿到内存条和插槽的对应关系再根据地址计算出它落在哪个 rank 的哪个颗粒上。这个计算需要知道内存条的几何结构有些信息可以从 SPD 里读有些需要根据经验公式推算。实测下来主流品牌的内存条定位准确率还不错杂牌条可能会偏。3. UEFI 环境下的内存测试原理与实操要点3.1 物理内存的直接读写校验UEFI 下做内存测试核心操作就两个往某个物理地址写一个值然后读回来比较。听起来简单但实际做的时候有一堆细节要注意。首先是地址对齐。你不能随便往一个未对齐的地址写 64 位数据有些架构会直接抛异常。一般按 8 字节对齐来操作比较稳妥。其次是访问宽度你可以按 8 位、16 位、32 位、64 位不同的宽度来读写不同宽度能暴露不同的问题。比如某些内存颗粒的某个 bit 只在大宽度访问时才出错小宽度访问就正常。再就是缓存的影响。CPU 有缓存你写一个值进去可能只是写到了缓存里还没真正落到内存颗粒上。要确保测试的是物理内存需要在每次写之后做缓存刷新或者把测试区域标记为不可缓存。UEFI 下可以通过修改页表的缓存属性来实现但操作比较底层容易出问题。我采用的是写完之后读一个远处的地址来冲刷缓存行简单粗暴但有效。还有一个容易被忽略的点内存测试区域的选择。你不能把整个物理内存都拿来测因为 UEFI 固件自己、你的测试程序、栈、堆都占着内存。需要先通过 UEFI 的内存映射服务GetMemoryMap拿到可用内存区域只测试那些标记为 EfiConventionalMemory 的区域。否则你把自己的代码覆盖了程序直接跑飞。3.2 测试模式的选型与参数计算内存测试的数据模式直接决定了能测出什么类型的问题。我实现了以下几种模式每种都有明确的针对性模式名称数据模式针对的故障类型测试速度全 0 / 全 10x00 / 0xFF数据线固定故障快棋盘模式0x55 / 0xAA 交替相邻数据线短路中行走位单个 1 在 0 中移动地址线故障、译码错误慢随机数伪随机序列综合故障中块移动大块数据搬移后校验时序相关故障快行走位模式是最慢但最彻底的。它的原理是先往整个测试区域写 0然后在第一个地址写 1读回校验再把第一个地址写回 0在第二个地址写 1读回校验……以此类推。如果有地址线短路比如 A3 和 A4 短接那么写 A3 的时候 A4 也会被影响读回的数据就会不对。参数计算方面测试块大小是个关键。块太小测试开销大因为每次都要重新建立测试环境块太大单次测试时间长出错后定位范围大。我实测下来每块 64MB 到 256MB 比较合适。以 16GB 内存为例分成 64 个 256MB 的块每块跑完所有模式大概需要几分钟整体跑一轮大概半小时到一小时。实操心得如果你时间有限优先跑行走位模式和棋盘模式这两个模式对常见故障的检出率最高。全 0 全 1 模式跑得快可以放在最前面做快速筛查。3.3 错误记录的格式与后续分析每次检测到错误需要记录足够的信息供后续分析。我记录的字段包括物理地址、期望值、实际值、出错的数据位、测试模式、测试轮次、时间戳。其中出错的数据位特别重要。比如期望 0x55实际读出 0x54异或一下得到 0x01说明是 bit 0 出了问题。如果多个地址的错误都集中在同一个 bit那很可能是数据线的问题如果错误分散在不同 bit那可能是颗粒本身的问题。这些错误记录会实时显示在界面上同时也会写到一个日志区域。免费版显示错误地址和出错位Pro 版会进一步分析这些错误地址结合内存条的物理映射算出是哪个颗粒的问题。3.4 UEFI 应用的编译与部署advmemtest 是用 EDK2 编译的。EDK2 是 UEFI 固件的开源参考实现提供了一套完整的开发框架。你需要先搭建 EDK2 的编译环境然后写一个 .inf 文件和 .dsc 文件来描述你的模块最后用 build 命令编译出 .efi 文件。编译命令大概长这样# 设置 EDK2 环境 source edksetup.sh # 编译 build -a X64 -t GCC5 -p AdvMemTestPkg/AdvMemTest.dsc编译出来的 .efi 文件放到 U 盘的 EFI/BOOT/ 目录下命名为 BOOTX64.EFI就能被 UEFI 启动菜单识别。或者你也可以把它放到任意目录然后在 UEFI Shell 里手动执行。注意不同主板的 UEFI 实现有差异有些主板对第三方 .efi 文件的签名有要求。如果启动时提示安全启动失败需要在 BIOS 里关闭 Secure Boot。这个操作在各大品牌主板的 BIOS 里位置不一样戴尔一般在 Secure Boot 菜单下惠普在 Boot Options 里联想在 Security 菜单下。4. LVGL 图形界面的移植与界面实现4.1 LVGL 在 UEFI 下的显示驱动对接LVGL 需要一个显示缓冲区draw buffer和一个刷新函数flush callback。在 UEFI 下显示缓冲区就是 GOP 提供的帧缓冲。你需要先通过 GOP 的 QueryMode 拿到当前分辨率然后计算出帧缓冲的大小和行间距。对接的关键代码逻辑是这样的LVGL 把要显示的内容渲染到它的内部缓冲区然后调用你注册的 flush 回调你在回调里把 LVGL 缓冲区的像素数据拷贝到 GOP 帧缓冲的对应位置。这里要注意颜色格式的转换。LVGL 默认用 ARGB8888 或 RGB565而 GOP 帧缓冲通常是 BGRA8888 或 BGR888。如果格式不匹配显示出来的颜色会不对比如红色变蓝色。我踩过的坑是一开始没注意字节序显示出来的界面颜色全反了红色变成青色蓝色变成黄色。后来在 flush 回调里加了一个字节交换的操作才正常。这个问题的隐蔽性在于它不会导致程序崩溃只是颜色不对你可能会以为是 LVGL 配置的问题查半天配置才发现是字节序。4.2 输入设备的对接与事件处理UEFI 下的输入主要通过 Simple Text Input 协议和 Simple Pointer 协议。键盘输入用前者鼠标输入用后者。LVGL 需要你注册一个输入设备驱动在驱动里读取 UEFI 的输入事件然后转换成 LVGL 的输入事件。键盘处理相对简单UEFI 返回的是扫描码你需要把它映射成 LVGL 的按键值。鼠标处理麻烦一点因为 UEFI 的 Simple Pointer 协议返回的是相对位移你需要自己维护一个光标位置然后判断点击是否落在某个控件上。实测下来键盘导航比鼠标操作更可靠。因为不同主板的 UEFI 鼠标协议实现质量参差不齐有些主板根本不支持鼠标有些支持但坐标漂移。所以我在界面设计上做了键盘优先所有功能都可以用方向键和回车完成鼠标作为可选操作方式。4.3 界面布局与交互设计界面整体分三个区域顶部是状态栏显示当前测试进度、已用时间、错误数量中间是主测试区域显示当前正在测试的地址范围和测试模式底部是操作提示和菜单入口。主测试区域用一个进度条加一个滚动日志窗口。进度条显示当前块的测试进度日志窗口实时滚动显示检测到的错误。这个设计的好处是你一眼就能看到有没有错误——有错误日志窗口会变红并开始滚动没错误就是安静的绿色进度条。菜单里可以配置测试参数测试模式选择、测试块大小、测试轮数免费版不限但你可以设置一个上限让它自动停止、是否启用 Pro 错误定位。这些配置项用 LVGL 的 tab 控件组织切换方便。实操心得LVGL 的 tab 控件在 UEFI 下有个小问题切换 tab 时如果内容较多会有明显的重绘延迟。我的解决办法是减少每个 tab 里的控件数量把不常用的配置放到二级菜单里保持每个 tab 的控件在 10 个以内。4.4 界面刷新与测试进度的平衡内存测试是 CPU 密集型任务界面刷新也需要 CPU 时间。如果测试线程和界面线程抢 CPU会导致界面卡顿或者测试速度下降。我的做法是分时复用主循环里先跑一小段内存测试比如测试 1MB 的数据然后调用lv_timer_handler()刷新界面再回去跑下一段测试。这个时间片的大小需要调。太小了界面刷新频繁测试速度慢太大了界面响应迟钝你按个键要等好几秒才有反应。实测下来每测试 4MB 数据刷新一次界面比较合适既保证界面每秒能刷新几次又不明显拖慢测试。另外错误日志的更新要单独处理。不能每检测到一个错误就刷新一次界面那样如果错误很多界面会疯狂重绘。我的做法是把错误先缓存起来每 500ms 批量更新一次日志窗口。5. 错误定位到颗粒的实现逻辑与实操5.1 从物理地址到内存条插槽的映射拿到一个出错的物理地址第一步是确定它属于哪根内存条。这个信息在 SMBIOS 的 Type 17 结构体里。每个 Type 17 结构体描述一个内存设备包含设备所在的插槽标识Device Locator、Bank 标识、起始地址有些实现不填、容量、厂商、序列号等。遍历所有 Type 17 结构体找到起始地址和容量覆盖出错地址的那个就知道是哪根内存条了。如果 SMBIOS 里没填起始地址那就需要根据插槽顺序和容量来推算。一般来说UEFI 会按插槽顺序分配地址第一个插槽从低地址开始依次往上。这里有个坑有些主板的 SMBIOS 信息不准确。我遇到过一台机器SMBIOS 里报告有 4 个插槽但实际上只有 2 个插槽有内存条另外 2 个是空的但也被报告了。这种情况下需要结合容量信息来判断——容量为 0 的插槽直接跳过。5.2 从内存条到颗粒的地址解码确定是哪根内存条之后下一步是算出出错地址对应哪个颗粒。这个计算需要知道内存条的几何结构有几个 rank、每个 rank 有几个 bank、每个 bank 的行列数、数据位宽是多少。这些信息一部分可以从 SPDSerial Presence Detect里读。SPD 是内存条上的一颗小 EEPROM里面存了内存条的详细参数。UEFI 下可以通过 SMBus 协议读取 SPD。但 SPD 里的信息是给内存控制器用的不一定直接给出颗粒的排列方式。实际操作中我采用的是一个简化模型假设地址线在颗粒之间均匀交错。一根有 8 个颗粒的内存条地址的低 3 位或者经过 rank/bank 交织后的某 3 位决定数据落在哪个颗粒上。具体是哪 3 位需要根据内存条的 rank 和 bank 配置来推算。这个模型的准确率不是 100%因为不同厂商的内存条设计不一样。但对于主流品牌的内存条定位到哪几个颗粒还是能做到的。Pro 版会给出一个疑似故障颗粒的列表而不是精确到某一个颗粒这样更稳妥。5.3 错误模式分析与故障类型判断光定位到颗粒还不够还要判断是什么类型的故障。我根据错误记录做了几种模式分析单 bit 错误集中在同一数据位大概率是数据线问题可能是颗粒的某个 DQ 引脚虚焊或走线问题。单 bit 错误分散在不同数据位可能是颗粒内部的存储单元故障。多 bit 错误集中在同一地址附近可能是某个颗粒的某个 row 或 bank 故障。错误地址随机分布可能是内存控制器、时序参数或者供电问题。这些分析结果会显示在 Pro 版的报告里帮你判断是该换内存条还是该调整 BIOS 里的内存时序参数还是该检查主板插槽。注意错误定位到颗粒这个功能前提是内存条本身没有物理损坏到无法读取 SPD 的程度。如果 SPD 都读不出来那就只能靠地址范围来判断是哪根条子了。5.4 Pro 版错误报告的生成与解读Pro 版的错误报告包含以下内容出错的内存条插槽位置、内存条厂商和序列号、疑似故障颗粒的编号、错误类型分析、建议的处理方式。报告会以文本形式显示在界面上同时可以保存到 U 盘。保存的文件名自动带上时间戳和内存条序列号方便批量测试时区分。解读报告的时候要注意定位到颗粒不等于一定就是那个颗粒坏了。可能是颗粒本身的问题也可能是焊接问题也可能是内存条 PCB 走线问题。报告给的是疑似最终判断还需要结合更换测试来确认。我一般建议用户先换一根内存条试试如果换了之后错误消失那说明确实是原来那根条子的问题。6. 常见问题与排查技巧实录6.1 启动相关问题速查问题现象可能原因解决方法U 盘启动后直接进系统UEFI 启动顺序不对进 BIOS 把 U 盘调到第一位提示 Secure Boot 失败安全启动拦截了未签名应用关闭 Secure Boot黑屏无显示GOP 不支持当前分辨率在 BIOS 里改显示输出为 UEFI GOP键盘无响应UEFI 输入协议不兼容换 USB 2.0 接口避免用 USB 3.0测试到一半卡死测试到了保留内存区域检查内存映射排除保留区域6.2 测试结果异常的分析思路情况一跑了很多轮都没错误但系统还是不稳定。这种情况不一定是内存的问题。可能是电源供电不稳、主板电容老化、CPU 内存控制器故障、或者散热问题导致内存高温下出错。advmemtest 测的是常温下的内存如果系统在高负载下才出问题可能需要结合压力测试一起看。情况二每次报错的地址都不一样。随机地址错误通常指向时序问题或供电问题而不是颗粒的固定故障。可以尝试在 BIOS 里降低内存频率、放宽时序参数再跑一次测试。如果错误消失说明是时序太紧导致的。情况三错误集中在某个地址范围。固定地址范围的错误大概率是那个范围的物理内存有问题。结合 Pro 版的颗粒定位可以判断是哪根条子的哪个区域。6.3 不同主板 UEFI 实现的兼容性坑我在不同主板上测过这个工具发现了一些兼容性问题戴尔的部分机型UEFI 的 GOP 实现只支持特定的分辨率如果你请求的分辨率不对它会返回一个默认值但帧缓冲的行间距可能和你预期的不一样。解决办法是先 QueryMode 拿到实际参数再根据实际参数配置 LVGL。惠普的某些机型Simple Pointer 协议返回的坐标范围是 0-1023而不是实际的屏幕像素坐标。你需要做一个缩放映射否则鼠标位置会偏。联想的机器Secure Boot 默认开启且不容易关闭有些型号需要在 BIOS 里先设置管理员密码才能改 Secure Boot 选项。实操心得如果你只是自己用建议在常用的那台机器上先跑通把 BIOS 设置记下来。如果是给别人用最好在说明里写清楚需要关闭 Secure Boot并附上常见品牌主板的设置路径。6.4 性能优化与测试速度提升内存测试的速度主要受限于内存带宽和 CPU 的读写效率。几个提速的思路使用大宽度访问。64 位访问比 8 位访问快得多在支持 64 位访问的平台上优先用 64 位。减少缓存刷新次数。缓存刷新是有开销的不需要每次写都刷新可以攒一批写操作再统一刷新。多核并行测试。如果 CPU 有多个核心可以让每个核心测试不同的内存区域。但要注意多核同时访问内存会互相干扰可能影响测试准确性。我的做法是默认单核测试在菜单里提供一个多核加速选项用户自己决定要不要开。跳过已知良好的区域。如果第一轮测试某个区域没错误第二轮可以降低这个区域的测试强度把时间省下来测其他区域。但这个策略有风险可能漏掉间歇性错误所以我没默认开启。6.5 从源码编译到实际部署的完整流程如果你想自己编译一份流程大概是这样的搭建 EDK2 开发环境安装 GCC 或 Clang 工具链。下载 advmemtest 源码放到 EDK2 的目录结构下。修改 .dsc 文件里的路径配置指向你的实际目录。运行 build 命令等待编译完成。找到生成的 .efi 文件复制到 U 盘的 EFI/BOOT/ 目录。重启目标机器进 BIOS 关闭 Secure Boot设置 U 盘启动。从 U 盘引导进入 advmemtest 界面。编译过程中最常见的错误是路径配置不对和工具链版本不匹配。EDK2 对工具链版本比较敏感建议用官方推荐的版本。如果编译报错说找不到某个头文件检查 .dsc 文件里的 Include 路径。7. 我对这个项目的一些个人体会做这个工具的过程中最大的感受是内存测试这件事难点不在测试本身而在错误定位和结果解读。写一个能跑的内存测试循环一天就能搞定但要让它准确告诉你哪根内存条有问题花了我好几周时间大部分时间都在处理各种主板 SMBIOS 信息不准确、SPD 读取失败、地址映射对不上的情况。另一个体会是UEFI 下的开发调试比操作系统下麻烦得多。没有 printf没有断点调试出了问题只能靠往屏幕写字符或者往串口输出日志。我一开始用串口输出调试信息后来发现很多笔记本根本没有串口只能改成往屏幕写。屏幕输出的问题是会干扰 LVGL 的界面所以调试信息得单独开一块区域显示。LVGL 在 UEFI 下的表现比我预期的好。本来担心资源占用和刷新效率实际跑下来在 1080p 分辨率下界面很流畅CPU 占用也可以接受。唯一的问题是 LVGL 的默认字体在 UEFI 下显示中文需要额外配置我目前用的是英文界面中文支持还在做。最后分享一个小技巧如果你要测试的内存容量很大比如 128GB 以上建议把测试块大小调大一些比如 512MB 一块。块太小的话块与块之间的切换开销会很明显整体测试时间会拉长。但块太大了单次测试时间长出错后定位范围大需要权衡。我一般建议按总内存的 1/64 来设置块大小比如 64GB 内存就用 1GB 的块。这个工具后续还可以扩展的方向支持内存超频后的稳定性测试、支持自定义测试模式、支持把测试结果导出成结构化报告方便批量分析。不过这些都是后话了先把当前版本的稳定性做好再说。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进