EDK2 202205 来了

目前最新的 EDK2 是 edk2-stable202205,可以在下面的链接下载到:

https://github.com/tianocore/edk2/releases/tag/edk2-stable202205

这次更新了如下内容:

和之前一样,补完下面三个模块的代码(下面的两个brotli是同一套):

  • CryptoPkg/Library/OpensslLib/openssl
  • MdeModulePkg/Universal/RegularExpressionDxe/oniguruma
  • MdeModulePkg/Library/BrotliCustomDecompressLib/brotli
  • BaseTools/Source/C/BrotliCompress/brotli

之后,可以正常编译 EmuPkg和OVMF代码。

EDK2 202205 EmuPKG 编译结果
EDK2 202205 EmuPKG 编译结果

下面是补完的代码,146MB:

链接: https://pan.baidu.com/s/1jSCa-3S9hWpVUQadDtgfRA?pwd=labz 提取码: labz

接下来是一个VirtualBox的镜像,其中是 Win10 + VS2019 + 上述的 EDK2 代码,如果你是初学者或者在配置 EDK2 过程中碰到奇怪的问题,不妨先使用这个版本:

链接: https://pan.baidu.com/s/1CK1F06xhE11VW8P7XNosSg?pwd=labz 提取码: labz

Teensy 3.6 触摸屏功能

Teensy 3.6 支持触摸屏,10指触摸,具体的库在\hardware\teensy\avr\cores\teensy3\usb_touch.c 文件中,下面是一个示例代码,使用了2个手指绘制直线:

#include <Bounce.h>

int yoffset = 4000;

void setup() {
  pinMode(A1, INPUT_PULLUP);
  TouchscreenUSB.begin();
}

void drawline(int x, int y) {
 for (int i=0; i < 6000; i += 100) {
   TouchscreenUSB.press(0, x + i, y + i/13);
   TouchscreenUSB.press(1, x + i+400, y + i/13+400);
   delay(10);
 }
 TouchscreenUSB.release(0);
 TouchscreenUSB.release(1); 
}

void loop() {
  if (digitalRead(A1)==LOW) {
    Serial.println("press");
    drawline(16000, yoffset);
    yoffset += 1200;
    if (yoffset > 24000) yoffset = 4000;
  }
}

特别的,需要在菜单中打开 Touch Screen

另外,如果你使用Windows 10 下面的画板进行测试,需要选中 Brushes,只有这个才支持多点触摸绘图:

Step to UEFI (265)QEMU ACPI Table 的来源

之前介绍过在 Shell 下面查看 ACPI Table 的工具,例如【参考1】提到的:ACPIView。

接下来的问题就是:上面的这个 DSDT 是从哪里来的?

首先,在代码中找了一圈,竟然一无所获;接下来在Debug Log中查找,发现如下信息,其中的一个 Table 长度是 0x1772(6002)非常接近我们在 Shell 下看到的 6009。

ProcessCmdAllocate: File="etc/acpi/tables" Alignment=0x40 Zone=1 Size=0x20000 Address=0x7BDE000
ProcessCmdAddChecksum: File="etc/acpi/tables" ResultOffset=0x49 Start=0x40 Length=0x1772
ProcessCmdAddPointer: PointerFile="etc/acpi/tables" PointeeFile="etc/acpi/tables" PointerOffset=0x17D6 PointerSize=4
ProcessCmdAddPointer: PointerFile="etc/acpi/tables" PointeeFile="etc/acpi/tables" PointerOffset=0x17DA PointerSize=4
ProcessCmdAddChecksum: File="etc/acpi/tables" ResultOffset=0x17BB Start=0x17B2 Length=0x74
ProcessCmdAddChecksum: File="etc/acpi/tables" ResultOffset=0x182F Start=0x1826 Length=0x78
……………….
Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDE000 (remaining: 0x20000): found "FACS" size 0x40
Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDE040 (remaining: 0x1FFC0): found "DSDT" size 0x1772
Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF7B2 (remaining: 0x1E84E): found "FACP" size 0x74
Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF826 (remaining: 0x1E7DA): found "APIC" size 0x78
Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF89E (remaining: 0x1E762): found "HPET" size 0x38
Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF8D6 (remaining: 0x1E72A): found "WAET" size 0x28

