64. checkbox를 가진 HII폼 만들기

파트 2: efivarstore가 올바르게 작동하는 데 필요한 코드 작성하기

UEFI 변수 생성하기

우리의 efivarstore에서는 특정 UEFI 변수를 참조한다. HII 하위 시스템은 이 변수에 대한 액세스를 관리할 수 있지만 이 변수가 시스템에 이미 존재해야한다.

그래서 드라이버 진입점에 이 변수를 생성해 보자. 재부팅 사이에 이 변수를 유지하려면 먼저 gRT->GetVariable 호출을 통해 변수가 이미 존재하는지 확인해야 한다.

변수가 없는 경우에만 기본값으로 변수를 만들자. 이 메커니즘은 EDKII 코드베이스에서 여러 번 사용된다.

#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/BaseMemoryLib.h>

#define FORMSET_GUID  {0xef2acc91, 0x7b50, 0x4ab9, {0xab, 0x67, 0x2b, 0x4, 0xf8, 0xbc, 0x13, 0x5e}}

EFI_STATUS
EFIAPI
HIIFormCheckboxEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS Status;

  EFI_GUID Guid = FORMSET_GUID;

  UINTN BufferSize;
  UINT8 EfiVarstore;
  BufferSize = sizeof(UINT8);
  Status = gRT->GetVariable(
                L"CheckboxValue",
                &Guid,
                NULL,
                &BufferSize,
                &EfiVarstore);
  if (EFI_ERROR(Status)) {
    ZeroMem(&EfiVarstore, sizeof(EfiVarstore));
    Status = gRT->SetVariable(
                  L"CheckboxValue",
                  &Guid,
                  EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS,
                  sizeof(EfiVarstore),
                  &EfiVarstore);
    if (EFI_ERROR(Status)) {
      Print(L"Error! Can't create variable! %r\n", Status);
    }
  }

  ...

}

이제 드라이버 로드 시 변수가 어떻게 생성되는지 확인하자.

FS0:\> dmpstore -guid ef2acc91-7b50-4ab9-ab67-2b04f8bc135e
dmpstore: No matching variables found. Guid EF2ACC91-7B50-4AB9-AB67-2B04F8BC135E
FS0:\> load HIIFormCheckbox.efi
Image 'FS0:\HIIFormCheckbox.efi' loaded at 687C000 - Success
FS0:\> dmpstore -guid ef2acc91-7b50-4ab9-ab67-2b04f8bc135e
Variable NV+BS 'EF2ACC91-7B50-4AB9-AB67-2B04F8BC135E:CheckboxValue' DataSize = 0x01
  00000000: 00                                               *.*

OVMF를 다시 시작하고 이제 로드된 드라이버 없이도 시스템에 변수가 있는지 확인할 수 있다.

FS0:\> dmpstore -guid ef2acc91-7b50-4ab9-ab67-2b04f8bc135e
Variable NV+BS 'EF2ACC91-7B50-4AB9-AB67-2B04F8BC135E:CheckboxValue' DataSize = 0x01
  00000000: 00                                               *.*

그러나 우리의 드라이버는 부팅 프로세스에 포함되어 있지 않으며 선택적 드라이버이다. 따라서 사용자가 진정으로 드라이버에 연결된 모든 항목을 제거하려는 경우 UEFI 변수 삭제를 위해 HIIFormCheckboxUnload에 코드를 추가할 수 있다.

EFI_STATUS
EFIAPI
HIIFormCheckboxUnload (
  EFI_HANDLE ImageHandle
  )
{
  if (mHiiHandle != NULL)
    HiiRemovePackages(mHiiHandle);

  EFI_STATUS Status;
  EFI_GUID Guid = FORMSET_GUID;
  UINTN BufferSize;
  UINT8 EfiVarstore;

  BufferSize = sizeof(UINT8);
  Status = gRT->GetVariable(
                L"CheckboxValue",
                &Guid,
                NULL,
                &BufferSize,
                &EfiVarstore);
  if (!EFI_ERROR(Status)) {
    Status = gRT->SetVariable(
                  L"CheckboxValue",
                  &Guid,
                  0,
                  0,
                  NULL);
    if (EFI_ERROR(Status)) {
      Print(L"Error! Can't delete variable! %r\n", Status);
    }
  }

  return EFI_SUCCESS;
}

