29. EFI_ACPI_SDT_PROTOCOL 및 ShellLib를 사용하여 ACPI BGRT 테이블에서 BMP 이미지 저장하기

지난 장에서 시스템에 BGRT ACPI 테이블이 있음을 확인했다. ACPI 스펙에 따르면:

BGRT(Boot Graphics Resource Table)는 다음을 나타내는 메커니즘을 제공하는 선택적 테이블입니다.
부팅하는 동안 화면에 이미지가 그려지고 이미지에 대한 일부 정보가 표시됩니다. 
이미지가 화면에 그려지면 테이블이 작성됩니다.
이 작업은 펌웨어 구성 요소가 화면에 기록될 것으로 예상되고 이미지만 화면에 표시된 후 수행해야 합니다.
부팅 경로가 중단된 경우(예시: 키 누름), 현재 이미지가 무효화되었음을 OS에 나타내려면
Status 필드를 0으로 변경해야 합니다.

이 테이블에는 실제로 이미지 데이터에 대한 포인터가 존재한다. https://github.com/tianocore/edk2/blob/master/MdePkg/Include/IndustryStandard/Acpi63.h

///
/// Boot Graphics Resource Table definition.
///
typedef struct {
  EFI_ACPI_DESCRIPTION_HEADER Header;
  ///
  /// 2-bytes (16 bit) version ID. This value must be 1.
  ///
  UINT16                      Version;
  ///
  /// 1-byte status field indicating current status about the table.
  ///     Bits[7:1] = Reserved (must be zero)
  ///     Bit [0] = Valid. A one indicates the boot image graphic is valid.
  ///
  UINT8                       Status;
  ///
  /// 1-byte enumerated type field indicating format of the image.
  ///     0 = Bitmap
  ///     1 - 255  Reserved (for future use)
  ///
  UINT8                       ImageType;
  ///
  /// 8-byte (64 bit) physical address pointing to the firmware's in-memory copy
  /// of the image bitmap.
  ///
  UINT64                      ImageAddress;
  ///
  /// A 4-byte (32-bit) unsigned long describing the display X-offset of the boot image.
  /// (X, Y) display offset of the top left corner of the boot image.
  /// The top left corner of the display is at offset (0, 0).
  ///
  UINT32                      ImageOffsetX;
  ///
  /// A 4-byte (32-bit) unsigned long describing the display Y-offset of the boot image.
  /// (X, Y) display offset of the top left corner of the boot image.
  /// The top left corner of the display is at offset (0, 0).
  ///
  UINT32                      ImageOffsetY;
} EFI_ACPI_6_3_BOOT_GRAPHICS_RESOURCE_TABLE;

BGRT에서 이미지를 저장하는 애플리케이션을 만들어보자. 이번에는 BGRT 테이블을 얻기 위해 EFI_ACPI_SDT_PROTOCOL 프로토콜을 활용한다. 이후 ACPI 테이블 데이터를 얻기 위해서 해당 프로토콜에서 GetAcpiTable() 함수를 사용한다. https://github.com/tianocore/edk2/blob/master/MdePkg/Include/Protocol/AcpiSystemDescriptionTable.h

EFI_ACPI_SDT_PROTOCOL.GetAcpiTable()

Summary:
Returns a requested ACPI table.

Prototype:
typedef
EFI_STATUS
(EFIAPI *EFI_ACPI_GET_ACPI_TABLE) (
 IN UINTN Index,
 OUT EFI_ACPI_SDT_HEADER **Table,
 OUT EFI_ACPI_TABLE_VERSION *Version,
 OUT UINTN *TableKey
 );

Parameters:
Index		The zero-based index of the table to retrieve.
Table		Pointer for returning the table buffer.
Version		On return, updated with the ACPI versions to which this table belongs.
TableKey	On return, points to the table key for the specified ACPI system definition table.

Description:
The GetAcpiTable() function returns a pointer to a buffer containing the ACPI table associated with the Index that was input. The following structures are not considered elements in the list of ACPI tables:
- Root System Description Pointer (RSD_PTR)
- Root System Description Table (RSDT)
- Extended System Description Table (XSDT)

모든 테이블을 가져오기 위해서는 0부터 시작하여 인덱스 값을 증가시키는 GetAcpiTable을 호출해야 하며 함수는 EFI_SUCCESS를 반환해야 한다. 모든 성공 호출을 하면 ACPI 테이블의 공통 헤더에 대한 포인터를 얻을 수 있다.

