UEFI - Parte 1

1 - UEFI

Quando seu computador inicia, em algum momento, a placa-mãe irá passar o controle de tudo para um firmware. Esse firmware, por outro lado, precisa seguir algum tipo de especificação para que seja mais conveniente o uso dele. O UEFI (Unified Extensible Firmware Interface) é uma especificação que diz como ele deve se comportar, quais abstrações fornecer, etc. Ele veio como uma alternativa para a BIOS Legada. A Intel, por exemplo, oferece suporte apenas para UEFI, atualmente.

O UEFI traz alguns recursos bem interessantes e que diminui o trabalho do desenvolvedor de bootloaders. Por exemplo, a especificação diz que devem ser fornecidas rotinas para lidar com sistema de arquivos FAT. A BIOS Legada não disponibiliza isso, ou seja, você tinha que criar suas próprias rotinas e isso tomava muito trabalho. A BIOS Legada também só carrega 512 bytes do seu HD antes de executar esse código. Com UEFI não existe esse limite, você precisa apenas compilar o código como PE, colocar ele em /EFI/BOOT de uma partição FAT e o firmware vai iniciar ele. Existem outros recursos também que falei no decorrer dessa série de artigos.

2 - GNU-EFI

O GNU-EFI é uma biblioteca usada no desenvolvimento de aplicações UEFI. Vamos utilizar ela nos nossos exemplos. Você pode instalar da seguinte forma:

$ sudo apt install gnu-efi

O código pode ser baixado aqui: http://sourceforge.net/projects/gnu-efi/

Existem alternativas ao GNU-EFI como EDKII e o POSIX-EFI. Além disso, você não precisa de uma biblioteca para programar para criar um bootloader. Elas apenas abstraem alguns dos recursos fornecidos pelo UEFI. Por exemplo, a especificação fala de uma função chamada OutputString(). Ela é usada para imprimir uma string na tela. Entretanto, essa função não vai receber um inteiro literal e imprimir ele. Você teria que converter ele para string e só depois imprimir. Ela também não formata como uma printf() do C. O GNU-EFI, por outro lado, abstrai tudo isso com a função Print(). Um “Hello, world!” seria da seguinte forma:

#include <efi.h>
#include <efilib.h>

EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table)
{
  InitializeLib(image, system_table);
 
  Print(L"Hello, world!");
  return EFI_SUCCESS;
}

Fácil, não é?

Você vai entender mais tarde para que serve esse image e system_table.

Para compilar o código acima, podemos usar o seguinte Makefile:

CFLAGS=-mno-red-zone -fno-stack-protector -fpic -fshort-wchar -I/usr/include/efi/
LDFLAGS=-T /usr/lib/elf_x86_64_efi.lds -shared -Bsymbolic -L /usr/lib -l:libgnuefi.a -l:libefi.a

BOOTx64.EFI: bootx64.so
    objcopy -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel -j .rela -j .reloc --target=efi-app-x86_64 $< $@

bootx64.so: efi_main.o
    $(LD) $^ /usr/lib/crt0-efi-x86_64.o $(LDFLAGS) -o $@

efi_main.o: efi_main.c
    $(CC) $(CFLAGS) -c -o $@ $<

.PHONY: clean run

run:
    qemu-system-x86_64 -L /usr/share/edk2-ovmf/x64/ -bios OVMF.fd -drive file=test.img,format=raw
clean:
    rm efi_main.o bootx64.so BOOTx64.EFI test.img

Usamos a -mno-red-zone, porque binários PE para x86-64 não usam red zone. O -fpic deve ser usado, pois o nosso código vai ser realocado. Como vamos compilar o código como uma lib, então vamos usar o -shared e o -Bsymbolic. Quanto ao -T, ele é usado para dizer ao ld que você quer usar um linker script próprio. Vamos usar o script elf_x86_64_efi.lds disponibilizado pelo GNU-EFI que fica em /usr/lib/.

