2007. 2. 16. 13:56
DevX Win32 Assembly Tutorial 21: Pipe

Tutorial 21: Pipe

이 번장에서는 파이프가 무엇이며, 언제 사용하는지와 같은 것에 대해서 설명한다.흥미를 더하기 위해서, 에디트 컨트롤의 배경색과 글자색을 어떻게 변경하는 지를 예를 들어서 설명한다.
 소스    리소스    실행 결과  

Theory:

파이프에는 2개의 끝이 존재 할 것이다. 여기서 말하는 「2개의 끝」이란, 2개의 프로세스사이의 입/출력을 의미하고, 2개의 프로세스의 사이에서 데이터를 교환할 수 있다. 2개의 프로세스는 서로 다른 프로세스 혹은 같은 프로세스라도 상관없다.

파이프에는 2가지의 타입이 있다. 익명파이프와 명명된파이프다. 익명 파이프는, 말그대로 익명이다. 즉, 이름을 몰라도 사용할 수 있다. 명명된파이프는 사용하려면 이름을 반드시 알고 있어야 사용이 가능하다.

또한, 파이프의 속성에는, 「단방향」과「양방향」이란 분류가 존재한다. 단방향 파이프는, 어디에서 어디로 데이터가 처리되는지가 정해져 있지만, 양방향 파이프는 어느 방향에서라도 데이터의 교환이 가능하다.

익명 파이프는 항상 단방향이지만, 명명된파이프는 단방향, 양방향, 어느 쪽으로도 사용 할 수 있다. 명명된파이프는 네트워크 환경에서 자주 사용된다.

이 번장에서는, 익명 파이프에 대해 자세히 설명한다. 익명 파이프의 주된 목적은, parent process와 child process, 혹은 child process끼리의 커뮤니케이션이다.

익명 파이프는, 콘솔 어플리케이션에서는 정말 편리한 존재이다. 콘솔 어플리케이션은 콘솔로부터 입출력을 수행하는 Win32 프로그램의 일종으로서, DOS창과 같은 것이지만, 콘솔 어플리케이션은 완전한 32비트 응용프로그램이다. GUI함수도 사용할 수 있고, GUI 프로그램과 같지만, 단지 콘솔환경을 사용하고 있는 것일 뿐이다.

콘솔 어플리케이션은 입출력에 3개의 핸들을 사용한다. 그 3개란, 「표준 입력」 「표준 출력」 「표준 에러 출력」의 3개이다. 표준 입력 핸들은 콘솔로부터 정보를 취득하는데, 표준 출력 핸들은 콘솔에 정보를 출력하는데 사용한다. 표준 에러 출력 핸들은 리디렉트(출력을 파일 등에 변경하는 것) 할 수 없는(사실은 할려면 할 수 있지만) 것으로 에러를 보고하기 위해서 사용한다.

콘솔 어플리케이션은 GetStdHandle 함수를 호출해서, 3개의 표준 핸들중 어느것의 핸들이라도 얻어서 사용할 수있다. GUI 어플리케이션은 콘솔을 사용하지 않기 때문에, 만약 GetStdHandle 함수를 호출하게 되면 에러가 표시될 것이다. 그래도 콘솔을 사용하고 싶다면, AllocConsole 함수를 사용해서 새로운 콘솔을 생성할 수 있지만, 이경우에는 반드시 FreeConsole 함수를 호출해서 콘솔을 해제 시켜줘야하는것을 잊지 말기 바란다.

익명 파이프는, 자식 콘솔 어플리케이션의 입출력을 리다이렉트 할 경우에 주로 사용된다. 이 때, parent process는 콘솔 어플리케이션일수도 있고 GUI 어플리케이션일수도 있지만, 자식 어플리케이션은 콘솔 어플리케이션 이여야만 한다. 이미 설명했지만, 콘솔 어플리케이션은 입출력에 사용되는 표준 핸들을 사용하고 있다. 만약에 콘솔 어플리케이션의 입출력을 리다이렉트 하고 싶다면, 이들 표준 핸들을 파이프의 핸들로 변경하면 된다. 콘솔 어플리케이션에서 핸들이 변경되었어도 검사하지 않고 표준핸들과 같이 사용하게 된다. 이것은 일종의 폴리몰피즘(다형성)이다. 폴리몰피즘이란 것은 OOP(객체 지향)의 용어이다. 이런 방법은, child process에는 어떤 변경도 필요없기 때문에 매우 강력한 것이다.

