This article was originally written back in 2014 to accompany the talk I gave at Shmoocon called "The History of Linux Kernel Module Signing". It is a discussion of various Linux kernel module signing implementations while highlighting some of the motivating factors behind various design decisions. I decided it was about time to publish this write up, so here it is.

Before I begin, here are a few notes:

  • I have not found kernel module signing implementations outside of those created by Red Hat and by the mainstream Linux developers so there may be other flavors of Linux module signing that I do not mention here.
  • The information I present here is based on Linux developer mailing lists and source code I have found. I do not have an insider perspective of the implementation decisions. If you want the ground truth, you should go talk to those developers who are a part of the narrative I present here.
  • Although I highlight potential implementation issues, I have not put in the time to build any proof-of-concept exploits (but perhaps you can build one and submit it to the International Journal of PoC || GTFO).

This article has been edited somewhat since it was originally written in my outdoor office. I blame any bugs I inserted on the mosquitoes that were attacking me at the time.

outdoor office

Introduction

Code signing is not just an algorithm, it is a lifestyle. By which I mean there is so much involved in a code signing implementation beyond cryptography and consequentially there are many ways to get it wrong. For example, how the code and signature are formatted, parsed, and interpreted is more important than it may first seem. As someone who has spent a lot of time working with ELF metadata I have decided to focus on two small components of the signing process:

  1. Where the signature is stored in terms of what metadata is consulted in order to locate the signature
  2. Which components/bytes in the file are signed and which are not

These may seem like trivial questions, but they are not. Any signature may be handled with multiple tools, each of which needs to parse the signature in order to examine it -- this in and of itself is not trivial. Yet all signature-handling tools must independently agree on the answers to these questions. Cryptographic signatures are useless when tools disagree on these simple questions, yet we can find multiple examples of such failures. Android Master Key-type bugs and X.509 parser differentials ("PKI Layer Cake") are famous examples of code signing failures due to parser differentials. Linux module signing is an interesting case study in that they were not only concerned with the signature scheme itself but also the supporting binary building infrastructure and we can see this reflected in their signature scheme design choices. Also by studying the evolution of the Linux signature systems we can see what the potential pitfalls of a code signing scheme are and design future signature-verifying loaders to avoid such pitfalls.

Background

Here I will briefly describe what code signing is and the ELF file format in which kernel modules are stored. If you are fed up with reading such introductory material, feel free to jump right into my discussion of Linux kernel module signing.

Code signing

The idea behind code signing is fairly simple: we can digitally sign a a file or chunk of code such that another party has high assurance of its origin and that it has not been altered. This is generally implemented as follows:

  1. Developer builds file/binary
  2. Developer hashes the file or parts of the file and digitally signs the hash using their private key, usually embedded the signature in a signed version of the file
  3. User retrieves the signed version of the file
  4. User hashes the same parts of the file that the developer hashed and checks that the hash they created matches the hash signed with the developer's public key

It is important that different tools operating on the same binary/signature agree on what is signed and where the signature is located. Failure to agree on exactly what is signed can not only lead to Master Key and Layer Cake bugs, but also enable cryptographic attacks. A classic attack that uses ambiguity in how a signature is represented in the PKCS #1 standard is Bleichenbacher's attack. Since then, many attacks of this kind have been discovered, for example the recent BERserk attack.

ELF

ELF, the Executable and Linkable Format, is a flexible file format used to store binaries such as kernel modules, libraries, and executables. ELF was first published in 1990 as part of UNIX System V Release 4 and documented in their ANSI C and Programming Support Tools guide. ELF files not only contain the binary's code and the static data the code operates on, but also metadata the runtime or static linker needs in order to load the file into memory and/or resolve any dependencies on other ELF files such as libraries. Due to the flexibility of ELF, we can easily find ELF files that contain metadata (including signatures) that are not defined in the ELF specification.

ELF layout

Every ELF file includes an ELF header which contains information such as the architecture the ELF's code was compiled for and how to locate the tables of program and/or section headers. Section headers denote named chunks of bytes in the file that are semantically the same, such as a section of code (e.g. ".text") or a section of strings (e.g. ".strtab""). ELF program headers denote groups of sections that should be loaded together at runtime, such as "all read only data including ELF headers" and "all executable code". The Linux kernel module loader, unlike most other ELF-loaders, only processes and loads ELFs based on their section headers.

Linux kernel module signing

Back in 2004 Greg Kroah-Harman noticed that unlike other kernels at the time Linux did not sign its kernel modules so he wrote a proof-of-concept module signing implementation which includes a tool that signs a module and kernel code the checks a module's signature when loaded (Kroah-Hartman, 2004). David Howells contributed to this implementation and continued on to maintain kernel module signing patches for Red Hat (corbet, 2004).

I will present 4 different kernel module signing implementations which I have labeled v0-v2 and the version included in Linux 3.7. These names are labels I created, you will see different labels used in the mailing list posts I reference in this article. Also please note that the code samples I have posted below have been altered, trimmed, and annotated (by me) to make them easier to read and understand.

The code examples I include in this post that locate the signature in an ELF will seem simple and straightforward since ELF metadata can be extracted via simple array traversals and string comparisons. However ELF is a complex format with many objects and metadata tables. The position of a particular object inside the ELF can often be computed in several ways (of which none is known as "canonical"); several distinct table entries must agree in order for all parsers to agree on the target object's location. In other words, ELF metadata can be inconsistent in many ways, and any particular tool or OS component may not be able to notice -- which can cause different tools to interpret the same bytes in such a file differently. The simple-looking parsers we will see below make a lot of assumptions about the file's consistency and merely sweep the mounds of complexity under the rug.

v0

v0 was proposed in 2004 by Greg Kroah-Hartman. Although it did not appear in any of the stable Fedora/RHEL releases, there is evidence that v0 existed in a Fedora rawhide/development kernel (corbet, 2004).

Where the signature is located and what metadata are trusted

In v0 the signature is stored in an ELF section named "module_sig". This means the signature verification code must process multiple layers of metadata to locate the sections themselves including the section that contains section name strings. The following is part of the code that locates the signature during signature verification:

1 sig_index = 0;
2 for (i = 1; i < hdr->e_shnum; i++)
3     if (strcmp(secstrings+sechdrs[i].sh_name, "module_sig") == 0) {
4         sig_index = i;
5         break;
6 }

This technique of storing and retrieving signatures is reminiscent of how Android apk packages are signed, which makes me curious if there is some "Android Master Key"-esque vulnerability lurking here (what if there are two sections named "module_sig"?) (Freeman, 2013).

What is signed