Bom, esses executáveis .EFI, em essência, são binários PE. Como estamos compilando no Linux, o objcopy serve para converter de ELF para PE. A flag -j é usada para dizer quais seções devem ser preservadas.

3 - Iniciando o BOOTx64.EFI

O firmware tem que buscar o BOOTx64.EFI em algum lugar. Ele vai procurar no diretório /boot/efi de uma partição FAT. O executável tem que estar nomeado como BOOTx64.EFI caso você esteja lidando com x86-64. Caso seja i386, então vai ter que ser BOOTIA32.EFI. Existe também o BOOTIA64.EFI para Intel Itanium, BOOTARM.EFI para AArch32 e BOOTAA64.EFI para AArch64. Você pode criar uma imagem usando o dd e o mkfs.fat para usarmos como teste:

test.img: BOOTx64.EFI
    dd if=/dev/zero of=$@ bs=3M count=1
    mkfs.fat -F 12 $@
    mkdir test/
    sudo mount $@ test/
    sudo mkdir test/efi/boot -p
    sudo cp $^ test/efi/boot
    sudo umount test/
    rm -rf test/

Vamos criar uma imagem de 3 MiB com o dd, formatar ela com FAT12 usando o mkfs.fat, criar o diretório /efi/boot e colocar o BOOTx64.EFI dentro. Isso já é o suficiente para o firmware carregar o seu executável. Você pode testar no qemu:

$ qemu-system-x86_64 -bios OVMF.fd -drive file=test.img,format=raw

Esse -bios é usado para você especificar a implementação de firmware que você quer usar no qemu. Você pode baixar esse OVMF pelo gerenciador de pacotes:

$ sudo apt install ovmf

Basta colocar -bios OVMF.fd que o qemu vai buscar ele no diretório que o apt colocou. Caso o qemu não ache, basta especificar o diretório usando -L:

$ qemu-system-x86_64 -L /usr/share/edk2-ovmf/x64/ -bios OVMF.fd -drive file=test.img,format=raw

Se você deu boot na imagem, vai perceber que o “Hello, world!” não aparece. Isso acontece, porque o código vai ser executado muito rápido. Um forma de ver a saída desse código é executando ele pelo Shell UEFI. Vamos falar disso no próximo tópico.

4 - Shell UEFI

O firmware disponibiliza um Shell com um conjunto de comandos e recursos. São comandos simples como cd e ls, que podemos também encontrar no Bash, como também outros mais exóticos como eficompress, que é usada para comprimir arquivos usando um algorítmo próprio do UEFI.

O modo de acessar o Shell UEFI vai depender da placa-mãe. No caso do qemu, precisamos pressionar ESC, logo que você iniciar ele, até aparecer um menu com as seguintes opções:

Note a opção Boot Manager. Ele é usada para selecionar o que queremos dar boot primeiro. Seleciona essa opção e você verá o seguinte resultado:

Escolha o EFI Internal Shell é o nosso Shell UEFI.

Vai aparecer a seguinte tela se você seguir os passos corretamente:

Note o FS0:. Isso é a partição FAT que criamos com o Makefile. Podemos acessar ela apenas digitando fs0: no Shell:

Veja, na imagem acima, que agora podemos acessar o diretório efi/boot e executar, manualmente, o BOOTx64.EFI.

5 - ABI

Como os binários .EFI são PE, é normal que a convenção de chamadas usado pelo UEFI seja a MS-ABI. Essa ABI disponibiliza os registradores RCX, RDX, R8 e R9 para os primeiros quatro argumentos. O resto deve ser passado pela pilha. Um exemplo:

sub	$0x38,%rsp
…
mov	$0x4,%r9d
mov	$0x2,%edx
mov	$0x1,%ecx
mov	$0x3,%r8d
movl    $0x5,0x20(%rsp)
call    do_somethinga
xor	%eax,%eax
add	$0x38,%rsp
ret

