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.