Page 1 of 1

microkernel debugging

Posted: Tue Jan 15, 2008 11:58 pm
by blound
I was asking in irc about how microkernels besides minix handle splitting of the kernel/driver/userspace and how any do on non-x86 since minix uses the rings to split the services. ( I only have looked at minix before - probably not the best thing ).

I was told by geist that most microkernels treat drivers as regular user processes ( do not split ) so that made me wonder how you could securely implement a debugger interface that allowed read/write/general tampering with of other processes since some user controlled processes would be running at the same level as the drivers. He told me to look at L4 and see how they do it and I found pages about l4kd ( l4 kernel debugger ) but they were just pages about how to use it - not how it is implemented.

I was hoping someone could explain/point me to some documents that explain further the l4 kernel debugger internals or how microkernels implement debugging interfaces in general.

Re: microkernel debugging

Posted: Thu Jan 17, 2008 8:25 am
by jal
blound wrote:or how microkernels implement debugging interfaces in general.
Yes, that would be interesting to know. Has anyone here implemented a kernel debugger yet, and care to share some knowledge?


JAL

Posted: Thu Jan 17, 2008 6:24 pm
by bewing
I'll third that question. It's the main thing that's keeping me from moving all my development onto my own OS -- I need a singlestepping debugger, and I haven't quite wrapped my head around the documentation yet -- to see the trick of it.

Posted: Sun Jan 20, 2008 9:32 am
by bewing
OK, since we aren't getting any suggestions :wink:, here is my brainstorming on the subject. Please add your thoughts, and maybe we can come up with a nice outline for how to make a generic debugger. I think I have to assume that the OS is not singletasking.

1. In general, there are 2 types of tasks running. There are "jobs", which are "supposed" to be protected from each other. And there are threads, which are little individual tasks that run as semi-independent sub-components of a job. Threads usually share memory with their parent job, whereas jobs have to go to some effort to share memory with each other.

2. There has to be some method of forking off new threads. To create a new job, you fork a thread, then the thread turns itself into a new job, by loading and executing a "job-type" app off the disk. A typical method is to use some form of a UNIX-like exec command.

3. When a user wants to debug an app, generally the debugger must be a second, independent application -- rather than something built into the kernel, or shell, or something. This allows the user to choose their favorite debugger, etc.

4. The way Intel built all their debugger stuff, "breakpoint" exceptions are generated while the targeted application is running. These presumably show up as "signal" events, which should be caught and handled.

5. (3) and (4) together mean that you have two apps that are supposed to be protected from each other, but you need signals from one app to be caught and handled by the other app.

6. The debugger app also needs to be able to access all of the user app's memory, to be able to properly display/modify/debug the memory of the user app.

7. (5) and (6) together seem to clearly mean that the user app must somehow be run as a *thread* of the debugger, rather than as an independent job.

8. (7) and (2) together seem to clearly indicate that there needs to be a special, secret way of calling the exec function. The exec function would do all the normal loading, but would not separate the exec'ed user app's memory space from the parent debugger app's memory space. It also probably leaves the parent debugger as the recipient of all system messages -- to be passed on to its child threads (including the user app) as the debugger chooses. Obviously, the debugger needs to remain in full control of all signal catching. Somehow, in fact, the debugger (as the parent job) needs to be able to completely override any subsequent "signal" calls made by the user app -- and then perhaps even simulate their function (the debugger cannot allow its signal catcher functions to be overridden by the user app)? The debugger parent function would set a breakpoint at the (system defined) load address of the user app, to start with. At that point, it could recover any other info it needed from the registers that got saved into a structure by the exception handler.

9. So, you have a debugger application that gets loaded into its own virtual memory space normally. Then you have a normal user app that expects to be loaded into an empty virtual memory space, that is actually getting loaded into the debugger's virtual memory space. This seems to clearly indicate that the debugger needs to relocate itself into a memory area that normal user apps would not be allowed to touch -- so that the two apps could coexist in the same virtual memory area without stomping on each other.

10. Not only does the debugger need to relocate itself in virtual memory, but it may also be necessary to temporarily replace bytes in the user app's executable with INT 3 breakpoint bytes. This means that the debugger must be able to somehow request that any write-protection on the executable code stored in memory be turned off, so that these "self-modifying code" techniques will work. (Of course, the debugger needs to be written to architecture-specific standards for self-modfying code.)

11. So, to make debuggers work, you probably need special features in exec(), and signal(), and you need some ability to manipulate the permissions in the debugger app's virtual memory area, to permit self-modifying code.

Re: microkernel debugging

Posted: Sat Feb 02, 2008 7:49 am
by rv6502
blound wrote: I was hoping someone could explain/point me to some documents that explain further the l4 kernel debugger internals or how microkernels implement debugging interfaces in general.
you mean debugging applications running under your OS or the kernel itself ?

if you want application debugging you can use GDB, aside from a full-blown port to run natively (which might be a LOT of work) you can use a "gdb stub" in your application to do soft-debugging as long as your OS allows self-inspection and self-manipulation of a thread's context (cpu registers)