In order to verify a signature, the kernel module loader needs to recreate the hash that was signed. In v0 only the contents of sections whose names contain the string "text" or "data" (but not ".rel.") are hashed, as shown below.

 1 for (i = 1; i < hdr->e_shnum; i++) {
 2     name = secstrings+sechdrs[i].sh_name;
 3 
 4     /* We only care about sections with "text" or
 5        "data" in their names */
 6     if ((strstr(name, "text") == NULL) &&
 7         (strstr(name, "data") == NULL))
 8         continue;
 9     /* avoid the ".rel.*" sections too. */
10     if (strstr(name, ".rel.") != NULL)
11         continue;
12     /* add contents of section to signature */
13     ...
14 }

This means that only contents of sections claiming to contain module code and data are signed, metadata are not checked. I find this worrisome given how powerful ELF metadata are and how much they can influence a program's execution (missing reference). In fact, this snippet of code is what prompted me to spend more time researching Linux kernel module signing.

The fact that only module code and data are signed is not the only worrisome thing about vo. There are multiple ways of specifying a section's purpose in ELF such as via a human-friendly string or a machine-friendly integer indicating the section type. This code signing mechanism relies on the human-friendly metadata instead of the metadata normally used by ELF parsers (i.e. the section header's flags, "sh_flags"). There is nothing that forces the human-friendly metadata to mach the machine-friendly metadata and there are examples of such discrepancies causing security problems such as those documented in Harrison and Li's 2014 Shmoocon talk entitled "Arms Race: The Story of (In)-Secure Bootloaders".

v1

In 2007 David Howells posted a set of module signing patches to the kernel mailing list, I will refer to these patches as v1 (Howells, 2007). v1 appears in Fedora Core 3-8 and RHEL 4-5, you can find the source code in kernel/module*.c (after you apply the module signing kernel patches). If you grab the FC3 kernel source code you will find the kernel module signing implementation in a patch called linux-2.6.7-modsign-core.patch. A rpm containing the source code/patches can be found here.

Where the signature is located and what metadata are trusted

