在 C (UEFI) 中获取 EDID 信息:读取 ES:DI 寄存器?

Get EDID info in C (UEFI): read the ES:DI register?

我正在开发一个 OS,我想从监视器中获取 EDID,我发现了一些汇编代码 (https://wiki.osdev.org/EDID) 以在 ES:DI 寄存器中获取 edid,

mov ax,     0x4f15
mov bl,     0x01
xor cx,     cx
xor dx,     dx
int         0x10
;AL = 0x4F if function supported
;AH = status (0 is success, 1 is fail)
;ES:DI contains the EDID

如何在 C 文件中获取 AL、AH 和 ES:DI 值?

实际上我正在开发一个 64 位 UEFI OS

LoadGDT:   
    lgdt [rdi]
    mov ax, 0x10 
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    pop rdi
    mov rax, 0x08
    push rax
    push rdi
    retfq
GLOBAL LoadGDT

我能够 运行 这些上面的 asm 代码并使用 C 中的全局函数在 C 中获取它,

当 CPU 处于 16 位实模式时,osdev.org 上的该页面包含旨在 运行 的代码。
您不仅可以从所涉及的寄存器中看出,还可以从使用 int 10h 的事实中看出。
This is a well-known BIOS interrupt service用16位实模式代码写的

如果您的目标是 UEFI,那么您的引导加载程序实际上是一个 UEFI 应用程序,它是一个 PE32(+) 映像。
如果 CPU 支持 64 位,固件将切换到长模式(64 位模式)并加载您的引导加载程序。
否则,它会切换到保护模式(32位模式)。
无论如何,UEFI 中从不使用实模式。

您可以使用 GDT/LDT 中的 16 位代码段从 protected/long 模式调用 16 位代码,但您不能调用实模式代码(即为工作而编写的代码与实模式分割),因为模式之间的分割工作完全不同。
另外,在实模式下,中断是通过 IVT 而不是 IDT 调度的,您需要获取中断 10h 的原始入口点。

UEFI 协议EFI_EDID_DISCOVERED_PROTOCOL

幸运的是,UEFI 可以替代传统 BIOS 接口提供的大多数基本服务。
在这种情况下,您可以使用 EFI_EDID_DISCOVERED_PROTOCOL 并最终使用 EFI_EDID_OVERRIDE_PROTOCOL.

应用来自平台固件的任何覆盖

EFI_EDID_DISCOVERED_PROTOCOL使用起来很简单,它只是一对(Size, Data)

typedef struct _EFI_EDID_DISCOVERED_PROTOCOL {
    UINT32   SizeOfEdid;
    UINT8   *Edid;
} EFI_EDID_DISCOVERED_PROTOCOL;

(来自 gnu-efi

缓冲区Edid的格式可以在VESA规范甚至Wikipedia上找到。

例如,我使用 gnu-efix64_64-w64-mingw32(GCC 的一个版本)编写了一个简单的 UEFI 应用程序和针对 PE 的工具)。
我避免使用 uefilib.h 以便仅将 gnu-efi 用于与 EUFI 相关的结构的定义。

代码糟透了,它假定最多有10个句柄支持EDID协议,而我只为EDID数据编写了部分结构(因为我很无聊)。
但这应该足以理解。

注意我的 VM 没有 return 任何 EDID 信息,所以代码没有完全测试!

#include <efi.h>

//You are better off using this lib
//#include <efilib.h>

EFI_GUID gEfiEdidDiscoveredProtocolGuid = EFI_EDID_DISCOVERED_PROTOCOL_GUID;
EFI_SYSTEM_TABLE* gST = NULL;

typedef struct _EDID14 {
    UINT8 Signature[8];
    UINT16 ManufacturerID;
    UINT16 ManufacturerCode;
    UINT32 Serial;
    UINT8 Week;
    UINT8 Year;
    UINT8 Major;
    UINT8 Minor;
    UINT32 InputParams;
    UINT8 HSize;
    UINT8 VSize;
    UINT8 Gamma;
    
    //...Omitted...
} EDID14_RAW;

VOID Print(CHAR16* string)
{
    gST->ConOut->OutputString(gST->ConOut, string);
}

VOID PrintHex(UINT64 number)
{
    CHAR16* digits = L"0123456789abcdef";
    CHAR16 buffer[2] = {0, 0};
    
    for (INTN i = 64-4; i >= 0; i-=4)
    {
        buffer[0] = digits[(number >> i) & 0xf];
        Print(buffer);
    }   
}

VOID PrintDec(UINT64 number)
{
    CHAR16 buffer[21] = {0};
    UINTN i = 19;
    
    do
    {
        buffer[i--] = L'0' + (number % 10);
        number = number / 10;
    }
    while (number && i >= 0);
    
    Print(buffer + i + 1);
}


#define MANUFACTURER_DECODE_LETTER(x)  ( L'A' + ( (x) & 0x1f ) - 1 )


EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable)
{
    EFI_STATUS Status = EFI_SUCCESS;
    EFI_HANDLE EDIDHandles[10];
    UINTN Size = sizeof(EFI_HANDLE) * 10;
    EFI_EDID_DISCOVERED_PROTOCOL* EDID;
     
    gST = SystemTable;
    
    if ( EFI_ERROR( (Status = SystemTable->BootServices->LocateHandle(ByProtocol, &gEfiEdidDiscoveredProtocolGuid, NULL, &Size, EDIDHandles)) ) )
    {
        Print(L"Failed to get EDID handles: "); PrintHex(Status); Print(L"\r\n");
        return Status;
    }

    for (INTN i = 0; i < Size/sizeof(EFI_HANDLE); i++)
    {
        if (EFI_ERROR( (SystemTable->BootServices->OpenProtocol(
            EDIDHandles[i], &gEfiEdidDiscoveredProtocolGuid, (VOID**)&EDID, ImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL)) ) )
        {
            Print(L"Failed to get EDID info for handle "); PrintDec(i); Print(L": "); PrintHex(Status); Print(L"\r\n");
            return Status;
        }
        
        if (EDID->SizeOfEdid == 0 || EDID->Edid == NULL)
        {
            Print(L"No EDID data for handle "); PrintDec(i); Print(L"\r\n");
            continue;
        }
        
        /*
            THIS CODE IS NOT TESTED!
            ! ! !   D O   N O T   U S E  ! ! !
        */
        EDID14_RAW* EdidData = (EDID14_RAW*)EDID->Edid;
        
        CHAR16 Manufacturer[4] = {0};
        Manufacturer[0] = MANUFACTURER_DECODE_LETTER(EdidData->ManufacturerID >> 10);
        Manufacturer[1] = MANUFACTURER_DECODE_LETTER(EdidData->ManufacturerID >> 5);
        Manufacturer[2] = MANUFACTURER_DECODE_LETTER(EdidData->ManufacturerID);
        Print(L"Manufacturer ID: "); Print(Manufacturer); Print(L"\r\n");
        Print(L"Resolution: "); PrintDec(EdidData->HSize); Print(L"X"); PrintDec(EdidData->VSize); Print(L"\r\n");
        
    }
    return Status;
}

ACPI

如果您不想使用这些 UEFI 协议,您可以使用 ACPI。每个显示输出设备都有一个 _DDC 方法,该方法记录在 ACPI 规范中,可用于 return EDID 数据(作为 128 或 256 字节的缓冲区)。
这种方法在概念上很简单,但在实践中它需要编写一个成熟的 ACPI 解析器(包括 AML VM),这需要大量工作。
但是,ACPI 是现代操作系统所必需的,因此您稍后可以使用它来获取 EDID 数据,而不必担心 UEFI 协议。