Iczelion - 21 - Pipe

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.
  1. 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.
  2. 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