也玩

我朝着光明走,却走向深渊。

希望这首歌是你喜欢的歌,希望这句话是你喜欢的话,希望一些都如你所愿


机械革命开机动画固件制作

之前折腾机械革命新版开机动画时,发现一个比较有意思的问题:

部分机械革命官方 BIOS 里面其实已经包含新版开机动画,但正常运行官方 BIOS 更新程序后,ROM Hole 并没有被刷新,所以机器还是显示旧版动画。

后来通过分析 AMI AFU 的 UCP 封装,发现可以在完全不修改官方 BIOS @ROM 内容的情况下,只修改更新程序携带的刷写参数,让 AFU 单独执行:

/L

也就是:

Program all ROM Holes

这样就可以生成对应的:

ROMHOLES_DIRECT.EXE

本文记录一下具体怎么手动制作。

本文以 XxRPxxxxN135MRO58_CAP.EXE 为例。

不同 BIOS 的 Offset、模块大小、原始命令可能不同,不能机械照抄本文中的十六进制地址。

真正通用的是 UCP 的结构和修改思路。


先说原理

机械革命这类 AMI Aptio BIOS 更新 EXE,本质上可以看成:

Windows PE 程序
+
AMI UCP 数据

UCP 部分通常类似:

@UAF
├── @UII
├── @W64
├── @CMD
└── @ROM

其中最重要的是两个模块:

@CMD

存放 AFU 启动时自动执行的参数。

以及:

@ROM

存放真正要刷入的官方 BIOS 固件。

比如这个官方更新包里的 @CMD 是:

/p /b /n /r /x /capsule

而我们想做的 ROM Hole 更新程序,只需要:

/L

所以整个修改过程的核心其实就是:

官方 CAP.EXE
        ↓
保留 @ROM 完全不变
        ↓
把 @CMD 改成 /L
        ↓
重新计算 UCP checksum
        ↓
生成 ROMHOLES_DIRECT.EXE

这样不会修改 BIOS 固件本身,只改变 AFU 对官方固件的刷写方式。


为什么不能直接修改 BIOS?

如果 BIOS 使用了 AMI Secure Flash,那么修改 @ROM 中的 BIOS 内容可能会导致数字签名失效。

之前测试过修改 BIOS 内部动画资源的固件,结构检查全部能够通过:

FFS Checksums ................. Pass
Check ROMLayout ............... Pass
ROM File Size checking ........ Pass
ROM ID Checking ............... Pass
ROM File Verification Status .. Pass

但真正刷写时仍然可能出现:

18 - Error: Secure Flash Rom Verify fail.

所以,如果官方 BIOS 本身已经包含新版动画,最理想的办法就是:

完全不要修改 @ROM。

只修改 AFU 的 /L 参数。


一、需要准备的软件

推荐准备:

HxD

或者:

010 Editor

用于查看和修改十六进制文件。

还建议准备:

Python 3

后面计算 checksum 会方便很多。

本文示例文件:

XxRPxxxxN135MRO58_CAP.EXE

二、先确认官方 BIOS 中确实有新版动画

这一点非常重要。

不是所有 BIOS 都已经放入新版动画。

我们之前确认过,机械革命新版动画对应的 FFS GUID 是:

1FB553B5-73E8-4290-9B95-B75D01D4CC9A

新版资源为:

800 × 600
GIF
60 帧
20 ms / 帧
总时长约 1.2 秒

如果官方 BIOS 里面这个位置只有:

800 × 600
1 帧 GIF

那即使执行 /L,最终也只会刷入静态 Logo。

所以:

官方 BIOS 本身有新版动画
→ 可以考虑做 ROMHOLES_DIRECT.EXE

官方 BIOS 本身没有新版动画
→ 单纯改 /L 没有意义

本文这个:

XxRPxxxxN135MRO58_CAP.EXE

里面已经确认存在新版 60 帧动画。


三、找到真正的 @UAF

用 HxD 打开:

XxRPxxxxN135MRO58_CAP.EXE

搜索 ASCII:

@UAF

需要注意:

文件内部可能出现不止一次类似字符串。

真正的 UCP overlay 一般位于 EXE 后部,并且后面能够连续解析出:

@UII
@W64
@CMD
@ROM

在本文这个文件中:

@UAF Offset:
0x119600

模块分布:

@UAF   0x119600
@UII   0x119610
@W64   0x119670
@CMD   0x11EF90
@ROM   0x11EFC0

可以理解成:

0x119600  @UAF
             │
             ├── @UII
             ├── @W64
             ├── @CMD
             └── @ROM

四、找到 @CMD

搜索:

@CMD

本文示例真正需要修改的位置是:

0x11EF90

原始十六进制:

40 43 4D 44 30 00 00 00 A7 11 00 00 00 00 00 00
17 00 00 00 17 00 00 00 2F 70 20 2F 62 20 2F 6E
20 2F 72 20 2F 78 20 2F 63 61 70 73 75 6C 65 00

其中:

40 43 4D 44

转换成 ASCII 就是:

@CMD

后面的:

2F 70 20 2F 62 20 2F 6E 20 2F 72 20 2F 78 20 2F
63 61 70 73 75 6C 65

就是:

/p /b /n /r /x /capsule

五、理解 @CMD 模块结构

这个模块大致可以拆成:

Offset +00   4 bytes   "@CMD"
Offset +04   4 bytes   Module Size
Offset +08   2 bytes   Checksum
Offset +0A   2 bytes   Reserved
Offset +0C   4 bytes   Reserved

Offset +10   4 bytes   Command Length
Offset +14   4 bytes   Command Length
Offset +18   ...       Command String

原来的命令:

/p /b /n /r /x /capsule

长度为:

23 bytes

也就是:

0x17

所以这里可以看到:

17 00 00 00
17 00 00 00

原 @CMD 模块总大小:

0x30

即:

48 bytes

六、把 @CMD 改成 /L

我们最终需要 AFU 执行:

/L

对应 ASCII:

2F 4C

长度:

2 bytes

所以两个 Command Length 都要变成:

02 00 00 00

字符串部分:

2F 4C 00

其中最后:

00

是字符串结束符。

再按照 16-byte alignment 补零。

新的完整 @CMD:

40 43 4D 44 20 00 00 00 20 2C 00 00 00 00 00 00
02 00 00 00 02 00 00 00 2F 4C 00 00 00 00 00 00

解释一下:

40 43 4D 44

即:

@CMD

模块大小:

20 00 00 00

也就是:

0x20

新的 checksum:

20 2C

也就是:

0x2C20

两个命令长度:

02 00 00 00
02 00 00 00

最后:

2F 4C

就是:

/L

七、不能只把原字符串覆盖成 /L

这是整个过程中最容易出错的地方。

原来的 @CMD:

0x30 bytes

新的:

0x20 bytes

少了:

0x10

也就是:

16 bytes

所以不能简单地:

/p /b /n /r /x /capsule

直接改成:

/L

然后让后面的数据留在那里。

正确做法是:

重新构造整个 @CMD module。

因此原始位置:

@CMD  0x11EF90
@ROM  0x11EFC0

修改之后:

@CMD  0x11EF90
@ROM  0x11EFB0

也就是说:

@ROM

整体向前移动:

0x10

字节。

注意:

Offset 变化不代表 @ROM 内容发生变化。

只是因为前面的 @CMD 变短了。


八、修改 @UAF 总大小

因为整个 UCP 少了:

0x10

字节,所以最外层:

@UAF

记录的 Size 也必须修改。

原始 @UAF Header:

40 55 41 46 D0 20 92 00 0F 43 10 00 FF FF FF FF

其中:

40 55 41 46

就是:

@UAF

原始 Size:

D0 20 92 00

按照 Little Endian:

0x9220D0