이제 코드를 검증해보자. 드라이버 언로드의 경우 Shell의 unload 명령을 사용할 수 있다.

FS0:\> unload -?
Unloads a driver image that was already loaded.

UNLOAD [-n] [-v|-verbose] Handle

  -n           - Skips all prompts during unloading, so that it can be used
                 in a script file.
  -v, -verbose - Dumps verbose status information before the image is unloaded.
  Handle       - Specifies the handle of driver to unload, always taken as hexadecimal number.

NOTES:
  1. The '-n' option can be used to skip all prompts during unloading.
  2. If the '-v' option is specified, verbose image information will be
     displayed before the image is unloaded.
  3. Only drivers that support unloading can be successfully unloaded.
  4. Use the 'LOAD' command to load a driver.

EXAMPLES:
  * To find the handle for the UEFI driver image to unload:
    Shell> dh -b

  * To unload the UEFI driver image with handle 27:
    Shell> unload 27

도움말 메시지에서 볼 수 있듯이 언로드하려면 드라이버 핸들을 찾아야 한다. 이를 위해 dh 명령을 사용할 수 있다. 드라이버 로드 후에 실행하면 출력 끝에 다음과 같은 내용이 표시된다.

FS0:\> dh
...
A9: ImageDevicePath(..xFBFC1)/\HIIFormCheckbox.efi) LoadedImage(\HIIFormCheckbox.efi)

따라서 이 경우 드라이버 핸들 번호는 A9이다.

FS0:\> unload A9
Unload - Handle [688E218].  [y/n]?
y
Unload - Handle [688E218] Result Success.

그 후 UEFI 변수가 삭제된다.

FS0:\> dmpstore -guid ef2acc91-7b50-4ab9-ab67-2b04f8bc135e
dmpstore: No matching variables found. Guid EF2ACC91-7B50-4AB9-AB67-2B04F8BC135E

모두 잘 작동하지만 우리 폼은 어떨까? 불행하게도 checkbox가 제대로 작동하기에는 이것만으로는 충분하지 않다. 동일한 오류 메시지와 함께 폼이 제출되도록 허용하지 않는다. 한 가지를 더 해야 한다.

Device Path 추가

ShowHII.efi 애플리케이션의 출력을 보면 FORMS 패키지가 있는 모든 PackageListDEVICE_PATH 패키지도 있음을 알 수 있다.

예를 들어,

PackageList[9]: GUID=D9DCC5DF-4007-435E-9098-8970935504B2; size=0x855
        Package[0]: type=FORMS; size=0x1F6
        Package[1]: type=STRINGS; size=0x62B
        Package[2]: type=DEVICE_PATH; size=0x1C
        Package[3]: type=END; size=0x4

이것은 실수가 아니다. HII 하위 시스템이 올바르게 작동하려면 폼과 함께 Device Path를 제공해야 한다.

이것이 checkbox 폼이 올바르게 작동하기 위해 해야할 마지막 일이다.

기억하겠지만 DevicePath 노드에는 다음과 같은 몇 가지 가능한 type이 있다.

typedef struct {
  UINT8    Type;    ///< 0x01 Hardware Device Path.
                    ///< 0x02 ACPI Device Path.
                    ///< 0x03 Messaging Device Path.
                    ///< 0x04 Media Device Path.
                    ///< 0x05 BIOS Boot Specification Device Path.
                    ///< 0x7F End of Hardware Device Path.

  UINT8    SubType; ///< Varies by Type

  UINT8    Length[2];
} EFI_DEVICE_PATH_PROTOCOL;