v1 performs a large set of ELF metadata sanity checks before validating the signature. The following code sample (which is a trimmed version of the original) will give you an idea of what these sanity checks look like:

  1 /*
  2  * verify the ELF structure of a module
  3  */
  4 static int module_verify_elf(struct module_verify_data *mvdata)
  5 {
  6     // data declarations
  7     ...
  8 
  9     size = mvdata->size;
 10     mvdata->nsects = hdr->e_shnum;
 11 
 12 #define elfcheck(X) do {if (unlikely(!(X))){line = __LINE__; goto error;}} while(0)
 13 
 14     /* validate the ELF header */
 15     elfcheck(hdr->e_ehsize < size);
 16     elfcheck(hdr->e_entry == 0);
 17     elfcheck(hdr->e_phoff == 0);
 18     elfcheck(hdr->e_phnum == 0);
 19 
 20     elfcheck(hdr->e_shnum < SHN_LORESERVE);
 21     elfcheck(hdr->e_shoff < size);
 22     elfcheck(hdr->e_shoff >= hdr->e_ehsize);
 23     elfcheck((hdr->e_shoff & (sizeof(long) - 1)) == 0);
 24     elfcheck(hdr->e_shstrndx > 0);
 25     elfcheck(hdr->e_shstrndx < hdr->e_shnum);
 26     elfcheck(hdr->e_shentsize == sizeof(Elf_Shdr));
 27 
 28     tmp = (size_t) hdr->e_shentsize * (size_t) hdr->e_shnum;
 29     elfcheck(tmp < size - hdr->e_shoff);
 30 
 31     ...
 32 
 33     /* validate the ELF section headers */
 34     mvdata->sections = mvdata->buffer + hdr->e_shoff;
 35     secstop = mvdata->sections + mvdata->nsects;
 36 
 37     sssize = mvdata->sections[hdr->e_shstrndx].sh_size;
 38     elfcheck(sssize > 0);
 39 
 40     section = mvdata->sections;
 41     elfcheck(section->sh_type == SHT_NULL);
 42     elfcheck(section->sh_size == 0);
 43     elfcheck(section->sh_offset == 0);
 44     secsize = mvdata->secsizes + 1;
 45     for (section++; section < secstop; secsize++, section++) {
 46         // check section header information and that certain sections exist
 47            ...
 48        /* perform section type specific checks */
 49        switch (section->sh_type) {
 50         case SHT_NOBITS:
 51             break;
 52         case SHT_REL:
 53             elfcheck(section->sh_entsize == sizeof(Elf_Rel));
 54             goto more_rel_checks;
 55         case SHT_RELA:
 56             elfcheck(section->sh_entsize == sizeof(Elf_Rela);
 57         more_rel_checks:
 58             elfcheck(section->sh_info > 0);
 59             elfcheck(section->sh_info < hdr->e_shnum);
 60             goto more_sec_checks;
 61         case SHT_SYMTAB:
 62             elfcheck(section->sh_entsize == sizeof(Elf_Sym));
 63             goto more_sec_checks;
 64         default:
 65         more_sec_checks:
 66             /* most types of section must be contained entirely
 67             * within the file */
 68             elfcheck(section->sh_size <= *secsize);
 69             break;
 70         }
 71     }
 72     /* validate the symbol table */
 73     ...
 74     
 75     /* validate each relocation table as best we can */
 76     for (section = mvdata->sections + 1; section < secstop; section++) {
 77         section2 = mvdata->sections + section->sh_info;
 78         switch (section->sh_type) {
 79         case SHT_REL:
 80             rels = mvdata->buffer + section->sh_offset;
 81             relstop = mvdata->buffer + section->sh_offset + section->sh_size;
 82             for (rel = rels; rel < relstop; rel++) {
 83                 elfcheck(rel->r_offset < section2->sh_size);
 84                 elfcheck(ELF_R_SYM(rel->r_info) > 0);
 85                 elfcheck(ELF_R_SYM(rel->r_info) < mvdata->nsyms);
 86             }
 87             break;
 88         case SHT_RELA:
 89             relas = mvdata->buffer + section->sh_offset;
 90             relastop = mvdata->buffer + section->sh_offset + section->sh_size;
 91             for (rela = relas; rela < relastop; rela++) {
 92                 elfcheck(rela->r_offset < section2->sh_size);
 93                 elfcheck(ELF_R_SYM(rela->r_info) < mvdata->nsyms);
 94             }
 95             break;
 96         default:
 97             break;
 98         }
 99     }
100 
101     _debug("ELF okay\n");
102     return 0;
103  error:
104     return -ELIBBAD;
105 
106 } 

It is nice to see them place less trust on the contents of the ELF metadata, however it is hard to determine whether their sanity checks are aggressive enough. They still need to interpret some metadata to locate the signature. In v1 the signature itself is stored in a ".module_sig" section just like in v0 and is located using a similar technique that v0 uses to locate its signature.

What is signed

In v1, more than just the code and data section contents are hashed, their corresponding section headers are hashed as well. Relocation section headers and entries get hashed too, along with any symbols they reference. The ELF header itself is not hashed which is interesting because it is often used to locate the table of section headers. We can see this below:

 1 /* load data from each relevant section into the digest */
 2 for (i = 1; i < mvdata->nsects; i++) {
 3     ...
 4     if (i == mvdata->sig_index) // Do not add signature section to digest
 5         continue;
 6     /* it would be nice to include relocation sections, but the act of adding a
 7       signature to the module seems changes their contents, because the symtab gets
 8       changed when sections are added or removed */
 9     if (sh_type == SHT_REL || sh_type == SHT_RELA) {
10         // add relocation section header information to hash
11         // for each relocation entry, add relocation information to hash
12      // if relocation entry uses a named symbol add symbol's name to hash
13  
14         ...
15       continue;
16     }
17     // hash allocatable loadable sections
18     if (sh_type != SHT_NOBITS && sh_flags & SHF_ALLOC)
19         goto include_section;
20     continue;
21 include_section:
22     // add section header and section data to signature
23     ...
24 }

Notice the comment on line 6 about not being able to include relocation sections in the signature. It is misleading because relocation entries are hashed. Although unlike other sections that are hashed as a single chunk of data, the loader iterates through each relocation entry in a relocation section, copies each entry's field into a separate (local) data structure (including any symbol information it references), and hashes the local data structure. The code that performs this is not shown here. My first guess was that this comment is a relic of a previous version of the module signing mechanism that I have not been able to locate. An alternative (and more likely) theory is that the comment is really about the symbol tables -- the act of adding the signature appears to alter the symbol tables. We can see this by inspecting the section header table of a kernel module built to be used with the v1 signature scheme. If you wish to play along at home, grab a copy of this kernel rpm and extract its contents.

$ readelf -S snd-mtpav.ko
There are 26 section headers, starting at offset 0x232c:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 0012f4 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 003308 0008c8 08     24   1  4
  [ 3] .altinstr_replace PROGBITS        00000000 001328 000006 00  AX  0   0  1
  [ 4] .init.text        PROGBITS        00000000 00132e 00010d 00  AX  0   0  1
  [ 5] .rel.init.text    REL             00000000 003bd0 0000e8 08     24   4  4
  [ 6] .exit.text        PROGBITS        00000000 00143b 00001f 00  AX  0   0  1
  [ 7] .rel.exit.text    REL             00000000 003cb8 000020 08     24   6  4
  [ 8] .modinfo          PROGBITS        00000000 001460 0001ea 00   A  0   0 32
  [ 9] __param           PROGBITS        00000000 00164c 000064 00   A  0   0  4
  [10] .rel__param       REL             00000000 003cd8 0000a0 08     24   9  4
  [11] .rodata.str1.1    PROGBITS        00000000 0016b0 0001cc 01 AMS  0   0  1
  [12] .altinstructions  PROGBITS        00000000 00187c 000017 00   A  0   0  4
  [13] .rel.altinstructi REL             00000000 003d78 000020 08     24  12  4
  [14] __versions        PROGBITS        00000000 0018a0 000680 00   A  0   0 32
  [15] .data             PROGBITS        00000000 001f20 00004c 00  WA  0   0  4
  [16] .rel.data         REL             00000000 003d98 000030 08     24  15  4
  [17] .gnu.linkonce.thi PROGBITS        00000000 001f80 000200 00  WA  0   0 128
  [18] .rel.gnu.linkonce REL             00000000 003dc8 000010 08     24  17  4
  [19] .bss              NOBITS          00000000 002180 000008 00  WA  0   0  4
  [20] .comment          PROGBITS        00000000 002180 000062 00      0   0  1
  [21] .module_sig       PROGBITS        00000000 0021e2 000041 00  WA  0   0  1
  [22] .gnu_debuglink    PROGBITS        00000000 002224 000018 00      0   0  4
  [23] .shstrtab         STRTAB          00000000 00223c 0000ee 00      0   0  1
  [24] .symtab           SYMTAB          00000000 00273c 000660 10     25  74  4
  [25] .strtab           STRTAB          00000000 002d9c 00056b 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

The above output of readelf shows us that the kernel module includes both string and symbol tables (sections 23-25) but neither are marked loadable even though it is clear from the code (not shown) that they are loaded into memory before the sections are hashed (which makes me question the meaning of these flags). It appears that when the signature section is added to the ELF, the string table is augmented to include the section's name (".module_sig") and a symbol referencing the signature section is added to the symbol table thus modifying those tables and making them harder to sign. I also speculate that this is why neither the section headers of the string and symbol tables nor their contents are fully hashed -- only strings and symbols that are referenced by relocation entries are added to the hash.

v2

Two similar kernel module signing patches were submitted to a Linux developer mailing list in 2011 within a week of each other (Howells, 2011; Howells, 2011). I will only be focusing on the second, slightly-younger patch because I did not see the older patch included in any Red Hat/Fedora releases. The slightly younger patch, which I will refer to as v2, is used in RHEL 6. The relevant sources are kernel/module*.c. An rpm containing the source code can be found here

Where the signature is located and what metadata are trusted

In v2 they wrapped the signature in a notes section (of type SHT_NOTE), which is located by the code signing mechanism using its human-friendly name ("module.sig"). To extract the signature, not only must it locate the correct section but it also should parse the note-section specific metadata that describes the signature. The note section metadata is defined as follows (in elf.h):

/* Note header in a PT_NOTE section */
typedef  struct elf32_note {
  Elf32_Word    n_namesz;       /* Name size */
  Elf32_Word     n_descsz;       /* Content size */
  Elf32_Word    n_type;            /* Content type */
} Elf32_Nhdr;

Notice that size field (n_descsz)? I wonder what happens when it disagrees with the length field in the note's section header. Perhaps nothing interesting, but I have not taken any time to examine this.

The following code snippet illustrates how the signature note section is located:

 1 int module_verify_signature(struct module_verify_data *mvdata, int *_gpgsig_ok)
 2 {
 3  ...
 4  _debug("looking for sig section '%s'\n", modsign_note_section);
 5 
 6  for (loop = 1; loop < mvdata->nsects; loop++) {
 7      switch (sechdrs[loop].sh_type) {
 8      case SHT_NOTE:
 9          if (strcmp(mvdata->secstrings + sechdrs[loop].sh_name,
10                 modsign_note_section) == 0)
11              mvdata->sig_index = loop;
12          break;
13      }
14  }
15  ...
16 }

We can see that it resembles how the signature is located in v0 and v1 except that it looks for a section of type SHT_NOTE named "module.sig".

What is signed

Slightly more metadata gets signed in v2 than in v1. For example, v1 does not sign section headers for sections that are not both allocatable and non-empty, this is not the case in v2. The following shows that empty and allocatable sections now have their section headers hashed.

1    /* include the headers of BSS sections */
2       if (sh_type == SHT_NOBITS && sh_flags & SHF_ALLOC) {
3           crypto_digest_update_data(mvdata, sh_name, strlen(sh_name));
4           crypto_digest_update_val(mvdata, sechdrs[sect].sh_type);
5           crypto_digest_update_val(mvdata, sechdrs[sect].sh_flags);
6           crypto_digest_update_val(mvdata, sechdrs[sect].sh_size);
7           crypto_digest_update_val(mvdata, sechdrs[sect].sh_addralign);
8           goto digested;
9       }

Linux 3.7 and beyond

Not long before the 2012 Kernel Summit, David Howells published a kernel module signing implementation that appended the signature to the end of the module file so that it could be located without parsing ELF metadata and also allowing for the entire module to be signed (Howells, 2012). This patch does not appear to be used in any of the major RHEL or Fedora releases so I will not discuss its implementation here. However not long after this patch was released we can find an email where David Howells discusses module signing design challenges. It turns out that Red Hat previously avoided signing whole modules because doing so would break their existing packaging and installation infrastructure (Howells, 2012).

At the 2012 Kernel Summit, developers announced that they would incorporate module signing into the mainstream kernel, an implementation that signs the entire module (Edge, 2012). Linux 3.7 was the first mainstream release that included kernel module signing support, which can be found in kernel/module_signing.c. This implementation was also incorporated into Fedora Core 18-20 releases (and perhaps beyond, I haven't checked the newer releases). You can find the source code here.

In this code snippet from the mainstream Linux implementation we can see that the signature is located at the end of the module and that everything but the signature itself is included in the signed hash.

 1 /* Verify the signature on a module. */
 2 int mod_verify_sig(const void *mod, unsigned long * modlen) {
 3     // data declarations and sanity checks
 4     ...
 5     // copy bytes at end of file into signature structure
 6     memcpy(&ms, mod + (modlen ­ sizeof(ms)), sizeof(ms));
 7     // do work to calculate length of module (modlen)
 8     ...
 9     sig = mod + modlen;
10     // add all module contents (but signature) to signature
11     pks = mod_make_digest(ms.hash, mod, modlen);
12     if (IS ERR(pks)) { ret = PTR_ERR(pks); goto error; }
13     ...
14     // Extract actual signature (in form of MPI array) from signature data
15     ret = mod_extract_mpi_array(pks, sig + ms.signer_len + ms.key_id_len, sig_len);
16     if (ret < 0) goto error;
17 
18     ret = verify_signature(key, pks);
19 error:
20     return ret;
21 }

Concluding remarks

killer cat

An ELF file does not just contain data and code, it also contains instructions on how the code and data should be loaded into memory, patched, and executed. Many implementations of Linux kernel module signing schemes signed only subsets of the ELF metadata and section contents. Those same implementations also needed to consult the module's ELF metadata in order to locate the signature itself. It takes a deep understanding of the kernel module loading process to understand how the unsigned metadata can affect how the module appears in memory so we must be careful about what metadata we trust outright. Problems may also arise when there are multiple parsers handling and interpreting the same data, each may have its own interpretation (i.e. a parser differential) causing vulnerabilities. The decision to hash the entire ELF and append the signature to the end of the file removes unnecessary complexity from the signing process and makes the signing process easier to understand and analyze, however this does not guarantee that the scheme is unbreakable. There are many other ways to break or weaken a signing scheme and the format it uses. Nevertheless, if we do not design our signature schemes to be trivial to parse and verify then we are likely setting ourselves up for failure.

Acknowledgments

I'd like to thank Sergey Bratus for the feedback and edits he provided for this blog post.

References

  1. Kroah-Hartman, G. (2004, January 1). Signed kernel modules. Linux Journal. http://www.linuxjournal.com/article/7130.
  2. corbet. (2004, July 7). Cryptographic signatures on kernel modules. https://lwn.net/Articles/92617.
  3. Freeman, J. (2013). Exploit (& Fix) Android "Master Key". saurik. http://www.saurik.com/id/17.
  4. Howells, D. (2007, February 14). MODSIGN: Apply signature checking to modules on module load. https://lkml.org/lkml/2007/2/14/164.
  5. Howells, D. (2011, November 29). [RFC][PATCH 00/16] Crypto keys and module signing [ver #2]. https://lkml.org/lkml/2011/11/29/475.
  6. Howells, D. (2011, December 2). MODSIGN: Apply signature checking to modules on module load [ver #3]. https://lkml.org/lkml/2011/12/2/251.
  7. Howells, D. (2012, May 22). Crypto keys and module signing. https://lkml.org/lkml/2012/5/22/471.
  8. Howells, D. (2012, August 16). [21/25] MODSIGN: Module signature verification. https://lkml.org/lkml/2012/8/15/738.
  9. Edge, J. (2012, September 6). KS2012: Module signing. https://lwn.net/Articles/515007.

signs

Welcome neighbors. In this blog I will be publishing notes I have taken on UEFI, BIOS, bootloading, ELF, and other technical topics that interest me and seem to lack documentation or explanation. I will also be keeping a list of UEFI, bootloading, and other resources I have found useful on my resources page.

The rest of this post will be a whirlwind toure of bootloading and thus fairly introductory, so if you are already familiar with the world of bootloaders you might as well move on and read something else (although I would like to encourage you to look at the section where I propose new general bootloader terminology). In case you want to stick around for the full blog post I will be discussing:

Bootloaders provide a window into how hardware and systems operate. They reveal dependencies between hardware and software components as well as complexities that are masked by drivers. The lowest-level bootloaders provide a structured way of studying hardware because they systematically initialize the most important hardware components (memory, input/output, etc). For all of these reasons I am researching bootloading partially because I want to better understand hardware.

security training

The other half of my motivation is that I am a security researcher and I know that if we cannot trust the bootloader or hardware, we cannot trust the system. Bootloaders contain the first code that runs on a system and are implicitly trusted by all software that acquire control from a bootloader (either directly or indirectly). I am especially interested in the data that drive the bootloading process since I have already discovered how well-formed ELF metadata can act as instructions to an unintentionally Turing-complete ld.so dynamic loader (missing reference).

The grande overview of booting

grande overview

Before I began to research bootloading, that which happened between the moment I hit my machine's power button and the moment when Linux was loaded seemed magical. I hope to help demystify some of that magic in this blog. The exact form of the magic invoked by the power button is highly dependent on the system's architecture, processor, chipset, and peripheral hardware. In this blog post I will mostly discuss bootloading in general and not focus on a single architecture. I will also not specifically discuss architectures with multiple processors but much of what is said here is applicable to such settings.

The many stages of bootloading

boot chain

Bootloaders work hand-in-hand with their hardware to configure it to execute more powerful and feature-rich software. The architecture, processor type, and hardware configuration all have a large influence over what a bootloader needs to accomplish and which bootloader flavors can be used. Most bootloading happens in stages where each stage is a package of code and data that is loaded and executed by the prior stage until the target payload (typically the kernel/operating system) has been loaded and invoked. This sequential combination of multiple bootloader stages is often referred to as a bootloading chain. Most systems implement multiple bootloading stages due to varying degrees of space and addressing constraints, flexibility requirements that exist throughout the boot process, and the need to incorporate low-level software from multiple sources/groups of developers. Early boot stages typically initialize the hardware just enough to locate and load a larger and more powerful stage (and perhaps to allow for debugging), eventually loading other types of stages until the the target kernel (OS) and/or application (on some embedded systems) is located and loaded.

boot kickoff

The bootloading process is kicked off when the system's processor receives a reset signal generated (for example) when it first receives power. Different processors handle this signal in different ways, but they all ultimately end up executing instructions that are either in ROM/firmware that is embedded in the same chip as the processor or some firmware external to the processor that is configured to be accessible from a physical address (as defined in the processor's specifications).

On Bootloader Terminology

UEFI, BIOS, coreboot, DAS U-Boot, firmware, barebox, GRUB, LILO, stage 1 bootloaders, secondary bootloaders: it can be easy to get lost in the world of bootloaders when you are simply trying to get a sense of the space. Wikipedia's table comparing bootloaders is a good place to start but its tables of bootloaders and features are not exhaustive. It also doesn't help that there is no standard language to describe bootloading stages -- everyone uses their own terminology and branding that sound similar but have discrepancies in meaning. This can be disorienting to those who are new to the space.

In my attempt to standardize and use consistent terminology I will refer to the first set of instructions the primary processor executes as the kickoff stage. After the kickoff stage there may be multiple intermediary stages, followed by a penultimate stage which loads the system's target. For example, consider how I labeled a sample Linux boot chain below that uses the GRUB bootloader for its intermediary and penultimate stages.

Example boot chain for Linux on x86 labeled with both GRUB terminology (inside boxes) and my terminology (above boxes).Example boot chain for Linux on x86 labeled with both GRUB terminology (inside boxes) and my terminology (above boxes).

Navigating the sea of bootloaders

navigating the sea

Many bootloaders are legacy BIOS or UEFI-based but different architectures such as PowerPC and SPARC have their own bootloading traditions. Embedded devices have their own limitations, requirements, end goals, and bootloader implementations. Whatever is first executed at startup may be fixed in ROM or in some re-writable non-volatile medium. In modern PCs we typically find this initial bootloader to be BIOS-syle or UEFI-compliant. Embedded systems may have ROM or re-programmable firmware-based initial bootloaders. There is no reason that bootloaders in embedded devices cannot be UEFI-compliant, but it is rare to find UEFI-compliant bootloaders in embedded devices.[1]

Some bootloaders are kickoff bootloaders, some transition between a resource-constrained/hardware-specific previous stage to a penultimate bootloader stage, and others exist simply to make it easier for an end-user to configure the bootloading process. Some assume that they will be loaded from the first sector of the disk drive by a previous stage in the bootloading process (such as MBR/boot sector-based bootloaders like GRUB), and others assume that they will be loaded by an EFI-based previous stage.

How do we navigate this sea of bootloaders? One way to do so is to think of the chain of bootloading stages as a sequence of adapters. Each link in the bootloading chain has its own expectations of what system resources it can use, how it can find the arguments passed to it, what arguments it expects, where it can find the next stage, and how it expects this next stage to be formatted. Whether or not two arbitrary bootloaders can be linked together consecutively greatly depends on these expectations. To understand and evaluate a given bootloader we should look to answer the following questions:

  1. What architectures does the bootloader run on (ARM? x86?)
  2. Where can this bootloader reside? (On disk? In firmware? In some other form of non-volatile memory? On a remote disk?)
  3. How large is this bootloader once compiled into a binary format?
  4. Where can this bootloader be located? (The first sector of a disk? In a UEFI System Partition? Some other previously known position on a disk? In a UEFI Firmware Volume/Firmware File System? At some known position in firmware? At some known address in memory? Via the network?)
  5. How is this bootloader packaged? (UEFI File? UEFI Firmware Volume? As an MBR? PE? ELF? With an OMAP 35xx Configuration Header?)
  6. What arguments does this bootloader expect and where can it locate them?
  7. What devices can this bootloader load its next stage from? (Disk? USB device? Network? Non-volatile memory?)
  8. What formats can this bootloader parse as it locates the next stage? (FAT? GPT? UEFI Firmware Volume/Firmware File System?)
  9. How can the next stage be packaged? (ELF? UEFI Firmware Volume? PE? TE? OMAP 35xx Configuration Header? Encryption? Compression? With a signature?)
  10. How does it pass arguments to the next stage?

Once we answer all these questions about a given bootloader we will have a clearer picture of how it fits in the whole bootloading chain of a system. These questions illuminate the adapter-like qualities of a given stage and how the stages link together, however they do not describe other qualities of a bootloading stage or ways they transform the system's state. Nevertheless, sometimes a bootloading stage does nothing more than act as an adapter.

Adapter chain

For example, the BeagleBoard UEFI implementation's SEC stage does not much else besides loading the DXE stage from the UEFI firmware image because the x-loader boot stage that executes immediately before the UEFI SEC stage performs most platform initialization (Tianocore, n.d.).

BeagleBoard boot chain with UEFI.BeagleBoard boot chain with UEFI.

Example bootloading chains

The Beagle*Bone* (more specifically the BeagleBone Rev A6b), which is different from the Beagle*Board* described earlier, is a development board that uses an AM3358 processor. The AM3358 contains on-chip boot ROM (which I refer to as a "kickoff stage" and TI refers to as a "Primary Program Loader" (PPL)) that by default performs some simple tests to determine where the next bootloading stage lives (which I will call the "intermediary stage" and TI/u-boot calls the "Secondary Program Loader" (SPL)) (Texas Instruments, 2015; Texas Instruments, 2015). BeagleBones are configured so the stage loaded by the kickoff stage lives in a MMC/SD card. This card must be formatted with a FAT file system so that the kickoff stage can find the intermediary stage in a file named MLO located in the root directory, load it at an address specified by the MLO's header, and execute it as the next stage. By default, the BeagleBone comes with a u-boot bootloader that is split into two stages: one called the u-boot Secondary Program Loader (the MLO file) and a second stage is just called u-boot. This second stage is larger and more feature-rich than the first stage (the MLO file) and acts as the penultimate stage that loads the target Linux kernel.

Example boot chain for Linux on the BeagleBone labeled with my terminology, u-boot/BeagleBone terminology,  and boot stage storage locations.Example boot chain for Linux on the BeagleBone labeled with my terminology, u-boot/BeagleBone terminology, and boot stage storage locations.

Meanwhile over in the land of the standard x86 PC, when the machine is first powered on the CPU begins to execute whatever code happens to be sitting 16 bytes from the end of its address space (Intel, 2015). The kickoff bootloader is typically located on a chip in non-volatile memory (such as in SPI Flash). Due to various constraints, the size of this primary bootloader is tiny and it is up to this small piece of code to setup interrupt vectors, memory, and hardware. This system's kickoff boot firmware also enumerates devices that could contain the next boot stage and eventually loads and executes an image (either an intermediary or penultimate stage) that drives the subsequent stage (intermediary, penultimate, or target) of the bootloading process. These layers/chains/stages of bootloaders allow for flexibility and customization of the bootloading process for the end user as well as generic mechanisms for initialization, configuration, and communication between the system's hardware components.

Example boot chain for Linux on x86 labeled with my terminology, GRUBExample boot chain for Linux on x86 labeled with my terminology, GRUB's terminology, and boot stage storage locations.

Specifications related to bootloading

If you crack open your computer or even just inspect an embedded computing device such as a BeagleBone you will likely find hardware components made by different manufacturers. Hardware manufacturers have adopted various conventions to allow mixed and custom hardware components to work together on a single system. For example, bus standards such as PCI allow multiple devices to communicate and share hardware resources. Most bus standards also allow for more flexibility by defining a method of enumerating the devices on the bus without prior knowledge of which devices are attached.

Some standards are highly documented and public, and others... not-so-much. In general, the "standards" (in the general sense of the word) involved in the bootloading process will fit into one or more of the following categories:

  1. Data format (and parser) standards. Be that a file/executable format, a filesystem format, an image format, signature format, or any other data that may be passed between multiple parties during the boot process. Examples include MBR, Firmware File System, FAT, ELF, TE, SREC, SMBIOS, and chip-specific formats.
  2. Calling convention standards. I mean "calling convention" in a general sense: how different actors in the system pass execution flow amongst themselves and what their expectations are regarding the state of the system/parameters/return values before and after execution is passed to a different actor as well as how/where shared data can be found. Examples include parts of the BIOS Boot Specification, UEFI, and application binary interface (ABI) specifications.
  3. Protocol standards. How different components (often working in parallel) expect to communicate and share system resources. Examples include PCI and SPI.

There are probably some pieces of bootloading-related standards that fit in a miscellaneous category, but I will not be discussing them here.

Since a system's boot loaders may not have hard-coded knowledge of all its peripheral components, it must use a combination of hard-coded knowledge of its environment and bus/data/calling convention standards to herd its hardware components into a more useful and powerful state so that all hardware works together as a cohesive whole during and after boot. For each separate component (including non-removable components) the system must be able to:

  1. Communicate with the component
  2. Allocate resources for the component
  3. Initialize the component
  4. (Optional) Allow other components to communicate with the component
  5. (Optional) Inform others about the component (such as the next bootloading stage)

BIOS and Cat Herding

cat

Although we can all agree that BIOS stands for Basic Input/Output System, there are mixed ideas of what a BIOS really is and how to use BIOS-related terminology. Some folks consider "BIOS" to mean the firmware that drives the boot process on any system, but I find this definition misleading because I view BIOS as more of a style of system firmware found in "IBM PC-compatible" systems (Wikipedia, n.d.).

It is easier to understand what "IBM PC-compatible" and "BIOS" mean and why BIOS is so loosely defined when they are placed in their historical context. The original IBM Personal Computer (5150) was introduced in 1981. The IBM 5150 and its successors were built mostly out of hardware and software from outside vendors and were designed to allow users to add and swap hardware components (missing reference). In fact, one of the few components that IBM designed specifically for their PC was the BIOS (missing reference), although they published much of its source code (including system diagrams) in their documentation (IBM, n.d.; Wikipedia, n.d.). IBM discouraged vendors from building software that interacted directly with hardware, instead encouraging them to interface with the BIOS thus allowing the BIOS to act as a consistent interface layered above an evolving set of hardware (Norton, 1985).

Due to the popularity of the IBM PC outside vendors began releasing their own IBM PC clones that included ROM that behaved like an IBM PC BIOS so that hardware and software that worked on the IBM PC would also be compatible with their clone (Computer History Museum, n.d.). The original IBM PC BIOS set the stage and tone for how BIOS evolved since the era of the IBM PC. What we consider to be legacy BIOS was never fully or centrally standardized, it was just what vendors needed to comply with in order to develop machines, hardware, and software that could exist and interact in the prominent PC ecosystem. The IBM PC BIOS was what cat herded outside vendors towards an ecosystem in which they not only co-exist but also interact.

cat in sink

Thus what we think of as the BIOS "standard" is really just a a set of expectations that exist between the hardware and software components in a system. Hardware attached to a system with BIOS-style firmware can make specific assumptions about how it is going to be communicated with, handled, and initialized by the system's firmware at boot; the subsequent bootloader that is invoked can make certain assumptions about how it is going to be located, loaded, and run; and the kernel and bootloaders can interact with the hardware via BIOS-defined interfaces (although modern kernels generally do not directly interact with the BIOS).

Although there is no single definitive BIOS specification, there are multiple published specifications that are part of (legacy) BIOS-style firmware.[2] BIOS-related specifications do not cover the breadth of what is expected to exist in BIOS-style firmware; some BIOS expectations are not defined in a specification but instead are copied from other popular BIOS implementations. Thus in order to understand BIOS, one should not only read specifications but also read a BIOS manufacturer's technical reference such as the PhoenixBIOS 4.0 Programmer's Guide, one of the older references such as an IBM PC technical reference, and it might do you some good to browse through the boot record/partition table information in a DOS Technical Reference to gain insight on how BIOS-style firmware locates the next bootloading stage on disk.

I am no BIOS historian, but judging from the language used in the BIOS-related specifications and their publication dates it appears that many of the written specifications associated with BIOS are more of a reflection of how the boot process worked in IBM PC-compatible system firmware as opposed to a premeditated design to which manufacturers were expected to conform. Thus the "cat herding" -- what BIOS really is is a snapshot of how structure emerged from the multiple parties involved in developing IBM PC-compatible systems and hardware.

UEF*I* with an Emphasis on Interface

UEFI, the Unified Extensible Firmware Interface is a boot firmware standard developed to be the industry-standard and successor to legacy BIOS firmware. To truly understand UEFI, we must constantly remind ourselves that the purpose of UEFI is to define standard firmware-related interfaces. UEFI-compliant firmware often work with the same bus protocols as legacy BIOS firmware, but the details of these protocols and other standards are abstracted away into UEFI-defined bus drivers/interfaces. It is also important to note that UEFI-compliant firmware do not require trusted boot to be in place, so all of you who deeply believe that UEFI is pure evil, take a few breaths and remember that the true purpose of UEFI is to define standard firmware interfaces.

UEFI vs. BIOS

UEFI is often compared to BIOS because it is the successor of legacy BIOS and addresses many of the limitations inherit in legacy BIOS firmware. UEFI and BIOS are similar in that they accomplish the same major goals such as initializing hardware and allowing for flexibility in which kernel ultimately gets booted, although UEFI-compliant firmware is much more feature-rich than its plain legacy BIOS counterpart. UEFI is a firmware interface standard whereas BIOS is a firmware interface convention. UEFI specifies how different hardware and software components can expect to interact during boot and runtime down to which functions calls should be used and how various data structures should look whereas the BIOS convention applies mostly to hardware-level interaction (how hardware components communicate via interrupts). Nevertheless, BIOS and UEFI are not mutually exclusive, system firmware can implement UEFI-compliant interfaces but still support legacy BIOS booting (with a feature called the "Compatibility Support Module").

How to jump on the bootloader bandwagon

So you want to write your own bootloader but you don't know where to begin? Let me suggest you write a MBR/boot sector-based bootloader. It may not be as fulfilling as writing a bootloader for an embedded system because it doesn't sit in firmware or require you to read large chunks of hardware specifications but it is a widely-used stage in legacy BIOS bootloading. There are many interesting boot sector toys and project seedlings such as:

Our toy bootloader does not have to do anything interesting, it does not even have to load additional images or stages. For sake of simplicity let us write a boot sector that also happens to be the bootloading target.

We will build a bootloader for the qemu-system-i386 virtual machine that has a legacy BIOS kickoff stage. In BIOS-style bootloading this bootloading stage is known as a boot sector because it located in the first sector of a hard or floppy disk.

What do we need to know to get started? We know that this machine has a BIOS-based kickoff stage and we are looking to write the next stage. We need to to answer the following questions:

  1. How should the bootloader code be packaged in non-volatile storage?
  2. What size restrictions are there (both in non-volatile and volatile storage)?
  3. Where should the bootloader image should be stored?
  4. What state is the system in (operating mode, available memory, stack)?
  5. How and where is our bootloader mapped to memory by the previous stage and what is the first address executed (the entrypoint)?
  6. How can our bootloader locate arguments passed to it (if any)?

If we want our bootloader to be able to load and execute a kernel image, then we must also answer these questions with respect to the kernel so that we can set the environment up to match the kernel's expectations, however we will be doing that here.

Since there is no authoritative BIOS standard or other primary source (that I can find) that answers all the questions above, we must resort to referencing existing boot sector code and tutorials. The OSDev Wiki page on "Rolling Your Own Bootloader" is a nice resource but does not answer all of these questions, but we can find most other answers in Pierre ANCELOT's blog post "Bootsector in Assembly". Armed with this information we can answer the above questions as follows:

  1. We don't know exactly what image "format" is used, but we know that the nasm assembler will give us what we need when passed "-f bin" option and passed source code with specific characteristics that I will describe later.
  2. The packaged bootloader image must be no bigger than 512 bytes. At runtime it initially cannot address memory above 1M and it has about 512KB of memory to work with.
  3. It must live in the first sector of a bootable disk.
  4. The CPU is executing in 16 bit real mode and we can make no assumptions about the stack.
  5. The previous stage has loaded our entire bootloader image to physical address 0x7c00 and it begins execution at address 0x7c00.
  6. We don't need to locate any arguments passed in by the previous stage.

If we want to do something interesting with our bootloader such as print characters to the screen, we will find that BIOS-based bootloaders allow us to accomplish this via interrupts. You can read more about BIOS interrupts in the "BIOS Interrupt Call" Wikipedia article. In case you are not familiar with interrupts, what you should know is that when a INT instruction is called, the processor starts executing a function as specified by its operand which acts as an index into a table of function pointers (an interrupt table) that (hopefully) has been setup before we invoked the interrupt. At this stage in the boot process, the BIOS should have set up an interrupt table for us. All BIOS-style firmware handle many interrupts similarly but not exactly the same. If you want to be sure of how your particular BIOS handles an interrupt, you should check your BIOS's technical reference, that is if you can find a copy. QEMU uses seabios and if you look for its developer documentation regarding BIOS interrupts you will find that it asks you to consult Ralf Brown's interrupt list which is a comprehensive listing of interrupt calls across many chipsets and also points out differences in implementations.

Here is a toy bootloader written for the NASM assembler that simply prints a string "Hello, boot sector!" and hangs. I will explain the code in grueling detail below.

 1 BITS 16    ; Tells the assembler that the processor is operating in 16-bit mode
 2 ORG 0x7C00 ; Assembler should assume the program is loaded at address 0x7C00 
 3 
 4 ; This is the first instruction that is executed when this bootloader is invoked
 5 mov ebx, msg  ; Load address of message into a general purpose register.
 6               ; ebx will hold the address of the next byte in the message to print 
 7 mov ah, 0x3   ; Setup "Get cursor position & shape" interrupt args
 8 int 0x10      ; Causes current page number to be stored in bh
 9 printnextchar:   
10  mov al, [ebx]; Move next character in message to al register 
11  inc ebx      ; Increment the address of the next byte to print
12  cmp al, 0    ; Have we passed the end of the string we are printing?
13  je finish    ; If we have printed the full string jump to the label 'hang'
14  mov ah, 0x0e ; Move the magic value 0x0e to the register ah
15  int 0x10     ; Invokes the "Video Services" interrupt
16               ; Because the magic value 0x0e is in register ah Video Services
17               ; Will write a character in TTY mode and it will print character
18               ; Specified by register al (which we set earlier)
19  jmp printnextchar; Loop to print the next character
20 
21 finish:  jmp finish; Loop in place to cause the bootloader to hang
22 
23 ; Define a null-terminated string we want to print
24 msg: db 'Hello, boot sector!', 0 
25 
26 ; Pad the rest of the bootloader with zeros through byte 510
27 times (510) - ($ - $$)  db 0   
28 
29 ; A valid boot sector is expected to have bytes 511-512 contain 0x55 and 0xaa
30 BIOS_signature:      
31  db 0x55
32  db 0xaa

The first thing the code does is load the address of the message it wants to print into register ebx. After that it calls interrupt 0x10 with register ah set to value 0x3. According to Ralf Brown's interrupt list, INT 10 is the video interrupt and when register ah is set to 0x03, the BIOS's video interrupt will execute the "get cursor position and size" function which stores cursor information in various registers. We call this interrupt because it stores the current page number into register bh which we need in order to print out a character. I will not be explaining what "page number" means beyond the fact that we need it to print a character to screen properly.

The code then iterates through each character of msg which is pointed to by the ebx register. For each character in the string, it copies the character into the al register, increments ebx to point to the next character, checks if the character's value in al is zero (null), and if it is null it jumps to the tight loop at offset 0x19. If the character is not null, it copies the magic value 0xe to the ah register, calls interrupt 0x10, and loops to print the next character. When this particular interrupt is invoked, the BIOS video services interrupt handler will invoke the "teletype output" function because register ah is set to 0xe. The "teletype output" function will print the character in register al to the screen at the cursor's current position and at the page number in bh (which was set by our first INT instruction), it will then move the cursor forward one position. After all the characters are printed, we will jump to the last line of code which will cause the bootloader to hang in a tight loop.

Let us now see what this boot sector image looks like on disk. You can grab a copy of this this source code with a Makefile and other goodies from my github repository. If you are playing along at home, go ahead and build the image (see the README file) so we can inspect the image to understand how the toy bootloader works. The Makefile builds a disk image called hello.img that can be executed via qemu a la:

$ qemu-system-i386 -fda hello.img

If we disassemble the boot sector image we can see that the assembly code we wrote is located at the start of the image up until offset 0x1b of the file.

$ ndisasm hello.img -o7c00h | head -n 12
00007C00  66BB1B7C0000      mov ebx,0x7c1b
00007C06  B403              mov ah,0x3
00007C08  CD10              int 0x10
00007C0A  678A03            mov al,[ebx]
00007C0D  6643              inc ebx
00007C0F  3C00              cmp al,0x0
00007C11  7406              jz 0x7c19
00007C13  B40E              mov ah,0xe
00007C15  CD10              int 0x10
00007C17  EBF1              jmp short 0x7c0a
00007C19  EBFE              jmp short 0x7c19
00007C1B  48                dec ax

In the ndisasm output above we can see from the first disassembled instruction that msg is at offset 0x7c1b. Remember this image will be loaded at offset 0x7c00 in memory (our assembler directive in line 1 of the code instructed the assembler of this loading offset) so our msg string should be located at offset 0x1b from the top of the image.[3]

If we use a hex viewer to inspect our boot sector, we will find that the "Hello, boot sector!" string we statically defined at line 24 is indeed at offset 0x1b.

$ xxd -a hello.img 
00000000: 66bb 1b7c 0000 b403 cd10 678a 0366 433c  f..|......g..fC<
00000010: 0074 06b4 0ecd 10eb f1eb fe48 656c 6c6f  .t.........Hello
00000020: 2c20 626f 6f74 2073 6563 746f 7221 0000  , boot sector!..
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.

Following the string, the file is padded with zeros until the last two bytes at offset 0x1fe-0x1ff (510-511). The padding of zeros comes from the assembler directive found at line 27 and the 0x55 0xaa bytes come from line 31-32. How did I know perform this padding and insert this signature? Mostly due to trial and error. QEMU did not execute the boot sector without the padding and signature and I realized the tutorials state that boot sectors needed to be padded in this manner.

Now that you know how to make a boot sector that prints characters to the screen, perhaps you too can do something interesting with the 510 bytes of a boot sector than can contain arbitrary data. However if you would like to first see more examples of simple boot loaders, you can find more resources on writing boot sectors such as Shikhin Sethi's "This OS is a Boot Sector" blog post and the "Creating a Bare Bones Bootloader" blog post by Joe Savage.

To be continued...

Thanks for reading this. Stay tuned for more loader-related blog posts. You can subscribe to my blog's feed or follow me on Twitter to find out when I publish new content.

to be continued

Acknowledgments

I'd like to thank Sergey Bratus, Rodrigo Branco, Shikhin Sethi, and Bruno Korbar for their technical guidance and help editing this blog post.

References

  1. Tianocore. Beagle Board Wiki. http://tianocore.sourceforge.net/wiki/Beagle_Board_Wiki#Beagle_Board_Boot_Flow.
  2. Texas Instruments. (2015, February). AM335x Sitara Processors Technical Reference Manual. http://www.ti.com/lit/ug/spruh73l/spruh73l.pdf.
  3. Texas Instruments. (2015, May). AM335x U-Boot User’s Guide. http://processors.wiki.ti.com/index.php/AM335x_U-Boot_User\ %27s_Guide.
  4. Intel. (2015, June). Intel 64 and IA-32 Architectures Software Developer’s Manual.
  5. Wikipedia. IBM PC compatible. https://en.wikipedia.org/wiki/IBM_PC_compatible.
  6. IBM. Technical Reference. http://www.pcjs.org/pubs/pc/reference/ibm/5150/techref/.
  7. Wikipedia. IBM Personal Computer. https://en.wikipedia.org/wiki/IBM_Personal_computer. Retrieved from https://en.wikipedia.org/wiki/IBM_Personal_computer
  8. Norton, P. (1985, February 5). Software for One and All. PC Magazine. https://books.google.com/books?id=BGNWNTJnuRcC&lpg=PA305&pg=PA103#v=onepage&q&f=true.
  9. Computer History Museum. Send in the Clones. http://www.computerhistory.org/revolution/personal-computers/17/302.
  10. Albertini, A. (2013, December). This OS is also a PDF. POC or GTFO.
  11. Haverinen, J., Shepherd, O., & Sethi, S. (2014, March 2). Tetranglix: This Tetris is a Boot Sector. POC or GTFO.
  12. Somerville, C. rustboot. https://github.com/charliesome/rustboot.

[1]: The tianocore UEFI-reference implementation has been ported to the BeagleBoard although only the last stages in its bootloader chain are UEFI-compliant, the first couple of stages are not. The MinnowBoard, a development board for embedded devices, has full UEFI support.

[2]: I have compiled a partial list of specifications I consider to be important to modern BIOS-style bootloading here.

[3]: For a boot loader written in C and compiled with gcc, a linker script must be used to produce the desired loading offset. This dc0d32 blog post explains how to accomplish this.