我们正在将一些系统设置为通过 iPXE 进行 PXEboot,并根据主服务器状态,正常启动或通过 wimboot 和 MDT 重新映像。系统配置为首先从网络启动。iPXE 和 wimboot 都在 UEFI 下运行。
它运行良好,但在 Windows 安装结束时,BIOS 已被修改为将新的 Windows 启动管理器指向为主启动设备。因此,如果不进入 BIOS 并更改设置,则无法再次对其进行映像。
我理解为什么启动顺序会改变,因为 wimboot/MDT 过程涉及多次重启。但我真的希望始终将 PXE 保留为主要启动,或者在完成后将启动顺序调整为网络优先。(我的 PXE 服务器将传递网络启动机会以允许安装工作,或者在不需要映像时让系统保持原样。)
更新 -我看到两种可能性:
- 弄清楚 Windows 安装程序如何告诉 UEFI 从目标安装磁盘启动,并在 Windows 安装完成后执行相同的操作以重新设置为 PXE 启动。
- 安装 Windows 后,使用 Windows 启动管理器和 BCDEdit 将 PXE 启动选项置于从本地磁盘启动之上(问题位于超级用户基本上与此处的问题相同。那里讨论的最终结果并不是我真正想要的(UEFI 设置中 PXE 优先),但可能会产生相同的行为(PXE 启动总是有机会在 Windows 启动之前执行)。
答案1
学到了以下内容:
- 在 Linux 上,这相当简单,通过启动管理器
- EasyUEFI 也能让我做我想做的事——命令行支持需要相当便宜的许可证;但依赖像这样的小众工具我感觉不太好,特别是如果有其他选择的话。
- UEFI 计算机上的 bcdedit 修改 UEFI 设置。我认为它会起作用。
- UEFI 规范启动顺序并不太复杂。API 实际上只是 GetVariable/SetVariable,其中包含名为 BootOrder(用于按尝试顺序获取/设置启动选项列表)和 Boot####(用于获取/设置有关每个启动选项的信息)的变量。
- 我不知道如何针对 Windows 上的 UEFI API 编写 Windows 应用程序(有人知道吗?)
- Windows 提供了一个 API除其他外,它包装了 UEFI 的 GetVariable/SetVariable。
一旦我理解了 UEFI 的启动顺序规范和 Windows API,代码(C++,为 64 位构建,因为我们只使用 64 位)就不算太糟糕了。这需要构建到一个需要管理权限并静态链接 Windows 运行时的 exe 中,然后在操作系统安装后在重启之前在 MDT 中运行它。
首先,您必须申请调用 API 的权限。使用一个小助手:
struct CloseHandleHelper
{
void operator()(void *p) const
{
CloseHandle(p);
}
};
BOOL SetPrivilege(HANDLE process, LPCWSTR name, BOOL on)
{
HANDLE token;
if (!OpenProcessToken(process, TOKEN_ADJUST_PRIVILEGES, &token))
return FALSE;
std::unique_ptr<void, CloseHandleHelper> tokenLifetime(token);
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
if (!LookupPrivilegeValueW(NULL, name, &tp.Privileges[0].Luid))
return FALSE;
tp.Privileges[0].Attributes = on ? SE_PRIVILEGE_ENABLED : 0;
return AdjustTokenPrivileges(token, FALSE, &tp, sizeof(tp), NULL, NULL);
}
然后调用
SetPrivilege(GetCurrentProcess(), SE_SYSTEM_ENVIRONMENT_NAME, TRUE));
接下来,获取启动选项列表(uint16_t 值的组合):
const int BUFFER_SIZE = 4096;
BYTE bootOrderBuffer[BUFFER_SIZE];
DWORD bootOrderLength = 0;
const TCHAR bootOrderName[] = TEXT("BootOrder");
const TCHAR globalGuid[] = TEXT("{8BE4DF61-93CA-11D2-AA0D-00E098032B8C}");
DWORD bootOrderAttributes;
bootOrderLength = GetFirmwareEnvironmentVariableEx(bootOrderName, globalGuid, bootOrderBuffer, BUFFER_SIZE, &bootOrderAttributes);
if (bootOrderLength == 0)
{
std::cout << "Failed getting BootOrder with error " << GetLastError() << std::endl;
return 1;
}
然后,您可以遍历每个启动选项,为其形成 Boot#### 变量名,然后使用它来获取包含有关该选项的信息的结构。您需要查看第一个活动选项的“Description”是否等于“Windows Boot Manager”。Description 是结构中偏移量为 6 的以空字符结尾的宽字符串。
for (DWORD i = 0; i < bootOrderLength; i += 2)
{
std::wstringstream bootOptionNameBuilder;
bootOptionNameBuilder << "Boot" << std::uppercase << std::setfill(L'0') << std::setw(4) << std::hex << *reinterpret_cast<uint16_t*>(bootOrderBuffer + i);
std::wstring bootOptionName(bootOptionNameBuilder.str());
BYTE bootOptionInfoBuffer[BUFFER_SIZE];
DWORD bootOptionInfoLength = GetFirmwareEnvironmentVariableEx(bootOptionName.c_str(), globalGuid, bootOptionInfoBuffer, BUFFER_SIZE, nullptr);
if (bootOptionInfoLength == 0)
{
std::cout << "Failed getting option info for option at offset " << i << std::endl;
return 1;
}
uint32_t* bootOptionInfoAttributes = reinterpret_cast<uint32_t*>(bootOptionInfoBuffer);
//First 4 bytes make a uint32_t comprised of flags. 0x1 means the boot option is active (not disabled)
if (((*bootOptionInfoAttributes) & 0x1) != 0)
{
std::wstring description(reinterpret_cast<wchar_t*>(bootOptionInfoBuffer + sizeof(uint32_t) + sizeof(uint16_t)));
bool isWBM = boost::algorithm::to_upper_copy<std::wstring>(description) == L"WINDOWS BOOT MANAGER";
// details - keep track of the value of i for the first WBM and non-WBM options you find, and the fact that you found them
}
}
现在,如果您找到了活动的 WBM 和非 WBM 启动选项,并且第一个 WBM 选项位于 wbmOffset,而第一个非 WBM 选项位于 nonWBMOffset,且 wbmOffset < nonWBMOffset,则将 BootOrder 变量中的条目交换为以下内容:
uint16_t *wbmBootOrderEntry = reinterpret_cast<uint16_t*>(bootOrderBuffer + wbmOffset);
uint16_t *nonWBMBootOrderEntry = reinterpret_cast<uint16_t*>(bootOrderBuffer + nonWBMOffset);
std::swap(*wbmBootOrderEntry, *nonWBMBootOrderEntry);
if (SetFirmwareEnvironmentVariableEx(bootOrderName, globalGuid, bootOrderBuffer, bootOrderLength, bootOrderAttributes))
{
std::cout << "Swapped WBM boot entry at offset " << wbmOffset << " with non-WBM boot entry at offset " << nonWBMOffset << std::endl;
}
else
{
std::cout << "Failed to swap WBM boot entry with non-WBM boot entry, error " << GetLastError() << std::endl;
return 1;
}
答案2
我想出了一个对我有用的 powershell 脚本。它并不完美,因为它只是“愚蠢地”将第一个非 Windows 启动项移到顶部。这对我来说是可行的,也许有一种方法可以让它更智能,只是我没有找到。
它看起来很长,但主要是注释,并且格式化以便于理解。它可以重写为 5 或 6 行。
https://github.com/mmseng/bcdedit-revert-uefi-gpt-boot-order
# This script looks for the first non-Windows Boot Manager entry in the UEFI/GPT boot order and moves it to the top
# For preventing newly installed Windows from hijacking the top boot order spot on my UEFI/GPT image testing VMs
# by mmseng
# https://github.com/mmseng/bcdedit-revert-uefi-gpt-boot-order
# Notes:
# - There's very little point in using this on regular production machines being deployed. Its main use is for machines being repeatedly imaged, or might be useful for lab machines.
# - AFAICT bcdedit provideds no way to pull the friendly names of the devices in the overall UEFI boot order list. Therefore, this script only moves the first entry it identifies in the list which is NOT "{bootmgr}" (a.k.a. "Windows Boot Manager"). It's up to the user to make sure the boot order will exist in a state where the desired result is achieved.
# - In my case, my test UEFI VMs initially have the boot order of 1) "EFI Network", 2) whatever else. When Windows is installed with GPT partitioning, it changes the boot order to 1) "Windows Boot Manager", 2) "EFI Network", 3) whatever else. In that state, this script can be used to change the boot order to 1) "EFI Network", 2) "Windows Boot Manager", 3) whatever else.
# - This functionality relies on the completely undocumented feature of bcdedit to modify the "{fwbootmgr}" GPT entry, which contains the overall list of UEFI boot devices.
# - AFAICT bcdedit is really only designed to edit Windows' own "{bootmgr}" entry which represents one of the "boot devices" in the overall UEFI list.
# - Here are some sources:
# - https://www.cnet.com/forums/discussions/bugged-bcdedit-349276/
# - https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/bcd-system-store-settings-for-uefi
# - https://www.boyans.net/DownloadVisualBCD.html
# - https://serverfault.com/questions/813695/how-do-i-stop-windows-10-install-from-modifying-bios-boot-settings
# - https://serverfault.com/questions/714337/changing-uefi-boot-order-from-windows
# Read current boot order
echo "Reading current boot order..."
$bcdOutput = cmd /c bcdedit /enum "{fwbootmgr}"
echo $bcdOutput
# Kill as many of the stupid characters as possible
echo "Removing extraneous characters from boot order output..."
$bcdOutput = $bcdOutput -replace '\s+',''
$bcdOutput = $bcdOutput -replace '`t',''
$bcdOutput = $bcdOutput -replace '`n',''
$bcdOutput = $bcdOutput -replace '`r',''
$bcdOutput = $bcdOutput.trim()
$bcdOutput = $bcdOutput.trimEnd()
$bcdOutput = $bcdOutput.trimStart()
$bcdOutput = $bcdOutput -replace ' ',''
echo $bcdOutput
# Define a reliable regex to capture the UUIDs of non-Windows Boot Manager devices in the boot order list
# This is difficult because apparently Powershell interprets regex is a fairly non-standard way (.NET regex flavor)
# https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions
# Even then, .NET regex testers I used didn't match the behavior of what I got out of various Powershell commands that accept regex strings
# However this seems to work, even though I can't replicate the results in any regex testers
$regex = [regex]'^{([\-a-z0-9]+)+}'
echo "Defined regex as: $regex"
# Save matches
echo "Save strings matching regex..."
$foundMatches = $bcdOutput -match $regex
# Grab first match
# If Windows Boot Manager (a.k.a. "{bootmgr}" was the first in the list, this should be the second
# Which means it was probably the first before Windows hijacked the first spot
# Which means it was probably my "EFI Network" boot device
$secondBootEntry = $foundMatches[0]
echo "First match: $secondBootEntry"
# Move it to the first spot
echo "Running this command:"
echo "cmd /c bcdedit $bcdParams /set `"{fwbootmgr}`" displayorder $secondBootEntry /addfirst"
cmd /c bcdedit $bcdParams /set "{fwbootmgr}" displayorder $secondBootEntry /addfirst