James Harris
2023-11-24 17:59:47 UTC
Here are some notes on getting UEFI to boot one's own code.
To be clear about where the code below is intended to fit into an OS
boot process the idea is to boot the same OS by various means such as,
in simple terms,
BIOS --> my BIOS bootloader \
PXE --> my PXE bootloader } my OS
UEFI --> my UEFI bootloader /
and this thread is intended to be about the UEFI bootloader part.
Furthermore, while I can add some comments later about a development
environment and accessing a boot filesystem but this post is just about
getting started with a UEFI Hello World. There's already plenty in here
to make the post over-long as it is.
There are specs for UEFI (and ACPI) at
https://uefi.org/specifications
I'll refer to sections of what is currently the latest spec, i.e. 2.10.
UEFI documents are not easy reading. The basic spec, alone, is something
like 2,000 pages and obscure.
To make things worse, I've seen tutorials which add further libraries.
Such libraries are intended 'to make things easier' but they give a
programmer more to learn and make it hard to tell what is UEFI and what
is an added library. I wanted to find out about the fundamentals of UEFI
itself so what I'll describe is a relatively low-level approach.
Perhaps the best place to start is the video at
and other videos by the same (very patient!) content creator.
At least in my test environment I found that even persuading UEFI to run
code was a major undertaking. For too long the boot process led to
nothing happening at all. No output, no error message, no response of
any kind. For a long while I couldn't tell whether my code had even been
booted. So getting to the point where one can output text is crucial:
once one can write text to the display then one has a chance of
developing and debugging.
With that said here's UEFI code (in Nasm assembly syntax) to write a
Hello message to the screen. I'll walk through it, below.
efi_main is:
;Called with x64 ms abi
; RCX = image
; RDX = systab
global efi_main
efi_main:
push rbp
mov rbp, rsp
sub rsp, 32
mov r10, rdi ;image
mov r11, rsi ;systab
lea rdx, [rel msg_hello]
mov rcx, [r11 + efi_systab.conout]
mov rax, [rcx + efi_stop.OutputString]
call rax
add rsp, 32
pop rbp
ret
section .data
msg_hello: dw __?utf16?__("Hello 64 UEFI..."), 13, 10, 0
The first thing to say is that the design of UEFI appears to be heavily
influenced by Microsoft. As such, the string to be printed needs CR as
well as LF to go to the start of a new line. Also, the file which we
want UEFI to boot needs to be in PE format. There's no option of flat
binary or ELF etc.
However, once one's own bootloader is running it can load the OS from
whatever type of file one wants. In other words, the OS loader has to be
in PE format but what it loads can be of some other format as required.
When UEFI calls our code and when we call UEFI routines we naturally
have to stick to the expected calling conventions. They have to match
the CPU and mode. For 64-bit x86 booting see section 2.3.4. which is
about x64 Platforms.
The spec says that a caller must always call with the stack 16-byte
aligned. One thing I haven't found out is whether that means the stack
should be 16-byte aligned before or after the call instruction.
(Naturally, the call instruction will add 8 bytes to the stack.) The
above code assumes 'before'. So pushing RBP will realign the stack. But
that needs to be revisited.
One thing UEFI gives the booted code is a pointer to the System Table.
The table is documented in section 4. It starts with the following
fields. (I included only as far as I needed.)
struc efi_systab ;UEFI system table
.hdr: resb efi_hdr_size
.fvend: resq 1 ;FirmwareVendor
.frev: resd 1 ;FirmwareRevision
resd 1 ;padding
.coninh: resq 1 ;ConsoleInHandle
.conin: resq 1 ;ConIn
.conouth: resq 1 ;ConsoleOutHandle
.conout: resq 1 ;ConOut
endstruc
The header is
struc efi_hdr ;Generic header
.Signature: resq 1
.Revision: resd 1
.HeaderSize: resd 1
.CRC32: resd 1
resd 1 ;reserved
endstruc
A note for anyone not familiar with C: where the spec in section 4.3.1 says
CHAR16 *FirmwareVendor;
UINT32 FirmwareRevision;
whereas FirmwareRevision in the second line is uint32, as you might
expect, the asterisk on the first line indicates that FirmwareVendor is
a /pointer/ to the stated type, char16, and so in this case the field is
64-bit.
Going back to the code, the System Table's conout field is a pointer to
a 'simple text output' (STO) table for the console, i.e. the screen at
boot time. An STO table includes the following fields.
struc efi_stop ;UEFI simple text output protocol
.Reset: resq 1
.OutputString: resq 1
.TestString: resq 1
.QueryMode: resq 1
.SetMode: resq 1
.SetAttribute: resq 1
.ClearScreen: resq 1
.SetCursorPosition: resq 1
.EnableCursor: resq 1
.Mode: resq 1
endstruc
In that, the OutputString field holds the address of the UEFI function
which can be called to write a string of 16-bit chars to the screen. It
is passed
ECX = the STO table for the console (i.e. conout)
EDX = the string to print
Lo and behold, text appears! At least it does if one has everything
right. I can say more about that if anyone wants but for now I'll stop
here and not make this post any longer.
Feel free to add comments, questions and corrections.
To be clear about where the code below is intended to fit into an OS
boot process the idea is to boot the same OS by various means such as,
in simple terms,
BIOS --> my BIOS bootloader \
PXE --> my PXE bootloader } my OS
UEFI --> my UEFI bootloader /
and this thread is intended to be about the UEFI bootloader part.
Furthermore, while I can add some comments later about a development
environment and accessing a boot filesystem but this post is just about
getting started with a UEFI Hello World. There's already plenty in here
to make the post over-long as it is.
There are specs for UEFI (and ACPI) at
https://uefi.org/specifications
I'll refer to sections of what is currently the latest spec, i.e. 2.10.
UEFI documents are not easy reading. The basic spec, alone, is something
like 2,000 pages and obscure.
To make things worse, I've seen tutorials which add further libraries.
Such libraries are intended 'to make things easier' but they give a
programmer more to learn and make it hard to tell what is UEFI and what
is an added library. I wanted to find out about the fundamentals of UEFI
itself so what I'll describe is a relatively low-level approach.
Perhaps the best place to start is the video at
and other videos by the same (very patient!) content creator.
At least in my test environment I found that even persuading UEFI to run
code was a major undertaking. For too long the boot process led to
nothing happening at all. No output, no error message, no response of
any kind. For a long while I couldn't tell whether my code had even been
booted. So getting to the point where one can output text is crucial:
once one can write text to the display then one has a chance of
developing and debugging.
With that said here's UEFI code (in Nasm assembly syntax) to write a
Hello message to the screen. I'll walk through it, below.
efi_main is:
;Called with x64 ms abi
; RCX = image
; RDX = systab
global efi_main
efi_main:
push rbp
mov rbp, rsp
sub rsp, 32
mov r10, rdi ;image
mov r11, rsi ;systab
lea rdx, [rel msg_hello]
mov rcx, [r11 + efi_systab.conout]
mov rax, [rcx + efi_stop.OutputString]
call rax
add rsp, 32
pop rbp
ret
section .data
msg_hello: dw __?utf16?__("Hello 64 UEFI..."), 13, 10, 0
The first thing to say is that the design of UEFI appears to be heavily
influenced by Microsoft. As such, the string to be printed needs CR as
well as LF to go to the start of a new line. Also, the file which we
want UEFI to boot needs to be in PE format. There's no option of flat
binary or ELF etc.
However, once one's own bootloader is running it can load the OS from
whatever type of file one wants. In other words, the OS loader has to be
in PE format but what it loads can be of some other format as required.
When UEFI calls our code and when we call UEFI routines we naturally
have to stick to the expected calling conventions. They have to match
the CPU and mode. For 64-bit x86 booting see section 2.3.4. which is
about x64 Platforms.
The spec says that a caller must always call with the stack 16-byte
aligned. One thing I haven't found out is whether that means the stack
should be 16-byte aligned before or after the call instruction.
(Naturally, the call instruction will add 8 bytes to the stack.) The
above code assumes 'before'. So pushing RBP will realign the stack. But
that needs to be revisited.
One thing UEFI gives the booted code is a pointer to the System Table.
The table is documented in section 4. It starts with the following
fields. (I included only as far as I needed.)
struc efi_systab ;UEFI system table
.hdr: resb efi_hdr_size
.fvend: resq 1 ;FirmwareVendor
.frev: resd 1 ;FirmwareRevision
resd 1 ;padding
.coninh: resq 1 ;ConsoleInHandle
.conin: resq 1 ;ConIn
.conouth: resq 1 ;ConsoleOutHandle
.conout: resq 1 ;ConOut
endstruc
The header is
struc efi_hdr ;Generic header
.Signature: resq 1
.Revision: resd 1
.HeaderSize: resd 1
.CRC32: resd 1
resd 1 ;reserved
endstruc
A note for anyone not familiar with C: where the spec in section 4.3.1 says
CHAR16 *FirmwareVendor;
UINT32 FirmwareRevision;
whereas FirmwareRevision in the second line is uint32, as you might
expect, the asterisk on the first line indicates that FirmwareVendor is
a /pointer/ to the stated type, char16, and so in this case the field is
64-bit.
Going back to the code, the System Table's conout field is a pointer to
a 'simple text output' (STO) table for the console, i.e. the screen at
boot time. An STO table includes the following fields.
struc efi_stop ;UEFI simple text output protocol
.Reset: resq 1
.OutputString: resq 1
.TestString: resq 1
.QueryMode: resq 1
.SetMode: resq 1
.SetAttribute: resq 1
.ClearScreen: resq 1
.SetCursorPosition: resq 1
.EnableCursor: resq 1
.Mode: resq 1
endstruc
In that, the OutputString field holds the address of the UEFI function
which can be called to write a string of 16-bit chars to the screen. It
is passed
ECX = the STO table for the console (i.e. conout)
EDX = the string to print
Lo and behold, text appears! At least it does if one has everything
right. I can say more about that if anyone wants but for now I'll stop
here and not make this post any longer.
Feel free to add comments, questions and corrections.
--
James Harris
James Harris