you need to compile a gdb stub into your application and have it hook up to signal handlers (exception handlers) and communicate with GDB on a serial stream (tcp/ip or serial port) that way GDB can run on another machine.

you also need a matching copy of the elf file with debug info for gdb on the other machine. then you'll be able to debug quite comfortably.

google for "GDB stub", it also works for debugging your kernel.

Posted: Sat Feb 02, 2008 1:32 pm
by blound
bewing wrote:3. When a user wants to debug an app, generally the debugger must be a second, independent application -- rather than something built into the kernel, or shell, or something. This allows the user to choose their favorite debugger, etc.
You still need the kernel interface ( not matter your type of kernel ) for allowing you to debug (read/write) into another "job" as your terminology puts it.
bewing wrote:7. (5) and (6) together seem to clearly mean that the user app must somehow be run as a *thread* of the debugger, rather than as an independent job.
7 is wrong ... Both unix(ptrace) and windows({read,write}processmemory) allow you to debug independent "jobs" from start of process running or even attach to them later. http://msdn2.microsoft.com/en-us/librar ... 85%29.aspx

bewing wrote:9. So, you have a debugger application that gets loaded into its own virtual memory space normally. Then you have a normal user app that expects to be loaded into an empty virtual memory space, that is actually getting loaded into the debugger's virtual memory space. This seems to clearly indicate that the debugger needs to relocate itself into a memory area that normal user apps would not be allowed to touch -- so that the two apps could coexist in the same virtual memory area without stomping on each other.
8 is off since its based on 7. Also 9 is saying that the debugger and app being debugged have to the be in the same address space. This would be nice, but again with ptrace ( man ptrace ) on *nix systems this is not the case. ( I do not know about windows ). Instead on these ptrace systems, the kernel presents the ptrace system call that takes your parameters and writes into the address space of another process for you.
bewing wrote:This means that the debugger must be able to somehow request that any write-protection on the executable code stored in memory be turned off,
For changing mappings, unix provides mprotect and windows provides VirtualProtect

Posted: Sat Feb 02, 2008 3:09 pm
by mathematician
There are probably other ways of doing it, based upon using the debugging registers, but the two ways I know of by which a debugger can gain control from the program being debugged is firstly for it to temporarily replace the first byte of an instruction with the byte 0cch. When that is hit an int 3 is generated, and that is caught by the debugger. Before execution can resume the 0cch has to be replaced by the original byte. That is basically how break points are implemented, although it can also be used to step over sontware ints and procedure calls. Another way in which the debugger can gain control is to set the trap flag in the flags register, and that will cause an int 1 to be generated after every instruction, and the debugger catches that. So that the debugger itself does not single step the trap flag is automatically cleared after the int 1 is generated.

One thing you have to be careful of when you are using a debugger to debug an oerating system is that the OS either has to be completely re-entrant, so that system variables aren't changed when the debugger itself makes calls to the OS, or else the debugger has to implement its own mini operating system. (Switching, for example, from the OS's IRQ0 handler to its own handler, every time it gains control).

A slight caveat to the above is that the only debugger I have ever written was back in the days of MS-DOS, and it ran in real mode.

Posted: Sat Feb 02, 2008 3:59 pm
by bewing
Very interesting info. I was under the impression that the trap flag generated an INT3 rather than an INT1, but I haven't reread the manual in the last week. ;)

However, blound's original question seemed more specifically aimed at microkernels -- which neither UNIX or Windoze can claim to be. Specifically, if two jobs running on a system are supposed to be isolated and protected from each other, then how can Job1's signal handler catch signals from Job2, how can it modify and remodify Job2's code (swapping INT3 bytes with code bytes), and how can Job1 control the restarting of Job2 after the signal is caught?

Adding a debugging stub to every application sounds dangerous from an application security standpoint. But I suppose it is workable. I think my suggested methodology (of using a shared userspace) is more elegant.

Posted: Sun Feb 03, 2008 2:24 pm
by rv6502
bewing wrote: However, blound's original question seemed more specifically aimed at microkernels -- which neither UNIX or Windoze can claim to be. Specifically, if two jobs running on a system are supposed to be isolated and protected from each other, then how can Job1's signal handler catch signals from Job2, how can it modify and remodify Job2's code (swapping INT3 bytes with code bytes), and how can Job1 control the restarting of Job2 after the signal is caught?
for this you need the micro kernel to provide signal trapping and memory transfer or mapping services to the debugger process.

the debugged process gets flagged as being under the debugger's control and any signals to the debugged process gets rerouted as a special signal to the debugger where the debugger can then tell the kernel to signal the debugged thread (or not).

the debugger also has access to system calls to read/write the debugged process' memory (or map sections of it to its own address space, which comes down to the same thing) and read-write registers of the debugged thread(s)

you also need a system call so the debugger can tell the kernel to run the debugged thread until the next signal/taskswitch and then halt it again (sending a signal to the debugger again when the debugged thread halts).

so the debugger can run the debugged thread time-slice by time-slice (which might be a single instruction in the case the step flag was set).

you should also allow the debugger to trap all kernel system calls of the debugged thread.