Bösen Bugs rückt der Applikations-Programmierer mit einem Debugger zu Leibe. Kernelhackern steht diese Möglichkeit nicht ohne weiteres offen. Zwar gibt es auch für den Linux-Kernel einen Debugger - doch weigert sich Linus Torvalds bis heute standhaft ihn offiziell in den Kernel zu übernehmen. Sein Hauptargument: Der Debugger lenke von der eigentlichen Fehlerursache ab, was dazu führe, dass eben nur Symptome, nicht jedoch das eigentliche Problem gefixt würden. Da zudem der Umgang mit dem extra erhältlichen Kernel-Debugger umständlich ist, haben sich die Programmierer daran gewöhnt, mit »printk()« Debug-Ausgaben einzubauen.
Wenn es um ladbare Module geht, ist Printk praktisch und einfach zu benutzen. Neue Printk-Statements sind schnell eingebaut und wieder entfernt. Das Kompilieren und Neuladen eines Moduls geht ebenfalls sehr schnell. Anders sieht es jedoch bei Code aus, der fester Bestandteil des Kernels ist. Hier muss der Kernel neu generiert und installiert werden. Noch schlimmer: Ein zeitaufwändiger Neustart des Systems ist notwendig.
Automatisierte Breakpoints
Findige Programmierer haben sich bereits vor drei Jahren eine alternative Debug-Methode für den Kernel ausgedacht: so genannte Kernel Probes (KProbes), die aus Kernel-Sicht automatisierte Breakpoints darstellen (auch Probe Points genannt). Damit kann offensichtlich auch Linus Torvalds leben: Er hat die KProbes jedenfalls fest in Kernel 2.6 aufgenommen.
Stößt die CPU bei der Abarbeitung auf einen solchen Punkt, ruft der Kernel dem Probe Point zugeordnete, vom Programmierer zur Verfügung gestellte Funktionen auf. Diese Funktionen protokollieren zum Beispiel durchgeführte Aktionen per »printk()« oder geben per »dump_stack()« einen Stacktrace aus. Allerdings ist es nicht möglich, die von einem Debugger bekannten interaktiven Kommandos zu verwenden. Innerhalb des Kernels wäre das ohnehin problematisch, da das Zeitverhalten des Kernels massiv durcheinander geriete.
Der Informatiker betrachtet ein Kernel Probe als Objekt. Es beherbergt neben der Breakpoint-Adresse (dem Probe Point) vier Methoden (Abbildung 1): die Pre-, Post-, Fault- und Break-Handler. Mit der Instanziierung des Objekts durch die Funktion »register_kprobe()« tauscht der Kernel den Maschinenbefehl (Opcode) an der Probe-Point-Adresse gegen den eines Debugger-Breakpoint aus.
Einstieg über Debug-Interrupt
Auf einer x86-Architektur lautet dieser Maschinenbefehl »INT 3« mit dem Opcode »0xcc«. Es ist der übliche Interrupt, um in einen Debugger zu wechseln (siehe Abbildung 2). Sobald der Prozessor auf diesen Breakpoint trifft, ruft er die Kernelfunktion »do_int3()« auf (Abbildung 3). Diese aktiviert den »pre_handler()«, schaltet Single-Stepping ein und setzt den Instruction Pointer auf den zuvor geretteten Maschinenbefehl, damit nach Beendigung des INT-3-Interrupts der ursprüngliche Maschinenbefehl abgearbeitet wird.
Dank Single-Stepping ruft der Kernel direkt danach die Funktion »do_debug()« auf, die schließlich den »post_handler()« aktiviert. Dann schaltet er das Single- Stepping wieder aus und setzt den Instruction Pointer auf den nächsten Opcode der ursprünglich abgearbeiteten Funktion. Tritt während des Ablaufs von Pre- oder Post-Handler ein Page Fault auf, ruft der Kernel den zu KProbe gehörigen Fault-Handler auf. Damit der Kernel diesen Page Fault unbeachtet lässt, sollte »fault_handler()« den Wert »1« zurückgeben.
|
In [1] sind insgesamt vier Methoden beschrieben, die die Kerneladresse einer Funktion bestimmen (hier vorgeführt am Beispiel des Systemcall »sys_setuid«):
-
Adresse der System-Map entnehmen: »grep sys_setuid
/usr/src/linux/System.map«
-
Adresse mittels »nm« aus dem Objektfile lesen:
»nm /usr/src/linux/vmlinux | grep sys_setuid«
-
Adresse über das Proc-Filesystem bestimmen: »cat
/proc/kallsyms | grep sys_setuid«
-
Adresse mit der Funktion »kallsyms_lookup_name()«
beziehen
|
Abbildung 1: Das KProbe-Objekt speichert verschiedene Komponenten, die den Ablauf des Programms beeinflussen, zum Beispiel den »pre_handler« und den »post_handler«, die vor respektive nach der Unterbrechung ablaufen.
Die Break-Funktion »break_handler()« rundet die Methodensammlung ab. Stößt der Kernel innerhalb der Probe-Funktion auf einen weiteren Breakpoint, ruft er diese Funktion auf. Normalerweise gibt sie eine »1« zurück: das Zeichen für den Kernel, den Breakpoint unbeachtet zu lassen. Der Post-Handler besitzt übrigens keinen Rückgabewert (»void«). Dagegen sollte der Rückgabewert der Pre-Handler-Funktion grundsätzlich »0« sein.
Um als Entwickler oder als neugieriger Administrator KProbes zu nutzen, muss die entsprechende Option beim Kernel eingestellt sein. Ob dies der Fall ist, lässt sich - ist »Kernel .config support« vorhanden - mit Hilfe von »zcat /proc/config.gz | grep CONFIG_KPROBES« leicht feststellen: Wer als Ergebnis »CONFIG_KPROBES is not set« erhält, muss seinen Kernel neu bauen (Abbildung 4). Er sollte außerdem sicherstellen, dass die Option »CONFIG_CALLSYMS« eingeschaltet ist.