그리고 모든 type에는 여러 하위 type이 있다. type + 하위 type의 조합은 뒤에 오는 실제 데이터의 구조를 정의한다. 모든 데이터 형식은 UEFI 스펙에 의해 엄격하게 정의된다. 그러나 일부 유형은 공급업체가 장치 경로에서 자체 사용자 지정 구조를 제공할 수 있는 가능성을 남긴다. 물론 EDKII lib는 특수 경로를 출력할 수 없지만 최소한 실패하지는 않을 것이다. 라이브러리는 EFI_DEVICE_PATH_PROTOCOL.Length 필드를 사용하여 알 수 없는 DeviceNode를 건너뛸 수 있기 때문이다.

이 모든 것은 다음과 같은 특별한 하위 type으로 인해 가능해진다.

#define HW_VENDOR_DP  0x04	// vendor subtype for the Hardware Device Path type

#define MSG_VENDOR_DP  0x0a     // vendor subtype for the Messaging Device Path type

#define MEDIA_VENDOR_DP  0x03   // vendor subtype for the Media Device Path type

모든 경우에 공급업체는 다음 데이터를 정의하는 특수 GUID를 제공한다.(https://github.com/tianocore/edk2/blob/master/MdePkg/Include/Protocol/DevicePath.h).

///
/// The Vendor Device Path allows the creation of vendor-defined Device Paths. A vendor must
/// allocate a Vendor GUID for a Device Path. The Vendor GUID can then be used to define the
/// contents on the n bytes that follow in the Vendor Device Path node.
///
typedef struct {
  EFI_DEVICE_PATH_PROTOCOL    Header;
  ///
  /// Vendor-assigned GUID that defines the data that follows.
  ///
  EFI_GUID                    Guid;
  ///
  /// Vendor-defined variable size data.
  ///
} VENDOR_DEVICE_PATH;

EDKII 코드베이스의 폼에 대한 장치 경로는 이 방법으로 표시된다. 일반적으로 HARDWARE_DEVICE_PATH HW_VENDOR_DP는 해당 벤더 노드의 Type 및 SubType으로 사용된다.

이를 염두에 두고 우리의 고유한 Device Path를 만들어보자. 우리의 특수 Device Node에는 Guid = FORMSET_GUID가 있다. 이것은 필수 사항이 아니며 경로에 자체 GUID가 있을 수 있으므로 나는 불필요한 GUID를 생성하고 싶지 않다.

우리의 특별한 GUID는 중요한 데이터가 없는 장치 경로 노드를 정의하므로 다음과 같이 전체 Device Path 구조를 정적으로 정의할 수 있다.

#pragma pack(1)
///
/// HII specific Vendor Device Path definition.
///
typedef struct {
  VENDOR_DEVICE_PATH             VendorDevicePath;
  EFI_DEVICE_PATH_PROTOCOL       End;
} HII_VENDOR_DEVICE_PATH;
#pragma pack()

HII_VENDOR_DEVICE_PATH  mHiiVendorDevicePath = {
  {
    {
      HARDWARE_DEVICE_PATH,
      HW_VENDOR_DP,
      {
        (UINT8) (sizeof (VENDOR_DEVICE_PATH)),
        (UINT8) ((sizeof (VENDOR_DEVICE_PATH)) >> 8)
      }
    },
    FORMSET_GUID
  },
  {
    END_DEVICE_PATH_TYPE,
    END_ENTIRE_DEVICE_PATH_SUBTYPE,
    {
      (UINT8) (END_DEVICE_PATH_LENGTH),
      (UINT8) ((END_DEVICE_PATH_LENGTH) >> 8)
    }
  }
};

이제 드라이버에 EFI_DEVICE_PATH_PROTOCOL을 설치해야 한다. 기억하겠지만 InstallProtocolInterface는 이제 사용되지 않는 것으로 간주되므로 InstallMultipleProtocolInterfaces를 사용하자.

EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces()

Summary:
Installs one or more protocol interfaces into the boot services environment.

Prototype:
typedef
EFI_STATUS
EFIAPI *EFI_INSTALL_MULTIPLE_PROTOCOL_INTERFACES) (
 IN OUT EFI_HANDLE *Handle,
 ...
 );