因为我们少了:

0x10

所以新的 Size 是:

0x9220C0

写入文件:

C0 20 92 00

九、重新计算 @CMD Checksum

AMI UCP 这里使用的是:

16-bit Little Endian Additive Checksum

要求整个模块所有 16-bit word 相加后满足:

sum & 0xFFFF == 0

计算时先把 checksum 字段清零:

@CMD + 0x08

设成:

00 00

然后按 Little Endian WORD 对整个 module 求和。

最终:

checksum = (-sum) & 0xFFFF

本文新的 /L 模块得到:

0x2C20

所以写入文件:

20 2C

十、重新计算 @UAF Checksum

最外层:

@UAF

也有自己的 16-bit checksum。

位置:

@UAF + 0x08

本文原始:

0F 43 10 00

修改后:

1F 43 10 00

也就是 checksum:

0x430F

变成:

0x431F

注意:

10 00

属于后面的字段,不要一起修改。

因此新的 @UAF Header:

40 55 41 46 C0 20 92 00 1F 43 10 00 FF FF FF FF

十一、最重要的一步:确认 @ROM 一个字节都没变

制作完成以后,必须检查:

@ROM

的 SHA-256。

原始文件:

@ROM Offset:
0x11EFC0

@ROM Size:
0x91C710

修改以后:

@ROM Offset:
0x11EFB0

@ROM Size:
0x91C710

虽然位置向前移动了:

0x10

但是 @ROM 内容必须完全一样。

本文这个官方 @ROM 的 SHA-256:

514cce8bceb61fb57b8d3fdb15ef55fb41dd55555e3b72e9a3e2dba51dc2e16d

所以生成前:

514cce8bceb61fb57b8d3fdb15ef55fb41dd55555e3b72e9a3e2dba51dc2e16d

生成后也必须是:

514cce8bceb61fb57b8d3fdb15ef55fb41dd55555e3b72e9a3e2dba51dc2e16d

如果两者不同:

不要运行生成的 EXE。


十二、检查最终文件大小

原始官方文件:

10,729,168 bytes

修改后的文件:

10,729,152 bytes

刚好减少:

16 bytes

也就是:

0x10

这与:

@CMD
0x30
↓
0x20

完全对应。


十三、最终得到的结构

修改前:

@UAF
├── @UII
├── @W64
├── @CMD
│   └── /p /b /n /r /x /capsule
└── @ROM
    └── 官方 BIOS

修改后:

@UAF
├── @UII
├── @W64
├── @CMD
│   └── /L
└── @ROM
    └── 官方 BIOS,完全不变

所以这个:

ROMHOLES_DIRECT.EXE

实际上并不是“修改版 BIOS”。

更准确地说,它是:

使用官方原始 BIOS,但改变 AFU 刷写区域选择的更新程序。


十四、为什么 /L 可以更新开机动画?

AMI AFU 对 /L 的定义是:

Program all ROM Holes

也就是刷新 BIOS 中的 ROM Hole。

机械革命新版开机动画就在对应 ROM Hole 内。

我们之前确认的动画 GUID:

1FB553B5-73E8-4290-9B95-B75D01D4CC9A

如果官方 BIOS 已经包含新版:

800 × 600
60 帧 GIF

但正常刷 BIOS 以后机器还是旧动画,那么很可能就是:

正常 BIOS Region 已更新
        ↓
ROM Hole 没刷新
        ↓
Flash 中仍然保留旧动画

执行:

/L

之后:

官方 BIOS 的 ROM Hole
        ↓
写入 SPI Flash
        ↓
新版开机动画

十五、哪些情况下不能这样做?

这个方法不是万能的。

至少要满足:

1. 官方 BIOS 使用 AMI UCP 封装

2. EXE 中存在 @UAF / @CMD / @ROM

3. 官方 @ROM 本身已经包含新版动画

4. AFU 支持 /L

5. ROM Hole 与当前机器平台匹配