Nesse exemplo, eu estou chamando uma função nomeada como do_something(). A função chamadora aloca espaço na pilha. Uma parte desse valor vai ser usado para armazenar o quinto argumento ($5) que vai ser passado para o do_something(). Os primeiros quatro argumentos ($1, $2, $3, $4) vão ser colocados nos registadores disponibilizados pela ABI. O equivalente deste código, em C, seria o seguinte:

void do_something(int a, int b, int c, int d, int e);

do_something(1, 2, 3, 4, 5);

Quanto ao retorno de uma função, o valor retornado é colocado em RAX. Por exemplo:

int sum(int x, int y)
{ return x + y; }

Versão em assembly:

sum:
  lea (%rcx,%rdx,1),%eax
  ret

A instrução lea soma os valores de RCX com o de RDX e coloca o retorno em EAX.

No caso de valores em pontos flutuantes, a ABI diz que devemos colocar os quatro primeiros argumentos do XMM0 ao XMM3 (4 registradores). O quinto argumento, caso seja necessário, deve ser colocado na pilha:

void do_something(float a, float b, float c, float d, float e);
...
do_something(1.0f, 2.0f, 3.0f, 4.0f, 5.0f);

Versão em assembly:

sub	$0x38,%rsp
…
movl   $0x40a00000,0x20(%rsp)
movss  0x15d3(%rip),%xmm3
movss  0x15cf(%rip),%xmm2 
movss  0x15cb(%rip),%xmm1
movss  0x15c7(%rip),%xmm0 
call  	 do_something
…
xor	%eax,%eax
add	$0x38,%rsp
ret

O retorno de valores em ponto flutuantes são feitos no registrador XMM0. Por exemplo:

float sum(float x, float y)
{ return x + y; }

Assembly:

sum:
  addss  %xmm1,%xmm0
  ret

Os registradores RBX, RBP, RDI, RSI, R12-15 e XMM6-XMM15 devem ter o valor conservado. Se você decidir usar algum deles, vai ter que salvar o conteúdo do registrador em algum lugar e colocar de volta no registrador quando terminar de usá-lo.

5.1 - GNU-EFI e a conversão entre ABIs

A coisa muda um pouco quando você usa o método acima de compilar o bínario como ELF e depois converter para PE. Saiba que ELFs usam a SysV ABI e não a MS-ABI. Para resolver isso, o GNU-EFI disponibiliza a macro uefi_call_wrapper(). Ela funciona da seguinte forma:

#if defined(HAVE_USE_MS_ABI)
#define uefi_call_wrapper(func, va_num, ...) func(__VA_ARGS__)
#else
/*
  Credits for macro-magic:
    https://groups.google.com/forum/?fromgroups#!topic/comp.std.c/d-6Mj5Lko_s
    http://efesx.com/2010/08/31/overloading-macros/
*/
#define __VA_NARG__(...)                        \
  __VA_NARG_(_0, ## __VA_ARGS__, __RSEQ_N())
#define __VA_NARG_(...)                         \
  __VA_ARG_N(__VA_ARGS__)
#define __VA_ARG_N(                             \
  _0,_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,N,...) N
#define __RSEQ_N()                              \
  10, 9,  8,  7,  6,  5,  4,  3,  2,  1,  0

#define __VA_ARG_NSUFFIX__(prefix,...)                  \
  __VA_ARG_NSUFFIX_N(prefix, __VA_NARG__(__VA_ARGS__))
#define __VA_ARG_NSUFFIX_N(prefix,nargs)        \
  __VA_ARG_NSUFFIX_N_(prefix, nargs)
#define __VA_ARG_NSUFFIX_N_(prefix,nargs)       \
  prefix ## nargs

...

UINT64 efi_call5(void *func, UINT64 arg1, UINT64 arg2, UINT64 arg3, UINT64 arg4, UINT64 arg5);

...

#define _cast64_efi_call5(f,a1,a2,a3,a4,a5) efi_call5(f, (UINT64)(a1), (UINT64)(a2), (UINT64)(a3), (UINT64)(a4), (UINT64)(a5))

