Tutorial 21: Pipe
In diesem Tutorial werden wir erkunden was Pipes sind und wofür sie gut sind. Um es interessanter zu machen, werfe ich noch eine Technik ein, wie man die Hintergrund und die Text-Farbe eines Steuerelementes ändern kann.
Laden Sie das Beispiel hier herunter.
Theorie:
Eine Pipe ist ein Kommunikations-Kanal oder Weg mit zwei Enden. Sie können Pipes dazu benutzen, um Daten zwischen zwei Prozessen oder innerhalb des selben Prozesses auszutauschen. Sie sind wie ein Walkie-Talkie. Sie können sie auf der anderen Seite einrichten und es kann mit Ihnen kommunizieren.
Es gibt zwei Arten von Pipes: Anonymous Pipes und Named Pipes. Anonymous Pipes sind, naja, anonym: Sie können sie benutzen ohne ihren Namen zu können. Eine Named Pipe ist das Gegenteil: Sie müssen den Namen kennen, bevor Sie sie benutzen können.
Sie können Pipes auch nach Ihren Eigenschaften kategorisieren: direktional (one-way) oder bidirektional (two-way). In einer direktionalen Pipe, können die Daten nur eine Richtung fließen: von einem Ende zum anderen. Während in einer bidirektionalen Pipe die Daten zwischen beiden Enden ausgetauscht werden können.
Anonymous pipes sind immer direktional, während eine Named Pipe sowohl direktional als auch bidirektional sein kann. Named Pipes werden normalerweise in Netzwerkumgebungen benutzt, wo ein Server zu mehreren Clients connecten kann.
In diesem Tutorial werden wir uns die Anonymous Pipes etwas genauer ansehen. Das Hauptaufgabengebiet von Anonymous Pipes ist der Kommunikations-Weg zwischen einem Parent und einem oder mehreren Child Prozess(en).
Anonymous Pipes sind sehr nützlich, wenn Sie mit Konsolen-Anwendungen zu tun haben. Eine Konsolen-Anwendung ist eine Art Win32-Programm, welchs eine Konsole für seine Ein & Ausgaben benutzt. Eine Konsole ist wie eine DOS Box. Wie dem auch sei, eine Konsolen-Anwendung ist ein ganzes 32-Bit Programm. Es kann jede GUI-Funktion benutzen, wie jedes andere GUI-Programm auch. Es hat lediglich eine Konsole für seinen Gebrauch.
Eine Konsolen-Anwendung hat drei Handles, die es für Ein & Ausgaben benutzen kann. Diese werden Standard-Handles genannt. Es gibt drei von ihnen: Standard-Eingabe (standard input), Standard-Ausgabe (standard output) und Standard-Fehler (standard error). Das Standard-Eingabe Handle wird benutzt um Informationen von der Konsole zu lesen / zu erhalten und das Standard-Ausgabe Handle wird benuzt um Informationen auf der Konsole auszugeben / zu drucken. Das Standard-Fehler Handle wird benutzt um Fehler-Zustände zu melden, da seine Ausgabe nicht umgeleitet werden kann.
Eine Konsolen Anwendung kann diese drei Standard-Handles ermitteln, indem die GetStdHandle Funktion aufgerufen wird und als Parameter das Handle angegeben wird, das man ermitteln möchte. Eine GUI Anwendung hat keine Konsole. Wenn Sie GetStdHandle aufrufen, werden Sie einen Fehler erhalten. Wenn Sie wirklich eine Konsole benutzen wollen, können Sie AllocConsole aufrufen um eine neue Konsole zu alloziieren. Vergessen Sie aber nicht FreeConsole aufzurufen, wenn Sie mit der Konsole fertig sind.
Anonymous Pipes werden meist dazu verwendet, Ein- und/oder Ausgaben einer Child-Konsolen Anwendung umzuleiten. Der Parent Prozess kann dabei eine Konsole oder eine GUI Applikation sein, aber das Child muss eine Konsole für diese Arbeit sein. Wie Sie wissen, benutzen Konsol-Applikationen Standard-Handles für seine Ein- und Ausgaben. Wenn wir die Eingabe und/oder Ausgabe einer Konsolen-Applikation umleiten wollen, können wir das Handle mit dem einen Ende der Pipe ersetzen. Eine Konsolen-Applikation wird nicht wissen, dass es ein Handle auf das Ende einer Pipe benutzt. Es wird es als Standard-Handle benutzen. Das ist im OOP-Jargon eine Art Polymorphismus. Diese vorgehensweise ist sehr mächtig, da wir den Child-Prozess in keinster Weise modifizieren müssen.
Etwas anderes, das Sie über Konsolen-Applikationen wissen sollten, ist, wo es diese Standard-Handles herkriegt. Wenn eine Konsolen-Applikation erzeugt wird, hat der Eltern-Prozess zwei Möglichkeiten: er kann eine neue Konsole für das Child erzeugen oder er kann das Child von seiner eigenen Konsole erben lassen. Wenn die zweite Mögichkeit in Betracht gezogen wird, muss der Eltern-Prozess eine Konsolen-Applikation sein oder, wenn es eine GUI Applikation ist, muss es AllocConsole als erstes aufrufen um eine Konsole zu alloziieren.
Lassen Sie uns mit der Arbeit beginnen. Um eine Anonymous Pipe zu erzeugen, müssen Sie CreatePipe aufrufen.
CreatePipe hat folgenden Prototypen:
CreatePipe proto pReadHandle:DWORD, \
pWriteHandle:DWORD,\
pPipeAttributes:DWORD,\
nBufferSize:DWORD
-
pReadHandle
ist ein Zeiger auf eine Doubleword Variable, die das Handle des Lese-Endes der Pipe erhält
-
pWriteHandle
ist ein Zeiger auf eine Doubleword Variable, die das Handle des Schreib-Endes der Pipe erhält.
-
pPipeAttributes
zeigt auf eine SECURITY_ATTRIBUTES Struktur, die bestimmt, ob die zurückgelieferten Lese- & Schreib-Handle an den Child-Prozess vererbar sind.
-
nBufferSize ist die vorgeschlagene Größe des Buffers, die die Pipe zur Benutzung reserviert. Das ist nur eine vorgeschlagene Größe. Sie können NULL benutzen, um der Funktion mitzuteilen, dass sie die Standard-Größe benutzen soll.
Wenn der Aufruf erfolgreich war, ist der Rückgabewert ungleich null. Wenn sie fehl schlägt, ist der Rückgabewert gleich null.
Nach erfolgreichem Aufruf erhalten Sie zwei Handle, eins um am Ende der Pipe zu lesen und eins um zu schreiben. Ich werde jetzt die nötigen Schritte aufzuzeigen, um die Standard-Ausgabe eines Child-Konsolen-Programes auf Ihren eigenen Prozess umzuleiten. Beachten Sie, dass meine Methode von der aus der Borland Win32 API Referenz differiert. Die Methode in der Win32 API Referenz geht davon aus, dass der Eltern-Prozess eine Konsolen-Applikation ist und somit das Child die Standard-Handles erben kann. In der Regel müssen wir aber die Ausgabe einer Konsolen-Applikation zu einer GUI umleiten.
-
Erzeugen Sie eine Anonymous Pipe mit CreatePipe.
Vergessen Sie nicht das bInheritable Element von SECURITY_ATTRIBUTES auf TRUE zu setzen, so dass die Handles vererbbar sind.
-
Nun müssen wir die Parameter, die wir CreateProcess übergeben, vorbereiten, da wir sie zum laden der Child-Prozess-Applikation benutzen. Eine wichtige Struktur ist die STARTUPINFO Struktur. Diese Struktur bestimmt das Erscheinungsbild des Haupt-Fenster des Child-Prozesses, wenn es das erste Mal erscheint. Diese Struktur absolut notwendig für unser Vorhaben. Sie können das Hauptfenster verstecken und das Pipe-Handle unseres Child-Konsolen-Prozesses mit ihr übergeben. Folgende Element müssen Sie füllen:
-
cb : die Größe der STARTUPINFO Struktur
-
dwFlags : die binären Bit-Flags bestimmen welches Element der Struktur gültig ist und regelt auch den Status, ob das Haupt-Fenster gezeigt wird oder nicht. Für unser Vorhaben, sollten Sie eine Kombiation aus STARTF_USESHOWWINDOW und STARTF_USESTDHANDLES benutzen
-
hStdOutput und hStdError : die Handles, die vom Child-Prozess als Standard-Ausgabe/Fehler Handle benutzt werden soll. Für unser Vorhaben übergeben wir das Schreiben-Handle der Pipe als Standard-Ausgabe und Fehler des Childs. So dass, wenn das Child etwas auf der Standard-Ausgabe / Fehler ausgibt, reicht es das tatsächlich über die Pipe an den Eltern-Prozess.
-
wShowWindow regelt, ob das Haupt-Fenster angezeigt wird oder nicht. Für unsere Zwecke, wollen wir das Konsolen-Fenster des Childs nicht anzeigen, weshalb wir SW_HIDE in dieses Element schreiben.
Rufen Sie CreateProcess auf die Child-Applikation zu laden. Nachdem CreateProcess erfolgreich war, schläft das Child noch. Es wird in den Speicher geladen aber läuft nicht sofort.
Schließen Sie das Schreiben-Handle der Pipe. Das ist nötig, da der Eltern-Prozess keinen weiteren Gebrauch mehr für das Schreib-Handle hat und die Pipe mehr als ein Schreib-Ende hat, wir MÜSSEN es schließen, bevor wir Daten aus der Pipe lesen. Wie dem auch sei, Sie sollten das Schreib-Handle nicht schließen, bevor Sie CreateProcess aufrufen, ansonsten wird Ihre Pipe nicht funktionieren. Sie sollten sie schließen, direkt nachdem CreateProcess zurückgekehrt ist und bevor Sie Daten vom anderen Ende der Pipe lesen.
Nun können Sie Daten vom anderen Ende der Pipe mit ReadFile lesen. Mit ReadFile bringen Sie den Child-Prozess zum laufen. Es wird seine Ausführung beginnen und wenn es etwas auf das Standard-Ausgabe-Handle schreibt (was tatsächlich das Schreib-Ende der Pipe ist), werden die Daten durch die Pipe an das Lese-Ende geschickt. Sie müssen ReadFile immer wieder aufrufen, bis es 0 zurückliefert, was bedeutet, dass keine Daten mehr zum lesen vorliegen. Sie können alles mit den aus der Pipe gelesenen Daten machen. In unserem Beispiel gebe ich sie in einem Edit-Steuerelement aus.
Schließen Sie das Lese-Handle der Pipe.
Beispiel:
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
.const
IDR_MAINMENU equ 101 ; die ID des Haupt-Menüs
IDM_ASSEMBLE equ 40001
.data
ClassName db "PipeWinClass",0
AppName db "One-way Pipe Beispiel",0 EditClass db "EDIT",0
CreatePipeError db "Fehler während der Pipe-Erzeugung",0
CreateProcessError db "Fehler während der Prozess-Erzeugung",0
CommandLine db "ml /c /coff /Cp test.asm",0
.data?
hInstance HINSTANCE ?
hwndEdit dd ?
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,IDR_MAINMENU
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL rect:RECT
LOCAL hRead:DWORD
LOCAL hWrite:DWORD
LOCAL startupinfo:STARTUPINFO
LOCAL pinfo:PROCESS_INFORMATION
LOCAL buffer[1024]:byte
LOCAL bytesRead:DWORD
LOCAL hdc:DWORD
LOCAL sat:SECURITY_ATTRIBUTES
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL
mov hwndEdit,eax
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetBkColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
.elseif uMsg==WM_SIZE
mov edx,lParam
mov ecx,edx
shr ecx,16
and edx,0ffffh
invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
.elseif uMsg==WM_COMMAND
.if lParam==0
mov eax,wParam
.if ax==IDM_ASSEMBLE
mov sat.nLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE
invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
.if eax==NULL
invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
.else
mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE
invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
.if eax==NULL
invoke MessageBox,hWnd,addr CreateProcessError,addr AppName,MB_ICONERROR+MB_OK
.else
invoke CloseHandle,hWrite
.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw
.endif
invoke CloseHandle,hRead
.endif
.endif
.endif
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
.endif
xor eax,eax
ret
WndProc endp
end start
Analyse:
Das Beispiel wird ml.exe aufrufen, um eine Datei names test.asm zu assemblieren und die Ausgabe von ml.exe in der Client Area eines Edit Steuerelementes ausgeben.
Wenn das Programm geladen ist, registriert es eine Fenster-Klasse und erzeugt das Haupt-Fenster wie immer. Die erste Sache, die es während der Erzeugung des Hauptfensters macht, ist, ein Edit-Steuerelement zu erzeugen, welches für die Ausgabe von ml.exe verwendet wird.
Nun der interessante Teil, wir werden die Text- und Hintergrund-Farbe des Edit-Steuerelementes ändern. Wenn ein Edit-Steuerelement seine Client Area zeichnet, sendet es eine WM_CTLCOLOREDIT Nachricht an sein Parent.
wParam enthält das Handle des Device Kontextes, den das Edit Steuerelement verwenden wird, um seine eigene Client Area zu schreiben. Wir können diese Möglichkeit nutzen um die Charakteristika von HDC zu verändern.
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetBkColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
SetTextColor ändert die Text-Farbe auf Gelb. SetBkColor ändert die Hintergrund-Farbe des Textes auf Schwarz. Und zu guter letzt, ermitteln wir das Handle des schwarzen Brushes (Pinsel) und geben es an Windows zurück. Mit der WM_CTLCOLOREDIT Nachricht müssen Sie ein Handle eines Brushes zurückliefern, welches Windows dazu benutzt den Hintergrund des Edit-Steuerelementes zu zeichnen. In unserem Beispiel möchte ich einen schwarzen Hintergrund haben, weshalb ich das Handle des schwarzen Brushes an Windows zurück gebe.
Wenn der Benutzer nun den Menüpunkt Assemble auswählt, wird eine Anonymous Pipe erzeugt.
.if ax==IDM_ASSEMBLE
mov sat.nLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE
Bevor CreatePipe aufgerufen wird, müssen wir als erstes die SECURITY_ATTRIBUTES Strukture füllen. Beachten Sie, dass wir NULL im lpSecurityDescriptor Element angeben können, wenn wir uns nicht um die Sicherheitseinstellungen kümmern wollen. Und das bInheritHandle Element muss gleich TRUE sein, so dass die Pipe-Handles an den Child-Prozess vererbar sind.
invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
Danach rufen wir CreatePipe auf, was bei Erfolg die hRead und hWrite Variable mit den Handles der Lese- und Schreib-Enden der Pipe jeweils füllt.
mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE
mov startupinfo.wShowWindow,SW_HIDE
Als nächstes müssen wir die STARTUPINFO Struktur füllen. Wir rufen GetStartupInfo auf, um die STARTUPINFO Struktur mit den Standard-Werten des Parent-Prozesses zu füllen. Sie MÜSSEN die STARTUPINFO Struktur mit diesen Werten füllen, wenn Sie Ihren Code sowohl unter Win9x als auch NT laufen lassen wollen. Nachdem der GetStartupInfo Aufruf zurückgekehrt ist, können Sie die wichtigen Elemente verändern. Wir kopieren das Handle des Schreib-Endes des Pipe nach hStdOutput und hStdError, da wir diese den Child-Prozess benutzen lassen wollen, statt des Standard Eingabe/Fehler Handlers. Wir wollen auch das Konsolen-Fenster des Child-Prozesses verstecken, weshalb wir den Wert SW_HIDE ins wShowWidow Element schreiben. Und zu guter letzt müssen wir indizieren, dass die hStdOutput, hStdError und wShowWindow Elemente gültig sind und genutzt werden müssen, was durch die Flags STARTF_USESHOWWINDOW und STARTF_USESTDHANDLES im dwFlags Element angegeben wird.
invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
Wir erzeugen nun einen Child-Prozess mit dem CreateProcess Aufruf. Beachten Sie, dass der bInheritHandles Parameter auf TRUE gesetzt sein muss, damit das Pipe-Handle funktioniert.
invoke CloseHandle,hWrite
Nachdem wir erfolgreich den Child-Prozess erzeugt haben, schließen wir das Schreib-Ende der Pipe. Erinnern Sie sich, dass wir das Schreib-Handle dem Child-Prozess über die STARTUPINFO Struktur übergeben haben. Wenn wir das Schreib-Handle an unserem Ende nicht schließen, wird es zwei Schreib-Enden geben. Und damit würde die Pipe nicht funktionieren. Wir müssen das Schreib-Handle nach CreateProcess, aber bevor wir Daten vom anderen Ende der Pipe lesen, schließen.
.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw
Nun sind wir bereit die Daten von der Standard-Ausgabe des Child-Prozesses zu lesen. Wir verbleiben in einer Endlos-Schleife bis keine Daten mehr zum Lesen übrig sind. Wir rufen RtlZeroMemory auf, um den Buffer mit Nullen zu füllen und dann ReadFile, wo wir gleich das Lese-Handle der Pipe, anstatt des Datei-Handles übergeben. Beachten Sie, dass wir maximal 1023 Bytes lesen, da wir die Daten als ASCIIZ String benötigen, um sie an das Edit-Steuerelement weiterreichen zu können.
Wenn ReadFile mit den Daten im Buffer zurückkehrt, füllen wir das Edit-Steuerelement mit diesen Daten. Allerdings gibt's hier ein kleines Problem. Wenn wir SetWindowText benutzen, um die Daten ins Edit-Steuerelement zu bringen, werden die neuen Daten die alten bereits vorhandenen überschreiben! Wir wollen aber, dass die Daten ans Ende der vorhandenen Daten angefügt werden.
Um dieses Ziel zu erreichen, bewegen wir den Cursor ans Ende des Textes, indem wir eine EM_SETSEL Nachricht mit wParam==-1 senden. Als nächstes hängen wir die Daten an diesem Punkt mit der EM_REPLACESEL Nachricht an.
invoke CloseHandle,hRead
Wenn ReadFile NULL zurückliefert, beenden wir die Schleife und schließen das Lese-Handle.
Deutsche Übersetzung: Joachim Rohde
Die original Win32Asm-Tutorials stammen von Iczelion's Win32 Assembly HomePage