This is the first of six course projects originally developed by Kai Li at Princeton and his colleagues at the University of Tromsø.
Over the course of these projects, you are going to develop a simple operating system, which will cover most of the topics discussed throughout the course. In this first project, you will build the basic environment that will be needed to develop the operating system for the rest of the semester. Specifically, your job is to implement two programs: bootblock.s and creatimage.c.
The bootblock resides on the first sector of the booting device (USB flash disk for this project) and is automatically loaded and executed at system booting process. It is responsible for loading the rest of the operating system from the booting device into memory. Because GCC, the compiler used to compile the bootblock and kernel source, generates executable files in a specific format which needs operating system support (of course not available before the operating system itself is loaded) to run, we need the createimage tool to transform and pack them up into an image file that can be directly loaded and run on a bare machine. This file is called an image because it is loaded into the memory exactly as what it is like on a disk.
To complete this project, you will need to learn about:
- IA32(a.k.a. x86) architecture and assembly language.
- The booting process of a PC.
- BIOS functions that needed to display messages and load disk data.
- The ELF binary format.
3.1. Getting the files
For both of these programs, you should start with a skeleton version that we will provide. Assuming you have obtained a Subversion login and password password as described in CS422/Assignments/HW0, you can check out a working copy of your Subversion directory with the command
svn co http://pine.cs.yale.edu/422/user/your-user-name-here
svn co http://pine.cs.yale.edu/422/group/your-group-name-here
Use the second command if you are working in a group. Your group name consists of your id and your partner's id in alphabetical order separated by a hyphen, e.g. pinky-thebrain. You can also find your group directory under your user directory as group.
You will find the initial project files in the common/1 directory underneath your working directory. You do not have commit access to this directory, so you will need to copy the files to your own working directory before working on them with the commands
svn cp common/1 . svn commit -m"Hey, I just copied hw1!"
You can now modify the files in your new 1 subdirectory. You should find the following files:
Makefile: A makefile for you to compile with the right options.
bootblock_examples.s: A series of assembly language examples
bootblock.s: A template for you to write the bootup code.
createimage.c: A template for you to write a Linux tool to create a bootable operating system image on a USB flash disk.
createimage.given: A precompiled Linux binary for you to use to test your bootblock.s code and validate your createimage.c.
kernel.s: A minimal kernel to test your bootup code and your tool.
bochsrc: A configuration file for bochs suitable for use with the resulting image.
The subdirectory also includes a man page for createimage.
You are expected to modify the files bootblock.s and createimage.c. You may create additional files if needed.
3.2. Submitting your assignment
To submit your assignment, run svn commit. We recommend that you commit early and often during development. This will not only guarantee that you have submitted something before the deadline (in case you succumb to tragedy), but will also (a) allow you to back out of any particularly bad mistakes you might make along the way, and (b) allow us to admire your progress and examine your code if you need to ask any questions about what you are doing.
Unless you make other arrangements, we will base your grade on the last version of your code committed.
If you only modify existing files, you will most likely not need to use any Subversion commands except up (to get changes made by your partner) and commit (to send your changes to the server). But if you add new files, remember to add them to the repository version with svn add. (Please do not add any files beyond your source files—especially not binaries!)
3.3. Design review
You should complete a design review one week before the final project deadline. This involves a face-to-face meeting with the TA to discuss your plans for completing the project. You should be prepared to demonstrate a stub bootloader that does something useful beyond the initial bootloader stub provided to you (e.g. prints a friendly message). The intent of the review is not to force you to have fully-functional code or even a perfect design at this point; instead, we want to make sure that you are on the right track and thinking about the project well before the final deadline. The design review accounts for roughly 20% of the project grade.
4. Details of the bootloader
The file bootblock.s will contain the code that will be written on the boot sector of your disk image. This should be written in IA32 assembly language (or C using the asm feature) and cannot exceed 510 bytes (1 sector less 2 bytes for the boot flag). This code will be responsible for (a) loading in the rest of the operating system, (b) setting up a stack for the kernel, and (c) invoking the kernel. You are required to handle kernels of size up to 127 sectors. You will get extra credit if your bootblock can handle sizes greater than 128 sectors.
After the BIOS has initialized the system, the bootblock gets loaded at memory address 0x07c0:0000 and control is jumped to that address. Your bootblock should load the OS starting at 0x0000:1000. In real mode, you have 640 KBytes between 0x0000:0000 and 0xA000:0000. The low memory area has system data like the interrupt vectors and BIOS data. To avoid overwriting them, you should only use memory above 0x0000:1000 for this assignment.
Note that even though you can assume that the entire OS is less or equal to 127 sectors for this project, this may still be large enough to overwrite your bootblock loaded at 0x0000:7c00. Your bootblock code should deal with this.
We have provided you with a working binary version of createimage so you don't have to get your own version done before you test the bootblock. To test your bootblock you can use the createimage.given file. Type ./createimage.given --extended ./bootblock ./kernel to generate an image file; the command bochs should then load this image file (if you have not modified the provided bochsrc).
5. Details of createimage
This program is a Linux tool to create a bootable kernel image. To make your life a bit easier, we have provided you with a man page in the man directory included in the project directory. You can view the man page by typing man -M man createimage.
Note: Please ignore the "-vm" option for this project.
Essentially, createimage takes a bootloader and one or more ELF-format executables and places them all into one image file. The bootloader must be placed in the first sector, while all other executables should be placed at offset specified in the ELF header. To read an ELF file use fopen, fread, and fseek. The program header (figure 2-1 of the ELF Documentation) contains information about each segment in the ELF and should be used to determine the address at which to place the segment.
The output image file should contain the kernel segments in linear order starting at address 0x0000:1000. Since the kernel segments may be provided in any order and are not necessarily contiguous, you will probably have to do some padding in the output file. The output file should be an even multiple of 512 bytes.
One more thing you will have to do is to mark the image as a "bootable device". You will have to put a signature in the boot sector (first sector) of the disk image so that the BIOS will recognize it as bootable. The signature consists of two bytes: "0x55 0xaa", and you will need to put this signature at the end of the boot sector at offset 0x1fe. You will also have to figure out a way to inform the bootloader how many sectors to read.
6. Assembly language examples
Here are some examples of x86 assembly language. The file bootblock_examples.s also contains several x86 assembly language
6.1. Loading a segment:offset
The project will require you to load values into the segment registers to setup the stack and data segments. The code segment register (CS) cannot be loaded directly, only indirectly through a JMP type instruction. When loading a value into the stack segment register (SS), interrupts are disabled for the next instruction, thus allowing you to set the stack pointer (SP). As an example of setting up segment registers for data, consider the following string copy:
# Setup the registers - see chapter 3 of Intel ISA reference volume 2 movw DATA_SEGMENT, %ax movw %ax, %ds movw OTHER_DATA_SEGMENT, %ax movw %ax, %es movw STRING_FROM_ADDRESS, %si movw STRING_TO_ADDRESS, %di # Move a byte from one string to the other - implictly DS:SI to ES:DI movsb # The values in %si and %di are automatically incremented/decremented based on the DF flag.
6.2. Display memory
During booting, you can write directly to the screen by writing to the display RAM which is mapped starting at 0xb800:0000. Each location on the screen requires two bytes---one to specify the attribute (use 0x07) and the second for the character itself. The text screen is 80x25 characters. So, to write to row i and column j (counting from zero), you write the 2 bytes starting at offset (i*80+j)*2.
The following code sequence writes the character 'K' (ASCII 0x4b) to the top left corner of the screen.
movw 0xb800,%bx movw %bx,%es movw $0x074b,%es:(0x0)
This code sequence is very useful for debugging.
7. Useful BIOS features
You are allowed to use these BIOS functions to complete this assignment.
7.1. BIOS Int 0x13 Function 2
Reads a number of 512-byte disk sectors starting from a specified location. The data read is placed into RAM at the location specified by ES:BX. The buffer must be sufficiently large to hold the data AND must not cross a 64K linear address boundary. (For our project, for simplicity, you can assume cylinder number bits 8 and 9, i.e. bits 6 and 7 of cl are zero).
- ah = 2.
- al = number of sectors to read.
- ch = cylinder number (lowest 8 bits of the 10-bit cylinder number, 0 based).
- cl bits 6 and 7 = cylinder number bits 8 and 9.
- cl bits 0-5 = starting sector number, 1 to 63 (note: head 0, cylinder 0, sector 1 is the bootblock).
- dh = starting head number, 0 to 255.
- dl = drive number: 0x80 + zero based hard drive number. Use dl = 0x80 for the first hard disk, which is what bochs should be presenting your image as. This may not be true if you boot your image elsewhere (e.g. off a flash drive on a real machine!) For maximum portability, use the value stored in dl when the BIOS jumps to your bootblock.
- es:bx = pointer where to place information read from the disk.
- ah = return status (0 if successful).
- al = burst error length if ah = 11; undefined on most BIOSes for other ah return status values.
- carry = 0 successful, 1 if error occurred.
7.2. BIOS Int 0x10 Function 0x0e
This function sends a character to the display at the current cursor position on the active display page. As necessary, it automatically wraps lines, scrolls and interprets some control characters for specific actions. Note: Linefeed is 0x0a and carriage return is 0x0d.
- ah = 0x0e.
- al = character to write.
- bh = active page number (Use 0x00).
- bl = foreground color (graphics mode only; use 0x02).
7.3. (For extra credfit) BIOS Int 0x13 Function 8
For extra credit, your bootloader will need to be able to load more than 128 sectors. In order to do that, you will need to use another BIOS Int 0x13 call listed below. With this BIOS call, you will be able to get the disk parameter and then read sector by sector to load more than 128 sectors.
- ah = 8.
- dl = drive number (as above).
- If successful:
- ah = 0
- al = undefined
- carry = 0
- ch = maximum cylinder number (lowest 8 bits of the 10-bit cylinder number, 0 based)
- cl bits 6 and 7 = cylinder number bits 8 and 9.
- cl bits 0-5 = maximum sector number, 1 to 63
- dh = maximum head number, 0 to 255
- dl = number of drives
- If failed (but at least one drive is active):
- ah = 7
- carry = 1
- al = cx = dx = 0
- If failed (because no hard disk exists):
- ah = 1
- carry = 1
- al = bx = cx = dx = es = 0