typedef struct {
 UINT32 Signature;
 UINT32 Length;
 UINT8 Revision;
 UINT8 Checksum;
 CHAR8 OemId[6];
 CHAR8 OemTableId[8];
 UINT32 OemRevision;
 UINT32 CreatorId;
 UINT32 CreatorRevision;
} EFI_ACPI_SDT_HEADER;

EFI_ACPI_SDT_PROTOCOL을 사용하기 위해서는 이전과 동일하게 c 파일의 헤더와 inf 파일에 프로토콜을 추가해야 한다.

#include <Protocol/AcpiSystemDescriptionTable.h>
[Protocols]
  gEfiAcpiSdtProtocolGuid

이를 토대로 코드를 짜면 아래와 같이 BGRT ACPI 테이블을찾을 수 있다.

EFI_ACPI_SDT_PROTOCOL* AcpiSdtProtocol;
EFI_STATUS Status = gBS->LocateProtocol (
                &gEfiAcpiSdtProtocolGuid,
                NULL,
                (VOID**)&AcpiSdtProtocol
                );
if (EFI_ERROR (Status)) {
  return Status;
}

BOOLEAN BGRT_found = FALSE;
UINTN Index = 0;
EFI_ACPI_SDT_HEADER* Table;
EFI_ACPI_TABLE_VERSION Version;
UINTN TableKey;
while (TRUE) {
  Status = AcpiSdtProtocol->GetAcpiTable(Index,
    &Table,
    &Version,
    &TableKey
  );
  if (EFI_ERROR(Status)) {
    break;
  }
  if (((CHAR8)((Table->Signature >>  0) & 0xFF) == 'B') &&
      ((CHAR8)((Table->Signature >>  8) & 0xFF) == 'G') &&
      ((CHAR8)((Table->Signature >> 16) & 0xFF) == 'R') &&
      ((CHAR8)((Table->Signature >> 24) & 0xFF) == 'T')) {
    BGRT_found = TRUE;
    break;
  }
  Index++;
}
if (!BGRT_found) {
  Print(L"BGRT table is not present in the system\n");
  return EFI_UNSUPPORTED;
}

이제 BGRT 테이블에서 이미지를 저장해야 한다. 현재 ACPI 스펙은 BMP 이미지 유형만 지원한다. https://uefi.org/specs/ACPI/6.4/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#image-type

따라서 type이 실제로 BMP인지 확인을 우선시 해야한다.

EFI_ACPI_6_3_BOOT_GRAPHICS_RESOURCE_TABLE* BGRT = (EFI_ACPI_6_3_BOOT_GRAPHICS_RESOURCE_TABLE*)Table;
if (BGRT->ImageType == 0) {
  ...
}

이제 실제로 BMP 이미지를 저장하는 코드를 짜야한다. BGRT에는 이미지의 크기가 포함되지 않고 데이터에 대한오프셋만 포함되어 있다:ImageAddress.

그렇기 때문에 이미지의 크기를 구하기 위해서는 BMP header를 사용해야 한다. https://github.com/tianocore/edk2/blob/master/MdePkg/Include/IndustryStandard/Bmp.h

typedef struct {
  CHAR8         CharB;
  CHAR8         CharM;
  UINT32        Size;
  UINT16        Reserved[2];
  UINT32        ImageOffset;
  UINT32        HeaderSize;
  UINT32        PixelWidth;
  UINT32        PixelHeight;
  UINT16        Planes;          ///< Must be 1
  UINT16        BitPerPixel;     ///< 1, 4, 8, or 24
  UINT32        CompressionType;
  UINT32        ImageSize;       ///< Compressed image size in bytes
  UINT32        XPixelsPerMeter;
  UINT32        YPixelsPerMeter;
  UINT32        NumberOfColors;
  UINT32        ImportantColors;
} BMP_IMAGE_HEADER;

잊지 않고 헤더도 추가해줍니다.

#include <IndustryStandard/Bmp.h>

이미지가 BMP인 것이 확인되면 서명(BM)을 확인하고 크기를 파싱 후 실제로 데이터를 파일에 쓰기가 가능하다. 여기서는 EFI_STATUS WriteFile(CHAR16* FileName, VOID* Data, UINTN* Size) 함수를 통해서 파일에 데이터를 쓰며 이에 대한 정의는 이후에 언급하겠다.

