June 12, 2002
Icebox is a kernel-level debugger for kernel version 2.4 (it may work with other versions but is untested) on the x86 architecture. The code should be considered alpha quality, and in fact the debugger will probably not be very useful for you. In any case, the code here is available under the GPL and anyone is free to tinker, provided they don't expect any support from me. Icebox supports the following features:
The installation scripts assume that the running version of your kernel sources are in /usr/src/linux. If this is not the case, first edit /src/Makefile.am, changing the include path near the beginning of the file to your correct location. Then, run:
If /usr/src/linux is the correct location, skip those steps. Once you are ready to compile, run
to prepare the makefiles. You then to generate a header file for the current version of your kernel symbols. Use the following command:
./map2h < /usr/src/linux/System.map > src/kernel_symbols.h
Replace the first path with the version of System.map that matches your running kernel. It is imperative that this file matches your kernel or the debugger will probably crash and bring your system down with it.
Once that is done, you are ready to compile. Type 'make' to do so.
If it completes successfully, a file called 'debug.o' will appear in the src/ directory. This is the kernel module. You can install it directly into the running kernel by running 'make install'.
Before using icebox, you will need to enable the Magic SysRq capability so that the kernel can intercept certain hotkey combinations. Ensure that it was enabled during the compilation of the running kernel. If not, configure the kernel to enable support for Magic SysRq and recompile/reboot. Once the support is available, the capability is enabled with the following command:
echo 1 > /proc/sys/kernel/sysrq
Once that is complete, icebox can be started by pressing Alt-SysRq-d on a text- based console. The UI should appear. To quit icebox, press Alt-SysRq. If icebox is invoked while X-windows is on the screen, it will detect this condition and avoid starting. This is because it cannot take over the display gracefully from X without knowledge of the video card. If a breakpoint is encountered while X is running, it will be ignored.
Icebox supports two ways of command execution: the command line, and shortcut keys. The available shortcut keys depend on what state the UI is in. When it starts, you will be in command line mode. Type 'help' or press F1 to see a list of commands that can be typed in this mode. Some of these commands may also list a function key such as F2. These function keys can be pressed at any time in icebox to execute the corresponding command. The TAB key cycles between the three windows in icebox: the command line, the code/data view, and the topmost pane (call trace or IDT). In the top two windows, the arrow keys can be used to scroll up and down in the display. There are also extra commands available which will be listed in the lower right of the screen. For example, when in the code/data window, the 'b' key sets or unsets a breakpoint at the current line of code. In the call trace or IDT windows, the right arrow key takes you to the address listed on that line.
Here is a listing of the important source files and their contents:
I'll discuss some of the main accomplishments that went in to making this debugger work.
Taking over the kernel
The key to preempting the kernel is hogging an interrupt. When the SysRq key is pressed, the kernel calls the handler for that key during the keyboard interrupt routine. Other interrupts are masked during that handler, so as long as we don't return, the kernel cannot continue to execute, even if a hardware interrupt is generated. The same thing works for breakpoints and single-stepping, because those events generate int 3 and int 1, respectively. However, it was necessary to change those handlers (int 1 and int 3) from a "trap gate" to an "interrupt gate" so that other interrupts would be masked during their execution.
Keyboard and screen control
Since we have free roam to execute code as we please without the kernel taking over, we need to be able to do input and output in order to do anything useful. The key to output is that the VGA text frame buffer is mapped to linear address 0xB8000-0xBFFFF in memory. This seems to be true on all PCs with a VGA or better display. Thus, simply by writing bytes to that region, they will appear on screen. Every even byte is a character and every odd byte is the color information for that character. Thus, we can also make an attractive user interface with 16 foreground colors and 8 background colors (and blink). There is also an I/O port which we can write to to change the location of the cursor and the origin of the screen within the framebuffer memory.
The keyboard is fairly easy to take over as well. There is an I/O port containing status information for the PS/2 port. Reading this port will tell us whether a key event or mouse event is available. Since we are not using interrupts for key events, we have to poll that register for any new events. Once an event is available, we process it by ignoring mouse events (although that would be an interesting future addition) and translating key scancodes.
Breakpoints and single-stepping
Normal debuggers set breakpoints by overwriting a given instruction with the one-byte opcode 'int3'. Thus, whenever an int3 is generated, we can check the instruction pointer to see if we set a breakpoint there, and process it accordingly. That same technique works in icebox, we just have to be clever and take over the kernel's normal handler for int 1 and int 3. To do this, we overwrite the kernel's stub handler for these ints with the address of our new handler. In that new handler, we return control to the kernel if the breakpoint was not set by us. For details of how this works, see breakpoint.c.
Single stepping is easy to implement. We simply set the Trap flag in the saved copy of the eflags register. Then, each instruction will generate an int 1, which we can handle.
When encountering a breakpoint, we launch the debugger. After leaving the debugger, we replace the original opcode, and single-step over it. Then we put back the int3 instruction and resume.
If the debugger were to take over the VGA framebuffer when X is running, bad things happen (I tried it...it's bad). Thus, we detect the presence of X by sensing if the current console is in text or graphical mode. The details of this are in main.c. It would be nice if we could force a console switch and then spawn the debugger. Unfortunately, console switching requires some user-space code (the kernel lets X deal with the specifics of the video card). Thus, we can't do that in an interrupt handler. For now, debugger invocations just get ignored while in X.