...

/* main wrapper (va_num ignored) */
#define uefi_call_wrapper(func,va_num,...)                       \
  __VA_ARG_NSUFFIX__(_cast64_efi_call, __VA_ARGS__) (func , ##__VA_ARGS__)

#endif

Primeiro é verificado se a macro HAVE_USE_MS_ABI está definida. Se estiver, então vai ser usado a MS-ABI e será feita uma chamada de função normalmente. Caso a HAVE_USE_MS_ABI não seja definida, quer dizer que estamos usando SysV ABI, então precisamos fazer uma conversão para MS-ABI quando formos chamar os serviços UEFI.

Como mostrado no código acima, a uefi_call_wrapper() seria assim se for usado SysV ABI:

#define uefi_call_wrapper(func,va_num,...)                       \
  __VA_ARG_NSUFFIX__(_cast64_efi_call, __VA_ARGS__) (func , ##__VA_ARGS__)

O parâmetro func é a função que será chamada. va_num é o total de argumentos que ela recebe. E, ..., claro, quer dizer que a macro uefi_call_wrapper() recebe um total de argumentos variáveis. Por exemplo, vamos supor que uma função chamada do_something() seja um tipo de serviço disponibilizado pelo UEFI, iriamos chamar ela assim:

void do_something(int a, int b, int c, int d, int e);
...
uefi_call_wrapper(do_something, 5, 1, 2, 3, 4, 5);

Não irei explicar como todo esse esquema funciona. Apenas saiba que, no final, a macro acima será expandida para:

efi_call5(do_something, 1, 2, 3, 4, 5);

O sufixo 5 no final quer dizer que essa função vai fazer a conversão de apenas 5 argumentos. Se o do_something() recebesse 6 argumentos, então seria efi_call6().

Procure em lib/x86_64/efi_stub.S e você vai ver que a efi_call5(), realmente, existe:

ENTRY(efi_call5)
  subq $40, %rsp
  mov %r9, 32(%rsp)
  mov %r8, %r9
  mov %rcx, %r8
  /* mov %rdx, %rdx */
  mov %rsi, %rcx
  call *%rdi
  addq $40, %rsp
  ret

Na SysV ABI, o primeiro argumento é colocado em RDI. Lembre-se que o primeiro argumento é o ponteiro da função que queremos chamar com MS-ABI, por isso que é feito um call *%rdi. O segundo argumento é recebido em RSI. Veja que é feito um mov %rsi, %rcx, pois precisamos mover o valor de RSI, que é o primeiro argumento da função que tem o ponteiro em RDI, para RCX, que recebe o primeiro argumento de uma função na MS-ABI.

Perceba também que a SysV ABI recebe o quinto argumento em R9, mas a MS-ABI precisa armazenar ele na pilha. Por isso que é alocado 40 bytes usando o subq $40, %rsp e colocado R9 na pilha com o mov %r9, 32(%rsp).

6 - System table, Boot services e Runtime services

Quando o firmware inicia um executável EFI, ele passa o ponteiro de uma estrutura chamada system table para a main(). É a partir dela que teremos acesso a todos os outros recursos do UEFI. A estrutura é a seguinte:

typedef struct
{
  EFI_TABLE_HEADER                Hdr;
  CHAR16                          *FirmwareVendor;
  UINT32                          FirmwareRevision;
  EFI_HANDLE                      ConsoleHandle;
  EFI_SIMPLE_TEXT_INPUT_PROTOCOL  *ConIn;
  EFI_HANDLE                      ConsoleOutHandle;
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
  EFI_HANDLE                      StandardErrorHandle;
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
  EFI_RUNTIME_SERVICES            *RuntimeServices;
  EFI_BOOT_SERVICES               *BootServices;
  UINTN                           NumberOfTableEntries;
  EFI_CONFIGURATION_TABLE         *ConfigurationTable;
} EFI_SYSTEM_TABLE;

Um dos elementos importantes é o RuntimeServices e, principalmente, o BootServices. Eles são ponteiros para estruturas que contém serviços que vamos usar.

6.1 - Boot Services

A estrutura apontada pelo BootServices vai dar alguns serviços intessantes. A estrutura é essa:

typedef struct _EFI_BOOT_SERVICES {

  EFI_TABLE_HEADER                Hdr;

  //
  // Task priority functions
  //

  EFI_RAISE_TPL                   RaiseTPL;
  EFI_RESTORE_TPL                 RestoreTPL;

  //
  // Memory functions
  //

  EFI_ALLOCATE_PAGES              AllocatePages;
  EFI_FREE_PAGES                  FreePages;
  EFI_GET_MEMORY_MAP              GetMemoryMap;
  EFI_ALLOCATE_POOL               AllocatePool;
  EFI_FREE_POOL                   FreePool;

  //
  // Event & timer functions
  //

  EFI_CREATE_EVENT                CreateEvent;
  EFI_SET_TIMER                   SetTimer;
  EFI_WAIT_FOR_EVENT              WaitForEvent;
  EFI_SIGNAL_EVENT                SignalEvent;
  EFI_CLOSE_EVENT                 CloseEvent;
  EFI_CHECK_EVENT                 CheckEvent;

  //
  // Protocol handler functions
  //

  EFI_INSTALL_PROTOCOL_INTERFACE  InstallProtocolInterface;
  EFI_REINSTALL_PROTOCOL_INTERFACE ReinstallProtocolInterface;
  EFI_UNINSTALL_PROTOCOL_INTERFACE UninstallProtocolInterface;
  EFI_HANDLE_PROTOCOL             HandleProtocol;
  EFI_HANDLE_PROTOCOL             PCHandleProtocol;
  EFI_REGISTER_PROTOCOL_NOTIFY    RegisterProtocolNotify;
  EFI_LOCATE_HANDLE               LocateHandle;
  EFI_LOCATE_DEVICE_PATH          LocateDevicePath;
  EFI_INSTALL_CONFIGURATION_TABLE InstallConfigurationTable;

  //
  // Image functions
  //

  EFI_IMAGE_LOAD                  LoadImage;
  EFI_IMAGE_START                 StartImage;
  EFI_EXIT                        Exit;
  EFI_IMAGE_UNLOAD                UnloadImage;
  EFI_EXIT_BOOT_SERVICES          ExitBootServices;

  //
  // Misc functions
  //

  EFI_GET_NEXT_MONOTONIC_COUNT    GetNextMonotonicCount;
  EFI_STALL                       Stall;
  EFI_SET_WATCHDOG_TIMER          SetWatchdogTimer;

  //
  // DriverSupport Services
  //

  EFI_CONNECT_CONTROLLER          ConnectController;
  EFI_DISCONNECT_CONTROLLER       DisconnectController;

  //
  // Open and Close Protocol Services
  //
  EFI_OPEN_PROTOCOL               OpenProtocol;
  EFI_CLOSE_PROTOCOL              CloseProtocol;
  EFI_OPEN_PROTOCOL_INFORMATION   OpenProtocolInformation;

  //
  // Library Services
  //
  EFI_PROTOCOLS_PER_HANDLE        ProtocolsPerHandle;
  EFI_LOCATE_HANDLE_BUFFER        LocateHandleBuffer;
  EFI_LOCATE_PROTOCOL             LocateProtocol;
  EFI_INSTALL_MULTIPLE_PROTOCOL_INTERFACES InstallMultipleProtocolInterfaces;
  EFI_UNINSTALL_MULTIPLE_PROTOCOL_INTERFACES UninstallMultipleProtocolInterfaces;

  //
  // 32-bit CRC Services
  //
  EFI_CALCULATE_CRC32             CalculateCrc32;

  //
  // Misc Services
  //
  EFI_COPY_MEM                    CopyMem;
  EFI_SET_MEM                     SetMem;
  EFI_CREATE_EVENT_EX             CreateEventEx;
} EFI_BOOT_SERVICES;

Grande, não é? Você pode encontrar essa estrutura no header inc/efiap.

Essa estrutura contém serviços (rotinas) de boot services. Essas rotinas são chamadas de boot services, porque, em algum momento, durante a execução do seu bootloader (geralmente, no final, quando o controle está sendo passado para o kernel), você vai ter que chamar o ExitBootServices(), que também é um boot service. Como o próprio nome diz, essa função é responsável por encerrar todos os boot services, ou seja, você não poderá mais usá-los.

6.2 Runtime Services

O RuntimeServices também é importante. A estrutura apontada é a seguinte:

typedef struct  {
  EFI_TABLE_HEADER                Hdr;

  //
  // Time services
  //

  EFI_GET_TIME                    GetTime;
  EFI_SET_TIME                    SetTime;
  EFI_GET_WAKEUP_TIME             GetWakeupTime;
  EFI_SET_WAKEUP_TIME             SetWakeupTime;

  //
  // Virtual memory services
  //

  EFI_SET_VIRTUAL_ADDRESS_MAP     SetVirtualAddressMap;
  EFI_CONVERT_POINTER             ConvertPointer;

  //
  // Variable serviers
  //

  EFI_GET_VARIABLE                GetVariable;
  EFI_GET_NEXT_VARIABLE_NAME      GetNextVariableName;
  EFI_SET_VARIABLE                SetVariable;

  //
  // Misc
  //

  EFI_GET_NEXT_HIGH_MONO_COUNT    GetNextHighMonotonicCount;
  EFI_RESET_SYSTEM                ResetSystem;

  EFI_UPDATE_CAPSULE              UpdateCapsule;
  EFI_QUERY_CAPSULE_CAPABILITIES  QueryCapsuleCapabilities;
  EFI_QUERY_VARIABLE_INFO         QueryVariableInfo;
} EFI_RUNTIME_SERVICES;

Essa estrutura contém o que é chamado de runtime services. Esses serviços, diferente dos boot services, podem ser usados mesmo depois de ser chamado o ExitBootServices().

Os runtime services são poucos se comparados com os boot services. Porém, tem coisas interessantes aí como o SetVariable() e o GetVariable.

7 - Protocolo

O protocolo é um conjunto de rotinas e dados que são usadas para lidar com algo que o UEFI tenta abstrair. Por exemplo, há protocolo para disco, sistema de arquivos, etc. Um protocolo simples é o EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL. Ele é usado para tratar a saída de texto na tela. A especificação descreve protocolos através de uma estrutura:

typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
  EFI_TEXT_RESET Reset;
  EFI_TEXT_STRING OutputString;
  EFI_TEXT_TEST_STRING TestString;
  EFI_TEXT_QUERY_MODE QueryMode;
  EFI_TEXT_SET_MODE SetMode;
  EFI_TEXT_SET_ATTRIBUTE SetAttribute;
  EFI_TEXT_CLEAR_SCREEN ClearScreen;
  EFI_TEXT_SET_CURSOR_POSITION SetCursorPosition;
  EFI_TEXT_ENABLE_CURSOR EnableCursor;
  SIMPLE_TEXT_OUTPUT_MODE *Mode;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

É responsabilidade do firmware fornecer o endereço de uma interface de protocolo caso exista e for requisitada.

8 - GUID

Existe também o conceito de GUID (Globally Unique Identifier). Pense nele como se fosse um modo de identificar um protocolo. A especificação diz que ele precisa ter um valor de 128 bits. Cada protocolo vai ter uma GUID único, ou seja, nunca vai exister dois ou mais protocolos com o mesmo GUID.

O GUID do EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL, por exemplo, é o seguinte:

#define EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL_GUID \
  {0x387477c2,0x69c7,0x11d2,\
  {0x8e,0x39,0x00,0xa0,0xc9,0x69,0x72,0x3b}}

A única diferença entre o nome do protocolo e do GUID é o sufixo _GUID.

O boot service LocateProtocol() é um função que podemos usar para procurar por um protocolo através de um GUID. O protópito dela é assim:

typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_PROTOCOL) (
  IN EFI_GUID *Protocol,
  IN VOID     *Registration OPTIONAL,
  OUT VOID    **Interface
);

Veja que ela recebe um GUID como primeiro argumento. O segundo argumento é opcional e não falaremos dele aqui. Por fim, ela recebe um ponteiro de ponteiro que vai ser usada para passar a interface de protocol. Podemos fazer assim para procurar, por exemplo, pelo EFI_GRAPHICS_OUTPUT_PROTOCOL:

...
  EFI_STATUS status;
  EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
  EFI_GRAPHICS_OUTPUT_PROTOCOL *gop_interface;
  status = uefi_call_wrapper(BS->LocateProtocol, 3, &gop_guid, NULL, &gop_interface);
  if(EFI_ERROR(status))
  {
    // Tratar Erro!
  }
...

O gop_guid vai receber o GUID do protocolo. Depois precisamos passar o ponteiro desse objeto para a LocateProtocol(). Vamos deixar como NULL o segundo argumento. E, a interface será passada através do gop_interface. Lembre-se de tratar o erro caso não seja achado uma interface. No caso da LocateProtocol(), ela retorna um EFI_SUCCESS. Poderiamos ter feito assim:

  if(status != EFI_SUCCESS)
  {
    //  Tratar Erro!
  }

Entretanto, o GNU-EFI disponibiliza a macro EFI_ERROR que já abstrai isso.

9 - Handle

Quanto a handle, ele é um conjunto de um ou mais protocolos. A especificação diz que deve existir um tipo void * chamado EFI_HANDLE para representar o handle. Esse tipo é usado por rotinas como HandleProtocol(), que recebe um handle e verifica se suporta um protocolo:

typedef
EFI_STATUS
(EFIAPI *EFI_HANDLE_PROTOCOL) (
  IN EFI_HANDLE Handle,
  IN EFI_GUID   *Protocol,
  OUT VOID      **Interface
);

Existe também o LocateHandleBuffer():

typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_HANDLE_BUFFER) (
  IN EFI_LOCATE_SEARCH_TYPE SearchType,
  IN EFI_GUID               *Protocol OPTIONAL,
  IN VOID                   *SearchKey OPTIONAL,
  IN OUT UINTN              *NoHandles,
  OUT EFI_HANDLE            **Buffer
);