这一段 Log 对应的代码在\OvmfPkg\AcpiPlatformDxe\QemuFwCfgAcpi.c 中。

为了验证猜想,在对应代码中加入调试信息,输出读取到QEMU 的 ACPI DSDT,具体在OvmfPkg\AcpiPlatformDxe\QemuFwCfgAcpi.c,修改如下:

Blob2Remaining -= (UINTN) PointerValue;
DEBUG ((DEBUG_INFO, "%a: checking for ACPI header in \"%a\" at 0x%Lx "
  "(remaining: 0x%Lx): ", __FUNCTION__, AddPointer->PointeeFile,
  PointerValue, (UINT64)Blob2Remaining));
 
TableSize = 0;
 
//
// To make our job simple, the FACS has a custom header. Sigh.
//
if (sizeof *Facs &lt;= Blob2Remaining) {
  Facs = (EFI_ACPI_1_0_FIRMWARE_ACPI_CONTROL_STRUCTURE *)(UINTN)PointerValue;
    //LABZDebug_Start
    if ((Facs->Signature==EFI_ACPI_1_0_DIFFERENTIATED_SYSTEM_DESCRIPTION_TABLE_SIGNATURE)) {
                DEBUG ((DEBUG_INFO,"DSDt found\n"));
                p = (CHAR8*)(Facs);
                //p=p+sizeof(EFI_ACPI_1_0_FIRMWARE_ACPI_CONTROL_STRUCTURE);
                for (i=0;i&lt;32;i++) {
                      DEBUG ((DEBUG_INFO,"%x ",p[i]&amp;0xFF));
                }
        }
    // LABZDebug_End
  if (Facs->Length >= sizeof *Facs &amp;&amp;
      Facs->Length &lt;= Blob2Remaining &amp;&amp;
      Facs->Signature ==
              EFI_ACPI_1_0_FIRMWARE_ACPI_CONTROL_STRUCTURE_SIGNATURE) {
    DEBUG ((DEBUG_INFO, "found \"%-4.4a\" size 0x%x\n",
      (CONST CHAR8 *)&amp;Facs->Signature, Facs->Length));
    TableSize = Facs->Length;
 
  }
}

输出结果如下:

Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDE000 (remaining: 0x20000): found "FACS" size 0x40
Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDE040 (remaining: 0x1FFC0): DSDt found
44 53 44 54 72 17 0 0 1 9E 42 4F 43 48 53 20 42 58 50 43 20 20 20 20 1 0 0 0 42 58 50 43
Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF7B2 (remaining: 0x1E84E): Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF826 (remaining: 0x1E7DA): Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF89E (remaining: 0x1E762): Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF8D6 (remaining: 0x1E72A): Process2ndPassCmdAddPointer: checking for ACPI header in "etc/acpi/tables" at 0x7BDF8FE (remaining: 0x1E702): InstallQemuFwCfgTables: installed 6 tables
PcRtc: Write 0x20 to CMOS location 0x32

可以看到这个结果和ACPIViewApp Dump 出来的 DSDT 结果是相同的:

这说明QEMU 中的 ACPI Table 来自 QemuFwCfgAcpi.c 取得的 ACPI Table。

参考:

1. https://www.lab-z.com/acpiview/

内存的奇怪问题

最近遇到了一个奇怪的问题,经过化简得代码表示如下:

#include "stdafx.h"
#include <malloc.h>

void foo2() {
	int *Handle;
	Handle = (int *)alloca(100);
	memset(Handle, 0x11, 100);
	return;
}
void foo1(int **Handle){
	*Handle = (int *)alloca(100);
	memset(*Handle,0xAA,100);
	return;
}

int main()
{
	int *Value=NULL;
	foo1(&Value);
	foo2();
	getchar();
    return 0;
}

简单的说,在 foo1() 中分配100bytes的内存空间,0然后在foo2() 中在分配100Bytes,但是实践发现,前面分配的内存空间被“冲掉”了。更具体的说:

1.运行 foo1(), 之后查看到 value 的内存地址已经赋值为 0xaa

2.接下来执行 foo2(),但是运行之后,Value 对应的内存空间被覆盖为0x11。

