厂商说你的主板不支持 Resizable BAR,所以你在 BIOS 里翻遍每一个菜单也找不到那个开关。但 Resizable BAR 这件事,本质上只是 PCIe 规范里的一个扩展能力位加上一段可写的配置空间寄存器——做不做,是固件愿不愿意做,不是硬件能不能做。xCuri0 的 ReBarUEFI 用不到 300 行 C 代码证明了这一点:往 UEFI 固件的 DXE 卷里塞一个驱动,把 PCI 总线枚举过程中的一个协议函数换掉,老主板就能开 Resizable BAR。
这篇文章拆的是这段代码怎么工作。不是"刷 BIOS 教程",是固件层的一次函数劫持。
一、先搞清楚 BAR 为什么需要"resizable"
PCI 设备在配置空间里有 6 个 BAR(Base Address Register)。以显卡为例,BAR0 是一段 MMIO 窗口,驱动往里读写就是在跟显存对话。传统 PCIe 的 BAR 大小由固件在枚举阶段一次性分配并锁死,显卡想改也改不了。
问题在于现代游戏和大模型推理的数据量。一个 8GB 显存的卡,如果 BAR 只映射 256MB,驱动就得靠窗口"翻页"访问显存——CPU 写一次显存要经过多次分页搬运。Resizable BAR 允许把这个窗口涨到 8GB 甚至更大,实现"CPU 直写全量显存"。
Intel 从 10 代酷睿、AMD 从锐龙 5000 系列开始官方支持,更早的平台固件根本没实现这段逻辑。ReBarUEFI 要补的就是这段逻辑。
二、劫持点:PciHostBridgeResourceAllocationProtocol
UEFI 启动分 SEC / PEI / DXE / BDS 等阶段。PCI 枚举发生在 DXE 阶段的 PciBusDxe 模块里,它遍历总线、给每个设备的 BAR 分配地址空间。分配之前,它会调用一个协议函数做预处理:
EFI_PCI_HOST_BRIDGE_RESOURCE_ALLOCATION_PROTOCOL *pciResAlloc;
// 该协议的成员
pciResAlloc->PreprocessController(...)
ReBarUEFI 干的事很直接——把这个函数指针换成自己的:
// 保存原始函数指针
o_PreprocessController = pciResAlloc->PreprocessController;
// 替换成我们的实现
pciResAlloc->PreprocessController = &PreprocessControllerOverride;
驱动先通过 LocateHandleBuffer 按协议 GUID 找到句柄,再用 OpenProtocol 取到协议实例,然后改写成员。这是 UEFI 里最经典的一类 hook——没有内核态的 inline hook,就是协议表里的一个函数指针赋值。
替换后的实现先调原始函数,保证原有枚举逻辑不受影响,再叠加自己的动作:
EFI_STATUS EFIAPI PreprocessControllerOverride(
IN EFI_PCI_HOST_BRIDGE_RESOURCE_ALLOCATION_PROTOCOL *This,
IN EFI_HANDLE RootBridgeHandle,
IN EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_PCI_ADDRESS PciAddress,
IN EFI_PCI_CONTROLLER_RESOURCE_ALLOCATION_PHASE Phase)
{
// 先执行固件原本的逻辑
EFI_STATUS status = o_PreprocessController(This, RootBridgeHandle, PciAddress, Phase);
// EDK2 的 PciBusDxe 会对 Resizable BAR 做两次处理,这里保持一致
if (Phase <= EfiPciBeforeResourceCollection) {
reBarSetupDevice(RootBridgeHandle, PciAddress);
}
return status;
}
Phase <= EfiPciBeforeResourceCollection 这个判断有讲究。EDK2 上游的 PciBusDxe 内部处理 Resizable BAR 时会走两遍,作者让 hook 也遵循同样的时序,避免和上层逻辑打架。注释里写得很明白:"EDK2 PciBusDxe setups Resizable BAR twice so we will do same"。
三、往哪个寄存器写
找到设备后,先读 Vendor/Device ID 过滤掉空槽位(0xFFFF):
pciReadConfigWord(pciAddress, 0, &vid);
pciReadConfigWord(pciAddress, 2, &did);
if (vid == 0xFFFF)
return;
然后沿 PCIe 扩展能力链(Extended Capability)找 Resizable BAR 能力结构,capability ID 是 PCI_EXT_CAP_ID_REBAR(=15)。扩展能力从配置空间偏移 0x100 开始,每个能力头用 32 位描述,低 16 位是 ID、高 12 位是到下一个能力的偏移,遍历上限是 (0x1000 - 0x100) / 8 次:
UINT16 pciFindExtCapability(UINTN pciAddress, INTN cap)
{
UINT16 pos = PCI_CFG_SPACE_SIZE; // 0x100
ttl = (PCI_CFG_SPACE_EXP_SIZE - PCI_CFG_SPACE_SIZE) / 8; // 最小 8 字节一个能力
pciReadConfigDword(pciAddress, pos, &header);
// 全是 0(无能力)或全是 F(设备缺失)都直接返回
if (header == 0 || PCI_POSSIBLE_ERROR(header))
return 0;
while (ttl-- > 0) {
if (PCI_EXT_CAP_ID(header) == cap && pos != 0)
return pos;
pos = PCI_EXT_CAP_NEXT(header);
if (pos < PCI_CFG_SPACE_SIZE)
break; // 越界即停,防御畸形固件
pciReadConfigDword(pciAddress, pos, &header);
}
return 0;
}
这段几乎是从 Linux 内核的 pci_find_ext_capability() 移植过来的——内核驱动里验证过十几年的遍历逻辑,直接搬进固件用,作者在注释里也承认了这点("adapted from linux pci_find_ext_capability")。
找到能力结构后,分两步:读能力寄存器拿到支持的 BAR 尺寸位图,再往控制寄存器写入选定的尺寸。
// 读支持尺寸:cap 的高位是位图,bit N 置位表示支持 2^N
pciReadConfigDword(pciAddress, pos + PCI_REBAR_CAP, &cap);
cap &= PCI_REBAR_CAP_SIZES;
return cap >> 4; // 位图对齐到 bit0
// 写选定尺寸:先清掉原有的 BAR_SIZE 字段,再写入新值
pciReadConfigDword(pciAddress, pos + PCI_REBAR_CTRL, &ctrl);
ctrl &= (UINT32)~PCI_REBAR_CTRL_BAR_SIZE;
ctrl |= (UINT32)size << PCI_REBAR_CTRL_BAR_SHIFT;
pciWriteConfigDword(pciAddress, pos + PCI_REBAR_CTRL, &ctrl);
设置尺寸时用 fls(find last set bit)从用户指定的上限往下试,命中第一个被支持的尺寸就停:
for (UINT8 n = MIN((UINT8)fls(rBarS), reBarState); n > 0; n--) {
if (rBarS & (1 << n)) { // 该尺寸被设备支持?
pciRebarSetSize(pciAddress, epos, bar, n);
break;
}
}
fls 在 MSVC 下用 _BitScanReverse64,在 GCC 下直接内联 bsrl 指令——UEFI 驱动能写内联汇编,是因为它跑在 ring 0 等价的特权环境里。
四、两个真实世界的坑
坑一:厂商固件写错的能力值。 蓝宝石 RX 5600 XT Pulse 的 BAR0 能力 dword 是错的,返回 0x7000。代码里硬编码了这个 quirk:
if (vid == PCI_VENDOR_ID_ATI && did == 0x731f &&
bar == 0 && cap == 0x7000)
cap = 0x3f000; // 修正为正确的尺寸位图
设备 ID、BAR 索引、错误值三条件同时命中才修正——这是针对具体型号打补丁,不是通用逻辑。
坑二:设错尺寸直接开不了机。 如果写入了设备/主板不支持的 BAR 尺寸,PCI 枚举会失败,机器连 BIOS 都进不去。作者的逃生方案是检测 CMOS 重置:
// 读当前时间,如果年份早于构建年份,说明 CMOS 被清过 → 自动禁用
status = gRT->GetTime(&time, NULL);
if (time.Year < BUILD_YEAR) { // BUILD_YEAR = 2023
reBarState = 0;
gRT->SetVariable(L"ReBarState", &reBarStateGuid, attributes, 1, &reBarState);
return EFI_SUCCESS; // 不 hook,安全退出
}
CMOS 被清会导致 RTC 归零到 1970 或厂商默认值,年份自然小于编译年份。用"时间倒退"当重置的信号,这个判定简单粗暴但有效——代价是如果你故意把系统时间调回 2023 年之前,Resizable BAR 就会自己关掉。
配置通过一个 NVRAM 变量传递,GUID 是固定的 a3c5b77a-c88f-4a93-bf1c-4a92-a32c65ce,值语义是 2 的幂次上限(32 表示不限,0 表示禁用):
// 0: disabled
// >0: maximum BAR size (2^x). UINT8_MAX for unlimited
gRT->GetVariable(L"ReBarState", &reBarStateGuid, &attributes, &bufferSize, &reBarState);
读变量时故意只给 1 字节缓冲区,如果实际值更长会返回 EFI_BUFFER_TOO_SMALL,此时直接把 reBarState 归零。这是个防御性设计:防止有人往变量里塞超长数据引发越界。
五、怎么确认它真的生效了
驱动跑起来只是第一步,得能验证。作者提供了 ReBarState 这个用户态工具——Windows 下直接从 Release 取,Linux 下用 CMake 自己编。它做两件事:读当前哪些设备支持 Resizable BAR,以及往 NVRAM 变量里写入目标尺寸。
手工验证的话,最直接的是看 PCI 配置空间。Linux 下:
# 确认显卡有 Resizable BAR 扩展能力(cap ID 15 = 0x0f)
sudo lspci -vvv -s 01:00.0 | grep -A4 "Resizable BAR"
# 输出形如:
# Capabilities: [resizable bar]
# BAR 1 size: 256M (8M, 16M, 64M, 256M, 1G, 8G...)
# BAR 1 size: 8G (8M, 16M, 64M, 256M, 1G, 8G) ← 当前生效值
括号里是设备支持的全部尺寸,括号前是当前生效值。生效值等于最大值,说明设置成功。
再看 MMIO 窗口够不够大——这是最容易被忽略的失败原因:
sudo dmesg | grep -i "pci.*mmio\|BAR.*assigned"
# 关注显卡 BAR0 的起始地址和长度
sudo lspci -vvv -s 01:00.0 | grep -A2 "Region 1"
如果 BAR 尺寸比 MMIO 窗口还大,枚举必然失败。这也是为什么必须配合 UEFIPatch 扩 MMIO 空间。
Windows 下最省事的是 GPU-Z:它直接把 Resizable BAR 状态标在界面上,"Enabled" / "Disabled" 一眼可见,还能看到最大 BAR 尺寸。配合 CPU-Z 一起截图,是社区里最常用的验证组合。
六、为什么还要打 UEFIPatch 补丁
光有驱动不够。老固件里的 PciBusDxe 常常有硬编码的限制,UEFIPatch 目录下的一堆补丁就是处理这些:
- 移除
< 4GB的 BAR 尺寸限制 - 移除
< 16GB/< 64GB限制 - 阻止 64 位 BAR 被降级成 32 位
- 扩大 MMIO 空间(Skylake/Kaby Lake 从 16-32GB 扩到 39 位 512GB 全量;Haswell/Broadwell 从 8-16GB 扩到 512GB;Sandy/Ivy Bridge 扩到 36 位 64GB)
MMIO 空间不足是硬约束:你给显卡开了 8GB BAR,但整个系统可用的 MMIO 窗口只有 4GB,枚举就会失败。所以扩大 MMIO 空间和设置 BAR 尺寸是一套动作的两半。
Sandy/Ivy Bridge 平台还需要改 DSDT——因为 _CRS 方法里写死的地址范围会阻止扩展。这些补丁用 Ghidra 逆向厂商固件后确定偏移,属于逐平台手工适配,没有通用解。
七、一个开发者的视角
这个项目的价值不在"让老显卡快 12%",而在于它演示了固件层的可编程性:
- DXE 阶段就是用户态。 协议表里的函数指针可以随便改,没有签名校验阻断。这个驱动在 Secure Boot 打开的情况下也能加载——注释里明确说 "Most firmware will accept unsigned/patched modules with Secure Boot on"。UEFI Secure Boot 验的是引导链(PE 镜像签名),不是每个 DXE 驱动都强制验签,厂商实现差异极大。
- Linux 内核是固件开发的教科书。 扩展能力遍历、BAR 尺寸位图、PCI 寄存器布局——这些在
drivers/pci/里都有经过实战检验的实现,移植成本远低于从零写。 - 风险确实存在。 刷错固件会变砖,设错 BAR 尺寸会开不了机。作者给了 CMOS 逃生机制,但刷固件前先确认主板有 Dual BIOS 或能拆下 SPI Flash 用编程器,是基本前提。
我自己的看法:这类项目最值得学的不是"如何刷 BIOS",而是当你发现一个能力"厂商说不支持",先去查规范,再看固件有没有实现它。多数时候,"不支持"是产品线的市场区分手段,不是硅片的物理限制。ReBarUEFI 把这层窗户纸捅破的方式很干净——不修改硬件,只替换枚举时的一个函数指针。
参考
- ReBarUEFI 项目源码:
github.com/xCuri0/ReBarUEFI(MIT 协议,核心为ReBarDxe/ReBar.c) - PCI Express Base Specification:Resizable BAR Capability(扩展能力 ID 15)
- Linux 内核
drivers/pci/pci.c:pci_find_ext_capability()实现 - EDK2
PciBusDxe:PciHostBridgeResourceAllocationProtocol定义
浙公网安备 33010602011771号