Parameters:
Handle 		The pointer to a handle to install the new protocol interfaces on, or a pointer to NULL if a new handle is to be
		allocated.
...		A variable argument list containing pairs of protocol GUIDs and protocol interfaces.

Description:
This function installs a set of protocol interfaces into the boot services environment. It removes
arguments from the variable argument list in pairs. The first item is always a pointer to the protocol’s
GUID, and the second item is always a pointer to the protocol’s interface. These pairs are used to call the
boot service EFI_BOOT_SERVICES.InstallProtocolInterface() to add a protocol interface to
Handle. If Handle is NULL on entry, then a new handle will be allocated. The pairs of arguments are
removed in order from the variable argument list until a NULL protocol GUID value is found.

따라서 드라이버에 대해 EFI_HANDLE mDriverHandle을 선언하고 Device Path를 설치해보자.

EFI_HANDLE      mDriverHandle = NULL;

EFI_STATUS
EFIAPI
HIIFormCheckboxEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS Status;

  Status = gBS->InstallMultipleProtocolInterfaces(
                  &mDriverHandle,
                  &gEfiDevicePathProtocolGuid,
                  &mHiiVendorDevicePath,
                  NULL
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  ...

}

드라이버 언로드에서 프로토콜을 제거하는 것을 잊지 말자.

EFI_STATUS
EFIAPI
HIIFormCheckboxUnload (
  EFI_HANDLE ImageHandle
  )
{
  ...

  Status = gBS->UninstallMultipleProtocolInterfaces(
              mDriverHandle,
              &gEfiDevicePathProtocolGuid,
              &mHiiVendorDevicePath,
              NULL
              );

  return Status;
}

모든 것이 올바르게 작동하는지 확인해보자. dh 명령으로 이제 드라이버가 DevicePath 프로토콜이 설치된 다른 핸들을 생성하는 것을 볼 수 있다.

FS0:\> load HIIFormCheckbox.efi
Image 'FS0:\HIIFormCheckbox.efi' loaded at 6887000 - Success
FS0:\> dh
...
A8: Shell ShellParameters SimpleTextOut ImageDevicePath(..9E3E-4F1C-AD65-E05268D0B4D1)) LoadedImage(Shell)
A9: ImageDevicePath(..xFBFC1)/\HIIFormCheckbox.efi) LoadedImage(\HIIFormCheckbox.efi)
AA: DevicePath(..7B50-4AB9-AB67-2B04F8BC135E))

그리고 드라이버 언로드에서 두 핸들이 성공적으로 제거되는 것도 확인 가능하다.

FS0:\> unload a9
Unload - Handle [688D818].  [y/n]?
y
Unload - Handle [688D818] Result Success.
FS0:\> dh
...
A8: Shell ShellParameters SimpleTextOut ImageDevicePath(..9E3E-4F1C-AD65-E05268D0B4D1)) LoadedImage(Shell)

이제 실제로 Device Path 패키지를 HII 데이터베이스에 추가해야 한다. HII 데이터베이스의 패키지 및 패키지 목록을 기억하고 있겠지만,

typedef struct _EFI_HII_DEVICE_PATH_PACKAGE {
 EFI_HII_PACKAGE_HEADER Header;
//EFI_DEVICE_PATH_PROTOCOL DevicePath[];
} EFI_HII_DEVICE_PATH_PACKAGE;

Header 		The standard package header, where Header.Type = EFI_HII_PACKAGE_DEVICE_PATH.
DevicePath	The Device Path description associated with the driver handle that provided the content sent to the HII database.

다행히 폼과 문자열을 등록하는 데 지속적으로 사용되었던 HiiLib의 HiiAddPackages를 Device Path 설치에도 쉽게 사용할 수 있다.

우리가 해야 할 일은 EFI_DEVICE_PATH_PROTOCOL이 설치된 EFI_HANDLE(우리가 이미 생성한 것)을 제공하는 것 뿐이다.

