Terra and Lua - UEFI
Posted on Wed 28 May 2025 in programming • 6 min read
Now in part 2 we will see how we can generate code and link it to GNU-EFI so it can run on the EFI environment, but first.
What is (U)EFI?
I asked ChatGPT... I'm kidding, I'm kidding. I'll copy more or less the wording on Wikipedia rather. UEFI (Unified Extensible Firmware Interface) is a specification for the firmware of computer systems that supports booting from a hardware-based EFI (Extensible Firmware Interface) environment, and it's used by both UEFI 1.0 & later versions as well with some older BIOSes like APM or MSC. So basically is the standard for modern BIOS. It started being used in 2000 on Itanium systems and was shipped with the brand new at the time Intel based Macintosh computers. All desktop class and server computers have been using EFI for decades now, even the ones that have an ARM or RISC-V CPUs. You should check the Wikipedia article, it's pretty good.
Unlike the BIOSes of the past developed in Assembly for the specific CPU architecture, the development in EFI is made using C using a specific SDK that Intel provides for free.
For Wintel reasons the EFI environment looks very Windows-like, it uses PE binaries and Windows calling conventions. Even the shell available on the Intel TianoCore EDK look more like MS-DOS than a Unix shell.
Good, but how do I get started with this mess?
We will create a Docker image with Terra, development tools to help us with FAT filesystems.
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND="noninteractive"
# Development tools and other tools we will need
RUN apt-get update -y && apt-get install -y tzdata && apt-get install -y build-essential curl mtools unzip gnu-efi ovmf libedit2 && apt-get clean
#RUN groupadd -g 1000 user && useradd -u 1000 -m -g user user
RUN mkdir /hello
RUN chown ubuntu:ubuntu /hello
# As user ubuntu download, check the sha256 hash of the file, then extracts it
USER ubuntu
WORKDIR /hello
RUN curl -s -L -O https://github.com/terralang/terra/releases/download/release-1.2.0/terra-Linux-x86_64-cc543db.tar.xz
RUN echo "32f6420330de4d7176396aa36929a76733fe5a1fbc5a0cf8b9a6d270f9630d8d terra-Linux-x86_64-cc543db.tar.xz" | sha256sum -c && tar xf terra-Linux-x86_64-cc543db.tar.xz
USER root
RUN mv terra-Linux-x86_64-cc543db /opt/terra-linux
# put the bin folder on the user PATH for the user
USER ubuntu
RUN rm -f terra-Linux-x86_64-cc543db.tar.xz
RUN echo "PATH=$PATH:/opt/terra-linux/bin" >> ~/.bashrc
ENTRYPOINT [ "/bin/bash" ]
Now we build it and launch it adding the current path in /hello
inside the container.
# build the Docker container and tag it as terra_end_lua
$ docker build -t terra_end_lua .
# run the container and place the current directory as the folder /hello inside the container
$ docker run -v `pwd`:/hello --rm -it terra_and_lua
Ok, but how do I compile Terra code to run without the systems's libC?
We will need three new things that weren't covered in the previous article, how do you add more paths to the include path, how to specify the target platform and some extra flags, you can see them highlighted in the code bellow:
terralib.includepath = terralib.includepath..";/usr/include/efi;/usr/include/efi/x86_64;/usr/include/x86_64-linux-gnu/"
local target = terralib.newtarget {
Triple = "x86_64-pc-none";
}
local C = terralib.includecstring([[
#include <stdbool.h>
#include <efi.h>
#include <efilib.h>
bool Cls()
{
EFI_STATUS status;
// We need to use uefi_call_wrapper to match the calling convention used in EFI
status = uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
// Set Text color to a light green
uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_TEXT_ATTR(EFI_LIGHTGREEN, EFI_BACKGROUND_BLACK));
if(EFI_ERROR(status)){
Print(u"ERROR: Cls status %d", status);
}
return EFI_ERROR(status);
}
]], {}, target)
-- EFI use 16bits strings by default so we can't use Print, we use AsciiPrint instead
-- this is just a convinience function to cast the int8 pointer to uint8
terra Print(str_p: &int8)
C.AsciiPrint([&uint8](str_p))
end
terra hello_efi(ImageHandler: C.EFI_HANDLE, SystemTable: &C.EFI_SYSTEM_TABLE)
C.InitializeLib(ImageHandler, SystemTable)
if C.Cls() then
Print("FAILED TO CLEAR SCREEN")
end
Print("Hello, World from Terra!\n")
Print("\n\n\n")
Print("Press any key to reboot\n")
C.WaitForSingleEvent(SystemTable.ConIn.WaitForKey, 0)
return C.EFI_SUCCESS;
end
-- we generate the object code and save it to hello_efi.o
terralib.saveobj('hello_efi.o', {
efi_main=hello_efi,
},{
"-fno-stack-protector",
"-fpic",
"-fshort-wchar",
"-mno-red-zone",
}, target)
The triplet we are specifying say we are targeting an x86_64 CPU without an OS. You will have to take my word about the flags in the bottom of the file, but one very important is -fshort-wchar
that tell the compiler... that our C strings are 16 bits wide (correct me if I'm wrong here). Check this article in OSDev if you want more information.
We added includes for the GNU-EFI library headers, and out entrypoint function hello_efi
is not expecting argc and argv as parameters now, but one struct and a pointer to another struct EFI initializes for us, ImageHandler (that has nothing to do with bitmaps and PNGs) and SystemTable. We the pass both to GNU-EFI's InitializeLib function.
Note that I added a C function called Cls that calls ClearScreen then uses SetAttribute to set the text color to light green. The Terra function Print() there is just a convenience to cast the correct pointer type, it then calls GNU-EFI's AsciiPrint() function.
I also created a Makefile to document and automate building our program. It basically compiles our Terra code, links with GNU-EFI and make a PE executable that EFI expects.
FLAGS = -fno-stack-protector -fpic -fshort-wchar -mno-red-zone -I /usr/include/efi -I /usr/include/efi/x86_64 -DEFI_FUNCTION_WRAPPER
.PHONY: all clean
all: hello.efi
hello.efi: hello_efi.so
objcopy -j .text \
-j .sdata \
-j .data \
-j .dynamic \
-j .dynsym \
-j .rel \
-j .rela \
-j .reloc --target=efi-app-x86_64 hello_efi.so hello.efi
hello_efi.so: hello_efi.o
ld hello_efi.o /usr/lib/crt0-efi-x86_64.o -nostdlib -znocombreloc -T /usr/lib/elf_x86_64_efi.lds -shared -Bsymbolic -L /usr/lib -l:libgnuefi.a -l:libefi.a -o hello_efi.so
hello_efi.o: hello_efi.t
terra hello_efi.t
clean:
rm -f hello_efi.efi hello_efi.so hello_efi.o
Again, details in https://wiki.osdev.org/GNU-EFI.
Now we need to build a FAT disk image with the .efi file we generated so we can boot it with QEmu. To make things simpler I added the package ovmf on the Docker container that provides a BIOS image with EFI support (it will be copied by the script bellow) so all you need to do is install the package for your host system that provides qemu-system-x86_64. Then inside the container you can run the following script:
#/usr/bin/env bash
set -e
if [ ! -f uefi.img ]; then
#dd if=/dev/zero of=uefi.img bs=512 count=$(((10 * 1024 * 2))) 2> /dev/null # 10MB
truncate --size 10MB uefi.img
#mformat -i uefi.img -f 1440 ::
mformat -i uefi.img
mmd -i uefi.img ::/EFI
mmd -i uefi.img ::/EFI/BOOT
fi
if [[ $(mdir -i uefi.img ::EFI/BOOT/BOOTx64.EFI 2> /dev/null) ]]; then
mdel -i uefi.img ::EFI/BOOT/BOOTx64.EFI
fi
mcopy -i uefi.img hello.efi ::/EFI/BOOT/BOOTx64.EFI
if [ ! -f OVMF.df ]; then
cp /usr/share/ovmf/OVMF.fd .
fi
echo "Run the following command outside of the docker container:"
echo "qemu-system-x86_64 -bios OVMF.fd -drive file=uefi.img,index=0,media=disk,format=raw,if=ide -enable-kvm -vga cirrus -cpu kvm64 -device virtio-mouse-pci -usb -device usb-mouse"
# If you want to use the serial port to debug
## socat -,raw,echo=0 tcp4:localhost:6666
#qemu-system-x86_64 -bios OVMF.fd -drive file=uefi.img,index=0,media=disk,format=raw,if=ide -enable-kvm -vga cirrus -cpu kvm64 -monitor stdio -serial tcp::6666,server -s
As instructed, copy the command line and run on your host system that line, you should see this:
If you want to play with the code you can keep calling $ make && ./pack.sh
inside the container when you want to try a change and in another terminal you call QEmu to test it.
If you have a computer with an unlocked EFI you can even try to run hello.efi on baremetal.
Conclusion
This article is inspired and based on some code I put together almost 10 years ago as a proof of concept using more C: https://github.com/xspager/hello_efi_from_terra. I could probably do things in better ways (let me know if have some ideas), and be better at showing the Terra language, but I was more interested sharing what I did. Also if you interested on UEFI check Queso Fuego's playlist on YouTube.
I might add a part 3 to show how you can you put some pixels in the screen. Feel free to leave a comment or message me on BlueSky. Bye for now!