Com essa função podemas obter um buffer de handles que suporta o protocolo especificado no Protocol.

Um exemplo com as duas funções mostradas acima:

...
  EFI_GUID text_guid = EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL_GUID;
  UINTN nhandles = 0;
  EFI_HANDLE *buffer;

  // Primeiro procuramos por todos os handles.
  status = uefi_call_wrapper(BS->LocateHandleBuffer, 5, ByProtocol, &text_guid, NULL, &nhandles, &buffer);
  if(EFI_ERROR(status))
  {
    // Tratar Erro.
  }

  // Podemos achar a interface do protocolo de texto instalado em cada handle.
  EFI_SIMPLE_TEXT_OUT_PROTOCOL *text;
  for(i = 0; i < nhandles; i++)
  {
    status = uefi_call_wrapper(BS->HandleProtocol, 3, buffer[i], &text_guid, &text);
    if(EFI_ERROR(status))
    {
      // Tratar Erro.
    }
    // Você que decide o que fazer com essa interface.
  }

  status = uefi_call_wrapper(BS->FreePool, 1, buffer);
...

Há várias outras funções para mexer com handle. Vai depender do que você quer fazer. Vamos falar delas nas próximas partes.

Referências

Unified Extensible Firmware Interface (UEFI) Specification

GNU-EFI Source Code