C/C++ – Homework 4: Boot into C

This assignment will teach you to build a minimal bootable code that boots on real hardware into C. Technically, you can do this assignment on a
operating system that supports the Unix API and can run Qemu (Linux Openlab machines, your laptop that runs Linux or Linux VM, and even MacO
etc.). You don’t need to set up xv6 for this assignment, but if you’re running on Openlab you’ll have to install QEMU, see QEMU setup instruction
Submit your programs and the shell through Gradescope (see instructions at the bottom of this page).
NOTE: YOU CANNOT PUBLICLY RELEASE SOLUTIONS TO THIS HOMEWORK. It’s ok to show your work to your future employer as a private G
repo, however any public release is prohibited. For Mac / OSX users: the support of 32 bit applications is deprecated in the latest version of your syste
So if you already updated your system to MacOS Catalina or have updated your XCode then we recommend you to do the homework at the Openl
machines. This assignment explains how to create a minimal x86 operating system kernel using the Multiboot standard. In fact, it will just boot and pr
“Hello, world!” on the screen, and then print “Hello from C!” on the serial line from the C main() function. Most of this assignment is based on intermezzO
project.
Boot overview
When you turn on a computer, it loads the BIOS from some special flash memory. The BIOS runs self test and initialization routines of the hardware, then
looks for bootable devices. If it finds one, the control is transferred to its bootloader, which is a small portion of executable code stored at the device’s
beginning. The bootloader has to determine the location of the kernel image on the device and load it into memory. It also needs to switch the CPU to the
so-called protected mode because x86 CPUs start in the very limited real mode by default (to be compatible to programs from 1978). We won’t write a
bootloader because that would be a complex project on its own (we partially covered this in class since xv6 implements a simple boot loader with two files
bootasm.S and bootmain.c). Instead we will use one of the many well-tested bootloaders out there to boot our kernel from a CD-ROM.
Multiboot headers
Let’s get going! The very first thing we’re going to do is create a ‘multiboot header’. What’s that, you ask Well, to explain it, let’s take a small step back a
talk about how a computer boots up.
One of the amazing and terrible things about the x86 architecture is that it’s maintained backwards compatibility throughout the years. This has been
competitive advantage, but it’s also meant that the boot process is largely a pile of hacks. Each time a new iteration comes out, a new step gets added
the process. That’s right, when your fancy new computer starts up, it thinks it’s an 8086 from 1976. And then, through a succession of steps, we transiti
through more and more modern architectures until we end at the latest and greatest.
The first mode is called ‘real mode’. This is a 16 bit mode that the original x86 chips used. The second is ‘protected mode’. This 32 bit mode adds ne
things on top of real mode. It’s called ‘protected’ because real mode sort of let you do whatever you wanted, even if it was a bad idea. Protected mode w
the first time that the hardware enabled certain kinds of protections that allow us to exercise more control around such things as RAM. We’ll talk mo
about those details later.
The final mode is called ‘long mode’, and it’s 64 bits. Since our OS will only enter 32bit mode we’ll not touch 64bit ‘long mode’.
So that’s the task ahead of us: make the jump up the ladder and get to 32bit mode. We can do it! Let’s talk more details.
Firmware and the BIOS
So let’s begin by turning the power to our computer on.
When we press the power button, a bunch of low-level initialization protocols are executed: Management Engine, BIOS, etc.
With the BIOS we’re already in the land of software, but unlike software that you may be used to writing, the BIOS comes bundled with its computer and
located in read-only memory (ROM).
One of the first things the BIOS does is run a ‘POST’ or power-on self-test which checks for the availability and integrity of all the pieces of hardware th
the computer needs including the BIOS itself, CPU registers, RAM, etc. If you’ve ever heard a computer beeping at you as it boots up, that’s the POS
reporting its findings.
Assuming no problems are found, the BIOS starts the real booting process.
By the way…
For a while now most commercial computer manufacturers have hidden their BIOS booting process behind some sort of splash screen. It’s
usually possible to see the BIOS’ logs by pressing some collection of keys when your computer is starting up.
The BIOS also has a menu where you can see information about the computer like CPU and memory specs and all the hardware the BIOS
detected like hard drives and CD and DVD drives. Typically this menu is accessed by pressing some other weird collection of keyboard keys
while the computer is attempting to boot.
The BIOS automatically finds a ‘bootable drive’ by looking in certain pre-determined places like the computer’s hard drive and CD and DVD drives. A dri
is ‘bootable’ if it contains software that can finish the booting process. In the BIOS menu you can usually change in what order the BIOS looks for bootab
drives or tell it to boot from a specific drive.
The BIOS knows it’s found a bootable drive by looking at the first few kilobytes of the drive and looking for some magical numbers set in that drive
memory. This won’t be the last time some magical numbers or hacky sounding things are used on our way to building an OS. Such is life at such a lo
level…
When the BIOS has found its bootable drive, it loads part of the drive into memory and transfers execution to it. With this process, we move away fro
what comes dictated by the computer manufacturer and move ever closer to getting our OS running.
Bootloaders
The part of our bootable drive that gets executed is called a ‘bootloader’, since it loads things at boot time. The bootloader’s job is to take our kernel, pu
into memory, and then transition control to it.
Some people start their operating systems journey by writing a bootloader. For example, in class we started by looking at the xv6 bootloader that is load
by the BIOS at the 0x7c00 address. In this assignment we will not be doing that.
In the interest of actually getting around to implementing a kernel, instead, we’ll use an existing bootloader: GRUB.
GRUB and Multiboot
GRUB stands for ‘grand unified bootloader’, and it’s a common one for GNU/Linux systems. GRUB implements a specification called Multiboot, which is
set of conventions for how a kernel should get loaded into memory. By following the Multiboot specification, we can let GRUB load our kernel.
The way that we do this is through a ‘header’. We’ll put some information in a format that multiboot specifies right at the start of our kernel. GRUB will re
this information, and follow it to do the right thing.
One other advantage of using GRUB: it will handle the transition from real mode to protected mode for us, skipping the first step. We don’t even need
know anything about all of that old stuff. If you’re curious about the kinds of things you would have needed to know, put “A20 line” into your favorite sear
engine, and get ready to cry yourself to sleep.
Writing our own Multiboot header
I said we were gonna get to the code, and then I went on about more history. Sorry about that! It’s code time for real! You can download the entire fold
that contains skeletons for the homework files here or save it file by file. Inside your homework folder there is a file called multiboot_header.asm. Open it
your favorite editor. I use vim, but you should feel free to use anything you’d like.
$ vim multiboot_header.asm
This is a .asm file, which is short for ‘assembly’. That’s right, we’re going to write some assembly code here. Don’t worry! It’s not super hard.
An aside about assembly
Have you ever watched Rich Hickey’s talk “Simple vs. Easy” It’s a wonderful talk. In it, he draws a distinction between these two words, which a
commonly used as synonyms.
Assembly coding is simple, but that doesn’t mean that it’s easy. We’ll be doing a little bit of assembly programming to build our operating system, but w
don’t need to know that much. It is completely learnable, even for someone coming from a high-level language. You might need to practice a bit, and take
slow, but I believe in you. You’ve got this. A good manual on NASM assembler is here. The Magic Number
Our first assembly file will be almost entirely data, not code. Here’s the first line:
dd 0xe85250d6 ; magic number
Ugh! Gibberish! Let’s start with the semicolon (;). It’s a comment, that lasts until the end of the line. This particular comment says ‘magic number’. As w
said, you’ll be seeing a lot of magic numbers in your operating system work. The idea of a magic number is that it’s completely and utterly arbitrary.
doesn’t mean anything. It’s just magic. The very first thing that the multiboot specification requires is that we have the magic number 0xe85250d6 right at t
start.
By the way…
Wondering how a number can have letters inside of it 0xe85250d6 is written in hexadecimal notation. Hexadecimal is an example of a “numeral
system” which is a fancy term for a system for conveying numbers. The numeral system you’re probably most familiar with is the decimal
system which conveys numbers using a combination of the symbols 0 – 9. Hexadecimal on the other hand uses a combination of 16 symbols: 0 –
9 and a – f. Along with its fellow numeral system, binary, hexadecimal is used a lot in low level programming. In order to tell if a number is
written in hexadecimal, you may be tempted to look for the use of letters in the number, but a more surefire way is to look for a leading 0x. While
100 isn’t a hexadecimal number, 0x100 is.
What’s the value in having an arbitrary number there Well, it’s a kind of safeguard against bad things happening. This is one of the ways in which we c
check that we actually have a real multiboot header. If it doesn’t have the magic number, something has gone wrong, and we can throw an error.
I have no idea why it’s 0xe85250d6, and I don’t need to care. It just is.
Finally, the dd. It’s short for ‘define double word’. It declares that we’re going to stick some 32-bit data at this location. Remember, when x86 first started
was a 16-bit architecture set. That meant that the amount of data that could be held in a CPU register (or one ‘word’ as it’s commonly known) was 16 bi
To transition to a 32-bit architecture without losing backwards compatibility, x86 got the concept of a ‘double word’ or double 16 bits.
The mode code
Okay, time to add a second line:
dd 0xe85250d6 ; magic number
dd 0 ; protected mode code
This is another form of magic number. We want to boot into protected mode, and so we put a zero here, using dd again. If we wanted GRUB to
something else, we could look up another code, but this is the one that we want.
Header length
The next thing that’s required is a header length. We could use dd and count out exactly how many bytes that our header is, but there’s two reasons w
we’re not doing that:
1. Computers should do math, not people.
2. We’re going to add more stuff, and we’d have to recalculate this number each time. Or wait until the end and come back. See #1.
Here’s what this looks like:
header_start:
dd 0xe85250d6 ; magic number
dd 0 ; protected mode code
dd header_end – header_start ; header length
header_end:
You don’t have to align the comments if you don’t want to. I usually don’t, but it looks nice and after we’re done with this file, we’re not going to mess with
again, so we won’t be constantly re-aligning them in the future.
The header_start: and header_end: things are called ‘labels’. Labels let us use a name to refer to a particular part of our code. Labels also refer to t
memory occupied by the data and code which directly follows it. So in our code above the label header_start points directly to the memory at the ve
beginning of our magic number and thus to the very beginning of our header.
Our third dd line uses those two labels to do some math: the header length is the value of header_end minus the value of header_start. Becau
header_start and header_end are just the addresses of places in memory, we can simply subtract to get the distance between those two addresses. Wh
we compile this assembly code, the assembler will do this calculation for us. No need to figure out how many bytes there are by hand. Awesome.
You’ll also notice that I indented the dd statements. Usually, labels go in the first column, and you indent actual instructions. How much you indent is up
you; it’s a pretty flexible format.
The Checksum
The fourth field multiboot requires is a ‘checksum’. The idea is that we sum up some numbers, and then use that number to check that they’re all what w
expected things to be. It’s similar to a hash, in this sense: it lets us and GRUB double-check that everything is accurate.
Here’s the checksum:
header_start:
dd 0xe85250d6 ; magic number
dd 0 ; protected mode code
dd header_end – header_start ; header length
; checksum
dd 0x100000000 – (0xe85250d6 + 0 + (header_end – header_start))
header_end:
Again, we’ll use math to let the computer calculate the sum for us. We add up the magic number, the mode code, and the header length, and then subtra
it from a big number. dd then puts that value into this spot in our file.
By the way…
You might wonder why we’re subtracting these values from 0x100000000. To answer this we can look at what the multiboot spec says about the
checksum value in the header:
The field checksum is a 32-bit unsigned value which, when added to the other magic fields (i.e. magic, architecture and
header_length), must have a 32-bit unsigned sum of zero.
In other words:
checksum + magic_number + architecture + header_length = 0
We could try and “solve for” checksum like so:
checksum = -(magic_number + architecture + header_length)
But here’s where it gets weird. Computers don’t have an innate concept of negative numbers. Normally we get around this by using “signed
integers”, which is something we cover in an appendix. The point is we have an unsigned integer here, which means we’re limited to
representing only positive numbers. This means we can’t literally represent -(magic_number + architecture + header_length) in our field.
If you look closely at the spec you’ll notice it’s strangely worded: it’s asking for a value that when added to other values has a sum of zero. It’s
worded this way because integers have a limit to the size of numbers they can represent, and when you go over that size, the values wrap back
around to zero. So 0xFFFFFFFF + 1 is…. 0x00000000. This is a hardware limitation: technically it’s doing the addition correctly, giving us the
33-bit value 0x100000000, but we only have 32 bits to store things in so it can’t actually tell us about that 1 in the most significant digit position!
We’re left with the rest of the digits, which spell out zero.
So what we can do here is “trick” the computer into giving us zero when we do the addition. Imagine for the sake of argument that magic_number
+ architecture + header_length somehow works out to be 0xFFFFFFFE. The number we’d add to that in order to make 0 would be
0x00000002. This is 0x100000000-0xFFFFFFFE, because 0x100000000 technically maps to 0 when we wrap around. So we replace
0xFFFFFFFE in our contrived example here with magic_number + architecture + header_length. This gives us: dd 0x100000000 – (0xe85250d6
+ 0 + (header_end – header_start))
Ending tag
After the checksum you can list a series of “tags”, which is a way for the OS to tell the bootloader to do some extra things before handing control over
the OS, or to give the OS some extra information once started. We don t need any of that yet, though, so we just need to include the required “end tag
which looks like this:
header_start:
dd 0xe85250d6 ; magic number
dd 0 ; protected mode code
dd header_end – header_start ; header length
; checksum
dd 0x100000000 – (0xe85250d6 + 0 + (header_end – header_start))
; required end tag
dw 0 ; type
dw 0

