Dynamic Function Call Tracing
Patrick Kirsch
20. Februar 2006
Inhaltsverzeichnis
1 Einleitung 4
1.1 Abstract 4
1.2 Situationsbeschreibung 4
1.3 Materialauswahl 5
1.4 Zielbeschreibung des dynamic function call tracings - fctrace 5
1.5 Präzisierung des Projektes 5
1.5.1 Vaterprojekt 6
1.5.2 Vergleiche der bis dato vorhandenen Linux Trace Projekte 6
2 Wurm 8
2.1 Theorie 8
2.1.1 User-Space 9
3 KProbes 10
3.1 Funktionsweise 10
3.1.1 KProbe Struktur 10
3.1.2 Registrierung einer KProbe 11
3.2 Abhandlung einer KProbe 12
3.3 Vorteile von KProbes 13
4 Interrupts im Linux Kernel 14
4.1 Softwareinterrupts 14
4.1.1 Darstellung im Linux Kernel 15
4.1.2 Interrupt 3 16
5 Darstellung der Programme im RAM 17
5.1 lineare Adressierung 17
5.2 Logische und physikalische Adressierung 17
5.2.1 Adressberechnung 19
6 Prozess Speicherabbilder 22
6.1 Prozessspeicher Organisation 22
6.2 Speicherabbild 22
6.2.1 ELF 23
6.3 ABI 23
6.3.1 ABI 23
6.3.2 ABI Sprung-Arten 24
2
7 Realisierung 26
7.1 One Shot Trace 26
7.1.1 Sprungadressen 26
7.1.2 Userland Proben 27
7.2 Dateien 28
7.2.1 fct funktions adressen.pl 28
7.2.2 fct nasse erde.c 28
7.2.3 fct wurm.c 28
7.2.4 fct wurm kprobe.h 29
7.3 Abschliessende Betrachtung 29
7.3.1 letzter Stand 30
8 Abbildungen 34
3
1 Einleitung
1.1 Abstract
In der folgenden Projekt III Arbeit soll die Möglichkeit der Erweiterung des Linux Tracings um die Fähigkeit Funktionen dynamisch zurück zu verfolgen, untersucht werden. Dabei soll auf vorhandene Open-Source Projekte zurückgegrien und eine Machbarkeitstudie angefertigt werden. Ziel ist es Aussagen über eine generelle Implementierung sowie mögliche Risiken zu treen. Die Theorie sieht einenWurm vor, der mithilfe einer vorherigen Programmcode-Analyse mögliche Sprungpunkte erkennt und diese manipulieren kann. Dabei sollen die Kriterien der Plattformunabhängigkeit und der einfachen Maintenance vorrangig sein.
1.2 Situationsbeschreibung
Mit den gröÿer und komplexer werdenden Systemen wird die Anforderung nach hoher Qualität, hoher Entwicklungsgeschwindigkeit und Sicherheit immer deutlicher[1]. Dieser Umstand verlangt ein Werkzeug, das Analysen schnell und einfach erstellt als auch nicht vorhergesehenes Verhalten visualisieren kann. Bevorzugter gedachter Einsatzbereich ist das Rapid Application Learning. Es soll einem erfahrenem, mit grundlegendem Wissen der Programmiersprache C ausgestattetem Benutzer die Möglichkeit gegeben werden, Parameterübergaben auf der Funktionsebene zu sehen. Dennoch ist dieses Projekt streitbar, da ein Teil der Zielgruppe den zu observierenden Quellcode entweder selbst geschrieben hat oder ein sehr detailliertes Wissen über den Kernel besitzt und benötigte Informationen aus dem Stack-Dump lesen kann 1 .
1 http://216.239.51.104/search?q=cache:BCri1pIBLYgJ:kroah.com/ persönlicher Blog, mit persönlicher Meinung von Greg Kroah-Hartman einem renomiertem Kernel Developer
4
1.3 Materialauswahl
Dieses Gebiet der Sichtbarmachung von Abläufen mit übergebenen Strukturen/Parametern ist, im Zuge der Nachvollziehbarkeit (Debugging), Bestandteil des sich rasch entwickelnten Kernel Software Entwicklungszyklusses. Für dieses Projekt ist vor allem der Linux Kernel Quellcode in der Version 2.6.9 verwendet worden. Durch diese Vorarbeit und dem Vorzug des Open Sources ist es möglich und auch unabdingbar, den bestehenden Code weiter zu verwenden und Redundanzen damit zu vermeiden. Einen maÿgeblichen Einuss haben vergleichbare etablierte Projekte wie SUN's Dtrace und KProbes gehabt. Neben dem Quellcode sind Teile der Intel x86 Prozessor Dokumentation sowie die Fachliteratur Understanding the Linux Kernel 2nd Edition in die Theorie eingebunden. An die Mailingliste von http://kernelnewbies.org 2 wurden kernelspezische Fragen gestellt.
1.4 Zielbeschreibung des dynamic function call
tracings - fctrace
Ziel ist es, nicht nur die begrenzte Anzahl der System Calls sondern auch die Function-Calls (einer beliebigen Ausführbaren) zu sehen. Genau das ist in bisher keinem Linux Trace Tool realisiert. Dabei nutzt man den Vorteile des Open Sources: der vorhandene einsehbare Quellcode. Der gcc-Compiler kennt nämlich bei der Übersetzung des Programms die möglichen Wegsprungadressen. Diese werden im Zusammenhang mit einem wurmartigen dynamischen Einpanzen von Interrupt-Traps benutzt, um einen Aufruf vollständig nachzuverfolgen. Damit ist das Projekt zwischen dem GNU Debugger und dem strace einzuordnen. Mit dem wesentlichem Vorteil, gegenüber dem GNU Debugger, auch Abläufe in Kernelmodulen sichtbar zu machen.
1.5 Präzisierung des Projektes
Die Zielbeschreibung innerhalb des gegebenen zeitlichen Rahmens umzusetzen, ist nicht realisierbar. Nach Absprache mit dem Betreuer Herrn Olaf Dabrunz wurde im erstem Schritt die Projektdokumentation mit einer Implementation, vergleichbar des strace, vereinbart. In diesem Sinne, dass nach der Übergabe der Startfunktionsadresse nur die
2 Alias: kernelnewbies@nl.linux.org
5
Aufrufe der gerufenen Funktion im syslog 3 sichtbar ist. Während der Meilensteinplanung waren schon folgender Knackpunkt als nicht schabar ersichtlich: der Quellcode Parser[1]. Aus dem derzeitigen Standpunkt wird dieser für die eindeutige Ermittlung der Funktions-Parameter benötigt. Der Parser müsste einen ähnlichen Funktionsumfang wie der Quellcode-Parser des gcc-Compilers haben 4 .
1.5.1 Vaterprojekt
SUN's DTrace ist ein dynamisches Analyse Tool, allerdings nur auf SPARC, x86 und x86-64. Das DTrace (Dynamic Trace) wurde auf der Granularität des Prozesses ausgerichtet. Diese Implementierung dient dazu einen schnellen, aussagekräftigen sowie sich selbst erklärenden Überblick auf einem Produktionssystem zu erhalten. Ausgehend von dem dynamic tracing module [5] möchte man mit diesem Projekt ein ähnlich hilfreiches Tracing Werkzeug für Linux bereitstellen. SUN's DTrace ermöglicht unter SOLARIS X das dynamische Beobachten von User bzw. Kernel Level Software. Ist DTrace nicht aktiviert so stellt sich der zero-probe Eekt ein. Das System verhält sich dann genau so, als ob überhaupt kein DTrace geladen wäre. DTrace stellt im SOLARIS Kernel mehr als 30.000 abfragbare Informationenspunkte zur Verfügung [6]. Die Aufbereitung der gesammelten Daten wird durch eine C-ähnliche Sprache mittels thread-lokalen Variablen sowie assoziativen Arrays unterstützt. Eine Portierung des Solaris DTrace erscheint zu diesem Zeitpunkt für nicht möglich. Das Problem stellt der Lizenzkonikt dar. Das komplette Betriebssystem Solaris X einschlieÿlich dem Modul DTrace wurde unter CDDL [7] veröentlicht. Der Linux-Kernel restriktive unter der GPL [2]. Hintergrund ist, dass für die Portierung des DTraces GPL Quellcode benutzt werden muss. Gemäÿ der General Public License muss dann das entstehende Modul wiederum unter der GPL veröentlicht werden. Aufgrund der Inkompatibilität der beiden Lizenzen sind die Folgen eines Verstoÿes durch Portierung nicht abzusehen. SUN's DTrace dient hiermit lediglich als gedankliche Grundlage sowie als theoretisches Ziel.
1.5.2 Vergleiche der bis dato vorhandenen Linux Trace Projekte
Die Untersuchung der bisher bekannten Tracing Projekte ergab, dass entweder hartkodierte Tracing Funktionen oder der prozessor-spezische Breakpoint Mechanismus benutzt wird. Fest im Quellcode verankerte Debug Funktionen kennzeichnen PTrace
3 siehe die Log-File: /var/log/messages
4 vgl. Kapitel Sprungarten 7.0.3
6
(kernel/ptrace.c). Der Nachteil dieser Methodik ist ihre permanente Anwesenheit im Quellcode des Kernels, der damit verbundene Overhead (Beispiel: Platzhalter sowie Aktivierungs-Abfragen), aber auch die Starrheit (nur vorher gespickte Funktionen können überwacht werden).
Einzig das DTrace auf der SPARC Plattform hat die performanteste Lösung. Durch das Einsetzen eines Branch-Always Befehls (RISC-Besonderheit) ist man in der Lage, den heraus gelösten Assembler-Quellcode an einer anderen (Speicher-)Stelle auszuführen 5 . Andere bedeutende Arbeiten sind: Linux Trace Toolkit (LTT), K42, Kerninst, Dprobes, Dtrace.
Der LTT zeichnet sich durch die eine statische Implementation sowie durch eine fest gelegte Anzahl von Proben aus. Durch die geringe Anzahl der Proben soll auch der Per-formanceverlust in Grenzen(<2,5%) gehalten werden. Es besteht die Möglichkeit nach bestimmten Prozessen/Usern/Gruppen-ID zu ltern und sich bestimmte Daten aufbereitet anzeigen zu lassen.
Das IBM Projekt K42 stellt den Ansatz des Tracings durch einen universitären Research Kernel bereit. Zurzeit ist der Quellcode nur für PowerPC-Linux verfügbar. Geplant wurde der Kernel für large-scale applications und für 64bit Power Prozessoren. Ansonsten ähnelt K42 dem LTT, in der statischen Festlegung der Proben im Quellcode, konkret wird in jeder interessanten Funktion ein Trap vermerkt.
Kerninst ist ein kompletter Framework, bedient sich der INT3 6 Traps auf der x86 Architektur. Insbesondere bedient man sich nicht der Kernel-Notier-Chain, sondern man sieht in der Interrupt Descriptor Table nach, ob der Aufruf ein INT3 war. Dadurch kann sich das Kerninst noch vor die Abarbeitung des DProbes setzen. Potenziell unsicher scheint dabei die Berechnung der Rücksprungadresse zu sein.
5 abstrahiert betrachtet ähnelt das einer Trampolin Funktion
6 Interrupt Nummer 3, prozessor spezisches Funktionsbyte, siehe 4.1.2
7
2 Wurm
Der Wurm besteht aus dem Erkennen der möglichen Wegsprungadressen, tauschen des ersten Bytes durch ein INT3, beim Erreichen des Instruction Pointers ein Zurückschreiben des originalen Codes, ein Single Stepping (Ausführung der einen Instruktion) über den einen Operationscode. Das raupenartige Setzen von Breakpoints in einem Ausführungspfad kennzeichnet diesen Mechanismus. Der Wurm setzt allerdings nur auf KProbes auf. Es ndet in verminderten Maÿen keine Codeduplizierung statt. Vorerst soll durch die Implementierung des Wurms die Funktionalität des strace auf der Funktionsebene nachgebildet werden. Das angesprochene Verhalten bedingt die Kenntnisse des Aufbaus von Programmen im Speicher. Hinsichtlich der Schichten: Kernelseitige Strukturierung des Speichers und Organisation der ausführbaren Programme. Dieses Wissen setzt voraus, das mittels der Umwandlung von logischen linearen Adresse in die physikalische Adresse umgegangen werden kann. Das letztere Problem ist gleichzeitig das interessanteste.
2.1 Theorie
Für eine Implementierung des Wurms wird ein Kernelmodul benötigt, welches ein Proc-Interface 1 zur User-Kommunikation bereitstellt. Das zu testende Beispiel soll ein Kernelmodul sein, das drei Funktionen implementiert und diese mittels Export_Symbol() im globalen Namensraum 2 verfügbar macht. Es wird ebenfalls ein Proc-Interface benötigt, dass eine Schalterfunktion einnimmt. In Folge der Aktivierung soll der Aufrufablauf im syslog erkennbar sein.
Durch diese getroene Vereinfachung kann das Proof-of-Concept gezeigt werden. Der Wurm setzt voraus, dass er alle möglichen 3 Funktionen und deren Subfunktionen kennt. Dies läÿt zwei Möglichkeiten der Realisierung zu. Die Eine ist, der Wurm bekommt eine Startadresse und liest direkt im Speicher die gerufenen Funktionen aus und die Andere ist, der Wurm kennt vor dem Aufruf alle möglichen anspringbaren Funktionen.
1 eine dedizierte Datei innerhalb des /proc Verzeichnisbaumes
2 besser: Adressraum 3 anspringbaren
8
Der Wurm verliert an Dynamik, denn es muss der gesamte Kernelmoduldump 4 an das /proc/fctrace übertragen werden. Der Wurm registriert zuerst alle Kernelfunktionen die in der main()/init_module stehen. Bei dem Aufruf einer Funktion fängt der Wurm an, die bekannte Call's bzw. Subfunktionen mit Proben zu versetzen. Zu dem Zeitpunkt ist nicht bekannt, welche davon gerufen werden wird. Wird eine der Subfunktionen gerufen, muss die Vater-Funktion unregistriert werden 5 .
2.1.1 User-Space
Das beschriebene Verfahren setzt ein User-Space-Programm zur Erst-Fütterung des Wurms voraus. Dieses Skript muss dem Wurm mitteilen, welche Vater-Funktionen welche möglichen Sohn-Funktionen aufrufen kann. In diesem Sinn müssen alle möglichen Ausführungspfade vorwärtsgerichtet nachgebildet werden. Durch die Dierenzierung in Vater- und Sohnfunktionen müssen dem angedachten Proc-Interface zur Kernelseitigen Kommunikation einige Steuersignale ergänzt werden. Die Benutzung eines Proc-Interfaces zum einseitigen Datenaustausch erkauft eine Plattformunabhängigkeit, aber auch einen Performanceverlust. Unausgesprochenes Ziel ist, diesen Overhead zu sparen, in dem man nicht beim Start, sondern beim Eintreen des Interrupts zu dem ASM-Quellcode der gerufenen Funktion springt und diesen Quellcode auswertet. Es entsteht ein Overhead durch die Verwaltung der Sprungadressen. Generell stellt sich die Frage nach einem schnellen Zugri auf den entsprechenden Sprungpunkte 6 . KProbes verwaltet seine gesetzten Proben mit den Hash-Funktionen des Linux Kernels. Die KProbes interne Probenverwaltung zu nutzen wäre nur bedingt vorteilhaft, denn der Wurm soll nicht nur momentan gesetzte Proben kennen 7 .
An dieser Stelle wird ersichtlich, dass die Wurmlogik in dem pre_handler der struct_kprobe erfolgen sollte. Gemäÿ dieser Formulierung lässt sich das Projekt in die Eckpunkte gliedern: Erweiterung des KProbes, Interruptabarbeitung, Speicherorganisation und Binär-formate.
4
Ausgabe von
objdump -d
5 Hierbei unterschlägt man gewisse Fälle, zum Beispiel: Rekursion 6 tabellenartige, hashbasierende Organisation
7 ein Erweiterungs-Patch für die Proben-Verwaltung bedarf den Zuspruch der KProbe Entwickler
9
3 KProbes
Für die geforderte genaue Untersuchung wurde die bestehende, im Linux Kernel integrierte Kernel-Probes (KProbes)[17] benutzt. KProbe ist ursprünglich für OS/2 (unter dem Namen: Dynamic Probe) entwickelt, dann für Linux portiert und erweitert worden. Anknüpfen will man an die Möglichkeit, dynamische breakpoint-basierende Kernel Proben während der Laufzeit einzupanzen und zu entnehmen. KProbes ist im Moment nicht in der Lage automatisiert Funktionen zu tracen. Es kann lediglich in der System-Map erzeugte System-Calls überwachen [18]. Damit ist es nicht in der Lage einen automatisierten, vollständigen Programmuss unter der Anzeige aller Funktionsaufrufe wiederzugeben.
3.1 Funktionsweise
Die Grundlage für den Wurm ist die Implementation der Kernel Probes im Linux Kernel. Demzufolge wird auf die wichtigsten Funktionen und Abhandlungschritte eingegangen. Interessant sind die KProbe Struktur und die Funktionen der Registrierung einer Probe.
3.1.1 KProbe Struktur
Die Kprobe Struktur enthält die Attribute, die einer Probe wie folgt zugeordnet sind:
Listing 3.1: Ausschnitt aus dem Source 2.6.9 include/linux/kprobes.h struct kprobe {
Wie man aus dem Quellcode erkennen kann, wird diese Probe einem Listeneintrag zu-geordnet. Die Originalinstruktion, die logische Proben-Adresse sowie der Zeiger sind auf der zu rufenden Funktion gespeichert.
3.1.2 Registrierung einer KProbe
Ausgehend von der Struktur möchte ich nun den Ablauf der Registrierung einer Probe beleuchten. Wie im Anhang [siehe Quellcodedatei fct_wurm_kprobes.h bzw. Sektion 7.2.4] ersichtlich, wird eine Adresse an die Funktion kprobe_init übergeben. An dieser Stelle werden alle benötigten Initialisierungsschritte unternommen. Diese sind: Zuweisen der pre_handler, post_handler sowie fault_handler Funktionen (ab Zeile 8) sowie der zu probenden Stelle. Die Handler-Funktionen dienen der einseitigen Interaktion (Kernel ⇒ User). Diese Funktionen werden unmittelbar vor bzw. nach dem gesetzten Interrupt ausgeführt.
In [kernel/kprobes.c] bendet sich die Funktion register_kprobe, diese nimmt die Sicherung der IRQ-Flags vor (spin_lock_irq_save zeigt nach include/asm-i386/system.h
11
local_irq_save()). Dabei wird der eventuell vorhandenen Multiprozessor-Maschine Tribut gezollt, denn in der [include/linux/spinlock.h] wird der Aufruf spin_lock_irq_save nach Symmetric-Multi-Processing Maschinen dierenziert. Die register_kprobe prüft auf das Vorhandensein der angeforderten Probe. Dabei sei angemerkt, dass das Suchen innerhalb der KProbes (internen) Listen optimal mit dem im Linux Kernel integriertem Hashverfahren implementiert ist (get_kprobe benutzt hlist-Funktionen aus der [include/linux/hash.h]). Arch_copy_kprobe kopiert die komplette Anweisung, die sich an der angegeben Adresse bendet. Die beiden folgenden Quellcodezeilen zeigen die Kernkomponente des KProbes, an dieser Stelle wird die Probe mittels des architekturspezischen Breakpoint-Codes gesetzt und der Originalwert kopiert. Diese Zuweisung verdeutlicht auch, dass keine Einschränkung (an dieser Stelle) hinsichtlich der linearen Adresse gemacht wird. Somit eignet sich das KProbes zum Setzen von frei denierbaren unabhängigen Proben.
3.2 Abhandlung einer KProbe
Um den Einblick in KProbes zu vervollständigen, wird der Ablauf nach dem Auslösen einer Probe geschildert. Der dazugehörige Quellcode bendet sich im architekturabhängigen Teil von Kprobes (e.g. arch/i386/kernel/kprobes.c). Nach dem der Interrupt 3 mittels der in der IDT 1 beschriebenen do_int3 Funktion ausgelöst wurde, folgt der Aufruf des kprobe_handlers. Der kprobe_handler sorgt für die Ausführung des mit der KProbe assozierten pre_handler Funktion. Die kprobe_handler Funktion deaktiviert die Preemption 2 , sieht nach ob eine Probe mit derselben Adresse vorhanden und gesetzt ist (ein Abbruch erfolgt, wenn die Breakpoint-Anweisung keiner Probe zugordnet ist), führt den Single-Step über die Anweisung aus. Das Single Stepping besteht aus dem zurück kopieren der Original Instruktion, Deaktivierung des Trap Flags, Setzen des Instruction Pointers auf den zurückgeschriebenen Operationscode. Dieser beschriebene Schritt sichert die Integrität des Originalprogrammes, da jetzt die ursprüngliche Handlungsabfolge ausgeführt wird. Es folgt der Aufruf des mit der KProbe assozierten post_handler Funktion. Die allgemeine post_kprobe_handler Funktion ruft die resume_execution Funktion. Deren Aufgabe es ist, den durch den Interrupt veränderten Stack wieder herzustellen. Die drei behandelten Fälle sind im Quellcode als Kommentar gut beschrieben, der Fall 1 absolute bzw. relationale Sprünge werden im Abschnitt 7 ABI
1 Interrupt Deskriptor Table, siehe
2 Fähigkeit eines Betriebssystems die Abarbeitung eines Prozesses zu unterbrechen
12
genauer erklärt. Nach der Wiederherstellung des Stacks wird die Preemption aktiviert und die KProbes internen Locks gelöst. Das Programm kann fortgesetzt werden.
3.3 Vorteile von KProbes
KProbe ist SMP sicher. Es verwendet eine Überwachung von schon gesetzten Breakpoints, damit wird einer Schleifenbildung vorgebeugt (realisiert durch die Deaktivierung der Preemption 3 ). Es ist bereits plattformunabhängig realisiert. Eine Auseinandersetzung mit dem Bytecode verschiedener Architekturen muss nicht mehr erfolgen. Der beschriebene Ablauf besitzt einen Single-Step-Mechanismus, der den veränderten Stack wiederherstellen kann. Optimal ist KProbes durch die Bestätigung des oziellen Einsatzes im 2.6.9 Linux Kernels.
3 über die spin_lock Funktion
13
4 Interrupts im Linux Kernel
In diesem Anwendungsfall sind synchrone, von Software erzeugte, Interrupts relevant. Die Intel Dokumentation[9] teilt die Exceptions generell in Prozessor- und Softwareereignis. Dabei wird eine Unterteilung der Prozessor-Exeption anhand des Wertes von EIP 1 in Fault,Trap und Abort vorgenommen. Das Softwareereignis ist eine explizite Anforderung/Aufruf einer Exception. Dieser Softwareinterrupt wird als Trap implementiert (Beispiel: System-Calls benutzen Interrupt 0x80). Für dieses Projekt ist die Klasse der Traps interessant. Traps zeichnen sich durch die zeitnahe Ausführung der Trap-Instruktion sowie des damit verbundenen Programmestücks (bzw. Task) aus. Interessant sind in diesem Sinn auch die EFLAGs Register 2 . Im Besonderen das RF 3 -Bit, dieses wird benutzt um loops durch gesetzte Instruktion-Breakpoints zu vermeiden. Benötigt wird das Bearbeiten dieses Flags, da Interrupts auf der IA-32 Plattform nicht reentrant 4 sind[10]. An dieser Stelle wird nicht auf Hardware Interrupts 5 eingegangen.
4.1 Softwareinterrupts
Softwareinterrupts stehen an der Abarbeitung aller verfügbaren Interrups nicht an aller erster Stelle: gemäÿ Tabelle Abbildung: 'Prioritäten zwischen den Interrupts' an Stelle 4. Diese können nur durch einen Reset-Befehl, den Trap eines Task-Switches und einem externen, hardware generiertem Interrupt überlagert werden 6 . Der hohe Stellenwert des Softwareinterrupts unterstützt die Methodik des dynamischen Einpanzens von Breakpoints. Zum Beispiel sind NMI 7 , Page Faults, Invalid OpCodes 8 oder Overows niedriger priorisiert (theoretisch sieht man zuerst den Zugri, über den Wurm, und dann den
1 Extended Instruction Pointer, IA-32 spezisch
2 prozessorspezisch, zur Initialiserung des Prozessors benötigt 3 Resume Flag
4 wiedereintrittsfähig, Vermeidung von globalen Variablen 5 asynchron 6 zum Beispiel die FLUSH-Anweisung 7 Nicht maskierbare (besser: abfangbare) Interrupts 8 Operation Code
14
Fehler. Dies garantiert die feste Abarbeitungsreihenfolge für den Wurm, zuerst soll der Zugri und dann erst der Fehler protokolliert werden.
4.1.1 Darstellung im Linux Kernel
Wie gelangt man vom Setzen des magischen INT3 zu der Funktion, die das Logging übernimmt? Die Koordination der Interrupts erfolgt über die globale IDT 9 . Diese Tabelle klärt den Zusammenhang: Interrupt ⇒ Prozessor ⇒ Funktion. Folgend werden die Schritte aufgezeigt, die benötigt werden, um zu der eigentlichen eingeplanzten Funktion zu kommen.
Die globale IDT wird in dem IDT-Register festgehalten und kann damit an einer beliebigen logischen Adresse stehen. Der Prozessor, der den Interrupt bearbeitet, kann über den Oset das entsprechende Interruptgate rufen. In unserem konkreten Fall ist das das Gate für den Interrupt Nummer 3, der wie aus dem Bild zu erkennen, bei der Basisadresse + 16 Byte liegt. Auf dem Bild 'IDT Gates' erkennt man den konkreten Aufbau aller Task Gates. Hierbei ist wiederum nur das Trap Gate für uns von Bedeutung. Aus diesem Bild und aus dem Hinweis, dass dem Prozessorkern jeweils nur einmal Interrupts mitgeteilt werden, schlieÿt man, dass eine korrekte Initialisierung der Speicherstrukturen notwendig ist.
9 Interrupt Deskriptor Table
15
4.1.2 Interrupt 3
Der Interrupt Nummer 3 gehört zur Exception Klasse Trap, ist ein Byte lang (OperationCode '0xcc' für Intel Architektur 32) und wird benutzt, in dem man im Speicher eine Anweisung mit dieser Breakpoint Exception austauscht. Dies hat zur Folge, dass die Register CS 10 und EIP, die auf die nächste Anweisung zeigen, auf den Stack gelagert werden[11]. Um die Interrupt-Handler Prozedur ausführen zu können, erhält der Pro-zessor den Interruptvektor (also die Interruptzahl, in unserem Fall die 3, per Software do_int3 [12]). Mit Hilfe der Interrupt Nummer kann der Prozessor, über den Index (als Oset), den Eintrag in der IDT bestimmen. Da der Interruptvektor 3 ein Trap Gate ist, wird die entsprechende Funktion wie Prozeduraufruf behandendelt (quasi wie CALL Gate[?] die angesprochenen Adressen werden direkt geladen). Zwecks Vereinfachung wird an diesem Punkt die vom Prozessor ausgeführten Privilegienvergleiche (CPL mit DPL aus dem IDT-Trap Gate) unterschlagen 11 .
Kernel Control Path
Erreicht ein Interrupt Signal ein Prozess, so unterbricht die CPU dessen Ausführung und beginnt mit der Sicherung des Programm-Counters (Register CS und EIP) in den Kernel Stack. In die Register wird dann die Adresse relativ zum ausgelösten Interrupttyps geschrieben. Der beschriebene Ablauf ähnelt dem eines Prozess-Switches. Der Vorzug des Interruptes ist, dass der Interruptcode direkt in die nicht veränderte Stack-Umgebung des Prozesses geschrieben und ausgeführt wird [arch/i386/kernel/entry.S common_interrupt]. Diese eingesetzte Methotik Kernel Control Path ist schneller als ein kompletter Prozesswechsel.
10 Codesegment
11 Der Wurm muss mit Root-Rechten ausgeführt werden, damit wird der ausgeführte Vergleich (3>0)[8] immer wahr wird
16
5 Darstellung der Programme im
RAM
Daten die den Prozess Adressspeicher beschreiben, stehen in der Struktur mm_struct [include/linux/sched.h]. Interessant ist hierbei die unsigned long start_code und end_code Denitionen, diese enthalten die entsprechenden logischen Adressen, die den ausführbaren Quellcode enthalten. Die Struktur mm in der task_struct zeigt auf den Memory Deskriptor des jeweiligen Prozesses. Active_mm zeigt auf den Memory Deskriptor des Prozesses während seiner Ausführung. Diese beiden Strukturen sollten für normale Prozesse den gleichen Pointer enthalten.
5.1 lineare Adressierung
Die Struktur mm_struct enthält alle Speicherregionen, die einem Prozess zugeordnet sind. Realisiert wird das durch eine verlinkte Liste, implementiert durch die Struktur vm_area_struct. Die Struktur vm_area_struct enthält in sich die Strukturen vm_area_struct vm_next. Die Übersicht wird durch die Struktur mmap in task_struct bewahrt. Das folgende Bild fasst das Zusammenspiel der Strukturen erkennbar zusammen: Fragt ein User-Space Programm nach Speicher, so bekommt es diesen nicht prompt, sondern es erwirbt erst einmal einen Adressbereich innerhalb der linearen Adresse (bekannt als 'memory region').
5.2 Logische und physikalische Adressierung
In dieser Sektion wird deutlich, dass die Plattformunabhängigkeit und die Portabilität verloren geht. Das Abfragen der Register muss architekturspezisch abgekapselt werden. Wie auf dem Bild 'Segment Oset' zu erkennen, unterteilt man die Adressierung in lo-
17
gische Adressen 1 , lineare Adressen 2 und die physikalische Adresse 3 . Die logische Adresse besteht aus Segment 16bit und Oset 32bit. Speziell auf dem Intel sind die Register CS-Code Segment SS-Stack Segment DS-Data Segment bedeutsam. Jedes Segment wird durch einen 8 Byte langen Segmentdeskriptor-Eintrag beschrieben. Dieser Deskriptor steht in der GDT bzw. in der LDT 4 . Die Register bauen sich wie folgt auf: Interessant sind die Code-Segmentregister, da direkt Einuss auf den auszuführenden Code genommen werden soll. Nun wurde gesagt, dass die logische Adresse aus 16bit Segmentwahl + 32bit Oset besteht, allerdings im Deskriptor nur 32bit hinterlegt werden. Die Segmentierung wird benutzt um die lineare Adresse (dem Adressbereich des Prozessors) in kleinere handelbare sowie beschützbare Bereiche zu zerlegen. Dieses Bild 'Segmentatierung und Paging' verdeutlicht die Einordnung bzw. die Verwendung der linearen Adresse. Interessant ist der rechte Teil des Bildes, die Schritte der Berechnung von der linearen Adresse zu der physikalischen Adresse. Schlieÿlich ist es das Ziel, Programme auÿerhalb ihres Kontextes (wenn diese nicht im Status running sind) mit INT3's zu bestücken. Wie man sieht, besteht die logische Adresse aus Segment und Oset. Die Segmente werden vom Prozessor intern verwaltet und sind nicht extern beschreibbar [14]. Im weiteren Verlauf spielt die Segmentverwaltung des Intel Prozessors in diesem
2 32 Bit, universale Adresse
3 Intel x86 32bit, Adressierung für 4GByte
4 die Adressen der globalen Register sind bei x86 aus den Registern ldtr und gdtr zu entnehmen
18
Dokument keine Rolle und wird als 'magic' abgetan 5 . In der GDT wird der Oset wie in Bild 'Segment Oset' gespeichert. In dem linearen Adressraum spricht man damit eine bestimmte Page an, Instruktionen werden als Teilelemente eines Blockes gespeichert. Es soll verdeutlicht werden, dass die lineare Adresse eine Abstrahierung ist, die benötigt wird, um zum Beispiel lückenlos Swap-Speicher einzublenden. Wenn das Paging aktiviert ist, teilt man die Segmente in z.B. 4KByte groÿe Pages. Dabei muss das Betriebssystem das Page Directory und die Page Table verwalten. Unter Angabe dieser beiden Informationen (also der linearen Adresse) und der prozessorinternen Segmentdarstellung wird die physikalische Adresse berechnet. Das bekannteste Beispiel ist das Page Fault: der Versuch des Zugris eines Prozessors auf eine nicht im RAM vorhandene Page, in dem Fall muss das Betriebssystem diese Page manuell nachladen.
5.2.1 Adressberechnung
Um nun eine physikalische Adresse zu berechnen werden, zwei Stufen der Adressumsetzung benutzt: die logische Segment Adressierung und das linear adressierte Paging. Die exakte Zerlegung der linearen Adresse wird gemäÿ Bild 'Lineare Adress Umsetzung' vorgenommen. Die Bits 22 bis 31 beschreiben den Oset zu einem Eintrag im Page Direc-tory. Die Bits 21 bis 12 beschreiben den Page Table Eintrag. Die restlichen 12 Bits (also 2 12=4KByte) adressieren das konkrete Byte im real vorhandenen Speicher. Die Aufgabe des Speichermanagaments ist es die Page Directorys für alle Prozesse zu verwalten. Nützlich scheint auch das Task State Segment (siehe Bild: 'Task State Segment'). Hier sieht man den Task-Kontext der bei einem Task Switch gesichert wird. Dabei hilft das Register CR3 6 weiter. Zusammen mit dem Aufbau der Page Directory, der Page Table und dem EIP kann man die Einträge bestimmen, welche im Ergebnis eine physikalische Page im RAM bestimmen.
Umsetzung. Im vorigen Absatz wurde die theoretische Berechnung hergeleitet. Im konkreten Fall muss eine Verknüpfung zwischen dem Task Deskriptor und dem TSS gesucht und benutzt werden. Verfolgt man einen Prozess, kann die task_struct als Ausgangspunkt benutzt werden: Task_struct deniert eine Struktur struct_mm, die beeinhaltet einen Verweis auf pgd_t dem Page Global Directory. Zusammenfassend kann aus den start_code und end_code aus der mm_struct mit dem Register CR3 die physikalische Speicherstelle berechnet werden, bei der der Wurm
5 mit dem Verweis auf die prozessorspezische Intel Dokumentation,
http://www.intel.com/design/Pentium4/documentation.htm 6 beeinhaltet die physikalische Adresse des Page Global Directory
19
seine ASM-Quellcode Analyse beginnen kann. Folgendes Problem trat bei dem Versuch der Realisierung auf, folgende Abfrage: 'current->thread.cr3' funktioniert im Kernel 2.6.9 nicht mehr 7 . Damit fällt dieser Weg zur Ermittlung des CR3 Registers weg. Ein anderer Weg ist die Ermittlung des CR3 über die Funktion switch_mm() [asm-i386/mmu_context.h], die die Funkion load_cr3 ruft. In dem Rumpf wird eine Dierenz zwischen der übergebenen Adresse der PGT und dem Oset des Kernels gebildet. Prozessiterierung. Ein Randproblem bei der Realisierung ist die Zuordnung Speicherstelle - Prozess. Die einfachste Lösung ist: alle Prozesse der Reihe nach durchzusuchen.
20
6 Prozess Speicherabbilder
6.1 Prozessspeicher Organisation
Als Aufgabe gilt es zu zeigen, wie der Compiler den Quellcode hinterlegt. Technisch ist die PID eines Prozesses der Oset der logischen Adresse. Die Prozessliste ist eine doppelt verkette Liste. Der Hardware Kontext einen Prozesses wird in der TSS (Task State Segment) abgelegt, realisiert wird dies durch die 'tss_struct'-Struktur. Der Linux-Kernel sieht nur ein TSS je Prozess vor[15], deswegen ist das 'busy' Bit immer 1. Die TSSD werden in der GDT gespeichert 1 , das Register TR beinhaltet den Selektor für das jeweilige TSS Element in der GDT. Librarys werden mit in den Prozesskontext einbettet. Prinzipiell führt der Systemcall 'execve()' eine Executable zum Start des gewählten Programms. Der Ablauf beinhaltet die Änderung der GID und EGID, das Laden der Ausführbaren in den Speicher und das verlinken der shared Librarys mit dem Binärcode der Executable. Das Linken spielt eine groÿe Rolle bei Programmen. Es gilt zu prüfen ob diese drei Arten des Linkens problematisch für den Wurm werden könnten. Die drei Arten des Linkens sind: statisch, dynamisch und Laufzeit[19].
6.2 Speicherabbild
Für Speicherabbild gilt: Programme werden segmentiert im Speicher abgebildet. Der vorhandene lineare Adressraum wird partitioniert in: Text, Data und Stack Segment. Das Text Segment enthält den ausführbaren Code. Das Data Segment unterteilt sich in initialisierte und nicht inititalisierte. Das initialisierte Segment enthält die mit Werte belegten globalen sowie statischen Variablen. Das nicht initialisierte Segment (auch als BSS, historischer Herkunft) enthält die globalen Variablen, deren Werte erst zur Laufzeit bestimmt werden. Das Stack Segment sichert den Stack, der zum Beispiel die return Adressen, Parameter, programmlokale Variablen der gerufenen Funktionen sichert.
1 siehe GDT-Register
22
6.2.1 ELF
Das standardisierte ELF (Electronic Linker Format) benutzt man, um den binären Programmcode zu strukturieren bzw. um Parameter zu beschreiben. Die Startadresse eines auszuführenden Programmes ist nicht frei wählbar, diese hängt vom Format (bina-ry_handler) des auszuführenden Prozesses ab. Der initial auszuführende Code[16] ist nicht relokierbar, weil dieser Mutmaÿungen über Lokation der Variablen treen muss. Globale initialisierte Variablen zum Beispiel werden an bestimmten Stellen vermutet. Speziell für das ELF-Format ist die erste auszuführende Instruktion an der linearen Adresse 0x08084000 erwartet[22].
Folgt man der Idee, die Sprungadressen vor dem Start einer Applikation herauszulesen, so ist die Funktion load_elf_binary [linux/fs/binfmt_elf.c] zu beachten. Die Initialiserung der mm_struct erfolgt über 'current->mm->start_data = 0;'. Ausgehend von der C-Routine execve(), welche der einzigste Linux System Call ist mit dem Programme ausgeführt werden können, wird folgender gekürzter Pfad wie folgt gegangen. Aus 'linux/fs/exec.c' wird ersichtlich, dass Speicher für den Binary Handler allokiert wird 'bprm = kmalloc(sizeof(*bprm), GFP_KERNEL);', die Datei nicht im Moment geschrieben wird 'le = open_exec(lename);', 'retval = prepare_binprm(bprm);'[linux/fs/exec.c] prüft nach den Nutzerrechen (UID, GID) und kopiert die ersten 128 Byte (die Magic 2 )des Programms in die buf-Variable 'memset(bprm->buf,0,BINPRM_BUF_SIZE);'. Die Funktion prepare_binprm probiert anhand der gespeicherten Magic alle registrierten Binary Handler aus. Der Einfachheit halber wird festgelegt, dass der Binary Handler load_elf_binary[linux/fs/binfmt_elf.c] ist.
6.3 ABI
Das System V Application Binary Interface (ABI) deniert eine Schnittstelle mit dem kompilierte Programme auf einem System V kompatiblen System ausgeführt werden können. Die Schnittstelle liegt im binär Format vor.
6.3.1 ABI
Bei dem Erstellen eines Prozesses werden logische Datei-Segmente (e.g. text,data,stack) in ein Speichersegment abgebildet. Dabei werden erst die physischen Seiten im RAM allokiert, wenn eine logische Referenz innerhalb des Programm-Codes erfolgt. Dadurch
2 umgangssprachlich für die Identikationsnummer des Formates
23
vermeidet man unnötiges physikalische Lesen im Speicher. Diese Ezienz kann nur gewährleistet werden, wenn die Segmente der shared librarys sowie des ausführbaren Code Vielfache der Page Size sind und die benannten Segmente kongruent sind.
6.3.2 ABI Sprung-Arten
Folgende Call Arten sind in der Intel 386 ABI deniert:
• absoluter direkter Funktionsaufruf,
• positions-unabhängiger direkter Funktionsaufruf,
• absolut indirekter Funktionsaufruf,
24
• positions-unabhängiger indirekter Funktionsaufruf
Diese Auszüge zeigen die mögliche Assembler-Darstellung von Funktionsaufrufen. Der Wurm sollte diesen Standard nutzen und verarbeiten können. Von der vollständigen Erkennung aller Sprungsequenzen im fortlaufenden Programmuss hängt der Wurm schlieÿlich ab. Wie erkennbar, realisieren Programme Funktionssprünge durch die 'call' Instruktion. Mit 'call' kann man den EIP(i386 Instruktion Pointer) soweit verändern, das dieser an jede beliebige Stelle innerhalb der linearen Adresse springen kann. Dies gilt insbesondere auch wenn der angesprungene Code in shared librarys liegt. Kann das Aussehen der ABI das Verhalten des Wurmes beeinussen? Ja, wenn Funktionssprünge anders deniert werden. Solange aber eine Binärkompatibilität (x86 und x86-64) besteht sollte zumindest an dieser Stelle keine Probleme auftreten.
25
7 Realisierung
Das Kapitel behandelt einige ausgewählte Themen aus Verlauf der Realisierung.
7.1 One Shot Trace
Im ersten Schritt der Erstellung des Prototypen wurde versucht den Groÿteil in Assembler zu verwirklichen. Dieser Ansatz landete schnell in der Sackgasse. Die Idee erwies sich als unbrauchbar, da ein Prozess nicht problemlos gezielt in den Speicher eines anderen schreiben kann. Weiterhin fehlt dem Ansatz das Prinzip der Sperrung vor Preemption und die Multiprozessorsicherheit. Dies hat den Vorteil der Schnelligkeit, man verliert aber dadurch die Plattformunabhängigkeit und einen Teil der Wartbarkeit. Letztendlich wurde die Entscheidung getroen auf die Optimierungseigentschaften des Compilers zu vertrauen und den Weg über die Kernel-Notier-Chain zu gehen. Der mitgelieferte Quellcode soll eine Diskussionsbasis bereitstellen. Es wird in diesem Kernel-Modul eine Speicheradresse von einem beliebigen System-Call hart kodiert. Der System-Call kann aus der Systemmap (nicht mehr standardmäÿig ab 2.6.x beim Kernelbuild aktiviert) oder aus '/proc/kallsyms' gewonnen werden. Der strukturelle Ablauf sieht vor: Setze an gegebene Speicheradresse ein INT3, benutze die notier-Warteschlange des Kernels für die Benachrichtigung, tausche den INT3 Befehl mit dem vorher ausgelesenen Wert aus, setze den Instruction Pointer auf die Speicheradresse des System-Calls. Der Aufruf wird nun ohne Unterbrechung abgearbeitet.
7.1.1 Sprungadressen
Als erste erkennbare Schwierigkeit stellt sich das Herausbekommen der Informationen über die Wegsprungadressen dar. Der gcc-Compiler benutzt zur Optimierung die RTL (Register Transfer Language 1 ), dieser plattformunabhängiger Pseudo Assembler Code[20] könnte erweitert werden. Ziel wäre es bei der Ausgabe der Debuginforma-
1 Bestandteilder GNU C-Compiler Collection
26
tionen die Sprungadressen (die Call Instruktionen[21])als Kommentar anzuzeigen. Es wurde bei einem Test 2 deutlich, wenn der Kernel als Grundlage dient entsteht ein Groÿ an Informationen. Ein schlüssiges Konzept zur performanten Verwaltung muss noch erarbeitet werden. Neben der RTL gibt es die einfachere Möglichkeit: die Ausgabe des dissasembliertem Binärcodes zu parsen.
7.1.2 Userland Proben
Bei der Entwicklung des Beispiel Kernel Moduls drängt sich die Frage auf, wie kann man als externer Prozess/Handlungsfolge innerhalb des Speichers eines beliebigen Prozesses lesen/schreiben ohne wichtige Qualitätsmerkmale(Performanz, Stabilität) zu verletzen. Ziel des Kernels ist es die Speicherbereiche zweier Prozesse nicht überlappen zu lassen. Jeder Prozess kann die lineare adressierte Speicherstelle 0xb988 lesen und schreiben, die genannte Adresse wird durch das Kernelinterface in eine andere physikalisch vorhandene Speicheradresse abgebildet. Der saubere Weg wäre also ein erzwungener Task Switch(über die Funktion schedule()). Diese Variante stellt sich aber als sehr unpraktikabel und unperformant für dieses Projekt heraus. Der Beweis liegt im Quellcode vor(schedule() in kernel/sched.c, bzw. das Exekutive switch_to in asm-i386/system.h). Der Blick in die Quellcodedateien bescheinigt einen intensiven Aufwand beim Wechsel einer Task. So wird beim Context Switch des Prozessors die gesamte Prozessumgebung hergestellt (siehe context_switch() in sched.c).
Eine Betrachtung wert ist die Frage, ob man Tracing im User und Kernelspace trennen sollte. Für die Trennung spricht die Möglichkeit, den zu tracenden Prozess als Childprozess zu starten. Dieser Vorteil spiegelt sich in dem Zugri auf den Speicher des zu überwachenden Programmes wieder. Eine einfach zu realisierende Möglichkeit wäre alle programmexternen Sprünge anzuzeigen (siehe Mapping des gcc-Compilers). Ebenfalls vereinfachen würde sich der Datenaustausch zwischen Status zur Breakpointaulösung und z.B. der Anzeige in einer Shell. Die dazu verwendeten Datenstrukturen könnten entlang der Prozesserblinie geteilt werden, ein umfangreich gesichertes Locking ist vermieden. Das Minimalgerüst besteht aus einem Ausführungspfad, der sich anordnet: aus dem Setzen der Sicherung des Originalen Operationscodes, dem Setzen der Breakpoint Anweisung (da Breakpoint einen Interruptaufruf impliziert, steht die Frage, ob auf die kernelseitige Implementierung von Interrupts zurückgegrien werden sollte oder eine Trampolin Funktion favorisiert werden sollte). Fakt ist, dass der Interrupt Aufruf einen
2 gcc mit Option '-dr' 'Dump after RTL generation'
27
kleinen Overhead durch das nachsehen in der IDT hat. Ziel ist es allerdings diesen zu vermeiden und bestenfalls auf die übergebenen Argumente (platziert im Stack) anzeigen zu können.
7.2 Dateien
Die für das Projekt erstellten Dateien sind nötig gewesen, um den Inhalt der Projektarbeit zu nden.
7.2.1 fct_funktions_adressen.pl
Das Perlskript benutzt die 'objdump -d
7.2.2 fct_nasse_erde.c
Den Basiscode für das Kernelmodul wurde aus Linux Kernel Modul Programming Guide[3] entlehnt und erweitert. In der procle_write Funktion wurde eine Abfrage hinzugefügt, die bei Übermittlung von Zeichen an '/proc/fctrace_nasse_erde' die Funktion 'eins' startet. Eins ist Bestandteil des Ablauftestes/Proof-of-Concepts. Mit den insgesamt drei Funktionen, die sich nacheinander aufrufen und einen Eintrag im Syslog hinterlassen, soll die prinzipielle Machbarkeit bewiesen werden.
7.2.3 fct_wurm.c
Das Kernelmodul stellt die Managementebene des Wurmes dar, es wurde wiederum auf die Vorlage vom Linux Documentation Project[3] aufgebaut. Bereitgestellt wird das Proc-Interface '/proc/fctrace', dies dient unter anderem zur Kommunikation mit dem User-Space. Die globale, zwei dimensionale Tabelle enthält die Zuordnung: Funktion(y-Richtung) und die dazugehörigen möglichen rufbaren Funktionen(x-Richtung). Weiterhin gibt es einen Zeichen-Zahl Extraktionsfunktion strtoint, diese ist erforderlich, weil die User-Space Bibliotheken nicht im Kernel-Space verfügbar sind. Problemmatisch an dieser Stelle ist, das keine Prüfung der übergebenen Daten stattndet, so ist es möglich einen Kernel-Oops herbeizuführen. Nach der Übernahme der Funktionsadressen, kann
28
mit dem Befehl '%' und Adresse die erste KProbe, damit der Anfangspunkt des Wurmes, gesetzt werden. Das Absetzen der '%' Anweisung erzwingt ein iteratives Suchen in y-Richtung der globalen Tabelle. Sollte die Gleichheit festgestellt werden, so wird die Probe über kprobe_init, aus der folgenden Header Datei, gesetzt.
7.2.4 fct_wurm_kprobe.h
Diese Bibliothek basiert auf einem KProbe-Beispiel[4]. Die pre_handler Funktion wird vor der gesetzte Probe 3 ausgeführt. Hier bendet sich die Wurm-Logik, anhand des ausgelösten Interrupts wird durch die globale Tabelle in y-Richtung iteriert. Diese y-Richtung enthält die Vaterfunktionen, die Schleife durchsucht diese und wird bei einem Treer alle zugehörigen long-Werte 4 als Probenadresse interpretieren und damit registrieren 5 . Der Schalter für das Deaktivieren der gesetzten Proben ist funktionell vorhanden.
7.3 Abschliessende Betrachtung
Das Projekt zeigt das von der eigentlichen Idee eine einfache Verfolgung von Aufrufen entlang eines Ausführungspfades eine Menge Grundlagenarbeit zu leisten ist. Als Schwierigkeit ist auch das Testen der Implementation zu vermerken. Einserseits können nicht alle im User-Space gewöhnten Funktionen und Bibliotheken zur Kernelprogrammierung benutzt werden. Es ist so zum Beispiel nicht möglich eine Datei(eingebunden über die 'le' Struktur aus 'stdio.h') im Kernel-Space zum lesen zu önen. Dies erzwingt zur Kommunikation in Richtung Kernel ein mit user-Rechten schreibbares proc-Interface. Bedeutend an dieser Stelle ist das andere Funktionen für zum Beispiel die Umrechnung von char-Arrays in einen Integer Wert benutzt werden müssen. Zwingend ist auch die Benutzung von printk anstelle von printf. Die Unterscheidung wird in der Art wie man den Namen der Funktion auöst vorgenommen. Prinzipiell gilt das die Namen der Funktionen von Kernelmodul beim 'insmod' aufgelöst werden. Es können nur Funktionen verwendet werden, die also im Kernel deniert sind (die also über 'cat /proc/kallsyms' angezeigt werden). Die Funktionsnamen im User-Space werden beim Linken des Compilers aufgelöst. An dieser Stelle wird sicher gegangen, dass der Code in einer Library verfügbar ist. Library Funktionen laufen aus jenem Grund komplett im UserSpace, weil
3 der Breakpoint-Anweisung
4 in der globalen Tabelle die x-Richtung
5 mittels kprobe_init
29
diese einen einfacheren Zugri auf Kernelseitige Funktionen(besser: System-Calls) bieten sollen. Eine andere Schwierigkeit ist die lange 'turn-around' Zeit. Anders als im Userspace verhindert ein Segmentation Fault in einem Kernelmodul das Entladen dieses Modules. Das zeigt sich durch den 'usage count' (siehe Ausgabe von 'lsmod'). Solang der 'usage count' grösser null ist, wird eine Benutzung dieses Modules durch andere angezeigt[in module_refcount Funktion kernel/module.c]. Dem nach muss nach einem gemachtem programmierten Fehler entweder der Kernel komplett neu gestartet werden oder ein anderes Kernelmodul setzt den 'usage counter' des nicht mehr entladenbaren Modules auf 0. Der Vollständigkeit wird auch der Fallstrick der Lizensen erwähnt. Das Kernelmodul muss MODULE_LICENSE(GPL) [include/linux/module.h] enthalten. Entfällt der gesamte String wird angenommen, das das Modul unter einer proprietären Lizenz verfasst wurde. Das äuÿert sich beim Laden des Moduls mit der Warnung 'Kernel tainted'. Aber um die Aussage zu unterlegen: in kprobes.c werden die spezischen KProbe-Funktionen (e.g. register_kprobe) per EXPORT_SYMBOL_GPL exportiert. Diese Makro sichert die Interessen der General Public License. Die Vereinbarung in der Lizenz legt fest, das jede von der GPL abgeleitete Arbeit (=Quellcode) wieder unter der GPL veröentlicht werden muss. Das heiÿt der Quellcode muss der Allgemeinheit verfügbar gemacht werden. Das Ergebnis der Aussparung dieser Quellcodezeile bedeutet, dass beim insmod des resultierende Modules die unresolved Symbol Warnung kommt, weil auf die GPL signierten Funktionen nicht zugegrien werden kann.
7.3.1 letzter Stand
Zu den beiden erwähnten Kriterien in 1.1 ist auswertend zu sagen, das keine Platt-formunabhängigkeit gewahrt werden kann. Die Speicherzugrie und die Umrechnung der linearen Adresse in eine phyikalische sind prozessorspezisch. Die einfache Maintenance wäre durch das KISS 6 Prinzip zu realiseren. Das Herauslesen der nächsten Weg-Sprungadressen aus den Folge-Instruktionen relativ zur Adresse des letzten gesetzten INT3 erwies sich als langwährig und nicht erfolgreich. Um das FCtrace eine ungefähre Handlung wie dem strace zu geben, wurde deswegen das Herausnden der Funktionsadressen der Sprungpunkte in den UserSpace verlagert. Aber ohne eine vorrausschauende, speicherbasierende Fortplanzung und die Erkennung der möglichen Wegsprungpunkte des Wurmes ist ein realistischer Einsatz nicht denkbar. Speziell im Quellcode des Wurmes wird deutlich, dass die Verwaltung der einzelnen
6 keep it simple and stupid
30
Sprungpunkt-Adressen in einer zweidimensionalen Tabelle naiv ist. Hier wäre eine Hash-Funktion angebracht, da einerseits das Suchen nach der Vaterfunktion im Worst-Case einen nahezu kompletten Durchlauf bedeutet und andererseits Hashing-Funktionen im Kernel schon verfügbar sind.
Im ersten Schritt des Projektes sollten Performanceuntersuchungen angefertigt werden. Diese können nur theoretisch behandelt werden. Der Wurm basiert auf einem Schleifenkonstrukt der Komplexität O(n) für die Vaterfunktionen und O(m) für die Sohnfunktionen. Der obere, feste Grenzwert ist im Quellcode durch die globale Tabellendenition von 256 gegeben. Diese lineare Suche nach den zu registrierenden Sohnfunktionen erfolgt bei jedem INT3 Aufruf. Zugegebenermaÿen wäre es interessant zu wissen, wie sich diese Verzögerung auf eine Applikation auswirkt. An dieser Stelle müsste dierenziert werden in die Dauer einer Interruptverarbeitung, die Dauer der Registrierung der Sohnfunktionen sowie das die Dauer des Logging vom syslog Mechanismusses. An dieser Stelle wird der Zeitverlust durch das Herausnden des Funktionsendes (dem Wegsprungpunkt) weggelassen. Diese Zeit wäre abhängig von der Implementierung 7 der zu observierenden Funktion und die beeinhaltenden Wegsprungpunkte.
Die Idee das KProbe um einen Patch zu erweitern wurde nicht weiter verfolgt. Zur möglichen realen Anwendbarkeit sei gesagt, dass ein Patch für das bestehende KProbes erstellt werden sollte, um eine minimale Chance auf eine Aufnahme in den Stable Linux Kernel Source Tree zu wahren. Bislang wurde ein kompletter Framework um KProbes herum geschrieben.
7 die CALL Instruktion auf x86 ist ein Byte lang, es müsste byteweise gesucht werden
31
Literaturverzeichnis
[1] Olaf Dabrunz, SUSE Technical Engineer, odabrunz@suse.de, 24.06.2004
[2] GNU General Public License, 21.09.2005, http://www.gnu.org/copyleft/gpl.html
[3] Linux Documentation Project, Kernel Modul 2.6,
http://www.tldp.org/LDP/lkmpg/2.6/html/x773.htm
[4] Linux Documentation Project, Kernel Modul 2.6,
http://www.tldp.org/LDP/lkmpg/2.6/html/x773.htm
[5] Homepage DTrace, Firma SUN Microsystems, 24.09.2005,
http://www.sun.com/bigadmin/content/dtrace/
[6] dtrace_usenix.pdf, 21.09.2005,
http://www.sun.com/bigadmin/content/dtrace/dtrace_usenix.pdf
[7] CDDL, Opensolaris Lizenz, 23.09.2005,
http://www.opensolaris.org/os/licensing/opensolaris_license/
[8] Intel Architecture Manual, Kernel Ring, 16.02.2006, Seite 131, ftp://download.intel.com/design/Pentium4/manuals/25366817.pdf
[9] Intel Architecture Manual, 16.02.2006, Seite 170,
ftp://download.intel.com/design/Pentium4/manuals/25366817.pdf
[10] Intel Architecture Manual, 16.02.2006, Seite 187,
ftp://download.intel.com/design/Pentium4/manuals/25366817.pdf
[11] Intel Architecture Manual, 16.02.2006, Seite 198,
ftp://download.intel.com/design/Pentium4/manuals/25366817.pdf
[12] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 152
[13] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 53
[14] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 54
[15] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 110
32
[16] Bovet, Daniel, Cesati, Marco: Understanding the Linux Kernel 2nd, O'Reilly, 2002, Seite 741
[17] KProbes Homepage, Firma RedHat, 24.10.2005,
http://sources.redhat.com/systemtap/kprobes/
[18] IBM KProbes HowTo, 12.11.2005, http://www-106.ibm.com/developerworks/library/l-kprobes.html?ca=dgr-lnxw07Kprobe
[19] Compilation Process, University of Illinois, 15.11.2005,
http://www.acm.uiuc.edu/sigmil/RevEng/ch02.html
[20] GNU, RTL Spezikation, 03.02.2006,
http://gcc.gnu.org/onlinedocs/gcc-4.0.0/gccint/RTL.html
[21] GNU, RTL Spezikation: call, 03.02.2006,
http://gcc.gnu.org/onlinedocs/gcc-4.0.0/gccint/Calls.html
[22] ABI Intel 386 DRAFT,
http://www.caldera.com/developers/devspecs/abi386-4.pdf
[23] ABI Intel 386 DRAFT, Seite 81,
http://www.caldera.com/developers/devspecs/abi386-4.pdf
33
Arbeit zitieren:
Patrick Kirsch, 2006, Dynamic function call tracing im Linux Kernel, München, GRIN Verlag GmbH
Dieser Text kann über folgende URL aufgerufen und zitiert werden:
Einbetten
DOI
Formatvorlage (Microsoft Word) für eine Diplomarbeit, Masterarbeit, Ha...
Für MS Word 2003 - Update 2010
Vorlagen, Muster, Formulare, Infobroschüren
Ausarbeitung, 25 Seiten
Formatvorlage (OpenOffice) für eine Diplomarbeit, Masterarbeit, Hausar...
Vorlagen, Muster, Formulare, Infobroschüren
Ausarbeitung, 35 Seiten
Formatvorlage / Vorlage zur Erstellung einer Diplomarbeit, Bachelorarb...
Vorlagen, Muster, Formulare, Infobroschüren
Ausarbeitung, 15 Seiten
Formatvorlage / Vorlage für eine Diplomarbeit / Hausarbeit
Für MS Word 2007 - dotx
Vorlagen, Muster, Formulare, Infobroschüren
Ausarbeitung, 25 Seiten
Anleitung zum Erstellen schriftlicher Arbeiten: Der Aufbau einer wisse...
Vorlagen, Muster, Formulare, Infobroschüren
Ausarbeitung, 20 Seiten
Erstellen einer schriftlichen Hausarbeit
Vorlagen, Muster, Formulare, Infobroschüren
Hausarbeit, 14 Seiten
Grundtechniken wissenschaftlichen Arbeitens
Bibliografieren - Reden - Schr...
Vorlagen, Muster, Formulare, Infobroschüren
Skript, 46 Seiten
Ratgeber zur Erstellung wissenschaftlicher Arbeiten. Diplomarbeiten - ...
Vorlagen, Muster, Formulare, Infobroschüren
Ausarbeitung, 39 Seiten
Ralf Kunze gefällt Dynamic function call tracing im Linux Kernel
Patrick Kirsch hat den Text Dynamic function call tracing im Linux Kernel veröffentlicht
Patrick Kirsch hat einen neuen Text hochgeladen
Kernel Functions and Elliptic Differential Equations in Mathematical P...
Stefan Bergman, Menahem Schiffer
A Kernel Function Approach for Interior Point Methods
Analysis and Implementation
Mohamed El Ghami
Reinforcement Learning and Dynamic Programming Using Function Approxim...
Lucian Busoniu, Robert Babuska, Bart De Schutter
IA-64 Linux Kernel: Design and Implementation
David Mosberger, Stephane Eranian, Bruce Perens
0 Kommentare