Handbuch zur dynamischen Ablaufverfolgung in Solaris

Zeiger auf Strukturen

Verweise auf Strukturen über Zeiger sind in C und D sehr üblich. Für den Zugriff auf Strukturkomponenten über einen Zeiger steht der Operator -> zur Verfügung. Besitzt struct s eine Komponente m und es liegt ein Zeiger namens sp auf diese Struktur vor (d. h. sp ist eine Variable des Typs struct s *), können Sie entweder den *-Operator einsetzen, um den Zeiger sp für den Zugriff auf die Komponente zunächst zu dereferenzieren:

struct s *sp;

(*sp).m

oder Sie können diese Schreibweise anhand des Operators -> kürzer fassen. Die beiden folgenden D-Fragmente sind gleichbedeutend, sofern sp ein Zeiger auf eine Struktur ist:

(*sp).m				sp->m

DTrace umfasst mehrere integrierte Variablen, die Zeiger auf Strukturen darstellen. Beispiele sind curpsinfo und curlwpsinfo. Diese Zeiger verweisen auf die Strukturen psinfo bzw. lwpsinfo und ihr Inhalt bietet eine Momentaufnahme der Informationen über den Zustand des aktuellen Prozesses und leichtgewichtigen Prozesses (LWP) des Threads, der den aktuellen Prüfpunkt ausgelöst hat. Ein Solaris-LWP ist die Kerneldarstellung eines Benutzer-Threads, auf dem die Solaris- und POSIX-Thread-Schnittstellen aufbauen. Der Einfachheit halber exportiert DTrace diese Informationen in derselben Form wie die /proc-Dateisystemdateien /proc/PID/psinfo und /proc/PID/lwps/LWPID/lwpsinfo. Die·/proc -Strukturen werden von Überwachungs- und Debugging-Tools wie ps(1), pgrep(1) und truss(1) genutzt, sind in der Systemdefinitionsdatei <sys/procfs.h> definiert und in der Manpage proc(4) beschrieben. Beispiele für Ausdrücke mit curpsinfo, ihre Typen und Bedeutung sind:

curpsinfo->pr_pid

pid_t

aktuelle Prozess-ID 

curpsinfo->pr_fname

char []

Name der ausführbaren Datei 

curpsinfo->pr_psargs

char []

anfängliche Befehlszeilenargumente 

Sie sollten sich die vollständige Strukturdefinition später noch einmal genauer ansehen. Untersuchen Sie dazu die Definitionsdatei <sys/procfs.h> und die entsprechenden Beschreibungen in proc(4). Im nächsten Beispiel wird mit der Komponente pr_psargs durch einen Vergleich von Befehlszeilenargumenten ein bestimmter Prozess angegeben.

Strukturen werden häufig zum Erzeugen komplexer Datenstrukturen in C-Programmen eingesetzt. Folglich stellt die Fähigkeit, Strukturen aus D zu beschreiben und zu referenzieren, eine leistungsfähige Einrichtung zur Beobachtung der inneren Abläufe des Solaris-Betriebssystemkernels und seiner Systemschnittstellen dar. Der nächste Beispielcode enthält nicht nur die bereits erwähnte Struktur curpsinfo, sondern untersucht auch einige Kernelstrukturen, indem er die Beziehung zwischen dem Treiber ksyms(7D) und den read(2)-Anforderungen beobachtet. Zum Reagieren auf die Anforderungen von Lesezugriffen auf die zeichenorientierte Gerätedatei /dev/ksyms stützt sich der Treiber auf die häufig verwendeten Strukturen uio(9S) und iovec(9S).

Die uio-Struktur, auf die über den Namen struct uio oder den Typ-Aliasnamen uio_t zugegriffen wird, ist in der Manpage uio(9S) beschrieben. Sie dient zum Beschreiben von E/A-Anforderungen, für die Daten zwischen dem Kernel und einem Benutzerprozess kopiert werden müssen. uio enthält selbst einen Vektor einer oder mehrerer iovec(9S)-Strukturen, die jeweils einen Teil der E/A-Anforderung beschreiben, falls unter Verwendung der Systemaufrufe readv(2) oder writev(2) mehrere Chunks erforderlich sein sollten. Eine der Kernel-DDI-Routinen (DDI = Gerätetreiberschnittstelle), die auf struct uio einwirken, ist die Funktion uiomove(9F). Sie gehört zu einer Familie von Funktionen, derer sich Kerneltreiber bedienen, um auf read(2)-Anforderungen für Benutzerprozesse zu antworten und Daten in Benutzerprozesse zurückzukopieren.

Der Treiber ksyms verwaltet eine zeichenorientierte Gerätedatei namens /dev/ksyms. Dabei handelt es sich scheinbar um eine ELF-Datei mit Informationen über die Symboltabelle des Kernels, die aber tatsächlich nur eine vom Treiber anhand der derzeit im Kernel geladenen Module erzeugte „Illusion“ ist. uiomove(9F)-Anforderungen reagiert der Treiber mit der read(2)-Routine. Das nächste Beispiel veranschaulicht, wie die Argumente und Aufrufe von read(2) durch /dev/ksyms die Treiberaufrufe von uiomove(9F) abgleichen, um das Ergebnis wieder in den Benutzeradressraum an der für read(2) angegebenen Position zu kopieren.