有兴趣的朋友可以自己先琢磨五分钟看看能否找到问题。

最终,这个问题是分配内存的 alloca()导致的:alloca分配的是栈区(stack)内存,程序自动释放;(注意,栈空间有限仅几kb左右,堆空间远大于栈空间)。当 foo1 执行完成,这个区域已经被释放;当执行 foo2 的时候,程序会再次使用这个内存【参考1】。

解决方法:改成 malloc,它是在堆上进行分配内存的。

参考:

  1. https://zhuanlan.zhihu.com/p/449165315
  2. https://cloud.tencent.com/developer/article/1729074

Step to UEFI (264)代码读取一个 FFS

最近有一个读取 FFS 的需求,具体来说就是在 DXE 阶段需要读取指定的 FFS 文件的内容。针对这个需求,使用QEMU 进行了一番研究。

OVMF 中继承了 Shell,读取这个的动作和我们需求非常类似,于是在 QEMU 的Log中搜索可以看到如下字样:

[Bds]Booting EFI Internal Shell
[Bds] Expand Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(7C04A583-9E3E-4F1C-AD65-E05268D0B4D1) -> Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(7C04A583-9E3E-4F1C-AD65-E05268D0B4D1)
InstallProtocolInterface: 5B1B31A1-9562-11D2-8E3F-00A0C969723B 6C14040
Loading driver at 0x000068AE000 EntryPoint=0x000068AE4E8 Shell.efi
InstallProtocolInterface: BC62157E-3E33-4FEC-9920-2D3B36D750DF 70F9418

对应在代码中位于\mdemodulepkg\library\uefibootmanagerlib\BmLoadOption.c

DEBUG ((DEBUG_INFO, "[Bds] Expand "));
BmPrintDp (FilePath);
DEBUG ((DEBUG_INFO, " -> "));
BmPrintDp (CurFullPath);
DEBUG ((DEBUG_INFO, "\n"));

经过追踪可以看到具体工作的是位于 \mdepkg\library\dxeserviceslib\DxeServicesLib.c的如下函数:

/**
  Get the image file buffer data and buffer size by its device path.
 
  Access the file either from a firmware volume, from a file system interface,
  or from the load file interface.
 
  Allocate memory to store the found image. The caller is responsible to free memory.
 
  If FilePath is NULL, then NULL is returned.
  If FileSize is NULL, then NULL is returned.
  If AuthenticationStatus is NULL, then NULL is returned.
 
  @param[in]       BootPolicy           Policy for Open Image File.If TRUE, indicates
                                        that the request originates from the boot
                                        manager, and that the boot manager is
                                        attempting to load FilePath as a boot
                                        selection. If FALSE, then FilePath must
                                        match an exact file to be loaded.
  @param[in]       FilePath             The pointer to the device path of the file
                                        that is abstracted to the file buffer.
  @param[out]      FileSize             The pointer to the size of the abstracted
                                        file buffer.
  @param[out]      AuthenticationStatus Pointer to the authentication status.
 
  @retval NULL   FilePath is NULL, or FileSize is NULL, or AuthenticationStatus is NULL, or the file can't be found.
  @retval other  The abstracted file buffer. The caller is responsible to free memory.
**/
VOID *
EFIAPI
GetFileBufferByFilePath (
  IN BOOLEAN                           BootPolicy,
  IN CONST EFI_DEVICE_PATH_PROTOCOL    *FilePath,
  OUT      UINTN                       *FileSize,
  OUT UINT32                           *AuthenticationStatus
  )

进一步研究,上述函数通过 EFI_FIRMWARE_VOLUME2_PROTOCOL 来完成读取。这个 Protocol 在 PI Spec (并不是 UEFI Spec)中有描述:

进一步追踪,可以看到是通过FwVol->ReadSection() 将Shell读取出来的:

SectionType = EFI_SECTION_PE32;
ImageBuffer = NULL;
Status = FwVol->ReadSection (
                  FwVol,
                  FvNameGuid,
                  SectionType,
                  0,
                  (VOID **)&amp;ImageBuffer,
                  &amp;ImageBufferSize,
                  AuthenticationStatus
                  );

ReadSection() 原型定义如下:

/**
  Locates the requested section within a file and returns it in a buffer.
 
  ReadSection() is used to retrieve a specific section from a file
  within a firmware volume. The section returned is determined
  using a depth-first, left-to-right search algorithm through all
  sections found in the specified file. The output buffer is specified by a double indirection
  of the Buffer parameter. The input value of Buffer is used to
  determine if the output buffer is caller allocated or is
  dynamically allocated by ReadSection(). If the input value of
  Buffer!=NULL, it indicates that the output buffer is caller
  allocated. In this case, the input value of *BufferSize
  indicates the size of the caller-allocated output buffer. If
  the output buffer is not large enough to contain the entire
  requested output, it is filled up to the point that the output
  buffer is exhausted and EFI_WARN_BUFFER_TOO_SMALL is returned,
  and then BufferSize is returned with the size that is required
  to successfully complete the read. All other
  output parameters are returned with valid values. If the input
  value of *Buffer==NULL, it indicates the output buffer is to
  be allocated by ReadSection(). In this case, ReadSection()
  will allocate an appropriately sized buffer from boot services
  pool memory, which will be returned in *Buffer. The size of
  the new buffer is returned in *BufferSize and all other output
  parameters are returned with valid values. ReadSection() is
  callable only from TPL_NOTIFY and below. Behavior of
  ReadSection() at any EFI_TPL above TPL_NOTIFY is
  undefined.
 
  @param  This                Indicates the EFI_FIRMWARE_VOLUME2_PROTOCOL instance.
 
  @param NameGuid             Pointer to an EFI_GUID, which indicates the
                              file name from which the requested section
                              will be read.
 
  @param SectionType          Indicates the section type to return.
                              SectionType in conjunction with
                              SectionInstance indicates which section to
                              return.
 
  @param SectionInstance      Indicates which instance of sections
                              with a type of SectionType to return.
                              SectionType in conjunction with
                              SectionInstance indicates which
                              section to return. SectionInstance is
                              zero based.
 
  @param Buffer               Pointer to a pointer to a buffer in which the
                              section contents are returned, not including
                              the section header.
 
  @param BufferSize           Pointer to a caller-allocated UINTN. It
                              indicates the size of the memory
                              represented by Buffer.
 
  @param AuthenticationStatus Pointer to a caller-allocated
                              UINT32 in which the authentication
                              status is returned.
 
 
  @retval EFI_SUCCESS   The call completed successfully.
 
  @retval EFI_WARN_BUFFER_TOO_SMALL   The caller-allocated
                                      buffer is too small to
                                      contain the requested
                                      output. The buffer is
                                      filled and the output is
                                      truncated.
 
  @retval EFI_OUT_OF_RESOURCES  An allocation failure occurred.
 
  @retval EFI_NOT_FOUND   The requested file was not found in
                          the firmware volume. EFI_NOT_FOUND The
                          requested section was not found in the
                          specified file.
 
  @retval EFI_DEVICE_ERROR  A hardware error occurred when
                            attempting to access the firmware
                            volume.
 
  @retval EFI_ACCESS_DENIED The firmware volume is configured to
                            disallow reads. EFI_PROTOCOL_ERROR
                            The requested section was not found,
                            but the file could not be fully
                            parsed because a required
                            GUIDED_SECTION_EXTRACTION_PROTOCOL
                            was not found. It is possible the
                            requested section exists within the
                            file and could be successfully
                            extracted once the required
                            GUIDED_SECTION_EXTRACTION_PROTOCOL
                            is published.
 
**/
typedef
EFI_STATUS
(EFIAPI * EFI_FV_READ_SECTION)(
  IN CONST  EFI_FIRMWARE_VOLUME2_PROTOCOL *This,
  IN CONST  EFI_GUID                      *NameGuid,
  IN        EFI_SECTION_TYPE              SectionType,
  IN        UINTN                         SectionInstance,
  IN OUT    VOID                          **Buffer,
  IN OUT    UINTN                         *BufferSize,
  OUT       UINT32                        *AuthenticationStatus
);

在 OVMF 中实际动作是 \mdemodulepkg\core\dxe\fwvol\FwVolRead.c 中的FvReadFileSection()函数完成的(通过输出 Log 的方法可以确认):