한가지 문제가 있다. 콘솔 어플리케이션이 어디에서 표준 핸들을 얻는가 이다. 콘솔 어플리케이션이 생성 될때, parent process는 두가지 선택사항이 존재한다. 한가지는, 자식 어플리케이션용으로 새롭게 콘솔을 생성하는 방법과, 자신의 콘솔을 자식 어플리케이션에 상속시키는 방법이다. 두번째 방법은, parent process가 콘솔 어플리케이션이여만 한다. 만약 GUI 어플리케이션이라면, AllocConsole 함수를 먼저 호출해서 새로운 콘솔을 생성해야만 한다.

자 그렇다면 실제로 생성해 보자. 익명 파이프를 생성하기 위해서, CreatePipe 함수를 호출한다.

CreatePipe proto pReadHandle     : DWORD, \
                 pWriteHandle    : DWORD, \
                 pPipeAttributes : DWORD, \
                 nBufferSize     : DWORD

  • pReadHandle
    이 변수에 파이프를 읽기 위한 핸들이 반환된다(포인터)
  • pWriteHandle
    이 변수에 파이프에 쓰기 위한 핸들이 반환된다(포인터)
  • pPipeAttributes
    반환된 읽기/쓰기 핸들을 child process에 상속여부를 결정하기 위해, SECURITY_ATTRIBUTES 구조체의 포인터를 지정한다
  • nBufferSize
    파이프가 사용하는 버퍼크기를 지정한다. 다만, 이것은 단순한 참고사항이며, 실제 그 값이얼마 인지는모른다. NULL을 지정하면,기본크기가 사용된다.

이 함수가 성공적으로 수행되면, 반환값은 0이 아닌값이 되고, 실패한다면 0 이 된다.

호출이 성공한다면, 2개의 핸들을 얻게된다. 한개는 읽기용이며 다른 하나는 쓰기용이다. 이들을 사용해서, 자식 콘솔 어플리케이션의 표준입출력을 자신이 작성한 프로세스로 리다이렉트 하는 방법을 설명한다. 하지만, 이 방법은 Borland's Win32API 레퍼런스와는 차이가 있다. 레퍼런스에서는 parent process는 콘솔 어플리케이션이라고 가정하므로, 자식 어플리케이션은 부모 어플리케이션으로부터 표준핸들을 상속받고 있다. 그러나, 대부분의 경우 콘솔 어플리케이션으로부터 GUI 어플리케이션으로 출력을 리다이렉트 하는 것이기 때문에 이를 설명한다.

  1. CreatePipe 함수를 호출해서 익명 파이프를 생성한다. SECURITY_ATTRIBUTES 구조체의 bInheritable 멤버에 TRUE를 설정하는 것을 잊지 말기 바란다.이 경우,핸들은 상속해서 사용 가능하게 된다.
  2. 자식 콘솔 어플리케이션를 로드하기 위해서, CreateProcess 함수의 인수를 준비한다. 중요한 파라미터로는 STARTUPINFO 구조체이다. 이 구조체는 child process가 표시될 때, child process의 메인 윈도우가 표시될지 어떨지를 결정한다. 이 구조체는 아주 중요한 것으로, 메인 윈도우를 숨기거나 child process에 파이프 핸들을 넘겨주는 등의 행동을 할 수 있다. 다음은 설정해야 하는 멤버에 대한 설명이다.
    • cb
      STARTUPINFO 구조체의 크기
    • dwFlags
      구조체의 어떤 멤버가 유효한지를 결정하는 비트 플래그로서, 이로인해, 메인 윈도우를 표시하거나 숨길수 있게 된다. 이예제에서는, STARTF_USESHOWWINDOW 와 STARTF_USESTDHANDLES 을 사용한다.
    • hStdOutput and hStdError
      child process가 표준 출력, 표준 에러 출력의 핸들로 사용하고 싶은 핸들을 지정한다. 예제에서는, 표준 출력 핸들, 표준 에러 출력 핸들로 파이프에 쓰기가능한 핸들을 넘겨준다. 즉, child process의 표준 출력, 표준 에러 출력의 어떤 출력은 그 인수의 파이프를 통해 parent process로 넘겨지게 된다.
    • wShowWindow
      메인 윈도우의 표시상태를 결정한다. 예제에서는, 자식 어플리케이션의 윈도우는 표시하지 않기 때문에, SW_HIDE를 지정한다.
  3. CreateProcess 함수를 호출해서 자식 어플리케이션을 로드한다. CreateProcess 함수가 성공적으로 수행되더라도, 아직 child process는 대기상태이다. 단지 메모리에 로드만 되어있고, 실행은 되지 않은 것이다.
  4. 파이프의 쓰기 핸들을 닫는 것을 잊지 말기 바란다. 왜냐면, parent process는 쓰기 핸들을 사용하지 않기 때문이다. 만약 사용하지 않는 핸들을 사용하게 되면 파이프는 작동하지 않기 때문에, 파이프로부터 데이터를 읽기 전에 반드시 닫아 주어야 만 하는 것이다. 그러나, CreateProcess 함수를 호출 하기 전에 쓰기 핸들을 닫아서는 안 된다. 만약 호출하게 되면 파이프가 파괴된다. 그래서, 닫는 시기는 CreateProcess 함수를 실행한 후, 파이프로부터 데이터를 읽어들이기 전이다.
  5. ReadFile 함수로 읽기 파이프로부터 데이터를 읽을 수 있다. ReadFile 함수로 인해 child process는 대기모드에서 작동모드가 된다. 그리고, 표준 출력 핸들에 무엇인가 출력하게 되지만, 그 데이터는 읽기파이프를 통과하게 되어있어서, 데이터가 차례대로 보내지게 된다. 데이터전부가 한꺼번에 보내지지는 않기 때문에, 반환값이 0 이 될 때까지 ReadFile 함수를 계속 호출해야 한다. 0 이 되면, 데이터를 모두 읽었다는 의미이다. 이렇게 읽어들인 데이터에 어떤 처리를 할 수도 있다. 예제에서는 에디트 컨트롤로 데이터를 전송하고 있다.
  6. 파이프의 읽기핸들을 닫는다.

