1. 为什么我要自己写一个内存测试工具
内存这东西,平时不坏的时候你根本感觉不到它存在,一旦出问题,那真是能把人逼疯。蓝屏、随机重启、编译到一半编译器崩溃、游戏加载到某个固定进度就闪退、跑虚拟机的时候宿主机莫名其妙死机——这些症状你拿去搜,十有八九会看到一句“先测内存”。问题是,市面上能用的内存测试工具,要么是闭源黑盒,要么免费版限制轮数,要么跑一晚上只告诉你“有错误”,却不告诉你错在哪。
我自己是做嵌入式和底层开发的,平时接触的机器从工控机、迷你主机到工作站都有,内存颗粒的品牌、批次、时序五花八门。有一次一批机器反复出问题,MemTest86 跑了一整夜报了几个错,但错误地址落在哪个颗粒、哪个 rank、哪个 bank 上,完全不知道。你只能整条内存换掉,成本高不说,还未必是内存本身的问题——有可能是插槽、有可能是主板走线、有可能是 CPU 内存控制器。这种时候你就特别想要一个工具,能把错误定位到具体的物理颗粒上。
advmemtest就是我为了解决这个问题写出来的。它是一个基于 UEFI 环境运行的内存测试工具,带图形界面,免费版不限测试轮数,Pro 版能把错误定位到内存颗粒级别。图形界面用的是 LVGL,整个东西跑在 UEFI 里,不依赖操作系统,所以哪怕你的机器根本进不了 Windows,只要还能进 UEFI 就能测。这篇文章我会把这个工具的来龙去脉、核心技术点、实操方法、踩过的坑全部讲清楚,适合做硬件测试、整机维修、嵌入式开发、以及任何需要判断内存稳定性的朋友参考。
2. 整体设计思路与关键技术选型
2.1 为什么选择 UEFI 而不是传统 DOS 或 Linux
做内存测试,第一个要决定的就是运行环境。传统方案有三条路:DOS 下的 MemTest86 老版本、Linux 下用 memtester、以及 UEFI 应用。
DOS 方案的问题是内存寻址受限,DOS 本身要占一部分常规内存,测试大容量内存时需要通过扩展内存规范来访问,而且 DOS 对现代硬件的支持很差,很多新主板根本不提供 DOS 的 CSM 兼容模式,你连启动都启动不了。Linux 方案的问题是它自己就要占用内存,你测不了 Linux 内核占用的那部分区域,而且 Linux 启动本身依赖内存正常工作,如果内存坏得比较厉害,Linux 可能根本起不来。
UEFI 方案的优势在于:它是固件层的东西,在操作系统加载之前就运行,可以访问几乎全部物理内存;现代主板全部支持 UEFI 启动,不存在兼容性问题;UEFI 提供了标准的运行时服务和启动服务,可以方便地做内存分配、定时器、图形输出。唯一的问题是 UEFI 环境下的开发资料相对少,图形界面更是几乎没有现成方案,这也是为什么大部分 UEFI 内存测试工具都是纯文本界面。
我选 UEFI 还有一个实际原因:很多出问题的机器,恰恰是进不了系统的。客户拿过来一台机器说“开不了机”,你插上 U 盘启动 advmemtest,如果能进界面,至少说明 CPU、基本内存、显卡输出是正常的,这本身就是一个快速的分诊手段。
2.2 图形界面为什么用 LVGL 而不是自己画
UEFI 本身提供了 Graphics Output Protocol,你可以直接往帧缓冲里写像素。理论上自己画界面也行,但你要处理字体渲染、控件布局、事件响应、刷新优化,工作量巨大,而且做出来的东西很难看。
LVGL 是一个轻量级嵌入式图形库,原本是给单片机用的,资源占用小,移植性好。它支持按钮、标签、进度条、表格、Tab 控件这些常用控件,正好满足内存测试工具的需求。把 LVGL 移植到 UEFI 上,核心工作就是实现一个显示驱动和一个输入驱动:显示驱动把 LVGL 的渲染结果刷到 GOP 的帧缓冲上,输入驱动把 UEFI 的键盘和鼠标事件喂给 LVGL。
这个组合的好处是,界面开发效率极高。我可以用 LVGL 的 Tab 控件把界面分成几个页:测试配置页、实时状态页、结果统计页、错误详情页。用户用键盘方向键或者鼠标就能切换,进度条实时刷新,错误列表滚动显示。相比纯文本界面,图形界面的信息密度和可读性完全不是一个级别。
2.3 测试算法怎么选:从 March 到随机模式
内存测试算法有很多种,常见的有 March C-、March X、Moving Inversions、Random Pattern 等。不同算法针对的故障类型不一样:有的擅长测固定为 0/1 的故障,有的擅长测地址译码故障,有的擅长测耦合故障。
advmemtest 免费版内置了多套算法,默认会跑一个组合:先写全 0 读回校验,再写全 1 读回校验,然后跑 March C- 的变体,最后跑随机数据模式。这样覆盖的故障类型比较全。每一轮测试会遍历所有可测试的内存区域,轮数不限,你可以让它一直跑到你手动停止。
这里有个关键点:测试区域的选择。UEFI 环境下,固件本身、advmemtest 自己、以及一些保留区域是不能动的,否则会直接死机。所以工具启动后第一件事是读取 UEFI 的内存映射表,把可用内存区域标记出来,只在这些区域里做测试。同时,advmemtest 自己运行需要的内存也要从可用区域里划出来,不能自己踩自己。
2.4 Pro 版的颗粒定位是怎么实现的
这是整个工具最核心的差异化功能。普通内存测试工具报错,最多告诉你“地址 0x1A2B3C4D 处数据错误,期望 0x55,实际 0xAA”。但 0x1A2B3C4D 这个物理地址对应哪条内存、哪个 rank、哪个 bank、哪个颗粒,它不告诉你。
Pro 版的做法是:通过 UEFI 的 SMBIOS 表读取内存设备的物理布局信息,包括每条内存的起始地址、容量、rank 数、颗粒位宽。然后结合内存控制器的地址映射规则(这个需要针对不同平台做适配,Intel 和 AMD 的映射方式不一样),把物理地址反推回具体的 rank、bank group、bank、row、column。最后根据颗粒的位宽和 rank 的组织方式,算出这个地址落在哪个颗粒上。
举个例子:一条 8GB 的 DDR4 内存,8 个颗粒,每个颗粒 8Gb(1GB),位宽 8bit,总位宽 64bit。如果错误地址落在某个 rank 的某个 bank 的某个 row 上,通过地址解码就能算出是第几个颗粒。这样你就能精确知道是哪个颗粒坏了,而不是整条内存换掉。
这个功能的实现难度在于,不同主板、不同内存控制器的地址映射规则不一样,需要做大量的逆向和适配工作。目前 Pro 版支持主流 Intel 和 AMD 平台,后续还在持续增加。
3. 核心细节解析与实操要点
3.1 UEFI 应用的开发环境搭建
开发 UEFI 应用,主流工具链是 EDK2。你需要准备:
- Linux 或 Windows 开发机
- EDK2 源码(可以从 GitHub 上的 tianocore/edk2 获取)
- GCC 或 MSVC 编译器
- Python 用于构建脚本
- QEMU 用于模拟测试
EDK2 的构建系统基于 DSC 和 INF 文件,你需要为 advmemtest 写一个 INF 描述文件,声明源文件、库依赖、编译选项。然后写一个 DSC 文件把 INF 包含进去,指定输出格式为 X64 的 UEFI 应用。
编译出来的东西是一个 .efi 文件,放到 FAT32 格式的 U 盘根目录下的 EFI/BOOT/ 目录里,命名为 BOOTX64.EFI,就能从 UEFI 启动菜单里选择启动。或者你也可以在 UEFI Shell 里直接运行它。
注意:UEFI 应用的内存管理跟操作系统完全不一样。没有 malloc/free 的标准实现,你要用 UEFI 的 Boot Services 提供的 AllocatePool 和 FreePool。而且这些服务在 ExitBootServices 之后就不可用了,所以所有内存分配必须在调用 ExitBootServices 之前完成。
3.2 LVGL 在 UEFI 上的移植要点
LVGL 移植到 UEFI,核心是实现两个回调:显示刷新和输入读取。
显示刷新方面,UEFI 的 GOP 提供了一个线性帧缓冲,你可以直接往里面写像素。LVGL 渲染完一帧后,会调用你注册的 flush_cb,你在这个回调里把 LVGL 的颜色缓冲区拷贝到帧缓冲的对应位置。注意像素格式要匹配,GOP 常见的是 BGR 或 RGB 24 位/32 位格式,LVGL 默认是 RGB565 或 ARGB8888,需要做转换。
输入方面,UEFI 的 Simple Text Input Protocol 提供键盘事件,Simple Pointer Protocol 提供鼠标事件。你需要在一个循环里轮询这些事件,转换成 LVGL 的输入事件,调用 lv_indev_read_cb 喂给 LVGL。
LVGL 的时基也很重要。LVGL 需要一个毫秒级的时间源来驱动动画和任务调度。UEFI 的 Boot Services 提供了 GetTimer 相关的接口,可以拿到高精度计时器,用它来实现 lv_tick_inc 的调用。
实操心得:LVGL 默认的刷新周期是 30ms,在 UEFI 环境下如果刷新太频繁,会明显拖慢内存测试的速度。我的做法是把刷新周期调到 100ms,只在测试进度有变化或者用户操作时才刷新,这样对测试性能的影响可以忽略不计。
3.3 内存测试算法的实现细节
以 March C- 为例,它的标准流程是:
- 从低地址到高地址,写 0 读 0
- 从低地址到高地址,写 1 读 1
- 从高地址到低地址,读 1 写 0
- 从高地址到低地址,读 0 写 1
- 从低地址到高地址,读 1 写 0
- 从低地址到高地址,读 0 写 1
每一步都要校验读回的数据是否等于期望值。如果不等,记录错误地址、期望值、实际值。
在 UEFI 环境下实现这些算法,有几个坑:
第一,编译器优化。你写*addr = 0; if (*addr != 0) error();这种代码,编译器可能会觉得这个判断永远为真,直接优化掉。解决办法是用 volatile 指针,或者把读写操作封装成函数并加上编译器屏障。
第二,缓存影响。CPU 缓存会缓存内存访问,你写进去的数据可能还在缓存里没落到内存颗粒上,读回来的是缓存里的值,测不出颗粒问题。解决办法是在测试前后做缓存刷新,或者用非临时访问指令绕过缓存。UEFI 环境下可以用 AsmFlushCacheLine 或者直接操作 CR3 切换页表来刷新 TLB 和缓存。
第三,多核并发。现代 CPU 都是多核的,如果你只在单核上跑测试,其他核心可能在后台访问内存,干扰测试结果。advmemtest 的做法是在测试开始前把其他核心都停住(通过 UEFI 的 MP Services 发送 INIT-SIPI 或者直接让它们进入死循环),只留一个核心跑测试。
3.4 错误定位到颗粒的地址解码逻辑
这部分是 Pro 版的核心,我尽量讲清楚原理但不涉及具体平台的敏感细节。
物理地址到颗粒的映射,大致分三步:
第一步,地址到 rank。内存控制器会把物理地址的高位用来选择 rank。比如双 rank 配置,地址的某一位为 0 选 rank0,为 1 选 rank1。具体是哪一位,取决于内存控制器的交错粒度。
第二步,rank 内地址到 bank/row/column。DDR 内存的地址线是复用的,RAS、CAS、WE 信号配合行地址和列地址。内存控制器会把物理地址的中间部分映射到 bank group、bank、row、column。这个映射规则每个平台都不一样,需要查芯片手册或者做逆向。
第三步,column 和 bank 到颗粒。一条内存条上的多个颗粒是并联的,它们共享地址线和控制线,但数据线是分开的。所以同一个 column 地址,会同时访问所有颗粒,每个颗粒贡献自己位宽的数据。如果错误数据的某一位出错,就能定位到对应的颗粒。
注意:这个定位逻辑依赖准确的内存拓扑信息。如果 SMBIOS 表里的信息不准确,或者主板厂商没有正确填写,定位就会出错。我在实际使用中发现,品牌整机的 SMBIOS 信息通常比较准确,DIY 主板有时候会偷懒。遇到这种情况,Pro 版会提示用户手动确认内存配置。
4. 完整实操流程与核心环节实现
4.1 制作启动 U 盘
你需要一个 FAT32 格式的 U 盘,容量不限,128MB 都够用。把编译好的 advmemtest.efi 放到 U 盘的 EFI/BOOT/ 目录下,重命名为 BOOTX64.EFI。如果你的 U 盘已经是 FAT32 且没有 EFI 目录,手动建一个就行。
然后重启目标机器,进入 UEFI 启动菜单。不同品牌的机器进入方式不一样:联想通常是 F12,戴尔是 F12,惠普是 F9,华硕是 F8,微星是 F11。如果按了没反应,可能需要先在 BIOS 里关闭快速启动,或者开启 UEFI 启动模式。
实操心得:有些机器默认只从内置硬盘启动,U 盘启动选项被隐藏了。这时候你需要进 BIOS 设置,在启动顺序里把 USB 设备调到第一位,或者临时禁用安全启动。安全启动会阻止未签名的 UEFI 应用运行,advmemtest 免费版没有签名,所以需要关闭安全启动。Pro 版提供了签名版本,可以兼容安全启动。
4.2 界面操作与测试配置
启动后你会看到 LVGL 画的界面,顶部是 Tab 栏,有“配置”“状态”“结果”“关于”四个页。
配置页里可以设置:
- 测试模式:快速(单轮 March C-)、标准(多算法组合)、压力(随机模式无限循环)
- 测试范围:全部可用内存、指定地址范围、指定大小
- 错误处理:遇到错误继续测试、遇到错误暂停、遇到错误停止
- 刷新频率:界面刷新间隔,默认 100ms
状态页显示当前测试进度、已用时间、已测轮数、当前算法、实时错误计数。进度条会随着测试推进而增长,每完成一轮会重置。
结果页显示错误列表,每条错误包含地址、期望值、实际值、所属算法、轮数。Pro 版还会显示颗粒编号。
关于页显示版本信息、支持的平台列表、以及一些快捷键说明。
4.3 测试执行与结果解读
配置好之后点“开始”,工具就开始跑测试。免费版不限轮数,你可以让它跑一晚上。第二天早上看结果页,如果有错误,记录下错误地址和数量。
这里要强调一个判断标准:偶尔一个错误和大量错误,意义完全不同。如果跑了一整夜只报了一两个错误,有可能是宇宙射线或者电源波动导致的软错误,不一定是内存坏了。如果错误成片出现,或者同一个地址反复报错,那基本可以确定是硬件问题。
Pro 版的颗粒定位会告诉你错误集中在哪个颗粒。如果多个错误都落在同一个颗粒上,那换掉那个颗粒(如果是可更换的颗粒)或者整条内存就行了。如果错误分散在多个颗粒上,可能是内存控制器或者主板走线的问题。
注意:测试过程中机器会发热,特别是内存和 CPU。如果散热不好,高温可能导致误报。建议在通风良好的环境下测试,或者监控温度。advmemtest 本身不读温度传感器,但你可以用其他工具配合监控。
4.4 参数计算:测试时间估算
内存测试的时间跟容量、算法复杂度、内存带宽都有关系。以 16GB 内存、March C- 算法为例,March C- 需要遍历内存 6 次,每次读写各一遍,总共 12 次内存访问。16GB 就是 16×1024×1024×1024 字节,乘以 12 次访问,再除以内存带宽。
假设内存带宽是 20GB/s,那么理论时间是 16×12/20 ≈ 9.6 秒。但实际上 UEFI 环境下的内存访问效率远低于操作系统,因为缓存没开、预取没开、多核没利用。实测下来,16GB 跑一轮 March C- 大概需要 3 到 5 分钟。如果跑标准模式(多算法组合),一轮大概 15 到 20 分钟。
所以如果你要跑一晚上,大概能跑 20 到 30 轮标准模式。这个轮数对于发现间歇性故障已经足够了。
5. 常见问题与排查技巧实录
5.1 启动失败:黑屏、卡死、自动重启
这是最常见的问题。可能的原因和排查方法:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 选择 U 盘启动后黑屏 | 安全启动未关闭 | 进 BIOS 关闭 Secure Boot |
| 启动后卡在厂商 Logo | U 盘格式不对 | 确认 U 盘是 FAT32,文件路径正确 |
| 启动后自动重启 | 内存故障严重 | 换一台机器测试 U 盘是否正常 |
| 提示“不是有效的 UEFI 应用” | 编译架构不对 | 确认编译的是 X64 架构 |
| 进入界面但鼠标键盘无响应 | 输入协议不支持 | 尝试用键盘操作,或换 USB 2.0 接口 |
实操心得:有些机器的 UEFI 实现有 bug,对非签名应用支持不好。我遇到过一台联想机器,必须把 U 盘插在特定的 USB 接口上才能启动。还有一台戴尔机器,需要在 BIOS 里把“USB 启动支持”从“仅限启动盘”改成“所有 USB 设备”。
5.2 测试过程中死机或重启
如果测试跑到一半死机,大概率是测到了坏的内存区域,而且坏得很严重,导致工具本身崩溃。这时候你可以:
- 缩小测试范围,避开已知的坏区域
- 降低测试强度,比如只用简单的全 0/全 1 模式
- 如果一测就死,那内存基本可以判死刑了
还有一种可能是工具本身的 bug。advmemtest 在测试时会保护自己运行所需的内存区域,但如果内存映射表有问题,可能会踩到不该踩的地方。遇到这种情况,可以在配置页里手动排除一段地址范围。
5.3 错误地址无法定位到颗粒
Pro 版的颗粒定位依赖 SMBIOS 信息和平台适配。如果遇到无法定位的情况:
- 检查 SMBIOS 信息是否完整:可以在 UEFI Shell 里用 smbiosview 命令查看
- 确认平台是否在支持列表里:目前支持 Intel 6 代到 13 代酷睿、AMD 锐龙 1000 到 7000 系列
- 如果是服务器平台,可能需要额外的适配
注意:颗粒定位不是万能的。如果错误发生在地址译码电路或者内存控制器的缓存里,可能无法对应到具体颗粒。这种情况下,错误地址会显示为“未知”。
5.4 测试速度太慢
UEFI 环境下的内存测试速度受限于几个因素:
- 单核运行:为了准确性,只用一个核心跑测试
- 缓存未优化:UEFI 不开启 CPU 缓存预取
- 界面刷新:LVGL 刷新会占用 CPU 时间
如果觉得太慢,可以:
- 把界面刷新频率调到 500ms 或更低
- 选择快速模式,只跑 March C-
- 缩小测试范围,只测怀疑有问题的区域
但要注意,降低测试强度可能会漏掉一些间歇性故障。我的建议是,第一次测用标准模式跑几轮,如果没问题再考虑加速。
5.5 与其他内存测试工具的结果不一致
不同工具的测试算法、测试范围、错误判定标准都不一样,结果不一致是正常的。比如 MemTest86 会测试一些 advmemtest 不测的保留区域,而 advmemtest 的随机模式可能测出 MemTest86 漏掉的故障。
如果两个工具结果矛盾,我的建议是:
- 以报错的工具为准,宁可信其有
- 用第三个工具交叉验证
- 如果只有一个工具报错,而且错误很少,可以先观察,不急着换内存
6. 后续扩展与个人体会
这个工具我还在持续迭代。接下来想做的几个方向:一是增加对 DDR5 平台的支持,DDR5 的地址映射和 DDR4 差别很大,需要重新适配;二是增加温度监控,把内存温度和错误率关联起来;三是做一个网络版,可以通过网络远程查看测试进度,适合批量测试的场景。
LVGL 这边也有优化空间。目前界面还比较朴素,后续想加一些图表,比如错误率随时间的变化曲线、不同算法的错误分布对比。LVGL 的图表控件已经比较成熟了,移植过来应该不难。
最后分享一个我在实际使用中总结的小技巧:如果你怀疑某条内存有问题,但测试又跑不出错误,可以试试把内存条换个插槽再测。有时候问题不在内存本身,而在插槽或者主板走线。我遇到过一条内存,在 A2 插槽上跑测试必错,换到 B2 插槽就完全正常,最后发现是 A2 插槽有一根针脚接触不良。这种问题,再好的测试工具也测不出来,只能靠换插槽来排查。