Demilade Sonuga's blog

All posts

Printing Hello World

2022-10-29

In the previous post, we created a working efi app that does nothing. Now we're going to print "Hello World!". At this point, your main.rs file should be looking like this:

#![no_std]
#![no_main]
#![feature(abi_efiapi)]

#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut u8) -> usize {
    0
}

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

In a regular Rust program, all it takes to print hello world is println!("Hello World"), but in this bare metal environment, we don't have a println! macro, because there is no OS to give us any standard output to print to. Instead, what we have are UEFI protocols and services. These protocols are just groups of functions that provide some nice functionality that we can use while building our app. The services, too, are just groups of functions, but these ones are meant for system management, like memory allocation, handling events, exiting....

The protocols and services are primarily accessed through a structure called the System Table. A pointer to this structure is the second argument in our efi_main function. The System Table contains a pointer to a protocol called the Simple Text Output Protocol, a protocol which contains functions for printing to the screen. This is exactly what we're looking for.

To access this protocol, we're going to need to model the System Table and the Simple Text Output Protocol in our code. For now, all we need to know is that the Simple Text Output Protocol pointer is at an offset of 60 bytes from the System Table's starting address and the output_string function in the Simple Text Output Protocol is at an offset of 8 bytes from the protocol's start address.

Modify your main.rs as follows:

#![no_std]
#![no_main]
#![feature(abi_efiapi)]

#[no_mangle]
// DELETED: extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut u8) -> usize {
// NEW:
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
    0
}

// NEW:
#[repr(C)]
struct SystemTable {
    unneeded: [u8; 60],
    simple_text_output: *mut SimpleTextOutput
}

// NEW:
#[repr(C)]
struct SimpleTextOutput {
    unneeded: [u8; 8],
    output_string: extern "efiapi" fn (this: *mut SimpleTextOutput, *mut u16)
}

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

output_string is a pointer to a function. This function takes a pointer to the SimpleTextOutput instance itself and a pointer to a null-terminated UTF-16 encoded string. To understand the purpose of the #[repr(C)] attribute, we first need to understand the idea of data layout.

Data Layout

Interpretation

What is a struct exactly? A struct is a blueprint which we (and the compiler) use to give meaning to blobs of bytes (A byte is a chunk of eight bits). To fully understand this, consider the following:

These are some bytes in memory:

  1         2
||00000001||00000001||

where the numbers at the top are the memory addresses of the bytes.

These bits in memory are nothing but, ..., bits.

But in a program, the type that we associate with these blobs of bits completely changes their meaning.

For example:

Interpreting the blobs as u8s

unsafe fn func() {
    let ptr = 1 as *mut u8;
    let x = *ptr;
}

In the above code, the value that x will contain is the number 1, because the bits at address 1 gives the decimal value 1.

In our code, by casting the number 1 to a pointer to u8, we have effectively told the compiler "Interpret the values in memory, starting at address 1 as unsigned 8 bit integers".

Interpreting the blobs as u16s

unsafe fn func() {
    let ptr = 1 as *mut u16;
    let x = *ptr;
}

In the above code, the value that x will contain is the number 257, because the bits at addresses 1, 2, taken together gives the decimal value 257.

Notice that the values in memory have not changed. The bits that were in memory when we were interpreting them as u8s are the same bits there when we're interpreting them as u16s, but the values are different. In our code, by casting the number 1 as to a pointer to u16, we have effectively told the compiler "Interpret the values in memory, starting at address 1 as unsigned 16 bit integers".

Interpreting the blobs with a struct

unsafe fn func() {
    let ptr = 1 as *mut StudentData;
    let x = *ptr;
}

#[derive(Clone, Copy)]
struct StudentData {
    pos_in_class: u8,
    overall_score: u8
}

With this struct, we're telling the compiler to interpret the values in memory at the address starting at 1 as unsigned 8 bit integers. But there is one big difference with the way we did it initially. With this struct, the u8s aren't just u8s. They are pos_in_class and overall_score. These are concepts with no meaning other than that given by us.