Example:

.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        ; the ID of the main menu
IDM_ASSEMBLE equ 40001

.data
ClassName           db "PipeWinClass", 0
AppName             db "One-way Pipe Example", 0 EditClass db "EDIT", 0
CreatePipeError    db "Error during pipe creation", 0
CreateProcessError    db "Error during process creation", 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

Analysis:

이 예제에서는, ml.exe에 test.asm이란 파일을 어셈블(assemble)하고, 출력 결과를 작업영역의 에디트 컨트롤로 리다이렉트 하고 있다. 프로그램이 로드 되었을 때, 보통 같이 윈도우 클래스를 등록하고, 메인 윈도우를 생성한다. 메인 윈도우를 생성할 때에 먼저, ml.exe의 출력 결과를 표시하기 위한 에디트 컨트롤을 생성하는 것이다.

재미있는 것은, 에디트 컨트롤의 문자색과 배경색을 변경하는 부분이다. 에디트 컨트롤이 작업영역에 표시될 때, 부모윈도우의 WM_CTLCOLOREDIT 메세지를 전송하고 있다.

그 때, wParam는, 에디트 컨트롤이 표시되는 작업영역의 DC핸들이 저장되어 있기 때문에, HDC의 특성을 변경한다.

.elseif uMsg==WM_CTLCOLOREDIT
    invoke SetTextColor, wParam, Yellow
    invoke SetTextColor, wParam, Black
    invoke GetStockObject, BLACK_BRUSH
    ret

SetTextColor 함수에 의해 글자색을 노란색으로 바꾸고 있고 배경색을 검정색으로 변경하고 있다. 마지막으로, Windows로 부터 검정색의 브러쉬 핸들을 넘겨받게 된다. 그리고, WM_CTLCOLOREDIT 메세지로, Windows가 에디트컨트롤의 배경색을 처리하기 위해서 브러쉬의 핸들을 설정해야 한다. 예제에서는, 배경색을 검정색으로 설정하기 때문에, Windows에 검정색브러쉬 핸들을 넘겨주고 있다.

유저가 Assemble 메뉴를 선택할 경우, 익명 파이프가 생성된다.