HiiAddPackages 설명을 다시 살펴보고 강조 표시된 인수에 주의하자.

https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Include/Library/HiiLib.h https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Library/UefiHiiLib/HiiLib.c

/**
  Registers a list of packages in the HII Database and returns the HII Handle
  associated with that registration.  If an HII Handle has already been registered
  with the same PackageListGuid and DeviceHandle, then NULL is returned.  If there
  are not enough resources to perform the registration, then NULL is returned.
  If an empty list of packages is passed in, then NULL is returned.  If the size of
  the list of package is 0, then NULL is returned.
  The variable arguments are pointers that point to package headers defined
  by UEFI VFR compiler and StringGather tool.
  #pragma pack (push, 1)
  typedef struct {
    UINT32                  BinaryLength;
    EFI_HII_PACKAGE_HEADER  PackageHeader;
  } EDKII_AUTOGEN_PACKAGES_HEADER;
  #pragma pack (pop)
  @param[in]  PackageListGuid  The GUID of the package list.
  @param[in]  DeviceHandle     If not NULL, the Device Handle on which	     <--------- !!
                               an instance of DEVICE_PATH_PROTOCOL is installed.
                               This Device Handle uniquely defines the device that
                               the added packages are associated with.
  @param[in]  ...              The variable argument list that contains pointers
                               to packages terminated by a NULL.
  @retval NULL   An HII Handle has already been registered in the HII Database with
                 the same PackageListGuid and DeviceHandle.
  @retval NULL   The HII Handle could not be created.
  @retval NULL   An empty list of packages was passed in.
  @retval NULL   All packages are empty.
  @retval Other  The HII Handle associated with the newly registered package list.
**/
EFI_HII_HANDLE
EFIAPI
HiiAddPackages (
  IN CONST EFI_GUID    *PackageListGuid,
  IN       EFI_HANDLE  DeviceHandle  OPTIONAL,
  ...
  )
;

그 전에는 항상 DeviceHandle 인수 대신 NULL을 사용했었다. 그래서 우리가 해야 할 일은 이것을 바꾸는 것이다.

mHiiHandle = HiiAddPackages(
               &gEfiCallerIdGuid,
-              NULL,
+              mDriverHandle,
               HIIFormCheckboxStrings,
               FormBin,
               NULL
               );

이제 폼이 올바르게 작동해야 한다. 하지만 테스트하기 전에 한 가지만 더 해보자. 우리의 정적 Device Path에는 FORMSET_GUID가 포함되어 있다. 따라서 GetVariable/SetVariable 코드를 약간 단순화할 수 있다.

EFI_GUID Guid = FORMSET_GUID 변수를 만들고 참조하는 대신 간단하게 &mHiiVendorDevicePath.VendorDevicePath.Guid를 사용할 수 있다.

- EFI_GUID Guid = FORMSET_GUID;

  Status = gRT->GetVariable(
              L"CheckboxValue",
-             &Guid,
+             &mHiiVendorDevicePath.VendorDevicePath.Guid,
              NULL,
              &BufferSize,
              &EfiVarstore);

이제 빌드하고 모든 것이 올바르게 작동하는지 확인할 차례이다.

FS0:\> load HIIFormCheckbox.efi
Image 'FS0:\HIIFormCheckbox.efi' loaded at 683A000 - Success
FS0:\> DisplayHIIByGuid.efi 771A4631-43BA-4852-9593-919D9DE079F1

기본적으로 checkbox는 체크되어 있지 않다. 이제 성공적으로 수정하고 저장할 수 있다. 또한 변경 사항이 영구적인지 확인할 수 있다. OVMF를 재부팅하고 드라이버를 다시 로드하면 이제 폼이 체크된 상태의 checkbox로 시작하는 것을 볼 수 있다.

그러나 드라이버를 수동으로 언로드하면 애플리케이션의 코드가 UEFI 변수를 삭제한다. 따라서 다시 로드하면 checkbox가 기본적으로 비어 있는 상태로 되돌아 간다.

Last updated