我使用 GRUB 的目的是UEFI PXE 启动或者UEFI HTTP 启动Linux Live 操作系统(由 Ubuntu Focal 或 Bionic 或 Jammy 制作)。我grub.cfg
对这两种方法都使用了相同的方法。它在 UEFI PXE 引导下运行良好,但在 UEFI HTTP 引导期间失败。
然后我发现了一些有趣的事情,关于谁在哪个阶段进行了http访问。
- UEFI PXE启动时,HTTP访问日志是这样的:
"GET /liveos/focal/kernel HTTP/1.1" 200 11780639 "-" "GRUB 2.06-2ubuntu14.1"
"GET /liveos/focal/initrd HTTP/1.1" 200 89168508 "-" "GRUB 2.06-2ubuntu14.1"
"GET /liveos/focal/squashfs HTTP/1.1" 200 388669747 "-" "Wget"
因此GRUB加载了内核和initrd,然后initrd使用wget加载了squashfs。好吧,这是有道理的。
- 然而,在UEFI HTTP启动时,HTTP访问日志是这样的:
"GET /bootx64.efi HTTP/1.1" 200 955941 "-" "UefiHttpBoot/1.0"
"GET /grubx64.efi HTTP/1.1" 200 1493150 "-" "UefiHttpBoot/1.0"
"GET /grub/grub.cfg HTTP/1.1" 200 1823 "-" "UefiHttpBoot/1.0"
"GET /liveos/focal/kernel HTTP/1.1" 200 11780639 "-" "UefiHttpBoot/1.0"
"GET /liveos/focal/initrd HTTP/1.1" 200 449680 "-" "UefiHttpBoot/1.0"
因此GRUB使用http代理“UefiHttpBoot/1.0”来加载内核和initrd。不幸的是,访问日志显示服务器没有向 UEFI HTTP 启动客户端响应完整大小的数据。全尺寸反应应该是这样的89168508
,但现在只是449680
。
问题来了:
- http代理“UefiHttpBoot/1.0”在哪里实现?在固件中?
- 为什么 GRUB2 不使用与 UEFI PXE 引导中相同的逻辑,即使用自己的 http 代理来获取 initrd?
编辑:看起来像这样,在UEFI HTTP启动中,从一开始,加载bootx64.efi的是http代理“UefiHttpBoot/1.0”,所以 “UefiHttpBoot/1.0”应该来自固件但是,它应该将执行控制权传递给bootx64.efi(GRUB2),那么GRUB2应该使用自己的http工具来读取initrd,但实际上不是,为什么?看来UEFI HTTP启动在某种程度上与Linux启动协议配合。
编辑:更多信息:
[ 1.947449] Unpacking initramfs...
[ 1.950954] Initramfs unpacking failed: junk in compressed archive
or
[ 1.943484] Unpacking initramfs...
[ 1.946991] Initramfs unpacking failed: broken padding
grub.cfg 是这样的:(请记住它在 UEFI PXE 引导中运行良好):
set default=0
set timeout=1
menuentry boot_liveos {
linux (http,100.0.0.101)/liveos/focal/kernel nomodeset ro root=squash:http://100.0.0.101/liveos/focal/squashfs ip=::::hostname:BOOTIF ip6=off overlayroot=tmpfs overlayroot_cfgdisk=disabled apparmor=0 ds=nocloud console=tty0 console=ttyS1,115200 BOOTIF=01-${net_default_mac}
initrd (http,100.0.0.101)/liveos/focal/initrd
}
PS,不仅是focal
,而且bionic
,jammy
都是同样的问题。我还尝试了不同的 GRUB2 版本,从 20200320.0 到 20230222.0,都是同样的问题。
问题是“UefiHttpBoot/1.0”出了问题,它无法接受大数据。
编辑:如果这是固件问题,那么是否有办法告诉 grub 自行加载 http 文件?
编辑:继续调查该问题。
$ strings bootx64.efi |grep UefiHttpBoot
UefiHttpBoot/1.0
$ strings grubx64.efi |grep UefiHttpBoot
UefiHttpBoot/1.0
GRUB2 二进制文件嵌入了 http 代理字符串“UefiHttpBoot/1.0”,因此执行控制应该已传递给 GRUB2,只是 GRUB2 继续使用相同的代理字符串进行 http 访问。我已经检查了源代码http://archive.ubuntu.com/ubuntu/pool/main/g/grub2-unsigned/grub2-unsigned_2.06.orig.tar.xz(父页面是https://packages.ubuntu.com/focal-updates/grub-efi-amd64)但没有找到代理字符串,但找到了一个常量 var PACKAGE_STRING
,可能是在构建二进制文件时定义的。
http_establish (struct grub_file *file, grub_off_t offset, int initial)
{
http_data_t data = file->data;
grub_uint8_t *ptr;
int i;
struct grub_net_buff *nb;
grub_err_t err;
nb = grub_netbuff_alloc (GRUB_NET_TCP_RESERVE_SIZE
+ sizeof ("GET ") - 1
+ grub_strlen (data->filename)
+ sizeof (" HTTP/1.1\r\nHost: ") - 1
+ grub_strlen (file->device->net->server)
+ sizeof ("\r\nUser-Agent: " PACKAGE_STRING
"\r\n") - 1
+ sizeof ("Range: bytes=XXXXXXXXXXXXXXXXXXXX"
"-\r\n\r\n"));
if (!nb)
return grub_errno;
在GRUB2的debian补丁源中找到http代理字符串:http://archive.ubuntu.com/ubuntu/pool/main/g/grub2-unsigned/grub2-unsigned_2.06-2ubuntu14.1.debian.tar.xz,是一个补丁文件suse-add-support-for-UEFI-network-protocols.patch,替换了一些http相关的功能来使用efi固件功能。
+static grub_err_t
+efihttp_request (grub_efi_http_t *http, char *server, char *name, int use_https, int headeronly, grub_off_t *file_size)
+{
+ grub_efi_http_request_data_t request_data;
+ grub_efi_http_message_t request_message;
+ grub_efi_http_token_t request_token;
+ grub_efi_http_response_data_t response_data;
+ grub_efi_http_message_t response_message;
+ grub_efi_http_token_t response_token;
+ grub_efi_http_header_t request_headers[3];
+
+ grub_efi_status_t status;
+ grub_efi_boot_services_t *b = grub_efi_system_table->boot_services;
+ char *url = NULL;
+
+ request_headers[0].field_name = (grub_efi_char8_t *)"Host";
+ request_headers[0].field_value = (grub_efi_char8_t *)server;
+ request_headers[1].field_name = (grub_efi_char8_t *)"Accept";
+ request_headers[1].field_value = (grub_efi_char8_t *)"*/*";
+ request_headers[2].field_name = (grub_efi_char8_t *)"User-Agent";
+ request_headers[2].field_value = (grub_efi_char8_t *)"UefiHttpBoot/1.0";
+
...
+ /* request token */
+ request_token.event = NULL;
+ request_token.status = GRUB_EFI_NOT_READY;
+ request_token.message = &request_message;
+
+ request_callback_done = 0;
+ status = efi_call_5 (b->create_event,
+ GRUB_EFI_EVT_NOTIFY_SIGNAL,
+ GRUB_EFI_TPL_CALLBACK,
+ grub_efi_http_request_callback,
+ NULL,
+ &request_token.event);
+
+
...
编辑:快到了。我使用的 GRUB2 经过优化,使用底层 efi 固件功能来访问 http 文件。听起来不错,但它有几个缺点:
- 看来确实很好地支持 url 查询字符串。
- 它不能很好地处理大数据。我会将此事报告给 GRUB2 开发人员。事实证明,存在大量的 TCP 重传和窗口大小调整。
答案1
“UefiHttpBoot/1.0”HTTP 客户端确实在 UEFI 固件(UEFI 规范版本 2.5 或更高版本)中实现。
当使用 UEFI PXE 启动时,固件将bootx64.efi
使用 TFTP 加载初始文件,因为这可能是固件可以自行执行的唯一网络文件传输协议。所以GRUB必须如果您想在这种情况下使用 HTTP,请携带其自己的 HTTP 客户端实现。
但快速grep
浏览 GRUB 源代码树后,我找不到任何对 UEFI HTTP 协议的引用。 GRUB 的 UEFI 网络驱动程序,grub-core/net/drivers/efi/efinet.c仅依赖于早期 UEFI 规范版本的固件提供的简单网络协议。
所以在我看来,当使用 UEFI HTTP 引导时,GRUB 显然根本不知道它正在使用网络。 UEFI规范表示,通过HTTP引导,固件提供了支持UEFI LoadFile协议的UEFI设备句柄,因此GRUB可以使用它来加载文件,就像从磁盘加载文件一样。当然是GRUB可以进一步调查设备句柄以查看它实际上是一个网络设备 -但它不必这样做以完成其主要工作。
这也清楚地表明 UEFI 启动无法加载 initrd 肯定是固件问题。
答案2
让我自己来回答吧。
解决方案是找到一个较旧的 grubx64.efi ,它不包含单词UefiHttpBoot
,幸运的是我20200320.0
在本地历史中找到了一个版本,它运行良好。 (它似乎不再存在于 Ubuntu 的存储库中或在互联网上实现。)相应的 bootx64.efi 仍然包含 word UefiHttpBoot
,但这并不重要,因为这只是一个填充程序,它只是加载 grubx64.efi 然后将控制权传递给它。
http访问是这样的:
"GET /bootx64.efi HTTP/1.1" 200 1335102 "-" "UefiHttpBoot/1.0"
"GET /grubx64.efi HTTP/1.1" 200 1164950 "-" "UefiHttpBoot/1.0"
...
"GET /grub/grub.cfg HTTP/1.1" 200 737 "-" "GRUB 2.02-2ubuntu8.15"
...
"GET /liveos/focal/kernel HTTP/1.1" 200 11780639 "-" "GRUB 2.02-2ubuntu8.15"
"GET /liveos/focal/initrd HTTP/1.1" 200 89168508 "-" "GRUB 2.02-2ubuntu8.15"
"GET /liveos/focal/squashfs HTTP/1.1" 200 388669747 "-" "Wget"
...