.if ax==IDM_ASSEMBLE
    mov sat.nLength, sizeof SECURITY_ATTRIBUTES
    mov sat.lpSecurityDescriptor, NULL
    mov sat.bInheritHandle, TRUE

CreatePipe 함수를 호출하기 전에, 먼저 SECURITY_ATTRIBUTES 구조체를 설정해야 한다.보안속성에 대해서 무시할려면, lpSecurityDescriptor 멤버는 NULL을 설정하면 된다. 그리고, 파이프핸들을 child process에 상속시키기 위해서, bInheritHandle를 TRUE로 설정한다.

invoke CreatePipe, addr hRead, addr hWrite, addr sat, NULL

그런 후, CreatePipe 함수를 호출해서, 성공적으로 수행되면, hRead와 hWrite가 각각, 파이프의 읽기핸들과 쓰기핸들로 설정된다.

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

다음으로, STARTUPINFO 구조체를 설정해야 하지만, 먼저 GetStartupInfo 함수를 호출해서 parent process의 기본값을 설정해준다. 만약 Win9x와 NT의 양쪽에서 실행시킬 생각이면, STARTUPINFO 구조체를 수정해야 한다. GetStartupInfo 함수를 호출한 후, 파이프의 읽기/쓰기 핸들을 hStdOutput와 hStdError에 복사해서, child process가 기본적으로 사용하고 있는 표준 출력, 표준 에러 출력의 핸들을 변경한다. 그리고, child process의 콘솔 윈도우를 감추기 위해서, wShowWidow 멤버에 SW_HIDE를 지정한다.끝으로, hStdOutput와 hStdError, wShowWindow가 유효하게 될수 있도록 dwFlags 플래그에, STARTF_USESHOWWINDOW 와 STARTF_USESTDHANDLES를 지정해 준다.

invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL,\
                      addr startupinfo, addr pinfo

이 상태에서, CreateProcess 함수를 호출해 child process를 생성한다. 파이프 핸들을 유효한것으로 만들기 위해 bInheritHandles 파라미터를 TRUE로 설정한다.

invoke CloseHandle, hWrite

child process의 생성이 성공적으로 수행되면, 쓰기파이프를 닫아줘야 한다. STARTUPINFO 구조체를 통해서, child process에 쓰기핸들을 넘겨준 것을 생각하기바란다. 만약, 쓰기핸들을 닫지 않는다면,쓰기장치가 2개가 되어 버리기 때문에 파이프는 작동하지 않게된다. 이렇듯 CreateProcess 함수를 호출한 다음에,파이프로부터 데이터를 읽기 전에 쓰기핸들을 닫아줘야 한다.

.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

이 로써 child process의 표준 출력으로부터 데이터를 읽을 준비가 되었다. 파이프로부터 모든 데이터를 읽을때까지 무한루프를 수행한다. RtlZeroMemory 함수를 호출해서 버퍼를 0 으로 설정하고, ReadFile 함수를 호출한다. 이 때, 파일 핸들이 아니고 파이프핸들을 인수로 설정한다. 예제에서는, 최대 1023바이트의 문자를 처리하는 것을 주의하기 바란다. 왜냐면, 에디트 컨트롤에 전송하는 데이터는 널종료문자여야 하기 때문이다. (1024 - 1 = 1023)

ReadFile 함수가 버퍼에 데이터를 읽으면, 그 데이터를 에디트 컨트롤로 전송한다. 이 경우는 약간의 문제가 발생한다. 에디트 컨트롤에 데이터를 전송하기 위해서 SetWindowText 함수를 호출하면, 기존에 존재하는 데이터를 덮어쓰게 된다. 여기서 수행하고 싶은것은, 덮어쓰기가 아니고 「추가」하는 것이다.

그렇기 때문에, 에디트 컨트롤의 wParam을 -1 로 설정해서 EM_SETSEL 메세지를 전송하면 문자열의 입력위치를 가장 마지막으로 옮기게 되고,EM_REPLACESEL 메세지로 인해 지정된 장소부터 데이터를 추가해 나가게 된다.

invoke CloseHandle, hRead

ReadFile 함수가 NULL을 반환하면, 루프를 빠져나가고 읽기핸들을 닫는다.


Posted by openserver