BMP_IMAGE_HEADER* BMP = (BMP_IMAGE_HEADER*)(BGRT->ImageAddress);

if ((BMP->CharB != 'B') || (BMP->CharM != 'M')) {
  Print(L"BMP image has wrong signature!\n");
  return EFI_UNSUPPORTED;
}
Print(L"BGRT conatins BMP image with %dx%d resolution\n", BMP->PixelWidth, BMP->PixelHeight);
UINTN Size = BMP->Size;
Status = WriteFile(L"BGRT.bmp", BMP, &Size);
if (EFI_ERROR(Status)) {
  Print(L"Error! Can't write BGRT.bmp file\n");
}

이전에는 EFI_SHELL_PROTOCOL을 사용하여 파일을 만들고 데이터를 기록했지만, 이번에는 ShellLib를 사용해보겠다. https://github.com/tianocore/edk2/blob/master/ShellPkg/Include/Library/ShellLib.h https://github.com/tianocore/edk2/blob/master/ShellPkg/Library/UefiShellLib/UefiShellLib.c

이전과 동일하게 파일 열기, 쓰기, 닫기 기능이 필요하다.

/**
  This function will open a file or directory referenced by filename.
  If return is EFI_SUCCESS, the Filehandle is the opened file's handle;
  otherwise, the Filehandle is NULL. Attributes is valid only for
  EFI_FILE_MODE_CREATE.
  @param[in] FileName           The pointer to file name.
  @param[out] FileHandle        The pointer to the file handle.
  @param[in] OpenMode           The mode to open the file with.
  @param[in] Attributes         The file's file attributes.
  ...
**/

EFI_STATUS
EFIAPI
ShellOpenFileByName(
  IN CONST CHAR16               *FileName,
  OUT SHELL_FILE_HANDLE         *FileHandle,
  IN UINT64                     OpenMode,
  IN UINT64                     Attributes
  );
/**
  Write data to a file.
  This function writes the specified number of bytes to the file at the current
  file position. The current file position is advanced the actual number of bytes
  written, which is returned in BufferSize. Partial writes only occur when there
  has been a data error during the write attempt (such as "volume space full").
  The file is automatically grown to hold the data if required. Direct writes to
  opened directories are not supported.
  @param[in] FileHandle          The opened file for writing.
  @param[in, out] BufferSize     On input the number of bytes in Buffer.  On output
                                 the number of bytes written.
  @param[in] Buffer              The buffer containing data to write is stored.
  ...
**/

EFI_STATUS
EFIAPI
ShellWriteFile(
  IN SHELL_FILE_HANDLE          FileHandle,
  IN OUT UINTN                  *BufferSize,
  IN VOID                       *Buffer
  );
/**
  Close an open file handle.
  This function closes a specified file handle. All "dirty" cached file data is
  flushed to the device, and the file is closed. In all cases the handle is
  closed.
  @param[in] FileHandle           The file handle to close.
**/

EFI_STATUS
EFIAPI
ShellCloseFile (
  IN SHELL_FILE_HANDLE          *FileHandle
  );

ShellLib 사용으로써 얻는 점은 EFI_SHELL_PROTOCOL을 찾아서 수동으로 작업할 필요가 없다는 것이다.

WriteFile 함수는 아래와 같다.

