Documentation of the protected mode debugger
Introduction
Capabilities and Salient Features
System Requirements
Design and Code overview
This is a bootable protected mode debugger for intel 386 class of processors, meaning that it can boot off a floppy into a protected mode environment and provide the user with a setup where he can debug his own code . It was made as a part of CSD course project .It can also be used as an educational demo for an operating system , since most of it is designed as a mini kernel.The author had a great time building it , and learnt about the 386, memory management and writing device drivers for commonly used periphrals.
Here are some of the salient features and capabilities it offers :
- Is bootable from a floppy into protected mode , and it offers an interface similar to the venerable dos program debug .
- The user can load a program from a floppy from a user specified sector.
- The user can trace through the user program. He also has an option to turn off tracing.
- The user can specify breakpoints .
- The user can display his code,data and stack segments .
- The user can dump the contents of all the registers
- The user can change the register contents
- The user can change contents of the program memory
- The user has an option to run the program at the highest privelage level of 0 . , thereby giving him same rights as an OS , this can be useful to debug OS like code.
- The user can remap the segment descriptors and create additional entries into the IDT,GDT for specifying his own interrupts and also create his own segments , This could be useful in the following scenario : he could remap his DS to the video mem area , and hence print characters on to the screen.
- The interface offered to system resources like the keyboard , screen is very similar to unix , and a few of the system calls can be used . The architecture is very flexible to implement new syscalls.
- The user can add his own interrupt routines , by plugging in code into the the kernel and modifying the idt.
- He can Plug in code into the kernel at runtime , when this is used in conjunction with create a segment feature , He will be able to call this code from the user programs that he loads , if he is running with the priv level of zero.
- He can dump the contents of a segment into a floppy disk
- While tracing , disassembly of the next instruction to be executed shall be provided , also he can disassemble his own code.
- Lastly , this would be an excellent introduction to writing an OS for an undergraduate course
The debugger is designed to boot from 1.44'' floppy , the system should have a minimum of 4mb of memory . It should be an intel 386 or a higher processor. This was tested on Pentium 4,on Bochs and on a real PC. The floppy drive should be intel compatible (most of them are).
Development Environment and Languages Used
Module Description
Details on the debugger implementation
The code was developed on an intel 386 linux PC . Linux is a free OS , any one can download it off the web and use it . What's more it can run on the barest of hardware. Hence I chose linux over developing in dos and windows. (which require extensive support).
I have used C and assembly to code this. Assembly language is used primarily in the boot sector code ,interrupt handlers ,accessing the i386 structures like GDT , LDT etc. However a large part of the debugger is written in C.Because of the following reasons :
- C code is a lot more readable , easy to understand and maintain than asm .
- I do not use any libraries , even printf and scanf that I use are my own , so there is absolutely no overhead , no dependencies involved. The code uses pure ANSI C .
- Since a large part of the code , sets up appropriate data structures , I have used C because it is easier to interpret that as a struct , whereas in asm , it will look like some meaningless moving of bytes here and there.
I have used nasm and gcc to compile the code . Also sed is used to find some addresses of symbols in the kernel. These tools can be found on any linux distribution , and are downloadable from the net. To test the code , I have used a real PC and and an IA32 emulator called bochs
To undersand the code , you need to understand a bit of the following : - Protected mode intel 386 programming
The authoritative source for this would be Volume 3 of the intel 386 manual(Download).For a list of comprehensive online references , check out this
- Assembly coding , I have used nasm . For those who do not know , nasm is a linux clone for the Microsoft assembler , masm , available on dos systems.
- Periphral specific drivers information
- PIC: Or the programmable interrupt timer, This is useful to understand how interrupts are controlled .
- intel FDC controller : To understand how the floppy driver can be written.
- intel 8042 , keyboard controller
The debugger is chiefly composed of three main modules , namely - Boot Up Module
- Kernel Proper
- Device Drivers
- Debugger Module
.
Their details are given below .
Bootup Module details
This resides in boot subdirectory. The bootup module composes of the following files :
- bootsect.s : This is executed in real mode , and is found on the first sector of the floppy image ,It is placed by the bios at memory location 0x7c00 .It proceeds to load the kernel into the main memory using bios routines to read the floppy. It then transfers control to the setup routine , which prepares the system for switch to protected mode.
- setup.s : The setup code , sets up the initial gdt ,idt and enables the A20 gate to go into protected mode . The pic is then reprogrammed to shift the irq's to after the processor interrupts. The switch to protected mode is done by setting the 0th bit in CR0 register , then doing a far jump to reload the segment registers with appropriate protected mode values. Interrupts are disables , and the pic is asked to mask all interrupts .It then jumps to the kernel entry point , which is the Main function found in main.cxx .
Kernel Proper module details
This resides in kernel subdirectory. The kernel proper is incharge of initization and providing useful routines to the rest of the system. The following files are there in this modules :
- main.cxx : This is the kernel entry point , it sets up the IDT,GDT,LDT, Sets up the memory manager , initializes keyboard,console , floppy driver and other functions like printk etc which require initialization. The pic is then told to allow only keyboard interrupts , other interrupts like floppy and timer (pit) are enabled and disabled as needed. Also it initializes the kernel TSS . (which will be used later to transfer control to the kernel by the debugger interrupt). After doing all this , interrupts are enabled and the control is passed on to the debugger .
- gdt.cxx , idt.cxx ,tss.cxx : Initialization and helper routines for accessing the GDT,TSS,IDT .
- kalloc.cxx : the kernel page based memory manager , it handles dynamic allocation of memory in units of a page size (4k).
- printk.cxx : Implements the printk function , which prepares a string for display and passes it onto the console driver.
- scanf.cxx: Implements the scanf function , which reads a string from the console driver , and calls sscanf to parse it.
- int.s : This contains the various interrupt routines for floppy , debugger, keyboard , timer.
This is contained in the files like keyboard.cxx, floppy.cxx,timer.cxx in the kernel subdirectory. The int.s file in the same directory has the interrupt routines for floppy,timer,keyboard,debugger . All the interrupt routines do the following : - Save State
- Switch to kernel ds ,cs
- Call the main interrupt routine handler of that particular device , eg : doDebuggerInt() or the doKeyboardInterrupt() .
- Send an EOI to the pic , to unmask that interrupt
- Restore state and do an iret.
These interrupts are initialized in initIDT() in idt.cxx.Note that the debugger interrupt is explained below.
- Keyboard Driver :In file keyboard.cxx The keyboard driver is a simple circular queue , where when a key is pressed , the keyboard interrupt handler calls the enque method , and to retrieve a key , I just deque it.
- Console Driver : In file console.cxx , It does the following :
- Keeps track of the cursor position and scrolls if neccessary , it also keeps updating the cursor position in the video controller .
- The string to be printed is just put into the video memory starting at the current cursor location
- Floppy Driver : In file floppy.cxx , It does the following :
- Sets up dma for data transfer
- enables the floppy interrupt
- recalibrates the drive if neccessary
- Sends commands to the disk controller to read/write a sector
- Handles floppy driver errors , like wrong seeking etc , by retrying
It consists of the debugger.cxx file in the kernel subdirectory and the int 1 handler in int.s.The debugger essentially consists of three peices of code ,
- Command Input Loop : This essentially waits for user command by calling a gets function to the console driver and then acts on the command . It calls functions like run(),loadProgram(),setTraps(),setBreakPoint() etc , in response to the commands .
- Int 1 : Interrupt 1 is the debug exception interrupt that the processor raises in response to trace trap , breakpoint reached , int 3(break command) . It's code is contained in int.s, It basically does the following :
- Saves State on state
- Moves to the kernel ds and cs
- Saves the linear address of ss:esp into a variable , this then points to the saved state of the user program on the stack. And can later be used for modifying his registers .
- Calls a function doDebuggerInt
- Pops (restores) state followed by an iret.
- DoDebuggerRoutine This routine is contained in debugger.cxx . It switches the processor back to the kernel state , ie switches to the kernel , the details of this operation are described later.
Launching of a user program :
When the user does a load program , the following things happen : - The program is read from the specified floppy location
Memory is allocated for the program's stack ,code,data . The size of code segmented is equal to the size of the code on the floppy rounded to the nearest page size multiple. - One page is allocated for data and stack , this can be configured by the user .
- Once all the memory is allocated , the TSS of the program is created , in which the registers are initialized to 0, esp is initialized to PAGE_SIZE (explained below) , and the cs/ds/ss are assigned as the indexes into the GDT pointing to the code/data/stack memory allocated above.
- The TSS descriptor is created in the GDT , and is made to point to the TSS structure created above. Note that we allocate one PAGE_SIZE ( 4k ) bytes for the TSS structure , however the tss struct occupies only 104 bytes , so we use the lower part of this memory as a ring 0 stack ( this is the stack , the cpu will switch to if an interrupt occurs) , this explains why the esp is set to (PAGE_SIZE)
- To run the program , we simply do a task switch specifying this program TSS created above. This way the cpu automatically stores the kernel context (useful later )
Handling of a debugger interrupt in detail :
The doDebugerRoutine simply does a task switch to the saved kernel tss, this way the kernel resumes exactly where it left off , and later when the program eventually has to be run again , we just do task switch to the program TSS . Hence the program starts off by returning from the DoDebuggerInt routine , and the proceeds to complete the interrupt 1 . Note that the earlier saved context saved on the context may have been changed before it does an iret. (this is described above).