💡 Buy the answer for only $12 Get it now →

; flags
dd 8 ; size
header_end:
Here we use dw to define a ‘word’ instead of just data. Remember a ‘word’ is 16 bits or 2 bytes on the x86_64 architecture. The multiboot specificatio
demands that this be exactly a word. You’ll find that this is super common in operating systems: the exact size and amount of everything matters. It’s just
side-effect of working at a low level.
The Section We have one last thing to do: add a ‘section’ annotation. We’ll talk more about sections later, so for now, just put what I tell you at the top of the file.
Here’s the final file:
section .multiboot_header
header_start:
dd 0xe85250d6 ; magic number
dd 0 ; protected mode code
dd header_end – header_start ; header length
; checksum
dd 0x100000000 – (0xe85250d6 + 0 + (header_end – header_start))
; required end tag
dw 0 ; type
dw 0 ; flags
dd 8 ; size
header_end:
That’s it! Congrats, you’ve written a multiboot compliant header. It’s a lot of esoterica, but it’s pretty straightforward once you’ve seen it a few times.
Assembling with nasm
We can’t use this file directly, we need to turn it into binary. We can use a program called an ‘assembler’ to ‘assemble’ our assembly code into binary cod
It’s very similar to using a ‘compiler’ to ‘compile’ our source code into binary. But when it’s assembly, people often use the more specific name.
We will be using an assembler called nasm to do this. You should invoke nasm like this:
$ nasm -f elf32 multiboot_header.asm
The -f elf32 says that we want to output a file as 32bit ELF.
After you run this command, you should see a multiboot_header.o file in the same directory. This is our ‘object file’, hence the .o. Don’t let the word ‘obje
confuse you. It has nothing to do with anything object oriented. ‘Object files’ are just binary code with some metadata in a particular format – in our ca
ELF. Later, we’ll take this file and use it to build our OS.
You can inspect the bytes of the header with hexdump
> hexdump -x multiboot_header.o
0000000 50d6 e852 0000 0000 0018 0000 af12 17ad
0000010 0000 0000 0008 0000
0000018
Summary
Congratulations! This is the first step towards building an operating system. We learned about the boot process, the GRUB bootloader, and the Multibo
specification. We wrote a Multiboot-compliant header file in assembly code, and used nasm to create an object file from it.
Next, we’ll write the actual code that prints “Hello world” to the screen.
Hello, world!
Now that we’ve got the headers out of the way, let’s do the traditional first program: Hello, world!
The smallest kernel
Our hello world will be just 20 lines of assembly code. Let’s begin. Open a file called boot.asm and put this in it:
start:
hlt
You’ve seen the name: form before: it’s a label. This lets us name a line of code. We’ll call this label start, which is the traditional name. GRUB will use th
convention to know where to begin.
The hlt statement is our first bit of ‘real’ assembly. So far, we had just been declaring data. This is actual, executable code. It’s short for ‘halt’. In oth
words, it ends the program.
By giving this line a label, we can call it, sort of like a function. That’s what GRUB does: “Call the function named start.” This function has just one lin
stop.
Unlike many other languages, you’ll notice that there’s no way to say if this ‘function’ takes any arguments or not. We’ll talk more about that later.
This code won’t quite work on its own though. We need to do a little bit more bookkeeping first. Here’s the next few lines:
global start
section .text
bits 32
start:
hlt
Three new bits of information. The first:
global start
This says “I’m going to define a label start, and I want it to be available outside of this file.” If we don’t say this, GRUB won’t know where to find
definition. You can kind of think of it like a ‘public’ annotation in other languages.
section .text
We saw section briefly, but I told you we’d get to it later. The place where we get to it is at the end of this chapter. For the moment, all you need to know
this: code goes into a section named .text. Everything that comes after the section line is in that section, until another section line.
bits 32
GRUB will boot us into protected mode, aka 32-bit mode (similar to how xv6 bootloader starts in 16bit real mode GRUB will be loaded by the BIOS and w
switch into protected 32bit mode for us). But we have to specify directly that assembler has to generate 32bit code. Our Hello World will only be in 32 bits
That’s it! We could theoretically stop here, but instead, let’s actually print the “Hello world” text to the screen. We’ll start off with an ‘H’:
global start
section .text
bits 32
start:
mov word [0xb8000], 0x0248 ; H
hlt
This new line is the most complicated bit of assembly we’ve seen yet. There’s a lot packed into this little line.
The first important bit is mov. This is short for move, and it sorta looks like this:
mov size place, thing
Oh, ; starts a comment, remember So the ; H is just for us. I put this comment here because this line prints an H to the screen!
Yup, it does. Okay, so here’s why: mov copies thing into place. The amount of stuff it copies is determined by size.
; size place thing
; | | |
; V V V
mov word [0xb8000], 0x0248 ; H
“Copy one word: the number 0x0248 to … some place.
The place looks like a number just like 0x0248, but it has square brackets [] around it. Those brackets are special. They mean “the address in memo
located by this number.” In other words, we’re copying the number 0x0248 into the specific memory location 0xb8000. That’s what this line does.
Why Well, we’re using the screen as a “memory mapped” device. Specific positions in memory correspond to certain positions on the screen. And t
position 0xb8000 is one of those positions: the upper-left corner of the screen.
By the way…
“Memory mapping” is one of the fundamental techniques used in computer engineering to help the CPU know how to talk to all the different
physical components of a computer. The CPU itself is just a weird little machine that moves numbers around. It’s not of any use to humans on
its own: it needs to be connected to devices like RAM, hard drives, a monitor, and a keyboard. The way the CPU does this is through a bus,
which is a huge pipeline of wires connecting the CPU to every single device that might have data the CPU needs. There’s one wire per bit
(since a wire can store a 1 or a 0 at any given time). A 32-bit bus is literally 32 wires in parallel that run from the CPU to a bunch of devices like
Christmas lights around a house.
There are two buses that we really care about in a computer: the address bus and the data bus. There’s also a third signal that lets all the
devices know whether the CPU is requesting data from an input (reading, like from the keyboard) or sending data to an output (writing, like to
the monitor via the video card). The address bus is for the CPU to send location information, and the data bus is for the CPU to either write data
to or read data from that location. Every device on the computer has a unique hard coded numerical location, or “address”, literally determined
by how the thing is wired up at the factory. In the case of an input/read operation, when it sends 0x1001A003 out on the address bus and the
control signal notifies every device that it’s a read operation, it’s asking, “What is the data currently stored at location 0x1001A003 ” If the
keyboard happens to be identified by that particular address, and the user is pressing SPACE at this time, the keyboard says, “Oh, you’re
talking to me!” and sends back the ASCII code 0x00000020 (for “SPACE”) on the data bus.
What this means is that memory on a computer isn’t just representing things like RAM and your hard drive. Actual human-scale devices like the
keyboard and mouse and video card have their own memory locations too. But instead of writing a byte to a hard drive for storage, the CPU
might write a byte representing some color and symbol to the monitor for display. There’s an industry standard somewhere that says video
memory must live in the address range beginning 0xb8000. In order for computers to be able to work out of the box, this means that the BIOS
needs to be manufactured to assume video lives at that location, and the motherboard (which is where the bus is all wired up) has to be
manufactured to route a 0xb8000 request to the video card. It’s kind of amazing this stuff works at all! Anyway, “memory mapped hardware”, or
“memory mapping” for short, is the name of this technique.
Now, we are copying 0x0248. Why this number Well, it’s in three parts:
__ background color
/ __foreground color
| /
V V
0 2 48 <- letter, in ASCII We'll start at the right. First, two numbers are the letter, in ASCII. H is 72 in ASCII, and 48 is 72 in hexadecimal: (4 * 16) + 8 = 72. So this will write H. The other two numbers are colors. There are 16 colors available, each with a number. Here's the table: | Value | Color | |-------|----------------| | 0x0 | black | | 0x1 | blue | | 0x2 | green | | 0x3 | cyan | | 0x4 | red | | 0x5 | magenta | | 0x6 | brown | | 0x7 | gray | | 0x8 | dark gray | | 0x9 | bright blue | | 0xA | bright green | | 0xB | bright cyan | | 0xC | bright red | | 0xD | bright magenta | | 0xE | yellow | | 0xF | white | So, 02 is a black background with a green foreground. Classic. Feel free to change this up, use whatever combination of colors you want! So this gives us a H in green, over black. Next letter: e. global start section .text bits 32 start: mov word [0xb8000], 0x0248 ; H mov word [0xb8002], 0x0265 ; e hlt Lower case e is 65 in ASCII, at least, in hexadecimal. And 02 is our same color code. But you'll notice that the memory location is different. Okay, so we copied four hexadecimal digits into memory, right For our H. 0248. A hexadecimal digit has sixteen values, which is 4 bits (for example, 0 would be represented in bits as 1111). Two of them make 8 bits, i.e. one byte. Since we need half a word for the colors (02), and half a word for the H (4 that's one word in total (or two bytes). Each place that the memory address points to can hold one byte (a.k.a. 8 bits or half a word). Hence, if our fi memory position is at 0, the second letter will start at 2. You might be wondering, "If we're in 32 bit mode, isn't a word 32 bits " since sometimes 'word' is used to talk about native CPU register size. Well, the 'word' keyword in the context of x86_64 assembly specifically refers to 2 bytes, or 16 bits of data. This is for reasons of backwards compatibility. This math gets easier the more often you do it. And we won't be doing that much more of it. There is a lot of working with hex numbers in operati systems work, so you'll get better as we practice. With this, you should be able to get the rest of Hello, World. Go ahead and try if you want: each letter needs to bump the location twice, and you need look up the letter's number in hex. If you don't want to bother with all that, here's the final code: global start section .text bits 32 start: mov word [0xb8000], 0x0248 ; H mov word [0xb8002], 0x0265 ; e mov word [0xb8004], 0x026c ; l mov word [0xb8006], 0x026c ; l mov word [0xb8008], 0x026f ; o mov word [0xb800a], 0x022c ; , mov word [0xb800c], 0x0220 ; mov word [0xb800e], 0x0277 ; w mov word [0xb8010], 0x026f ; o mov word [0xb8012], 0x0272 ; r mov word [0xb8014], 0x026c ; l mov word [0xb8016], 0x0264 ; d mov word [0xb8018], 0x0221 ; ! hlt Finally, now that we've got all of the code working, we can assemble our boot.asm file with nasm, just like we did with the multiboot_header.asm file: $ nasm -f elf32 boot.asm This will produce a boot.o file. We're almost ready to go! Linking it together Okay! So we have two different .o files: multiboot_header.o and boot.o. But what we need is one file with both of them. Our OS doesn't have the ability do anything yet, let alone load itself in two parts somehow. We just want one big binary file. Enter 'linking'. If you haven't worked in a compiled language before, you probably haven't had to deal with linking before. Linking is how we'll turn these tw files into a single output: by linking them together. Open up a file called linker.ldand put this in it: ENTRY(start) SECTIONS { . = 0x100000; /* Tells GRUB to load the kernel starting at the 1MB mark */ .rodata : { /* ensure that the multiboot header is at the beginning */ KEEP(*(.multiboot_header)) *(.rodata .rodata.*) . = ALIGN(4K); } .text : { *(.text .text.*) . = ALIGN(4K); } .data : { *(.data .data.*) . = ALIGN(4K); } .bss : { *(.bss .bss.*) . = ALIGN(4K); } } This is a 'linker script'. It controls how our linker will combine these files into the final output. Let's take it bit-by-bit: ENTRY(start) This sets the 'entry point' for this executable. In our case, we called our entry point by the name people use: start. Remember In boot.asm Same nam here. SECTIONS { Okay! I've been promising you that we'd talk about sections. Everything inside of these curly braces is a section. We annotated parts of our code w sections earlier, and here, in this part of the linker script, we will describe each section by name and where it goes in the resulting output. . = 0x100000; This line means that we will start putting sections at the one megabyte mark. This is the conventional place to put a kernel, at least to start. Below o megabyte is all kinds of memory-mapped stuff. Remember the VGA stuff It wouldn't work if we mapped our kernel's code to that part of memory garbage on the screen! .boot : This will create a section named boot. And inside of it... *(.multiboot_header) ... goes every section named multiboot_header. Remember how we defined that section in multiboot_header.asm It'll be here, at the start of the bo section. That's what we need for GRUB to see it. .text : Next, we define a text section. The text section is where you put code. And inside of it... *(.text) ... goes every section named .text. See how this is working The syntax is a bit weird, but it's not too bad. We do the same for the code and bss section. That's it for our script! We can then use ld to link all of this stuff together: $ ld -m elf_i386 -T linker.ld -o kernel.bin multiboot_header.o boot.o Recall that on Mac OS X you will want to use the linker we installed to ~/opt and not your system linker. For example, if you did not change any of t defaults in the installation script, this linker will be located at $HOME/opt/bin/x86_64-pc-elf-ld. By running this command, we do a few things: -m elf_i386 ask the linker to generate the 32bit. -T linker.ld This is the linker script we just made, we ask the linker to use it. -o kernel.bin This sets the name of our output file. In our case, that's kernel.bin. We'll be using this file in the next step. It's our whole kernel! multiboot_header.o boot.o Finally, we pass all the .o files we want to link together. That's it! We've now got our kernel in the kernel.bin file. Next, we're going to make an ISO out of it, so that we can load it up in QEMU. Making an ISO Now that we have our kernel.bin, the next step is to make an ISO. Remember compact discs Well, by making an ISO file, we can both test our He World kernel in QEMU, as well as running it on actual hardware! To do this, we're going to use a GRUB tool called grub2-mkrescue. We have to create a certain structure of files on disk, run the tool, and we'll get a hello.iso file at the end. Doing so is not very much work, but we need to make the files in the right places. First, we need to make several directories: $ mkdir -p build/isofiles/boot/grub The -p flag to mkdir will make the directory we specify, as well as any 'parent' directories, hence the p. In other words, this will make an build directory w a isofiles directory inside that has boot inside, and finally the grub directory inside of that. Next, create the grub.cfg file inside of that build/isofiles/boot/grub directory, and put this in it: set timeout=0 set default=0 menuentry "cs143Aos" { multiboot2 /boot/kernel.bin boot } This file configures GRUB. Let's talk about the menuentry block first. GRUB lets us load up multiple different operating systems, and it usually does this displaying a menu of OS choices to the user when the machine boots. Each menuentry section corresponds to one of these. We give it a name, in th case, cs143Aos

💡 Buy the answer for only $12 Get it now →