EFI_STATUS WriteFile(CHAR16* FileName, VOID* Data, UINTN* Size)
{
  SHELL_FILE_HANDLE FileHandle;
  EFI_STATUS Status = ShellOpenFileByName(
    FileName,
    &FileHandle,
    EFI_FILE_MODE_CREATE | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_READ,
    0
  );
  if (!EFI_ERROR(Status)) {
    Print(L"Save it to %s\n", FileName);
    UINTN ToWrite = *Size;
    Status = ShellWriteFile(
      FileHandle,
      Size,
      Data
    );
    if (EFI_ERROR(Status)) {
      Print(L"Can't write file: %r\n", Status);
    }
    if (*Size != ToWrite) {
      Print(L"Error! Not all data was written\n");
    }
    Status = ShellCloseFile(
      &FileHandle
    );
    if (EFI_ERROR(Status)) {
      Print(L"Can't close file: %r\n", Status);
    }
  } else {
    Print(L"Can't open file: %r\n", Status);
  }
  ret

ShellLib를 사용하기 위해서 헤더 파일을 추가하고 [Packages], [LibraryClasses]에도 ShellLib를 추가해야 한다.

[Packages]
  MdePkg/MdePkg.dec
+  ShellPkg/ShellPkg.dec

[LibraryClasses]
  UefiApplicationEntryPoint
  UefiLib
+  ShellLib

그리고 우리가 사용하는 패키지지인 dsc 파일에 ShellLib 라이브러리 클래스를 포함되어야 한다.

하지만 이후에 빌드를 하더라도 아래와 같이 에러가 뜰 것이다. 왜냐하면 ShellLib를 사용하려면 다른 라이브러리들도 필요하기 때문이다.

build.py...
/home/kostr/tiano/edk2/UefiLessonsPkg/UefiLessonsPkg.dsc(...): error 4000: Instance of library class [FileHandleLib] is not found

이전에 사용했던 방식을 사용하면 간단하게 해결이 된다.

$ grep FileHandleLib -r ./ --include=*.inf | grep LIBRARY_CLASS

여러가지 LibraryClasses들을 추가 후 성공적으로 빌드가 되었다.

[LibraryClasses]
  ...
  FileHandleLib|MdePkg/Library/UefiFileHandleLib/UefiFileHandleLib.inf
  HiiLib|MdeModulePkg/Library/UefiHiiLib/UefiHiiLib.inf
  SortLib|MdeModulePkg/Library/UefiSortLib/UefiSortLib.inf
  UefiHiiServicesLib|MdeModulePkg/Library/UefiHiiServicesLib/UefiHiiServicesLib.inf
FS0:\> SaveBGRT.efi
BGRT conatins BMP image with 193x58 resolution
Save it to BGRT.bmp7
FS0:\>

애플리케이션을 통해서 제작된 BGRT.bmp 사진을 보면 TianoCore 로고가 나오는 것을 볼 수 있다. https://raw.githubusercontent.com/tianocore/edk2/master/MdeModulePkg/Logo/Logo.bmp

BGRT 드라이버는 플래시 이미지를 사용하지 않고 실제로 부팅 화면을 가져와 BMP 이미지로 변환하기 때문에 파일 자체는 동일하지 않는다. BGRT가 플래시 이미지를 사용하는 대신 화면을 잡는 것이 이상하다고 생각되면 ACPI 스펙에서 BGRT가 정의된 방식을 다시 생각하는 것이 좋다.

BGRT(Boot Graphics Resource Table)는 부팅 중에 이미지가 화면에 그려졌음을 나타내는 메커니즘을 제공하는 선택적 테이블입니다.

바이너리 부팅 로고 이미지의 파일 GUID는 아래의 링크에 정의되어 있다. https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Logo/Logo.inf

FILE_GUID                      = 7BB28B99-61BB-11D5-9A5D-0090273FC14D

일반적으로 BIOS에서 로고이미지에서 사용되는 GUID는 아래의 주소에 하드코딩 되어 있다. https://github.com/tianocore/edk2/blob/master/BaseTools/Source/Python/Eot/Report.py

## GenerateFfs() method
#
#  Generate FFS information
#
#  @param self: The object pointer
#  @param FfsObj: FFS object after FV image is parsed
#
def GenerateFfs(self, FfsObj):
    self.FfsIndex = self.FfsIndex + 1
    if FfsObj is not None and FfsObj.Type in [0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xA]:
        FfsGuid = FfsObj.Guid
        FfsOffset = FfsObj._OFF_
        FfsName = 'Unknown-Module'
        FfsPath = FfsGuid
        FfsType = FfsObj._TypeName[FfsObj.Type]

        # Hard code for Binary INF
        if FfsGuid.upper() == '7BB28B99-61BB-11D5-9A5D-0090273FC14D':
            FfsName = 'Logo'

        if FfsGuid.upper() == '7E374E25-8E01-4FEE-87F2-390C23C606CD':
            FfsName = 'AcpiTables'

        if FfsGuid.upper() == '961578FE-B6B7-44C3-AF35-6BC705CD2B1F':
            FfsName = 'Fat'
        ...

만약 Logo 및 BGRT가 EDKII에서 어떤 방식으로 작동하는지 알고 싶다면 다음의 드라이버들을 확인하는 것이 좋다.

Last updated