Project 1: Bootloader
Overview
In this first project, you will build the basic environment that
is needed to develop the operating system (OS) for the rest of the semester:
the bootloader (bootblock).
Specifically, it will be your job to implement
bootblock.s and
createimage.c.
Every OS is responsible for enabling other programs to be
run. That said, who enables the OS? This function
is performed by a small program known as the bootloader.
Although bootloaders take a different form with each computer
architecture, the core ideas remain the same. In this project,
you will create a bootloader for the x86 (
technically IA-32) architecture.
In the x86 architecture,
the bootup procedure looks for a bootloader on
the first sector of the booting device (a USB flash disk, hard drive, image file, etc.). This is
automatically loaded and executed during the system booting process. The bootloader is
responsible for loading the rest of the OS from the booting device
into memory. The bootloader cannot rely upon any functionality from
the OS, such as OS calls for disk I/O, or linking an
object file against static libraries. However, the x86 architecture
requires the presence of the Basic Input Output System (BIOS), which
provides minimal terminal, keyboard and disk support. Your
bootloader will use these BIOS calls to load the kernel from
disk into memory, set up a stack space, and then switch control to
the kernel. At this point, the OS begins running.
Because GCC, the compiler used to compile the bootblock and
kernel source, generates executable files in a specific format which needs
OS support (of course not available before the OS
itself is loaded) to run, we need the
createimage
tool to put the bootloader and kernel executables into a bootable OS image file
that can be directly loaded and run on a machine.
To fulfill the requirements of this project, you must learn the:
We have provided starter code which contains ten files:
- Makefile: Used to compile and create your files
- assembly_example.s: A series of assembly language examples
- bochsrc: The configuration file for the Bochs emulator
- bootblock.s: The template for the bootloader
- createimage.c: The template for the Linux tool to create a
bootable OS image
- createimage.given:
An executable Linux tool for you to test bootblock.s and validate createimage.c
- kernel-large.s:
A large-sized kernel (160 sectors) used for extra credit
- kernel-medium.s:
A medium-sized kernel (84 sectors) used for extra credit
- kernel.s:
A minimal kernel (9 sectors) to test your bootup code and your Linux tool
- man/: Manual for createimage.
Invoked by man -M man createimage in the shell, or found
here
The starter code is available on the lab machines at
/u/318/code/project1/
Setup
First, SSH into the courselab machines as described
here.
Then copy the starter code with the following command:
cp -r /u/318/code/project1/ ./project1
Finally, work through the
Bochs quickstart tutorial to get familar with the emulator you will use for these projects.
If you are using a Windows machine, please use MobaXTerm instead of PuTTY, which will allow bochs to run properly without any additional setup. Otherwise, you will need to enable X11 forwarding when connecting to courselab via ssh in order for Bochs to work (otherwise it will throw the error: "Cannot connect to X display").If you are not yet familiar with X Window Systems and X11 forwarding, please read
this handout from COS 217.
Add the directory /u/318/bin/ containing the bochs and bochsdbg binaries to your PATH environment variable (e.g. add export PATH=/u/318/bin:$PATH to your .bashrc file in your home directory).
We also recommend looking through
assembly_example.s
before working on the rest of the assignment.
Design Review
First, take a look at
what we
expect from you in general.
For this project,
at your design review, we want you to write
print_char
and
print_string functions in assembly.
The first outputs a single character to the screen, and the second outputs
a full string to the screen. Both should start printing at the cursor and
advance the cursor as a result. You may implement these using any BIOS
interrupts. Note that these are functions: they will be called with
call and returned with
ret, and they should save and restore any modified registers. You may
choose your own calling convention. The goal for this part (what we're
grading) is your understanding of the organization of the stack and the use of interrupt calls. This
is more important than getting the two functions to work perfectly, although getting the functions to
work may help you test your bootloader in its early stages.
Therefore, when implementing these functions, do it in a copy of
bootblock.s.
You should be able to describe:
-
How to move the kernel from disk to memory:
- Where to find it on disk
- Where to copy it in memory
- How to do the copying in assembly
- In which case(s) must your bootloader be relocated?
- How to create the disk image:
- Given an executable (ELF), show how to use the header info to find
where the first code segment begins
- Show how to determine where in the image file this segment must be
placed
- Where to write padding
Though not required for the design review, we also encourage
you to investigate BIOS interrupt 0x13,
which provides low-level access to read or write disk sectors.
You will need this functionality to complete project 1; familiarize yourself early.
Each group must schedule a 10-minute design review slot during the allotted times.
Additional flexibility will be provided only for extreme circumstances.
To sign up for a design review, use
our signup page.
Building
A
Makefile is provided for you. To compile all your files and to generate
the
image file, use the
make command
from within the directory that contains your project.
To remove any files generated by the last
make, use the
make clean command.
The
Makefile initially uses
createimage.given to build
image.
Once you have implemented
createimage.c, you will have to change
line 62 to use your own
createimage.
Do not make this change until you are sure your bootloader is working.
Bootblock
When compiled,
bootblock.s, will be written to the
first sector of your boot device (USB, an image, etc.). You must write it in IA-32
assembly language and its assembled machine language representation cannot exceed 512 bytes
(the size of one disk sector).
bootblock.s must:
- Set up the any registers needed to load the kernel
- Load the kernel
- Set up the stack for the kernel
- Invoke the kernel
In particular, make sure the
%cs
,
%ds
, and
%ip
registers are set to the correct values when the kernel is invoked.
You are required to handle kernels up to 54 sectors in size. Note that reading up to 128 sectors can be done with
a single BIOS call, but you must be aware that you cannot cross a
physical
segment (every 64K starting at address
0) in
one read. In addition, when using Bochs, reading more than 64 sectors at once will fail if your image is
used as a floppy disk instead of a hard disk.
When a PC boots the image/disk that you have prepared, and the system
has been initialized, the first sector of the boot disk (the bootloader) is
loaded at address
0x07c0:0000
(
0x7c00 in real addressing mode) and control
jumps to that address.
Your bootloader can safely modify memory in the range
[
0x0a00,
0xa0000) without having to worry about
overwriting video memory, the interrupt vector table, or BIOS.
The kernel should be loaded at address
0x0000:1000. You may assume that the entire kernel is no more than
54 sectors long. Notice that a loaded larger kernel may overlap with the
bootloader, so you will have to relocate the bootloader to a higher address
that can't be overwritten before loading the kernel (see
extra credit).
In order to load sectors, you must know the boot device number, which is
stored in
%dl. Common values are
0x0 for a floppy drive,
0x80 for the first hard drive,
0x81 for the
second, etc. You should save this value for later use.
We have provided
createimage.given which is a
Linux-compiled binary version of
createimage
so that you may test your bootloader independently of the next half of the
project. You can create an image using:
./createimage.given --extended bootblock
kernel or simply use the
Makefile.
See
testing for information on how to test the
resulting
image.
Reading
Createimage
This is a Linux tool that combines the bootloader and kernel into a bootable image file.
Part of this tool is to tell the bootloader how many disk sectors to read in order to
fully load the kernel.
When a program is compiled in Linux, an ELF executable file is generated. This file contains
the program code and information telling the OS how
to load the code into memory and what resources (dynamically-linked
libraries, etc.) are required.
However, at this stage, you do not have an OS to load any executable files.
Thus, you must extract the bootloader and kernel from the corresponding
generated ELF executables and carefully lay them out in the image file as if it
were memory.
In order to write
createimage, you will need to become especially
familiar with the following features of the ELF file format:
-
ELF headers, and their e_phnum and e_phoff fields
-
Program headers, and their p_offset and p_filesz fields. Note that the p_filesz and
p_memsz properties of a segment in an
executable may differ
We encourage you to read the
Documentation for the ELF file format
as this document contains extensive explanations of the features you will need for this project.
You are also required to implement the --extended option.
This option is supposed to print information to help debug the OS image.
In particular, when given this option, createimage
is expected to display the offset of the text segment from the start of the file,
the virtual address to load the segment into (from the perspective of the ELF file),
the size of the segment on disk and in memory, how many bytes were actually written
to the image, and the total number of bytes written to the file at that point (including padding)
for both the bootblock and the kernel.
You must also print the size of the kernel (os_size)
in sectors so that you can have a sense of whether the bootloader will need to
relocate itself to accomodate a larger kernel (especially useful for the extra credit).
We have provided the Linux-compiled binary of createimage
to help you keep track of this. Your output should contain all the information
provided in createimage.given, and be printed in
roughly the same format.
To complete
createimage, you will need to implement the following
functions in addition to completing
main():
-
read_exec_file(): Reads in an executable file in ELF format
-
write_bootblock(): Writes the bootblock to the image file
-
write_kernel(): Writes the kernel to the image file
-
count_kernel_sectors(): Counts the number of sectors in the kernel
-
record_kernel_sectors(): Tells the bootloader how many sectors the kernel has
-
extended_opt(): Prints the information for the --extended option
To help you get started, you should refer to the man page in the code template by typing
man -M man
createimage to get more information about the functionality of
createimage. You should ignore the "
--vm" option for this project.
Finally, note that to mark a sector as "bootable" (which is required for the bootblock),
the sector must contain the two-byte signature 0x55,
0xAA as its final two bytes.
The BOOTLOADER_SIG_OFFSET constant contains the offset in the
bootblock sector to write this signature to.
Testing
While developing your bootloader, we encourage you to test it using the free
Bochs emulator (
bochs.sourceforge.net).
However, you will be developing a real operating system in this course so your project 1 must work
on real computers.
To complete this project, you must ensure your bootloader works when booted
from a USB flash disk on the lab machines. We will provide you with USB sticks. If your code is
correct, you should see the following message upon booting:
Kernel > Running a trivial test... Test passed!
With the previous caveat in mind, Bochs
can make debugging your bootloader much easier. The
lab machines have a debugging version of Bochs in
/u/318/bin/bochsdbg.
When it loads your image, it will enter a debug prompt where you can set breakpoints, step through
assembly code, etc. You can get familiar with the Bochs testing and debugging workflow using the
Quickstart Tutorial. You can find full documentation of
the Bochs internal debugger
here.
We strongly suggest you use the Bochs installed on lab machines. The way to login to lab
machines is similar to COS 217. See
How to SSH to Courselab.
Booting off a USB disk
See
these instructions
Resources | Tools |
|
-
objdump -M i8086,att [-D | -d]
filename: disassembles a program, showing symbols
and offsets.
-
hexdump: dumps file in hex and other
formats
-
od: octal (and other formats) dump of
a file
|
Checklist
Here is a rough checklist to help you complete the two phases of the project:
-
bootblock
- Set up stack for kernel (COS 217 review: what are the pointers defining the stack?)
- Transfer kernel data from disk into memory using answers to "from where", "to where", and "how" from the design review
- Start executing kernel code at the location where you loaded it into memory
-
createimage
- Read executable header and program header
- Read from bootfile and then write into image and pad
- Read from kernelfile and then write into image and pad
- Count number of sectors in kernel
- Record number of sectors to bootloader into image file at right location
- Handle extended option calculation and printing
Extra Credit
You will get 1 point extra credit if your bootloader can load larger kernels and/or read kernel data from more than one disk head. For the first part, your bootloader will need to relocate itself. In order to do some extra credit, you will need to know
BIOS Int 0x13 Function 8
which can help you get disk parameters and a little bit about the cylinder-head-sector (CHS). In brief, successful BIOS Int 0x13 will return:
CH = low eight bits of maximum cylinder number
CL = maximum sector number (bits 5-0)
high two bits of maximum cylinder number (bits 7-6)
DH = maximum head number
Notice that 1 cylinder contains maximal head number of heads, 1 head contains maximal sector number of sectors. You will resort to these disk parameters to read kernel sector by sector. More information about CHS will be covered in the later lectures.
We provided two kernels for your extra credit test. There's one 84-sector medium kernel and one 160-sector large kernel that can be built using make medium and make large, respectively. The medium kernel is self-explanatory.
By loading the large kernel correctly, you should see:
Passed Test 1
Passed Test 2
Passed Test 3
Some thoughts to consider for the extra credit:
- Two adjacent segments in the executable file may
not be contiguous when loaded into memory. The simplest method to handle this case
is to add padding between the segments, so that the two segments seem to be one contiguous segment.
-
A segment in a large kernel might occupy offset
0x7c00, which would overlap with the
bootloader. (0x0000:0x7c00 is equivalent to
0x07c0:0x0000). Your bootloader will have
to copy itself to a higher address that cannot be overwritten when
it eventually loads the kernel.
Program Submission
Don't forget to complete the general
requirements for each project! In particular, you need to
submit a README file, which concisely describes your design and
implementation, and what parts work and what parts you didn't get to work.
You don't have to repeat anything you presented in the design review.
Submit via
Tigerfile; only one person per group should submit. When submitting the assignment, please turn in the files specified, along with your README (less than 500 words in text format).