如果官方 BIOS 本身只有:

1 帧静态 Logo

那么:

/L

只会把那个静态 Logo 刷进去。

不会自动变成新版动画。

另外,不同平台不能混用:

X6AR45xU
XxMT4xxx
XxRPxxxx
X6AR5xxx

虽然 BIOS 的某些结构可能很像,但并不代表 ROM 可以跨平台刷。


十六、可以用 Python 自动制作

如果不想每次都用 HxD 手工修改,可以直接用下面这个 Python 脚本自动完成。

脚本会做这些事情:

读取原厂 EXE
↓
寻找真正的 @UAF
↓
解析 @UII / @W64 / @CMD / @ROM
↓
读取原始 @CMD
↓
重建成 /L
↓
重新计算 @CMD checksum
↓
调整 @UAF size
↓
重新计算 @UAF checksum
↓
确认 @ROM 完全没有变化
↓
生成 ROMHOLES_DIRECT.EXE

其中最重要的一项保护是:

如果脚本发现生成后的 @ROM 和原始 @ROM 有任何一个字节不同,就会直接报错,并拒绝生成输出文件。

脚本如下:

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import hashlib
import struct
from pathlib import Path

ALIGN = 0x10


def u32(b: bytes | bytearray, off: int) -> int:
    return struct.unpack_from('<I', b, off)[0]


def p32(b: bytearray, off: int, v: int) -> None:
    struct.pack_into('<I', b, off, v)


def p16(b: bytearray, off: int, v: int) -> None:
    struct.pack_into('<H', b, off, v)