/**
  Locates a section in a given FFS File and
  copies it to the supplied buffer (not including section header).
 
  @param  This                       Indicates the calling context.
  @param  NameGuid                   Pointer to an EFI_GUID, which is the
                                     filename.
  @param  SectionType                Indicates the section type to return.
  @param  SectionInstance            Indicates which instance of sections with a
                                     type of SectionType to return.
  @param  Buffer                     Buffer is a pointer to pointer to a buffer
                                     in which the file or section contents or are
                                     returned.
  @param  BufferSize                 BufferSize is a pointer to caller allocated
                                     UINTN.
  @param  AuthenticationStatus       AuthenticationStatus is a pointer to a
                                     caller allocated UINT32 in which the
                                     authentication status is returned.
 
  @retval EFI_SUCCESS                Successfully read the file section into
                                     buffer.
  @retval EFI_WARN_BUFFER_TOO_SMALL  Buffer too small.
  @retval EFI_NOT_FOUND              Section not found.
  @retval EFI_DEVICE_ERROR           Device error.
  @retval EFI_ACCESS_DENIED          Could not read.
  @retval EFI_INVALID_PARAMETER      Invalid parameter.
 
**/
EFI_STATUS
EFIAPI
FvReadFileSection (
  IN CONST  EFI_FIRMWARE_VOLUME2_PROTOCOL  *This,
  IN CONST  EFI_GUID                       *NameGuid,
  IN        EFI_SECTION_TYPE               SectionType,
  IN        UINTN                          SectionInstance,
  IN OUT    VOID                           **Buffer,
  IN OUT    UINTN                          *BufferSize,
  OUT       UI

运行结果如下:

oading driver at 0x00007AC4000 EntryPoint=0x00007AC4494 PcRtc.efi
InstallProtocolInterface: BC62157E-3E33-4FEC-9920-2D3B36D750DF 7097A98
ProtectUefiImageCommon - 0x7097740
  - 0x0000000007AC4000 - 0x0000000000005300
  Image - c:\buildbs\stable202108\Build\OvmfX64\DEBUG_VS2015x86\X64\PcAtChipsetPkg\PcatRealTimeClockRuntimeDxe\PcatRealTimeClockRuntimeDxe\DEBUG\PcRtc.pdb
!!!!!!!!  ProtectUefiImageCommon - Section Alignment(0x20) is incorrect  !!!!!!!!
!!!!!!!!  Image - c:\buildbs\stable202108\Build\OvmfX64\DEBUG_VS2015x86\X64\PcAtChipsetPkg\PcatRealTimeClockRuntimeDxe\PcatRealTimeClockRuntimeDxe\DEBUG\PcRtc.pdb  !!!!!!!!
[EFI_FIRMWARE_VOLUME2_PROTOCOL Success 944416]
4D 5A 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
A5 A0 A5 C0 A5 D8 A5 F0 A5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
InstallProtocolInterface: 27CFAC87-46CC-11D4-9A38-0090273FC14D 0

使用工具可以看到 Shell.efi 的大小是 94416Bytes,读取到Buffer 中的内容也是 Shell 的内容。

总结:BIOS 工程师可以通过 EFI_FIRMWARE_VOLUME2_PROTOCOL 来获取指定 FFS 的内容。

一个让你轻松配置ESP32 WIFI的库

ESP32 带有 WIFI 功能,而众所周知要想让一个设备连接WIFI AP 需要告知设备对应 AP 的名称和密码,简单实验的话,可以直接在代码中写死这两个参数,但这种情况下烧写之后设备只能在固定的环境下使用。https://github.com/tzapu/wifimanager 这个项目可以解决上述问题。先说一下这个东西如何使用:

  1. 编译下载Arduino\libraries\WiFiManager-master\examples\OnDemand下面的代码
  2. 上电运行之后短接 Pin0
  3. 用手机查找OnDemandAP 这个 AP
  4. 连接之后自动打开下面的界面

5.选择 Configure WIFI 会显示当前能搜索到的WIFI AP名称,选择你要的

6.输入对应的密码设备即可连接

我在 DFRobot的 FireBeetle 上实验(需要注意他自带的WIFI库太老,运行期AP 无法启动,需要用ESP32 Arduino库中的 FireBeetle),工作正常。

Step to UEFI (263)实验:直接修改QEMU的启动顺序

当我们使用如下 Command 启动 QEMU 的时候,能够将 d:\tmp 目录映射为 fs0:

.\qemu-system-x86_64.exe -bios ovmf.fd  -hda fat:rw:d:\tmp\

之后,可以在这个目录中看到一个名为 NvVars 的文件。这次实验的目标是直接修改 NvVars 文件来实现调整启动顺序。例如:启动之后在 Setup 中可以看到如下4个启动设备:

1.进入Shell 后使读取变量的命令,将变量保存在 Result.txt 文件中:

Dmpstore >> result.txt

2.在 Debug Log 中可以看到如下字样,就是说 OVMF 在启动过程中会读取NvVars这个文件作为变量:

InstallProtocolInterface: 964E5B22-6459-11D2-8E39-00A0C969723B 6B6F030
Installed Fat filesystem on 6C14698
FsAccess.c: LoadNvVarsFromFs
FSOpen: Open 'NvVars' Success
FsAccess.c: Read 10590 bytes from NV Variables file

3.进一步分析,读取动作来自 ovmfpkg\library\nvvarsfilelib\FsAccess.c 文件中的LoadNvVarsFromFs() 函数

/**
  Loads the non-volatile variables from the NvVars file on the
  given file system.
 
  @param[in]  FsHandle - Handle for a gEfiSimpleFileSystemProtocolGuid instance
 
  @return     EFI_STATUS based on the success or failure of load operation
 
**/
EFI_STATUS
LoadNvVarsFromFs (
  EFI_HANDLE                            FsHandle
  )

4.具体读取动作在ReadNvVarsFile() 函数中:

/**
  Reads the contents of the NvVars file on the file system
 
  @param[in]  FsHandle - Handle for a gEfiSimpleFileSystemProtocolGuid instance
 
  @return     EFI_STATUS based on the success or failure of the file read
 
**/
EFI_STATUS
ReadNvVarsFile (
  IN  EFI_HANDLE            FsHandle
  )

读取内容

FileContents = FileHandleReadToNewBuffer (File, FileSize);
if (FileContents == NULL) {
  FileHandleClose (File);
  return EFI_UNSUPPORTED;
}

5.读取之后使用 IterateVariablesInBuffer() 函数进行解析

/**
  Iterates through the variables in the buffer, and calls a callback
  function for each variable found.
 
  @param[in]  CallbackFunction - Function called for each variable instance
  @param[in]  Context - Passed to each call of CallbackFunction
  @param[in]  Buffer - Buffer containing serialized variables
  @param[in]  MaxSize - Size of Buffer in bytes
 
  @return     EFI_STATUS based on the success or failure of the operation
 
**/
STATIC
EFI_STATUS
IterateVariablesInBuffer (
  IN VARIABLE_SERIALIZATION_ITERATION_CALLBACK  CallbackFunction,
  IN VOID                                       *CallbackContext,
  IN VOID                                       *Buffer,
  IN UINTN                                      MaxSize
  )

上面是一些基本的研究,下面尝试直接修改。在Dump 的变量文件Result.txt 中,可以看到 BootOrder 变量:

00000080: 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  *................*
  00000090: 00 00 00 00 7F FF 04 00-4E AC 08 81 11 9F 59 4D  *........N.....YM*
  000000A0: 85 0E E2 1A 52 2C 59 B2-                         *....R,Y.*
Variable NV+RT+BS 'EFIGlobalVariable:BootOrder' DataSize = 0x0A
  00000000: 00 00 01 00 02 00 03 00-04 00                    *..........*
Variable NV+RT+BS 'EFIGlobalVariable:Key0002' DataSize = 0x0E
  00000000: 00 00 00 40 3C 4A 2D 14-05 00 11 00 00 00        *...@&lt;J-.......*

对应在 NvVars 文件中位于下面几行:

这里,将 00 00 01 00 02 00 03 00-04 00 修改为 00 00 04 00 02 00 03 00-01 00,下次再启动 QEMU 会会先启动到网卡上了。