Project Goals Part I - Learn Your Tools
This week's goal is to learn how to use your OS development tools and get used to working with low level memory commands. A sub-goal of this activity is to acclimate you to staring at hexadecimal numbers and gleaning meaning from them!
There won't be any gradeable output of this week's actions, but I would wager good money that if you don't do this, you'll probably have a very hard time with the remaining project parts.
Ok, so let's dive in!
Configuring Your Environment
In order to use gdb with the init files supplied with xv6, we will have to make a little configuration change to your account. Open up a terminal, and using your favorite text editor (vim, emacs, nano, etc.) open the following file:
You'll have to do this from a terminal as the SMC editor seems to freak out when I try to create this file. Add one single line to the file:
Save the file and exit. That's it! Your environment is now configured!
Compiling and Running xv6
You should have a copy of xv6 in your smc directory. If you want to run this on a local machine, just grab a copy of xv6 from its website https://pdos.csail.mit.edu/6.828/2012/xv6.html.
Change into your xv6 folder and build the operating system by simply typing:
After a while, it will complete the build of both the kernel and the user file system. Now, we want to run xv6 in the terminal. To do this, execute the command:
This will launch xv6 in the qemu machine emulator in tty mode. Note that qemu can run graphically if you are on your local machine. You aren't missing anything, however, because xv6 just sends the same text to both the terminal and the graphical console.
Now, you can run xv6 commands. Try an "ls" or two. Look at the very limited set of programs and try them out. The operating system itself is very sparse, but we would expect that because this is really meant to be used to learn how to write kernels.
The Emulated Hardware
As mentioned before, qemu is a hardware emulator. It emulates an Intel i386 based PC. This virtual PC can have many different attributes depending upon the command-line options specified. If you run it via the makefile (as we have done here), it emulates a PC with the following attributes:
- 2 i386 cores
- 2 Hard disks, one containing the kernel the other containing the user filesystem.
- 512MB of RAM
- No Graphics Card (Headless)
- One single serial terminal connected to COM1. (This is your SMC terminal)
The qemu Monitor
In addition to providing you with a running system, qemu also provides a monitor which allows you to control the system and inspect its state. Entering the monitor will halt the emulation and allow you to exam the registers and ram.
To Enter the qemu monitor: Type Control-a followed by c
Once you are in the monitor you will be greeted by a prompt:
Type "help" and press enter and you can see the list of commands available to you. As you can see, there are a lot of commands. Fortunately, we are only going to use a few of them. These are the key commands to master:
- info registers - Display the contents of the registers
- cont - Resume emulation (leave the monitor)
- x /fmt expr - Examine virtual address (virtual memory dump)
- xp /fmt expr - Examine physical address (physical memory dump)
- q - Quit qemu
The /fmt strings bear a little bit of explanation. These allow you to specify the length of a dump and what the basic unit of display are. They have a basic pattern of /[count][base][size]. For example, the following commands (which you should try now) will dump the first 10 bytes of memory in hex, then the first 10 bytes in decimal, then the first 10 words in hex, and the first 10 words in decimal
xp/10xb 0x00 xp/10db 0x00 xp/10xw 0x00 xp/10dw 0x00
If you leave off the base, qemu will default to whatever the previous base was. If you never specify it, it will start out in hex. Let's make sure to switch back to hex because decimal display is basically useless!
Another useful thing to do is look at characters in memory. This can be done like so:
Of course, this is fairly nonsensical as this is not meant to be string data! It's probably instructions! Let's see if we can decode them:
Much better. Also, note that the address don't rise by a constant increment in instruction mode. This is because x86 instructions have variable size. The addresses on the left hand side show the beginning address of each instruction.
Before we leave the monitor, let's take a look at the registers:
Now, play around with these commands, and when you are done, type
to resume emulation.
Now, go back into the monitor, then resume emulation. Do this a few times until you can move in and out of emulation and monitor. Once you have the knack for it, shut down the virtual machine by entering the monitor and executing the command
That last command is effectively turning the power off on your emulated machine!
Getting Symbol Addresses
Ok, so now we are going to look at how we can debug a kernel running in a virtual machine. In order to do this, we need to know which addresses to watch. That's where the nm command comes in. nm prints the addresses of symbols in a binary file. Let's try it!
That dumps the xv6 kernel symbol table. Memorize these numbers, an then we'll launch the debugger.
Of course I'm kidding! Let's focus on one address, the kernel's entry point.
nm kernel | grep _start
This should give you an address like "0010000c" Write down this address, and then we'll be good to go.
In the future, you can find addresses of functions using that "nm kernel | grep name" method.
Running gdb on xv6
Ok, so now we want to get some real control over the kernel. We're going to manually step through the startup procedure for the kernel. Before, we do that, however, we have to set up a situation where we have two screens, one for xv6 and one for gdb.
You could do this by creating a second terminal file, but I prefer to have both gdb and my os on the screen at the same time. To do this, we are going to use a terminal multiplexer: screen
screen - The Terminal Multiplexer
In your terminal, run the following command:
This will greet you with a message. Press enter to continue, and you'll be back to a normal looking shell. However, we are running in something very powerful! We won't cover all of how it works now, but basically you could launch multiple shells and switch between them or split the screen in various ways.
To issue a command to screen, you press the screen escape character. This defaults to Control+a (which is denoted C-a in screen speak). Let's split the screen by typing the following:
This means "press control + a, then release, and then press |" This will split the screen in two. Switch to the other pane by typing:
Now spawn a new shell over here by typing:
Now you have two shells running side by side! Awesome!
Screen can do a lot more, I'll leave it to you to figure out how. (There are manages and countless online tutorials about this thing.)
Switch back to the left hand pane before continuing.
Oh, you may be asking yourself something (if you've been paying close attention). Clearly, screen uses C-a, but then so does qemu! How can you get to the qemu monitor from within screen? Well the answer is that you can send the control character by typing:
So when running screen and running qemu inside of screen, you can enter the monitor with the sequence:
C-a a c
Now, on to gdb!
Starting xv6 with Debugging
To start xv6 in debug mode, do the following:
Then, switch to the other pane and run:
If it works, gdb will start and you'll see something like this:
The target architecture is assumed to be i8086 [f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b 0x0000fff0 in ?? () + symbol-file kernel (gdb)
Now, this is another tool with a lot of commands to master. You can type "help" to see a list. For our purposes though, these are the ones to master:
- info reg - Display register contents
- x /fmt - Examine memory (by virtual address)
- break - Set a breakpoint
- cont - Continue execution, until the next breakpoint
- next - Execute the current line of code, skipping sub routines
- nexti- Execute the next machine code instruction, skipping sub routines
- step - Execute to the next line of code, stepping into subroutines
- stepi - Execute the next machine code instruction, stepping into subroutines
- display - Display an expression with each new instruction
- print - Print an expression just once
- list - Print source code (if it's available for this location)
You may have noticed the gdb is waiting for you type something. qemu, in the left hand pane, is waiting for gdb to give it the go ahead. We're going to set a breakpoint so that it will stop after the bootloader enters the kernel code. Execute the following:
break * 0x0010000c
(This is the address I asked you to write earlier.) After you do that, type:
qemu will run until the program counter reaches 0x0010000c, and then gdb will stop it. Let's try out listing the C source code for this spot:
Ooooooh cool! Now play around with next, nexti, step, and stepi to get an idea for how that works.
Next, try using the x command. This works as it does in qemu's monitor. For example, to examine the first 10 instructions of the kernel, do this:
Continue to play around with gdb, get the hang of the commands. When you are done, close gdb by typing "quit" and then shutdown qemu.
Here's some stuff for you to try. For each one of these, start with qemu and gdb shut down and start up and use them.
- Find the address of fork and set the breakpoint to this address. Examine the stack during a fork call.
- Find other syscalls, and watch them happen.
- See if you can get the debugger to step into a userspace program.
- Compare the register listings of the qemu monitor with that of gdb. Why are they so different?