Mit dem Dienstprogramm strings(1); und der Option -a können zahlreiche Lesezugriffe durch /dev/ksyms erzwungen werden . Führen Sie strings -a /dev/ksyms in Ihrer Shell aus und betrachten Sie die Ausgabe. Geben Sie in einen Texteditor die erste Klausel des Beispielskripts ein und speichern Sie es unter dem Namen ksyms.d:

syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
	printf("read %u bytes to user address %x\n", arg2, arg1);
}

In dieser ersten Klausel kommt der Ausdruck curpsinfo->pr_psargs zum Zugriff auf und den Vergleich der Befehlszeilenargumente unseres Befehls strings(1) vor. So wird gewährleistet, dass das Skript vor der Verfolgung der Argumente die richtigen read(2)-Anforderungen auswählt. Beachten Sie: Durch Verwendung des Operators == mit einem Argument auf der linken Seite, das ein Vektor des Typs char ist, und einem Argument auf der rechten Seite, bei dem es sich um eine Zeichenkette handelt, schließt der D-Compiler, dass das linke Argument auf den String-Typ erweitert und ein Zeichenkettenvergleich durchgeführt werden soll. Geben Sie den Befehl dtrace -q -s ksyms.d ein und führen Sie ihn in einer Shell aus. Geben Sie dann in eine andere Shell den Befehl strings -a /dev/ksyms ein. Während der Ausführung von strings(1) generiert DTrace eine Ausgabe wie diese:


# dtrace -q -s ksyms.d
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
...
^C
#

Dieses Beispiel lässt sich mithilfe einer gebräuchlichen D-Programmiertechnik so ausweiten, dass ein Thread von dieser anfänglichen read(2)-Anforderung bis tief in den Kernel verfolgt werden kann. Bei Eintritt des Kernels in syscall::read:entry setzt das nächste Skript eine thread-lokale Flag-Variable, die angibt, dass dieser Thread von Interesse ist und bei syscall::read:return wieder gelöscht wird. Das einmal gesetzte Flag kann als Prädikat für andere Prüfpunkte zur Instrumentierung von Kernelfunktionen wie zum Beispiel uiomove(9F) verwendet werden. Der DTrace-Provider fbt (function boundary tracing) veröffentlicht Eintritt- und Rückkehr-Prüfpunkte für Funktionen, die im Kernel definiert sind, einschließlich jener in der Gerätetreiberschnittstelle. Geben Sie folgenden Quellcode ein, in dem der Provider fbtzur Instrumentierung von uiomove(9F) eingesetzt wird, und speichern Sie ihn unter dem Namen ksyms.d:


Beispiel 7–2 ksyms.d: Beziehung zwischen Ablaufverfolgung von read(2) und uiomove(9F)

/*
 * When our strings(1) invocation starts a read(2), set a watched flag on
 * the current thread.  When the read(2) finishes, clear the watched flag.
 */
syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
	printf("read %u bytes to user address %x\n", arg2, arg1);
	self->watched = 1;
}

syscall::read:return
/self->watched/
{
	self->watched = 0;
}

/*
 * Instrument uiomove(9F).  The prototype for this function is as follows:
 * int uiomove(caddr_t addr, size_t nbytes, enum uio_rw rwflag, uio_t *uio);
 */
fbt::uiomove:entry
/self->watched/
{
	this->iov = args[3]->uio_iov;

	printf("uiomove %u bytes to %p in pid %d\n",
	    this->iov->iov_len, this->iov->iov_base, pid);
}

Die abschließende Klausel dieses Beispiels verwendet die thread-lokale Variable self->watched, um festzustellen, wann ein bestimmter Kernel-Thread die DDI-Routine uiomove(9F) betritt. An diesem Punkt angelangt, greift das Skript mithilfe des integrierten args-Vektors auf das vierte Argument (args[3]) von uiomove() zu, das ein Zeiger auf struct uio ist, die wiederum die Anforderung darstellt. Der D-Compiler weist jedem Element des args-Vektors automatisch den dem C-Funktionsprototypen für die instrumentierte Kernel-Routine entsprechenden Typ zu. Die uio_iov-Komponente enthält einen Zeiger auf die Struktur struct iovec für die Anforderung. Zur Verwendung in unserer Klausel wird eine Kopie dieses Zeigers in der klausel-lokalen Variable this->iov gespeichert. In der letzten Anweisung dereferenziert das Skript this->iov, um auf die iovec-Komponenten iov_len und iov_base zuzugreifen, die die Bytelänge bzw. die Ziel-Basisadresse von uiomove(9F) darstellen. Diese Werte sollten mit den Eingabeparametern des auf dem Treiber ausgeführten Systemaufrufs read(2) übereinstimmen. Wechseln Sie zu Ihrer Shell und führen Sie dtrace -q -s ksyms.d aus. Geben Sie anschließend wieder den Befehl strings -a /dev/ksyms in eine andere Shell ein. Es müsste eine Ausgabe wie im folgenden Beispiel produziert werden:


# dtrace -q -s ksyms.d
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
...
^C
#

Die Adressen und Prozess-IDs in Ihrer Ausgabe weichen zwar hiervon ab, doch sollten Sie beobachten, dass die für read(2) eingegebenen Argumente mit den vom Treiber ksyms an uiomove(9F) übergebenen Parametern übereinstimmen.