In other words, with this struct, we have effectively bestowed the values in addresses 1 and 2 with meaning. We have used this struct as a sort of blueprint to interpret those bits. Those bits can then be said to be an instance of this struct.

Alignment and Padding

It's widely accepted that processors have a preference for values that are aligned in memory, that is, the address of the value in memory and the value's size are expected to be multiples of the some number which is called the alignment. How this number is determined exactly, I really don't know, but it's safe to assume that the alignment of primitive types (u8, u32, u64, ...) is the size of the type, that is, u32s must be kept at addresses that are multiples of 4 bytes (the size of a u32), u16s must be kept at addresses that are multiples of 2 bytes (the size of a u16). But this isn't always the case. For example, u64s (8 byte numbers) on x86 usually have alignments of 4, meaning they can be stored at addresses which are multiples of 4. This is valid since 8 is a multiple of 4. The 8 byte number can be said to have an alignment of 4, since 4 is the number which the type's size and addresses are multiples of.

Consider the following:

struct SomeData {
    a: u8,
    b: u8,
    c: u32,
    d: u8
}

This struct has a size of seven bytes (1 + 1 + 4 + 1). If an instance of this struct was placed in memory the exact way it's described here, there will be alignment problems. We first need to remember that the processor doesn't know anything about SomeData. SomeData is a conceptual invention of ours that has meaning solely to us. The processor just sees u8s and a u32, not SomeData, so the individual fields of the struct still maintain their alignment requirements, that is the addresses of the u8s must be multiples of 1 and the address of the u32 must be a multiple of 4. To see the problem in action, consider this instance of SomeData in memory:

7 | 8 9 10 11 12 13
a | b c c  c  c  d

The struct's address is 7, which is a multiple of 7. So, the size of the struct and the address are multiples of 7, meaning the struct has an alignment of 7.

The alignment requirement dicatates that each value in the struct must also be aligned, that is, their addresses and their sizes must be multiples of the same number. The field a is 1 byte in size, so any address is valid for it, since all numbers are multiples of 1. The field b is also 1 byte, so any address is valid for it. The field c is 4 bytes, so it's address can only be a multiple of 4 (we're assuming that 4 is the alignment of u32 (it usually is)). But in this instance, it's at address 9, and 9 is not a multiple of 4. The field b is not aligned.

The solution to this problem is to insert some space between a and b, so the SomeData struct as seen by the compiler, rather than exactly what we defined, will look like this instead:

struct SomeData {
    a: u8,
    b: u8,
    _pad1: [u8; 2],
    c: u32,
    d: u8,
    _pad2: [u8; 3]
}

Some space is added between b and c. Some more space is added after d. These extra spaces are called padding and their purpose is to make the overall size of the struct a multiple of 4 bytes (the size of c). SomeData's alignment will now be c's alignment. A question that immediately arises here is why is the padding inserted both in the middle and at the end? Why don't we just stick all the extra padding at the end like this:

struct SomeData {
    a: u8,
    b: u8,
    c: u32,
    d: u8,
    _pad: [u8; 5]
}

since this still give a struct whose size is a multiple of 4.

The problem with that is, c will still not be aligned. Consider this instance:

4 | 5 | 6 7 8 9 | 10 11 12 13 14 15
a | b | c c c c | d  _  _  _  _  _

c's address is 6 which is not a multiple of 4. c is still not aligned. So, adding that padding after all the fields, although it made the struct size a multiple of 4, still wasn't sufficient to get c's address to be aligned. So, rather than sticking in all the padding at the end, we stick 2 of it after b, giving this layout:

4 | 5 | 6 7 | 8 9 10 11 | 12 13 14 15 | 16
a | b | _ _ | c c c  c  | d  _  _  _  |

This way, the c is aligned, and everyone's happy.

But there's one more thing that may not really be clear at the moment: the padding after d doesn't seem to be needed, I mean, if d's padding wasn't there, c will still be aligned, and so will all the other fields:

4 | 5 | 6 7 | 8 9 10 11 | 12 |
a | b | _ _ | c c c  c  | d  |

c's address is still a multiple of 4, so it's aligned. Why the padding after d.

To understand the importance of the padding after d, consider this:

fn func() {
    let data: [SomeData; 2];
}

How would data be laid out in memory. Arrays in Rust are packed together, so immediately after the first SomeData instance, the next SomeData instance comes next, or in other words, if the first SomeData instance ends at address n, the next instance will start at address n + 1.

So, with our representation of d with no padding afterwards, the struct instances will look like this in memory:

4 | 5 | 6 7 | 8 9 10 11 | 12 | 13 | 14 | 15 16 | 17 18 19 20 | 21 |
a | b | _ _ | c c c  c  | d  | a  | b  | _  _  | c  c  c  c  | d  |

where 4..=12 holds data[0] and 13..=21 holds data[1]. The first struct instance is well aligned but the second isn't. The second's address starts at 13, which is not a multiple of 4, so it's not aligned. data[1]'s c starts at address 17, which is definitely not a multiple of 4.

And now we see why need that padding after d:

4 | 5 | 6 7 | 8 9 10 11 | 12 | 13 14 15 | 16 | 17 | 18 19 | 20 21 22 23| 24 25 26 27 | 28
a | b | _ _ | c c c  c  | d  | _  _  _  | a  | b  | _  _  | c  c  c  c | d  _  _  _  |

With this representation, we can have an array of SomeData instances and they'll all still be aligned. data[1] now starts at address 16, a multiple of 4, aligned. data[1]'s c now starts at 20, a multiple of 4, aligned. If there was a data[2], it would start at 28, a multiple of 4, aligned.

So padding is added between fields to make the size of a struct instance a multiple of 4, and to make the u32's offset from the struct's starting address a multiple of 4. This way, the u32 is aligned, and all instances in arrays of SomeData are also aligned.

In general, a struct instance's alignment is the alignment is the biggest alignment of all its fields and the overall size of the struct is a multiple of that alignment.

If you take another look at SomeData

struct SomeData {
    a: u8,
    b: u8,
    c: u32,
    d: u8
}

you'll notice that by reordering the fields like so:

struct SomeData {
    c: u32,
    a: u8,
    b: u8,
    d: u8
}

the only padding we'll need is a single byte at the end:

struct SomeData {
    c: u32,
    a: u8,
    b: u8,
    d: u8,
    pad: u8
}

This way, struct instances will still have the required alignment of 4, all their fields will be aligned, and instances in an array will also be aligned. The previous approach of leaving the fields as they are and adding padding after b and d was just a waste of memory.

The approach to data layout described here is the way Rust lays out data in memory. It will reorder fields if it thinks it's the right (less wasteful) way to go.

#[repr(C)]

All structs have definite approaches to data layout associated with them. By default, the approach to data layout described above is the default approach used on structs defined in Rust. This approach will lead to reordering your fields if need be.

In our code, our SystemTable and SimpleTextOutput struct have definite requirements on the ordering of the fields. The UEFI spec dictates that the pointer to the Simple Text Output Protocol must be at an offset of 60 bytes from the System Table's starting address and that the output_string function must be at an offset of 8 bytes from the Simple Text Output Protocol's starting address.

If Rust deems it right, it will reorder the fields of our structs. This will lead to chaos. Imagine for a moment that Rust reorders our field simple_text_output to come before the 60 bytes before it. If this happens, then whenever we access that field in memory, we will be interpreting the SystemTable bits at offset 0 to be the pointer to the Simple Text Output Protocol, when, in fact, we don't even know what's there! The result of this will be headaches, loss of time and lots of pain.

The solution to this problem is simply to layout the struct in memory using an approach other than Rust's.

In Rust, we do this with the #[repr(repr_name)] attribute.

In the C language, field order is maintained, so we simply annotate our structs with the attribute #[repr(C)], and we can be sure that the order of the fields won't be changed. simple_text_output field is guaranteed to always be at an offset of 60 bytes from the beginning of the struct.

Problem solved, for now.

There are a few other issues with doing things this way, but we won't go into them now because we don't need to.

Hello World

At this point, all that is left is to print "Hello World!" with our minimal models of the System Table and the Simple Text Output Protocol.

Modify your efi_main function:

/* DELETED:
#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
    0
}
*/
// NEW:
#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
    // The array that will hold the UTF-16 characters.
    // The length of "Hello World!\n" is 13, but the array's length is 14 because it must
    // be null-terminated, that is, it must end with a 0.
    let mut string_u16 = [0u16; 14];
    // The string as a string slice
    let string = "Hello World!\n";
    // Converting the string slice to UTF-16 characters and placing the characters
    // in the array
    string.encode_utf16()
        .enumerate()
        .for_each(|(i, letter)| string_u16[i] = letter);
    // Getting the pointer to the Simple Text Output Protocol from the System Table
    let simple_text_output = unsafe { (*sys_table).simple_text_output };
    // Getting the output_string function from the Simple Text Output Protocol and
    // calling it with the required parameters to print "Hello World!\n"
    unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
    // Returning 0 because the function expects it
    0
}

The code above is quite straightforward if you're familiar with iterators. If you're not, let's just say an iterator is anything which returns a sequence of values, one at a time. The string_u16 is the array which will hold our UTF-16 encoded string. The string slice string has a function encode_utf16 which when called, returns an iterator over the string slice's characters in UTF-16 which is exactly what we want. The enumerate function is an iterator function which associates a number with every value returned by the iterator. This number always starts from 0 and increases by 1 so in this code sample, the numbers associated with each UTF-16 encoded character will correspond to the index of the string_u16 array position that the character ought to be in. The for_each function then executes a function for each value iterated over. The function we provided here builds up the string_u16 with all the UTF-16 encoded characters of string.

The last item in the string_u16 array remains 0, because the length of the array is 1 + the length of the string. This way, our string_u16 is null-terminated.

After building our UTF-16 encoded string, we retrieve the Simple Text Output Protocol from the System Table by dereferencing the sys_table pointer and accessing the simple_text_output field. We have to wrap this in an unsafe block because dereferencing pointers is an unsafe operation. The compiler has absolutely no way of determining whether or not the memory we're interpreting as SystemTable is actually supposed to be interpreted as such.

Finally, we retrieve the output_string function from the Simple Text Output Protocol, call it with the expected parameters and return 0.

Okay, we're done.

Build the project with cargo build and run it. If you don't know how to run it, read this.

After running, your screen should look like this:

Hello World

Now we've printed "Hello World!"

Take Away

  • UEFI firmware provides groups of functions called protocols and services, which can be accessed by the System Table
  • The screen can be printed to with the Simple Text Output Protocol.
  • A value's alignment is a number that both the value's size and the address the value is at in memory is must be multiples of.
  • Padding is placed in between struct fields in memory to satisfy alignment requirements.
  • Rust reorders struct fields, if need be, to reduce memory waste due to padding.
  • Since C's approach to laying out data in memory respects field ordering, #[repr(C)] can be used to define structs whose field order matters.

Your code should be looking like this now:

Directory view:

blasterball/
| .cargo/
| | config.toml
| src/
| | main.rs
| .gitignore
| Cargo.lock
| Cargo.toml

main.rs contents

#![no_std]
#![no_main]
#![feature(abi_efiapi)]

#[no_mangle]
extern "efiapi" fn efi_main(handle: *const core::ffi::c_void, sys_table: *mut SystemTable) -> usize {
    let mut string_u16 = [0u16; 14];
    let string = "Hello World!\n";
    string.encode_utf16()
        .enumerate()
        .for_each(|(i, letter)| string_u16[i] = letter);
    let simple_text_output = unsafe { (*sys_table).simple_text_output };
    unsafe { ((*simple_text_output).output_string)(simple_text_output, string_u16.as_mut_ptr()); }
    0
}

#[repr(C)]
struct SystemTable {
    unneeded: [u8; 60],
    simple_text_output: *mut SimpleTextOutput
}

#[repr(C)]
struct SimpleTextOutput {
    unneeded: [u8; 8],
    output_string: extern "efiapi" fn (this: *mut SimpleTextOutput, *mut u16)
}

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

In the Next Post

We'll be taking a look at a few things about computer graphics that we need to continue building this project.

References

  • https://doc.rust-lang.org/nomicon/data.html
  • https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html