def sum16le(data: bytes | bytearray) -> int:
    if len(data) & 1:
        data = bytes(data) + b'\x00'

    return sum(
        struct.unpack(
            '<' + 'H' * (len(data) // 2),
            data
        )
    ) & 0xFFFF


def fix_low16_checksum(blob: bytearray, checksum_off: int) -> None:
    # UCP 的 +8 位置低 16 bit 使用 16-bit additive checksum。
    # 保留 DWORD 的高 16 bit。
    p16(blob, checksum_off, 0)

    p16(
        blob,
        checksum_off,
        (-sum16le(blob)) & 0xFFFF
    )

    if sum16le(blob) != 0:
        raise RuntimeError('checksum calculation failed')


def find_uaf(data: bytes) -> int:
    # 寻找 size 字段刚好延伸到文件结尾的 @UAF。
    # 这样可以避免误匹配 PE 主体中的 "@UAF" 字符串。

    pos = 0
    candidates = []

    while True:
        pos = data.find(b'@UAF', pos)

        if pos < 0:
            break

        if pos + 16 <= len(data):
            size = u32(data, pos + 4)

            if size >= 16 and pos + size == len(data):
                candidates.append(pos)

        pos += 1

    if len(candidates) != 1:
        raise RuntimeError(
            f'expected exactly one terminal @UAF, '
            f'found {len(candidates)}: {candidates}'
        )

    return candidates[0]


def parse_modules(data: bytes | bytearray, uaf_off: int):
    uaf_size = u32(data, uaf_off + 4)

    p = uaf_off + 16
    end = uaf_off + uaf_size

    mods = []

    while p < end:

        if p + 16 > end:
            raise RuntimeError('truncated module header')

        tag = bytes(data[p:p + 4])
        size = u32(data, p + 4)

        if (
            size < 16
            or size % ALIGN
            or p + size > end
        ):
            raise RuntimeError(
                f'bad module at 0x{p:X}: '
                f'tag={tag!r}, size=0x{size:X}'
            )

        mods.append((tag, p, size))

        p += size

    if p != end:
        raise RuntimeError(
            'module parsing did not end at UAF boundary'
        )

    return mods


def sha256(x: bytes | bytearray) -> str:
    return hashlib.sha256(x).hexdigest()


def build_cmd_module(old: bytes, command: str) -> bytes:

    cmd = command.encode('ascii')

    if b'\x00' in cmd:
        raise ValueError(
            'command may not contain NUL'
        )

    # 结构:
    #
    # 16-byte module header
    # +
    # 两个 DWORD command length
    # +
    # NUL 结尾的命令字符串
    # +
    # 16-byte alignment padding

    raw_len = 24 + len(cmd) + 1

    new_size = (
        raw_len + ALIGN - 1
    ) & ~(ALIGN - 1)

    m = bytearray(new_size)

    # 保留原始 module header
    m[:16] = old[:16]

    m[0:4] = b'@CMD'

    # 更新 module size
    p32(m, 4, new_size)

    # +8 的低 WORD 是 checksum。
    # +0xA 的高 WORD / reserved 保持原值。
    p16(m, 8, 0)

    # 两个 command length
    p32(m, 16, len(cmd))
    p32(m, 20, len(cmd))

    # command
    m[24:24 + len(cmd)] = cmd

    # NUL terminator
    m[24 + len(cmd)] = 0

    # 修复 @CMD checksum
    fix_low16_checksum(m, 8)

    return bytes(m)


def main() -> None:

    ap = argparse.ArgumentParser(
        description=(
            'Convert an AMI UCP CAP EXE to a /L '
            'ROM-Holes-only variant without modifying @ROM.'
        )
    )

    ap.add_argument(
        'input',
        type=Path
    )

    ap.add_argument(
        'output',
        type=Path
    )

    ap.add_argument(
        '--command',
        default='/L',
        help='replacement @CMD string (default: /L)'
    )

    args = ap.parse_args()

    # 读取原始 EXE
    original = args.input.read_bytes()

    # 找到真正的 @UAF
    uaf = find_uaf(original)

    # 解析 UCP modules
    mods = parse_modules(
        original,
        uaf
    )

    by_tag = {
        tag: (off, size)
        for tag, off, size in mods
    }

    if (
        b'@CMD' not in by_tag
        or b'@ROM' not in by_tag
    ):
        raise RuntimeError(
            'required @CMD/@ROM modules not found'
        )

    cmd_off, cmd_size = by_tag[b'@CMD']
    rom_off, rom_size = by_tag[b'@ROM']

    old_cmd = original[
        cmd_off:
        cmd_off + cmd_size
    ]

    old_rom = original[
        rom_off:
        rom_off + rom_size
    ]

    # 检查原始 module checksum
    if sum16le(old_cmd) != 0:
        raise RuntimeError(
            'original @CMD checksum is not valid'
        )

    if sum16le(old_rom) != 0:
        raise RuntimeError(
            'original @ROM checksum is not valid'
        )

    # 读取原始命令字符串
    old_len1 = u32(old_cmd, 16)
    old_len2 = u32(old_cmd, 20)

    old_text = old_cmd[
        24:
        24 + min(old_len1, old_len2)
    ].decode(
        'ascii',
        'replace'
    )

    # 构建新的 /L @CMD
    new_cmd = build_cmd_module(
        old_cmd,
        args.command
    )

    # 替换整个 @CMD module。
    #
    # 允许 @CMD 变短或变长,
    # 所以后面的 @ROM Offset 会自动移动。
    out = bytearray(
        original[:cmd_off]
        + new_cmd
        + original[cmd_off + cmd_size:]
    )

    delta = len(new_cmd) - cmd_size

    # @UAF 位于 @CMD 前面,
    # 所以 @UAF 自己的 Offset 不变。
    old_uaf_size = u32(
        original,
        uaf + 4
    )

    p32(
        out,
        uaf + 4,
        old_uaf_size + delta
    )

    # 重新计算 @UAF 的低 16-bit checksum,
    # 保留高 16-bit metadata。
    uaf_blob = bytearray(
        out[uaf:]
    )

    fix_low16_checksum(
        uaf_blob,
        8
    )

    out[uaf:] = uaf_blob

    # ==============================
    # Validation
    # ==============================

    # @UAF Size 必须刚好到 EOF
    if (
        uaf + u32(out, uaf + 4)
        != len(out)
    ):
        raise RuntimeError(
            'new @UAF size does not reach EOF'
        )

    # 重新解析修改后的 modules
    new_mods = parse_modules(
        out,
        uaf
    )

    # 检查每个 module checksum
    for tag, off, size in new_mods:

        if sum16le(
            out[off:off + size]
        ) != 0:

            raise RuntimeError(
                f'bad checksum after rebuild: {tag!r}'
            )

    # 检查整个 @UAF checksum
    if sum16le(out[uaf:]) != 0:
        raise RuntimeError(
            'bad @UAF checksum after rebuild'
        )

    # 重新寻找 @ROM
    new_rom_entry = next(
        (
            x
            for x in new_mods
            if x[0] == b'@ROM'
        ),
        None
    )

    if not new_rom_entry:
        raise RuntimeError(
            '@ROM disappeared after rebuild'
        )

    _, new_rom_off, new_rom_size = (
        new_rom_entry
    )

    new_rom = out[
        new_rom_off:
        new_rom_off + new_rom_size
    ]

    # 最重要的保险:
    # @ROM 必须逐字节完全不变。
    if new_rom != old_rom:
        raise RuntimeError(
            '@ROM changed; refusing to write output'
        )

    # 所有验证通过后才写入输出文件
    args.output.write_bytes(out)

    print(
        f'Input:          {args.input}'
    )

    print(
        f'Output:         {args.output}'
    )

    print(
        f'@UAF offset:    0x{uaf:X}'
    )

    print(
        f'Old @CMD:       {old_text!r}'
    )

    print(
        f'New @CMD:       {args.command!r}'
    )

    print(
        f'@CMD size:      '
        f'0x{cmd_size:X} -> '
        f'0x{len(new_cmd):X}'
    )

    print(
        f'File size:      '
        f'{len(original)} -> '
        f'{len(out)} '
        f'({delta:+d})'
    )

    print(
        f'@ROM SHA-256:   '
        f'{sha256(old_rom)} '
        f'(unchanged)'
    )

    print(
        f'Output SHA-256: '
        f'{sha256(out)}'
    )

    print(
        'Validation:     PASS'
    )


if __name__ == '__main__':
    main()

使用方法

把上面的代码保存成:

make_romholes_direct.py

然后把 Python 脚本和原厂 BIOS EXE 放在同一个目录。

例如:

BIOS\
├── make_romholes_direct.py
└── XxRPxxxxN135MRO58_CAP.EXE

在这个目录打开 CMD 或 PowerShell。

执行:

python make_romholes_direct.py XxRPxxxxN135MRO58_CAP.EXE XxRPxxxxN135MRO58_ROMHOLES_DIRECT.EXE

脚本默认把 @CMD 修改成:

/L

也可以显式指定:

python make_romholes_direct.py XxRPxxxxN135MRO58_CAP.EXE XxRPxxxxN135MRO58_ROMHOLES_DIRECT.EXE --command "/L"

如果一切正常,最后应该看到类似:

Input:          XxRPxxxxN135MRO58_CAP.EXE
Output:         XxRPxxxxN135MRO58_ROMHOLES_DIRECT.EXE
@UAF offset:    0x119600

Old @CMD:
'/p /b /n /r /x /capsule'

New @CMD:
'/L'

@CMD size:
0x30 -> 0x20

File size:
10729168 -> 10729152 (-16)

@ROM SHA-256:
514cce8bceb61fb57b8d3fdb15ef55fb41dd55555e3b72e9a3e2dba51dc2e16d
(unchanged)

Output SHA-256:
5c24e34d98b37461037926ad1f0e8c3ac881bd11d2a0cc032b3d89cda0da7221

Validation:
PASS

其中最重要的是下面两项:

@ROM SHA-256 ... (unchanged)

Validation: PASS

只有看到:

unchanged

以及:

PASS

才说明:

@ROM 没有被修改
UCP module checksum 正常
@UAF checksum 正常
模块结构正常

如果脚本出现任何:

RuntimeError

或者没有显示:

Validation: PASS

不要运行生成出来的 EXE。

脚本做了什么?

整个自动处理过程相当于:

原厂 CAP.EXE
        ↓
自动寻找 @UAF
        ↓
解析所有 UCP Module
        ↓
找到 @CMD
        ↓
读取原始命令
        ↓
重新构建 @CMD = /L
        ↓
重新计算 @CMD checksum
        ↓
自动调整 @UAF size
        ↓
重新计算 @UAF checksum
        ↓
重新寻找 @ROM
        ↓
逐字节比较原始 @ROM 和新 @ROM
        ↓
完全一致
        ↓
输出 ROMHOLES_DIRECT.EXE

因此它比手工用 HxD 修改更适合批量处理不同机型的官方 BIOS。

不过这个脚本只能解决:

怎么把官方 AFU 参数改成 /L

它不会自动判断官方 BIOS 里有没有新版开机动画。

所以在制作之前仍然需要先确认:

官方 @ROM
        ↓
对应的 ROM Hole
        ↓
1FB553B5-73E8-4290-9B95-B75D01D4CC9A
        ↓
确实包含新版 800×600 / 60帧 GIF

如果官方 BIOS 本身没有新版动画,那么即使成功生成:

ROMHOLES_DIRECT.EXE

刷进去的也仍然是官方 BIOS 自带的旧动画或静态 Logo。

十七、最终检查清单

在真正运行自己制作的 ROMHOLES_DIRECT.EXE 之前,至少检查:

[ ] 平台型号正确

[ ] BIOS版本对应

[ ] @ROM 中确实存在新版动画

[ ] 原始 @ROM SHA-256 与修改后完全一致

[ ] @CMD 已变成 /L

[ ] @CMD checksum 正确

[ ] @UAF size 正确

[ ] @UAF checksum 正确

[ ] 文件结构可以正常解析

其中最重要的是:

@ROM 必须完全没变

如果修改 @CMD 的过程中连 @ROM 都变化了,说明文件重建过程有问题,不要拿去刷。


总结

手动制作 ROMHOLES_DIRECT.EXE 的核心其实只有一句话:

保留官方 BIOS
只把 AFU 参数改成 /L

完整流程:

官方 CAP.EXE
↓
找到 @UAF
↓
找到 @CMD
↓
读取原始 AFU 参数
↓
重建 @CMD 为 /L
↓
重新计算 checksum
↓
调整 @UAF
↓
确认 @ROM 完全没变
↓
输出 ROMHOLES_DIRECT.EXE

这样做的好处是:

不用修改 BIOS 内部固件,也不用重新签名,只是让官方 AFU 把原本没有刷新的 ROM Hole 写进去。

如果官方 BIOS 中本来就包含新版机械革命开机动画,这种方式通常比直接修改 BIOS 本身干净得多。


风险提示

BIOS 属于底层固件。

即使只是修改 AFU 的刷写参数,也仍然属于固件刷写操作。

本文中的 Offset:

0x119600
0x11EF90
0x11EFC0

以及:

@CMD Size
@UAF Size
Checksum

都是针对本文示例:

XxRPxxxxN135MRO58_CAP.EXE

得到的结果。

其他 BIOS 不要直接照抄这些地址。

正确做法应该是重新解析自己手里的官方 EXE,再根据实际结构修改。

操作前请确认机器型号、平台和 BIOS 版本,并自行承担刷写固件产生的风险。

最近的文章

本文是2025版机械革命耀世15 Pro(X6AR45xU)升级 N.1.15MRO10 BIOS,以及解决升级 BIOS 后仍然显示旧版开机动画的方法。 BIOS 刷写存在风险,操作前请确认自己&…

继续阅读