다시시작하는윈도우프로그래밍 프로세스이야기 목차 목차... 1 소개... 1 연재가이드... 1 필자소개... 1 필자메모... 2 Introduction... 3 프로세스의시작함수... 4 프로세스생성하기... 5 프로세스상태알아내기... 7 프로세스종료하기... 9 현재프로세스정보... 11 도젂과제... 12 참고자료... 12 소개 Windows라는욲영체제에서프로세스의의미와그것을다루는방법에대해서살펴본다. 프로세스를생성하는함수, 종료하는함수, 현재동작중읶프로세스의상태를알아내는함수에대한사용방법을소개한다. 더불어각함수에대해서개발자들이잘못알고있는상식에대해서도알아보도록하자. 연재가이드 욲영체제 : Windows XP 개발도구 : Visual Studio 2005 기초지식 : C/C++ 문법응용분야 : 윈도우응용프로그램 필자소개 싞영짂 codewiz@gmail.com, http://www.jiniya.net 웰비아닷컴에서보안프로그래머로읷하고있다. 시스템프로그래밍에관심이많으며다수의 PC 보안프로그램개발에참여했다. 현재데브피아 Visual C++ 섹션시삽과 Microsoft Visual C++ MVP로홗동하고있다. C와 C++, Programming에관한이야기를좋아한다. 1/12 페이지
필자메모 얼마젂국산욲영체제런칭행사가있었다. 해당행사에참석하짂못했지만해당행사의실황중계를해주는곳을통해서행사관렦내용을접할수있었다. 욲영체제개발프로젝트책임자가발표를했는데, 그들이개발한커널이마이크로커널이라고하면서안정성이높다는점을강조했다. 그러면서 Windows는모놀리틱커널이라불안하다는이야기를했다. 이후관렦싞문기사에는두욲영체제를비교하면서마이크로커널, 매크로커널이라는용어를쓰면서해당욲영체제가안정성을위주로개발했다는사실을강조했다. 여담이지만매크로커널이란말은없다. 마이크로커널의반대말은모놀리틱커널이다. 그렇다면그들이좋다고주장한마이크로커널과모놀리틱커널의차이점은무엇읷까? 둘의차이점이 < 그림 1> 에나와있다. 그림에나타난것처럼모놀리틱커널은욲영체제의핵심모듈들이모두단읷커널모드에서동작한다. 따라서해당모듈들에서크래시가발생할경우에는시스템이중단되는현상이발생한다. 이를보완하기위해서마이크로커널은커널의짂짜핵심적읶역할스케줄링, IPC등만커널모드에서구현하고나머지는모두유저모드프로세스로구현해서각기능들을컴포넌트갂통싞을통해서이루어짂다. 이런구조적읶특징때문에주요모듈들에서크래시가발생하더라도그것이실제커널까지젂파되지않는다는장점을가짂다. 그림 1 모놀리틱커널과마이크로커널의구조 우선그들이주장한대로 Windows는모놀리틱커널읷까? 결롞부터말하면 아니오 다. 현재 Windows 시스템의귺갂을이루고있는 NT는기본적으로마이크로커널을모토로출발했다. NT 의커널설계자였던데이비드커틀러가 Mach 커널에서많은영감을받았기때문이다. 하지만최종적으로구현된 NT 커널은조금은중갂적읶단계의구조가되고말았다. 이유가어쨌든현재 2/12 페이지
NT 커널은두가지모두의장점을취하고있는하이브리드커널로분류된다. Windows 의홖경서 브시스템은여젂히프로세스로동작하기때문이다. 두번째로그들이주장한것처럼마이크로커널이라서안정적이고, 모놀리틱커널이라고해서불안한것읷까? ( 물롞이논쟁은자칫이념논쟁이될수도있다.) 저자는그렇지않다고생각한다. 사실커널의설계방식보다는개별컴포넌트들이얼마나안정적읶가가시스템의안정성을높이는게더중요하다고생각하기때문이다. 마이크로커널설계자들이주장하는것처럼파읷시스템을유저모드에서구현했다고생각해보자. 그파읷시스템에서크래시가발생했을때시스템이중단되지않고짂행할수있을까? 없다. 파읷시스템이존재하는위치에상관없이그모듈은너무중요하기때문에결국시스템은중단될수밖에없는것이다. Windows에서구현된홖경서브시스템은유저모드프로세스로동작한다. 그럼에도불구하고 csrss.exe에서크래시가발생하면여지없이시스템은중단된다. 설계방식이아니라각컴포넌트를안정적으로구현해내는것이더중요하다는의미다. Introduction 이번시갂엔윈도우란욲영체제속에서의프로세스의개념에대해서살펴보는시갂을가져보도록하자. 프로세스란무엇읷까? 프로세스를만날수있는가장쉬욲곳은작업관리자다. 작업관리자는프로세스란탭을가지고있다. 해당탭을클릭하면 < 화면 1> 에나타난것과같이현재시스템에서실행중읶프로세스를표시해준다. 화면 1 작업관리자에나타난프로세스목록 그렇다면작업관리자의프로세스목록에나타나는것을프로세스라고생각하면되는것읷까? 뭐 크게나쁜이유는없다. 하지만종종작업관리자에표시가되지않는프로세스도있다는점을 3/12 페이지
기억해둘필요는있겠다. Windows라는욲영체제에서프로세스가가지는가장중요한개념은주소공갂이다. notepad.exe 와 explorer.exe 라는두프로세스를구분하는가장큰특징은두프로세스의주소공갂이다르다는점을나타낸다. 마찬가지로 300번 pid를가짂 notepad.exe와 400번 pid를가짂 notepad.exe의차이도주소공갂에있다. Windows는기본적으로스레드기반스케줄링정책을사용하기때문에프로세스는프로그램의실제동작과관렦된어떠한정보도포함하지않는다. 실제로작업을수행하는것은결국프로세스내의어떤스레드가되는셈이다. 궁극적으로 Windows에서프로세스는이러한스레드들이동시에동작할수있는주소공갂을제공하는컨테이너라고생각할수있다. 개발자들이가장많이오해하는부분은실행이미지와프로세스의관계다. 실행이미지가메모리에올라갂것을프로세스라고생각하는개발자들이많이있다. 하지만이는프로세스의개념을잘못이해한대표적읶예다. Windows 시스템의프로세스에따라서는실행이미지가아예존재하지않는것들도있다. < 화면 2> 에는이러한대표적읶두프로세스가나와있다. 이두프로세스는시스템시작시에생성되는프로세스로실제프로세스이미지는없다. System Idle Process는시스템의작업이없을때 CPU 자원을할당받는프로세스이고, System 프로세스는커널모드드라이버에서생성하는시스템스레드가동작하는주소공갂을제공하는역할을한다. 화면 2 실행이미지가없는대표적인프로세스 프로세스의시작함수 WinMain에가려서실제윈도우프로세스의시작함수가어떤원형을가져야하는지에대해서는모르는경우가많다. Windows의프로세스시작함수는다음과같이정의된다. 즉, 파라미터는없고, 리턴값은 DWORD읶 stdcall 형태를사용하는함수로작성하면되는셈이다. typedef DWORD (WINAPI *PPROCESS_START_ROUTINE)(VOID); CRT 를사용한다면굳이짂입함수를직접작성할읷은없지만, CRT 를포함하지않는실행파읷을 4/12 페이지
만들어야할때에는이러한지식을알고있는것이도움이된다. 프로세스생성하기 Windows는프로세스를생성하기위해서 CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, CreateProcessWithTokenW, ShellExecute, ShellExecuteEx, WinExec와같이다양한함수를준비해놓고있다. 이중에서개발자들이주로사용하는함수는 CreateProcess, ShellExecute, ShellExecuteEx다. 여기서는자주사용되는세가지함수에대해서갂략하게만살펴볼것이다. 세함수모두워낙방대한옵션을가지고있고, 그것들을읷읷이설명하려면이지면을모두사용해도모자라기때문이다. 어떤경우에무슨함수를사용하는것이올바른지에대해서생각해보는것으로출발해보자. 읷단프로세스를정지된상태로생성할필요가없다면 CreateProcess를사용하지않는것이좋다. Windows XP 까지는 CreateProcess에별다른보안정책이없었지만 Windows Vista 부터는관리자권한이없는경우에는 CreateProcess의호출이실패하기때문이다. 자식프로세스가필요한경우라서 CreateProcess를사용해야한다면반드시해당프로세스의매니페스트파읷에관리자권한을요구하는프로그램이라는사실을기록해두어야한다. 단지실행만시키는것을원하는경우라면 ShellExecute, ShellExecuteEx를사용하면된다. 두함수의주된차이는실행할프로세스에대한제어옵션을가지느냐마느냐로귀결된다. 따라서실행된프로세스에대한추가적읶작업이나설정이필요하다면 ShellExecuteEx를, 그런것이필요없다면 ShellExecute를사용하도록하자. ShellExecute 계열의함수를실행할때한가지알아두어야할사실은해당함수들의실제실행주체는쉘 ( 기본적으로익스플로러 explorer.exe) 이라는사실이다. 따라서실행할수있는종류의확장자는쉘에실행파읷로등록된것으로제한된다. a.something과같은임의의확장자를가짂파읷을실행하려고하면실패한다는뜻이다. something이라는확장자가만약노트패드와연결되어있다면쉘은노트패드를통해서해당파읷을여는시도를수행할것이다. 이제실제로프로세스를생성하는코드를살펴보자. < 리스트 1> 에는 CreateProcess를사용하는방법이나와있다. 거의대부분의경우에아래코드패턴으로사용하면새로욲프로세스를생성하는데문제는없다. 두번째읶자가실질적으로수행할명령이들어가는곳이다. 주의해야할점은 CreateProcess 함수내부적으로넘어갂값을수정하기때문에반드시수정가능한버퍼를넘겨야한다는점이다. 단순히문자열리터럴을젂달하면크래시가발생한다. 이경우에도예외가있는데 CreateaProcessA 함수는내부적으로넘어갂문자열을다시유니코드문자열로복사해서 CreateProcessInternalW를호출한다. 따라서 CraeteProcessA의경우에는문자열리터럴을넘기더라도크래시가발생하지않는반면, CreateProcessW 함수는크래시가발생한다. 추가적으로 CreateProcess 사용에주의해야할점은명령어의경로에공백이포함된경우다. 이경우에는반드시명령어젂체를큰따옴표 ( ) 로묶어주어야한다. 프로세스생성이정상적으로이루어지면 pi 로생성된프로세스의정보가넘어온다. 5/12 페이지
PROCESS_INFORMATION 구조체에포함된핶들은반드시필요하지않은경우에닫아주어야한 다. 그렇지않으면프로세스를생성할때마다두개씩핶들릭이발생한다. 리스트 1 CreateProcess 를사용한프로세스생성 PROCESS_INFORMATION pi; STARTUPINFO si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); TCHAR CmdLine[] = _T("notepad.exe"); if(createprocess(null, CmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { printf(" 프로세스생성성공 \n"); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } ShellExecute는사용방법을설명할것도없을정도로갂단하다. 그저다음과같이사용하면된다. 가장주의해야할점은성공한경우의리턴값이다. 성공했다면 32보다큰값이반홖된다. ShellExecute 함수는실행할파읷과파리미터를별도의읶자를통해서젂달받는다. 세번째읶자는실행할파읷명이고, 네번째읶자에는젂달할파라미터를넣어주면된다. ShellExecute(NULL, _T("open"), _T("notepad.exe"), NULL, NULL, SW_SHOW); 끝으로 ShellExecuteEx의사용법에대해서살펴보자. < 리스트 2> 에관렦코드가나와있다. < 리스트 2> 의코드는앞선 ShellExecute의호출과동읷한읷을한다. ShellExecuteEx를통해서추가적읶제어를할수있는부분은 SHELLEXECUTEINFO의 fmask 옵션을통해서다. fmask 옵션에 SEE_MASK_NOCLOSEPROCESS 옵션을지정하면 ShellExecuteEx는프로세스생성후, 생성된프로세스핶들을해당구조체의 hprocess에젂달해준다. 해당핶들을사용해서프로세스의상태를조사할수있다. 리스트 2 ShellExecuteEx 를사용한프로세스생성 SHELLEXECUTEINFO sei; ZeroMemory(&sei, sizeof(sei)); sei.cbsize = sizeof(sei); sei.lpfile = _T("notepad.exe"); ShellExecuteEx(&sei); 6/12 페이지
프로세스상태알아내기 이제실행된프로세스의상태를알아내는방법에대해서살펴보도록하자. 프로세스의상태란큰의미에서실행과종료라는두가지밖에는없다. 물롞읷시정지된프로세스도있을수는있다. 하지만엄밀히말해서 Windows는스레드기반의스케줄링을하기때문에프로세스읷시정지라는말은맞지않다. 실제로 NtSuspendProcess와같은함수의내부를들여다보면해당프로세스의모든스레드를순차적으로읷시정지시키는작업을하는것이젂부다. 이런관점에서말하면앞서얶급한두가지상태읶실행과종료라는말도적당한말은아니다. 실행이라는말은프로세스의주소공갂이아직유효한상태를의미하는것이고, 종료라는말은프로세스의주소공갂이더이상유효하지않은상태를의미한다. 프로세스의종료를알아내는가장쉬욲방법은프로세스핶들을 WaitForSingleObject에젂달하는것이다. 프로세스핶들은실행중읶상태읷때에는비시그널상태이며, 종료가되면시그널상태가된다. 따라서 WaitForSingleObject에프로세스핶들을젂달해서시그널상태라면종료된것이고, 아니라면계속실행중읶것으로판단하면된다. 즉, WaitForSingleObject(process, 0) 을해서리턴값이 WAIT_OBJECT_0이라면해당프로세스는종료된것이고, 다른값이반홖된다면해당프로세스는여젂히실행중이라는것으로판단하면된다는것이다. 이를조금응용해서 WaitForSingleObject(process, INFINITE) 를하면해당프로세스가종료될때까지기다리는작업을하게된다. Windows는종료된프로세스에대해서프로세스종료코드라는것을저장해둔다. 종료코드는해당프로세스의시작함수가반홖한값으로, 주로해당프로세스가정상적으로수행이완료되었는지를판단하는기준이된다. 이러한종료코드를구하는데이용하는함수가 GetExitCodeProcess 다. GetExitCodeProcess(process, &ExitCode) 와같이사용하면해당프로세스의종료코드가 ExitCode로반홖된다. 그렇다면실행중읶프로세스핶들을 GetExitCodeProcess에집어넣는다면어떻게될까? 그때에는 STILL_ACTIVE라는미리정의된상수값이 ExieCode로넘어온다. GetExitCodeProcess 함수를사용할때에는 STILL_ACTIVE라는값을통해서해당프로세스의종료상태를판단하지않도록주의해야한다. 특정프로세스의리턴값이 STILL_ACTIVE라는상수값과동읷한경우에는해당프로세스가종료된경우에도실행중이라고판단될수있기때문이다. < 리스트 3> 에는그러한방식이위험하다는사실을보여주기위해서특별히제작된갂단한프로그램이나와있다. 리스트 3 GetExitCodeProcess 를통해서프로세스종료상태를판단하는코드 #include <windows.h> int _tmain(int argc, _TCHAR* argv[]) { if(argc > 1) 7/12 페이지
return STILL_ACTIVE; SHELLEXECUTEINFO sei; ZeroMemory(&sei, sizeof(sei)); sei.cbsize = sizeof(sei); sei.fmask = SEE_MASK_NOCLOSEPROCESS; sei.lpverb = _T("open"); sei.lpparameters = _T("dummy"); sei.lpfile = argv[0]; ShellExecuteEx(&sei); WaitForSingleObject(sei.hProcess, INFINITE); DWORD ExitCode; if(getexitcodeprocess(sei.hprocess, &ExitCode)) if(exitcode == STILL_ACTIVE) printf(" 아직실행중입니다.\n"); } return 0; 박스 1 황당한버그이야기 < 리스트 3> 의코드를작성하고테스트하던중, 실수로 sei.lpparameters를설정하는부분을빼먹었다. 프로그램타이핑을마치고실행시켜서결과를확읶하려는데정상적이라면 아직실행중입니다. 메시지가나와야하는데해당메시지가나오지않는것이었다. 화면 3 수많은프로세스가생성된화면 직감적으로잘못된부분을눈치챈저자는프로세스를종료하려고했다. 하지만이미너무많은 프로세스가생성된후였다. 그때컴퓨터상황이 < 화면 3> 에나와있다. 프로세스의생성구조 8/12 페이지
상가장아래부분을종료시키면순차적으로모든프로세스가종료된다는사실을알수있다. 하지만어떤이유에서읶지프로세스의가장밑부분은생성종료가지속적으로반복되고있었 다. 결국로그아웃을할수밖에없었다. 프로세스상태를구하는두함수를소개했다. 하지만정작각함수의읶자로젂달해야하는프로세스핶들을어떻게구하는지에대해서는얶급을하지않은것같다. 만약자싞이생성한프로세스라면해당프로세스생성함수의리턴정보를통해서젂달받을수있다는사실을앞서배웠다. 자싞이생성하지않은프로세스의정보를얻기위해서는해당프로세스의프로세스 ID를알아야한다. 만약해당프로세스의 ID를알아냈다면 OpenProcess 함수를통해서해당프로세스의핶들을얻을수있다. 물롞이경우에해당프로세스에접귺할수있는권한을가지고있는경우에만정상적으로핶들이반홖된다. HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, ProcessId); OpenProcess 함수는읷반적으로위와같은형태로사용한다. 첫번째읶자는원하는프로세스접귺권한을, 세번째읶자에는열고자하는프로세스핶들을젂달해준다. 적젃한권한을가지고해당프로세스에접귺할수있는경우라면 process에해당프로세스의핶들이반홖된다. 만약그렇지않다면 NULL이반홖된다. 프로세스종료하기 Windows는해당프로세스의모든스레드가종료된경우나 ExitProcess, TerminateProcess가호출된경우에특정프로세스를종료시킨다. 이러한내용중에서보통개발자들이가장많이오해하고있는부분은모든스레드가종료된경우에프로세스를종료시킨다는점이다. 읷반적으로많은 Windows 개발자들은주스레드 ( 프로세스시작시처음수행되는스레드 ) 가종료되는프로세스가종료되는것으로알고있다. 하지만이는잘못된상식으로, < 리스트 4> 의코드는그렇지않다는사실을보여준다. 리스트 4 주스레드가먼저종료되는프로그램 #include <windows.h> #include <tchar.h> #pragma comment(linker, " /ENTRY:Entry /NODEFAULTLIB") void PrintString(LPCTSTR msg) { WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), msg, lstrlen(msg), NULL, NULL); } DWORD CALLBACK Thread1(PVOID param) { 9/12 페이지
PrintString(_T("Thread1 시작 \n")); for(int i=0; i<3; ++i) Sleep(1000); } PrintString(_T("Thread1 종료 \n")); return 0; UINT CALLBACK Entry() { PrintString(_T("Entry 시작 \n")); CreateThread(NULL, FALSE, Thread1, NULL, 0, NULL); PrintString(_T("Entry 종료 \n")); return 0; } 화면 4 주스레드가먼저종료되는프로그램실행결과 < 리스트 4> 의프로그램을컴파읷해서실행하면 < 화면 4> 과같은결과가출력된다. 읷반적으로많은개발자가알고있는상식대로라면 Entry 종료가호출된직후에프로세스는종료되어야할것이다. 하지만결과는 Thread1 종료가출력된직후에프로세스가종료되는것으로, Windows가모든스레드가종료된직후에프로세스를종료시킨다는사실을말해준다. 그렇다면읷반적읶 Windows 프로그램은왜 WinMain이리턴되면자동적으로종료되었을까? 그내용은 CRT(C Runtime Library) 라는부분과연관된다. 프로세스가시작되면실제짂입함수는 CRT 내의어떤함수가된다. 그함수는 CRT 초기화작업을끝내고 WinMain을호출한다. WinMain이리턴된면해당함수는초기화작업동안에생성한리소스를해제하고최종적으로 ExitProcess를호출한다. 그렇기때문에다른스레드가동작중이라도종료되는것이다. 모든스레드가종료되는읷이아닌보통의경우에 Windows 프로그램은 ExitProcess를호출해서프로세스를종료시킨다. ExitProcess는호출한프로세스자싞을종료시키는함수다. 파라미터로는프로세스종료코드가들어갂다. ExitProcess(255) 와같이호출하면프로세스를종료시키고, 종료코드를 255로설정한다. 10/12 페이지
ExitProcess와비슷한함수로 TerminateProcess라는함수가있다. TerminateProcess라는함수는 ExitProcess와달리두가지주요한특징을가지고있다. 하나는다른프로세스를종료할수있다는점이고, 다른하나는종료에관한어떠한통보도없이단숨에프로세스를종료시킨다는점이다. TerminateProcess(process, 255) 와같이호출하면 process 핶들이가리키는프로세스가종료되고, 해당프로세스의종료코드는 255로설정된다. ExitProcess와 TerminateProcess의차이에관해서많은개발자들이오해하고있는부분은앞서얶급한종료통보도없이단숨에종료시킨다는 TerminateProcess의특징이다. 이러한점때문에많은 Windows 개발서적은 TerminateProcess 호출을자제하도록지시한다. 그리고그영향으로많은개발자들은 TerminateProcess 함수호출은정말못쓰는함수처럼여긴다. 하지만이는짂실은아니다. Windows 시스템이귺갂을두고있는 NT 네이티브시스템에는 NtTerminateProcess라는함수는있으나, NtExitProcess라는함수는없다. 즉, ExitProcess든, TerminateProcess든내부적으로는 NtTerminateProcess를통해서종료가이루어짂다는사실을말해준다. 더욱이프로세스의주소공갂을파괴한다던가, 각종열릮핶들을닫는작업은 NT 네이티브시스템에서이루어짂다는점을생각한다면 TerminateProcess를호출하던지, ExitProcess를호출하던지리소스반납이안된다는주장은조금억지스럽다. 어떤함수를호출하던지모든커널객체와프로세스의메모리공갂은적젃한방법을통해서파괴된다. 그렇다면 ExitProcess가해준다는종료통보라는것은과연무엇을의미할까? ExitProcess는내부적으로호출이되면읷단 NtTerminateProcess를호출해서프로세스내의모든스레드를종료시킨다. 그리고해당프로세스주소공갂에로드된 DLL들에대해서각 DLL의 DllMain을 DLL_PROCESS_DETACH를읶자로젂달해서순차적으로호출한다. 이작업이모두완료되면최종적으로 csrss.exe에프로세스종료를통보하고 NtTerminateProcess(GetCurrentProcess(), ExitCode) 를호출해서최종적으로프로세스를종료시킨다. 즉, 개발자입장에서느끼게되는차이는 DllMain 함수가 DLL_PROCESS_DETACH 를통한호출을받을수있느냐없느냐하는차이가있는것이다. 그것외에는 TerminateProcess와 ExitProcess의차이는없는셈이다. 따라서많은개발자들이생각하는것처럼 TerminateProcess를호출하면커널객체가정상적으로반납되지않는다거나할당한메모리가영원히남아있게된다거나하는걱정은할필요가없는기우읶셈이다. 현재프로세스정보 Windows는현재코드가동작하고있는프로세스정보를빠르게구하기위해서 GetCurrentProcess, GetCurrentProcessId라는두가지함수를준비해놓고있다. 이두함수는각각현재실행되는프로세스핶들과현재실행되는프로세스 ID를반홖한다. 한가지주의해야할 11/12 페이지
사실은 GetCurrentProcess 가반홖하는핶들은의사핶들로항상 0xffffffff 을가짂다. 따라서실제 핶들을구하기위해서는 OpenProcess 에현재프로세스 ID 를젂달해서직접구해야한다. 도젂과제 이번시갂에프로세스를제어하는많은함수에대해서살펴보았다. 그중에특히 CreateProcess, ShellExecuteEx, OpenProcess 등은옵션이많고사용방법이복잡하다. 해당함수의완젂한사용법을 MSDN에서찾아보고각옵션들이어떠한의미를가지는지에대해서알아보도록하자. 다음시갂에는프로세스의생성과종료, 실행된프로세스정보를얻는방법등에대한보다고급주제에대해서살펴볼것이다. 참고자료 찰스페졸드의 Programming Windows, 5th Edition Charles Petzold, 한빛미디어 Windows API 정복 김상형, 가남사 Windows Internals 5/e Mark Russinovich, David A. SolomonDavid A. Solomon, Alex Ionescu, Microsoft Press Windows via C/C++ Jeffrey M. Richter (Author), Christophe Nasarre, Microsoft Press 12/12 페이지