반응형

1. 특정 디렉토리 뒤지기

지정한 디렉토리에 있는 모든 파일을 찾아내는 코드를 만들려면 어떻게 해야 합니까 ?

이 때 사용할 수 있는 API가 바로 FindFirstFile과 FindNextFile, FindClose라는 API들입니다. 사용 예제는 다음과 같습니다.

	WIN32_FIND_DATA  findFileData;
	HANDLE hFileHandle;

	// szDir에 뒤지고자 하는 디렉토리의 경로명을 준다. 예를 들면 "C:\\TEMP\\*.*"
        // 찾아진 파일의 속성은 findFileData의 dwFileAttributes를 살펴본다.
	hFileHandle = FindFirstFile(m_szDir, &findFileData);  
	if (hFileHandle != INVALID_HANDLE_VALUE)   // 파일을 찾은 경우 
	{
		// 찾은 파일의 이름은 cFileName 필드로 들어온다.
		...
		// 다음 파일을 찾는다.
		while(FindNextFile(hFileHandle, &findFileData)) 		{
			...
		}
		FindClose(hFileHandle);
	}     

2. API를 이용하는 유니코드와 ANSI 문자열간의 변환 방법

API를 이용해서 유니코드와 ANSI 문자열간의 변환은 어떻게 수행합니까 ?

Visual C++에서 유니코드 문자열은 BSTR이란 타입으로 표시됩니다. 또 유니코드와 ANSI 문자열간의 변환을 위해서 윈도우 시스템에는 MultiByteToWideChar와 WideCharToMultiByte라는 API가 존재합니다. MFC에서의 BSTR 타입 변환방법이나 ATL로 하는 BSTR 타입 변환도 참고하시기 바랍니다.

  • ANSI 문자열에서 유니코드로의 변환 방법
    	// sTime이란 ANSI 문자열을 bstr이란 이름의 유니코드(BSTR 타입) 변수로 변환
    	char sTime[] = "유니코드 변환 예제";
    	BSTR bstr;
    	// sTime을 유니코드로 변환하기에 앞서 먼저 그 길이를 알아야 한다.
    	int nLen = MultiByteToWideChar(CP_ACP, 0, sTime, lstrlen(sTime), NULL, NULL);
    	// 얻어낸 길이만큼 메모리를 할당한다.
    	bstr = SysAllocStringLen(NULL, nLen);
    	// 이제 변환을 수행한다.
    	MultiByteToWideChar(CP_ACP, 0, sTime, lstrlen(sTime), bstr, nLen);
             // 필요없어지면 제거한다.
             SysFreeString(bstr);
    
  • 유니코드에서 ANSI 문자열로의 변환 방법
    	// newVal이란 BSTR 타입에 있는 유니코드 문자열을 sTime이라는 ANSI 문자열로 변환
    	char *sTime;
             int nLen = WideCharToMultiByte(CP_ACP, 0, newVal, -1, sTime, 0, NULL, NULL);
             sTime = malloc(nLen+1);
    	WideCharToMultiByte(CP_ACP, 0, newVal, -1, sTime, 128, NULL, NULL);
            // 필요없으면 메모리를 제거한다.
            free(sTime);
    
  • 유니코드 문자열을 UTF-8으로 변환하기
         WideCharToMultiByte 함수를 호출할 때 첫 번째 인자로 CP_UTF8을 지정하면 된다. UTF-8은 유니코드의 인코딩 스킴 중의 하나로 쉽게 말하자면 문자열 스트림에서 0을 빼고 표현하는 방법이라고 볼 수 있다.
    

    3. 레지스트리 읽기/쓰기

    API를 이용해서 레지스트리에 한 항목을 생성하거나 기존 항목의 값을 읽어들이려면 어떻게 해야합니까 ?

    레지스트리 관련 API를 사용하려면 winreg.h라는 헤더 파일을 소스에 포함해야 합니다. 레지스트리에 키를 생성하는 방법과 레지스트리에 존재하는 키의 값을 읽는 방법을 차례로 살펴보겠습니다.

  • 레지스트리 키 생성 예제
    	// 예를 들어 HKEY_LOCAL_MACHINE밑의 System\CurrentControlSet\Services\GenPort라는 키를
            // 생성하고 거기에 DWORD 타입의 값으로 Type을 만들고 문자열 타입의 값으로 Group
            // 을 만들어 본다.
    	#include "winreg.h"
    	LONG error = 0;
    	HKEY hKey;
    	DWORD dwDisp, dwData;
    	char lpData[] = "Write this down";
    
    	// 먼저 만들려는 키가 이미 존재하는 것인지 살혀본다.
    	error = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "System\\CurrentControlSet\\Services\\GenPort",
                             0, KEY_ALL_ACCESS, &hKey);
    
    	if (error != ERROR_SUCCESS) // 없다면 새로 생성한다.
    	{
    		// 키를 생성한다.
    		error = RegCreateKeyEx(HKEY_LOCAL_MACHINE,
    			"System\\CurrentControlSet\\Services\\GenPort",	0, "REG_BINARY", 
    		        REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, 0, &hKey, &dwDisp);
                    // 위의 키 밑에 Type이란 DWORD 타입의 값을 만들고 1로 초기화
    		dwData = 0x1;
    		error = RegSetValueEx( hKey, "Type", 0, REG_DWORD,&dwData,4); 
                    // 위의 키 밑에 Group이란 문자열 타입의 값을 만들고 lpData의 값으로 초기화
    		error = RegSetValueEx( hKey, "Group", 0, REG_SZ, lpData, strlen(lpData)); 
    
                    // 키를 닫는다.
    		RegCloseKey(hKey);	
    	}
    
  • 기존의 레지스트리 키에서 값 읽기
    	// HKEY_CURRENT_USER\Software\Netscape\Netscape Navigator\Main 밑의 Install Directory
            // 값의 문자열 값을 읽어들인다.
    	DWORD dwType, cbData;
    	HKEY hSubKey; 
    	long lRet;
    	char pszString[255];
    
    	// 키를 오픈한다.
    	if ((lRet = RegOpenKeyEx(HKEY_CURRENT_USER, 
    	                "Software\\Netscape\\Netscape Navigator\\Main",
    			0, KEY_READ | KEY_QUERY_VALUE , &hSubKey)) == ERROR_SUCCESS)
    	{
    		cbData = 255;	// 문자열 값을 읽어올 데이터의 크기를 준다.
    		if ((lRet = RegQueryValueEx(hSubKey, "Install Directory",
    			NULL, &dwType, pszString, &cbData)) == ERROR_SUCCESS)
    		{
    			// 제대로 읽힌 경우
    		}
    		else
    		{
    			// 에러가 발생한 경우
    		}
    		RegCloseKey(hSubKey);
    	}
    
  • 레지스트리 키 삭제하기 - RegDeleteKey 함수를 사용한다.

    4. 윈도우 탐색기로부터의 Drag&Drop을 받으려면

    윈도우 탐색기로부터 제가 만든 윈도우로의 drag&drop이 가능하게 하려면 어떻게 해야 합니까 ?

    다음 순서를 따라서 프로그래밍하시면 됩니다.

    1. 프로그램의 초기화시에 DragAcceptFiles(hWnd, TRUE) 함수를 호출한다. 첫 번째 인자인 hWnd는 드롭의 타겟이 되는 윈도우의 핸들이다.
    2. 탐색기로부터 파일이 드롭되는 순간에 WM_DROPFILES 메시지가 날라온다. 이를 처리한다.
      	case WM_DROPFILES :
      	{
      		POINT pt;
      		// 어느 위치에 드롭되었는지 그 항목을 알아낸다.
      		if (DragQueryPoint((HDROP)wParam, &pt)) 
      		{
      			UINT i = 0;
      			// 모두 몇 개의 파일이 드롭되었는지 알아낸다.
      			// 만일 폴더가 드롭되었다면 폴더의 이름만 넘어온다.
      			UINT uCount = DragQueryFile((HDROP)wParam, 0xFFFFFFFF, NULL ,0);
      
      			for(i = 0;i < uCount;i++)
      			{
      				// 드롭된 파일의 이름을 알아온다.
      				DragQueryFile((HDROP)wParam, i, buffer ,255);
      				// 드롭된 파일 이름을 출력해본다.
      				MessageBox(hWnd, buffer, "File Name", MB_OK);
      			}
      		}
      		// drag and drop 작업을 끝낸다.
      		DragFinish((HDROP)wParam);
      		break;
      	}
      
    3. Drag&drop을 더 사용할 필요가 없어지면 DragAcceptFiles를 호출한다.
      	DragAcceptFiles(hWnd, FALSE);
      

    5. 시스템의 모든 드라이브 알아내기

    현재 시스템에 붙어있는 모든 드라이브(네트웍 드라이브 포함)에 대한 정보를 알아내고 싶습니다.

    1. GetLogicalDriveStrings로 시스템에 마운트되어있는 모든 드라이브 정보를 알아낸다. 두 번째 인자인 buffer로 드라이브 정보가 들어오는데 그 구조는 c:\,d:\과 같은 형식이며 리턴값으로 그 버퍼의 크기가 들어온다.
      	char buffer[256];
      	DWORD dwRet;
      	LPSTR token;
      
      	dwRet = GetLogicalDriveStrings(256, buffer);
      
    2. 루프를 돌면서 드라이브별 정보를 알아낸다. 이 때는 GetVolumeInformation 함수를 이용한다.
      	token = buffer; // token이 지금 처리해야할 드라이브를 가리킨다.
      	while (dwRet > 0)
      	{
      		DWORD FileSystemFlag;
      		char FileSystemName[64];
      				
      		strcpy(DriveString, token);
      		// VolumeName으로 드라이브에 대한 설명 문자열이 넘어온다.
      		if (GetVolumeInformation(token, VolumeName, 255, NULL, NULL, 
                                &FileSystemFlag, FileSystemName, 63))
      		{
      	        // 원하는 작업을 수행한다.		
      		}
      		dwRet -= (strlen(token)+1);
      		token = token + strlen(token)+1; // 다음 드라이브로 진행한다.
      	}
      

    6. 드라이브/디렉토리/파일의 이미지 리스트 인덱스 얻기

    특정 드라이브/디렉토리/파일이 시스템 이미지 리스트에서 어떤 인덱스를 갖는지 알고 싶습니다.

    각 파일이나 드라이브 및 디렉토리에 대한 정보는 Shell 라이브러리에서 제공해주는 SHGetFileInfo 함수를 이용하면 됩니다. 다음의 함수는 첫 번째 인자인 lpFileName으로 주어진 파일에 대한 설명을 두 번째 인자로 받아오고 세 번째 인자로는 시스템 이미지 리스트에서의 인덱스를 얻어옵니다.

    	void GetFileInfo(LPSTR lpFileName, LPSTR lpDesc, int *nIndex)
    	{
    	    DWORD dwAttr;
    	    SHFILEINFO sfi;
    
    	    int hIcon = SHGetFileInfo(lpFileName, dwAttr, &sfi, sizeof(SHFILEINFO), 
    				SHGFI_TYPENAME | SHGFI_SYSICONINDEX); 
    	    *nIndex = sfi.iIcon;
    	    strcpy(lpDesc, sfi.szTypeName);
    	}
    

    7. 리스트 컨트롤에 칼럼 헤더 넣기

    리포트뷰 형식의 리스트 컨트롤에 컬럼 헤더를 집어 넣으려면 어떻게 해야합니까 ?

    	// <문서명, 등록날짜, 상태> : 3개의 헤더를 만든다.
    	LV_COLUMN col;
    
    	col.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
    	col.fmt = LVCFMT_LEFT;
    	col.cx = 100;
    	col.pszText = "문서명";
    	col.cchTextMax = strlen(col.pszText);
    	ListView_SetColumn(hListView, 0, &col); 
    
    	col.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
    	col.fmt = LVCFMT_LEFT;
    	col.cx = 100;
    	col.pszText = "등록날짜";
    	col.cchTextMax = strlen(col.pszText);
    	ListView_InsertColumn(hListView, 0, &col); 
    
    	col.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
    	col.fmt = LVCFMT_LEFT;
    	col.cx = 100;
    	col.pszText = "상태";
    	col.cchTextMax = strlen(col.pszText);
    	ListView_InsertColumn(hListView, 1, &col); 
    

    8. 리스트뷰에 항목 삽입하기

    리스트뷰에 한 항목을 추가하고 싶습니다.

    	// 이미지 리스트와 부가 정보를 사용하지 않는 리스트뷰 컨트롤이다.
    	int nIndex;
    	LV_ITEM item;
    
    	// - 첫번째 컬럼 -
    	item.mask = LVIF_TEXT; // 이미지 리스트를 사용하려면 LVIF_IMAGE를 추가하고
                                   // 부가정보를 지정해야할 일이 있다면 LVIF_PARAM을 추가한다.
    	item.pszText = lpDocName;
    	item.cchTextMax = strlen(lpDocName);
    	item.iItem = 1;
    	item.iSubItem = 0;
    	nIndex = ListView_InsertItem(hListView, &item);	
    	// - 두번째 컬럼 -
    	item.mask = LVIF_TEXT;
    	item.iItem   = nIndex;
    	item.pszText = lpDate;
    	item.cchTextMax = strlen(lpDate);
    	item.iSubItem = 1;
    	ListView_SetItem(hListView, &item); 
    	// - 세번째 컬럼 -
    	item.mask = LVIF_TEXT;
    	item.iItem   = nIndex;
    	item.pszText = "";
    	item.cchTextMax = strlen(lpDocName);
    	item.iSubItem = 2;
    	ListView_SetItem(hListView, &item); 
    

    9. 리스트뷰 컨트롤에서의 정렬 구현

    리스트뷰 컨트롤에서 칼럼 헤더를 눌렀을 때 정렬이 되도록 하려면 어떻게 해야합니까 ?

    1. 일단 리스트뷰 컨트롤의 생성시 윈도우 스타일로 LVS_NOSORTHEADER를 주지 않는다.
    2. 리스트뷰로부터 칼럼 헤더가 눌렸을 때 오는 이벤트를 받아들인다.
      	NM_LISTVIEW *pnmtv = (NM_LISTVIEW FAR *)lParam;
      
      	switch(pnmtv->hdr.code)
      	{
      		case LVN_COLUMNCLICK :
      		{
      			// 어느 항목(pnmtv->iSubItem)이 눌렸는지부터 검사한다. 
      			// g_iSubItem은 어느 항목이 눌렸는지 기록해두는 인덱스이다.
      			g_iSubItem = pnmtv->iSubItem;
      			// 정렬함수를 호출한다. CompareFunc가 정렬함수이다.
      			ListView_SortItems(hListView, (PFNLVCOMPARE)CompareFunc, (LPARAM)this); 
      			break;
      		}
      
    3. 리스트뷰 항목을 정렬하는데 사용되는 CompareFunc라는 함수를 만든다. 이는 보통 C 함수로 만들거나 클래스를 사용할 경우에는 클래스 내의 static 함수로 만든다. CompareFunc의 코드는 다음과 같다.
      	int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
      	{
      		LV_FINDINFO lvfi;
      		int iFirstItem, iSecondItem;
      
      		lvfi.flags = LVFI_PARAM;
      		lvfi.lParam = lParam1;
      		iFirstItem = ListView_FindItem(hListWnd, -1, &lvfi); 
      
      		lvfi.flags = LVFI_PARAM;
      		lvfi.lParam = lParam2;
      		iSecondItem = ListView_FindItem(hListWnd, -1, &lvfi); 
      
      		char lpFirst[100];
      		char lpSecond[100];
      		ListView_GetItemText(hListWnd, iFirstItem, g_iSubItem, lpFisrt, 100);
      		ListView_GetItemText(hListWnd, iSecondItem, g_iSubItem, lpSecond, 100);
      
      		// g_iSubItem 컬럼의 성격에 따라 비교한다. 문자열이라면 아래와 같이 한다.
      		int iRet = strcmpi(lpFirst, lpSecond);	
      		return iRet;
      	}
      

    10. 버전 정보 알아내기 코드

    파일의 버전을 API를 통해 알아내려면 어떻게 해야합니까 ?

    Resource의 한 타입으로 VERSIONINFO라는 것이 존재합니다. 여기에 해당 파일의 버전 정보를 기록하도록 되어있습니다. 이 버전 정보를 읽어오는데 ver.dll이라는 DLL에 들어있는 API들을 사용합니다. 주의할 점은 버전 리소스는 언어별로 설정이 되기 때문에 영어로도 읽어보고 한국어 로도 읽어봐야 한다는 것입니다. 다음 예제를 참고하시기 바랍니다.

    	// szDrvName이란 파일에 들어있는 버전 정보를 읽어온다. 
    	#include 
    
    	DWORD      dwSize, handle;
    	LPSTR      lpstrVffInfo;
    	HANDLE     hMem;
    	LPSTR      lpVersion;	   // 이 변수로 파일의 버전 정보가 들어온다.
    	char       szDrvName[80];  // 버전 정보를 알아내고자 하는 파일 이름이 여기에 들어온다.
    
    	....
    	// 버전 정보 블록의 크기를 알아온다.
    	dwSize = GetFileVersionInfoSize(szDrvName , &handle);
    	if (dwSize) // 버전 정보 블록이 존재하면
    	{ 
    		// 버전 정보 블록을 포함할 메모리 블록을 할당 받아둔다.
    		hMem = GlobalAlloc(GMEM_MOVEABLE, dwSize);
    		lpstrVffInfo  = GlobalLock(hMem);
    		// 버전 정보 블록의 내용을 읽어온다.	
    		GetFileVersionInfo(szDrvName, handle, dwSize, lpstrVffInfo);
    	        // 버전 정보 블록에서 버전 정보를 읽어온다.
            	VerQueryValue((LPVOID)lpstrVffInfo, 
                            (LPSTR)"\\StringFileInfo\\041204B0\\FileVersion",
                            (void FAR* FAR*)&lpVersion, (UINT FAR *)&dwSize);
    		// lpVersion에 들어있는 버전 정보를 사용한다.
    		....
    		GlobalUnlock(hMem);
    		GlobalFree(hMem);
        }
    
    위에서 041204B0가 바로 버전 리소스에 사용된 언어가 무엇인지를 나타냅니다. 이는 영어를 나타내며 한국어의 경우에는 040904B0를 사용하면 됩니다. 이 밖에도 version.lib를 링크의 라이브러리 항목에 추가해야 합니다.

    11. 시스템 사양 알아내기

    현재 시스템에 부착되어 있는 메인 메모리의 양과 CPU와 운영체제의 종류를 알고 싶습니다.

    먼저 시스템에 부착되어 있는 메인 메모리의 크기는 GlobalMemoryStatus라는 API를 이용하면 됩니다. 예제 코드는 다음과 같습니다.

    	//===========================================================
    	// lMemTotal      : 실제 메모리의 전체 크기 (KB 단위)
    	// lAvailMemTotal : 사용 가능한 실제 메모리의 크기 (KB 단위)
    	// lVirtualTotal  : 가상 메모리의 전체 크기  (KB 단위)
    	//===========================================================
    	void GetMemoryStatus(long *lMemTotal, long *lAvailMemTotal, long *lVirtualTotal)
    	{
    		double var;
    		MEMORYSTATUS memoryStatus;
    
    		memset (&memoryStatus, sizeof (MEMORYSTATUS), 0);
    		memoryStatus.dwLength = sizeof (MEMORYSTATUS);
    
    		GlobalMemoryStatus (&memoryStatus);
    
    		lMemTotal = memoryStatus.dwTotalPhys / 1024;
    		lAvailMemTotal = memoryStatus.dwAvailPhys / 1024;
    		lVirtualTotal = memoryStatus.dwTotalVirtual / 1024;
    	}
    
    다음으로 CPU의 종류를 알아내는 코드는 다음과 같습니다.
    	//===============================================================
    	// GetProcessorInfo : 프로세서에 대한 정보를 읽어온다.
    	// lpCPUSpeed      : CPU의 속도. 기록된 시스템에서만 읽어온다.
    	// lpProcessorType : 프로세서의 종류
    	// lpNumProcessors : 프로세서의 개수. NT의 경우에만 의미가 있다.
    	//===============================================================
    	void GetProcessorInfo(LPSTR lpCPUSpeed, LPSTR lpProcessorType, LPSTR lpNumProcessors)
    	{
    		SYSTEM_INFO sysInfo;
    		LONG result;
    		HKEY hKey;
    		DWORD data;
    		DWORD dataSize;
    
    		lpCPUSpeed[0] = 0;
    		// ---------------------------------------------
    		// 프로세서의 속도를 얻어낸다.
    		// ---------------------------------------------
    		result = ::RegOpenKeyEx (HKEY_LOCAL_MACHINE,
    			"Hardware\\Description\\System\\CentralProcessor\\0", 0, KEY_QUERY_VALUE, &hKey);
    		if (result == ERROR_SUCCESS) 
    		{
    			result = ::RegQueryValueEx (hKey, "~MHz", NULL, NULL,(LPBYTE)&data, &dataSize);
    			wsprintf(lpCPUSpeed, "%d MHz", data);
    		}
    		RegCloseKey (hKey);
    
    		// ------------------------------------------
    		// 하드웨어 정보를 얻어낸다.
    		// ------------------------------------------
    		GetSystemInfo (&sysInfo);
    
    		// 프로세서 타입부터 검사한다.
    		if (sysInfo.dwProcessorType  == PROCESSOR_INTEL_386)
    			strcpy(lpProcessorType,  "Intel 386");
    		else if (sysInfo.dwProcessorType  == PROCESSOR_INTEL_486)
    			strcpy(lpProcessorType,  "Intel 486");
    		else if (sysInfo.dwProcessorType  == PROCESSOR_INTEL_PENTIUM)
    		{
    			if (sysInfo.wProcessorLevel == 6) 
    				strcpy(lpProcessorType, "Intel Pentium (II/Pro)");
    			else
    				strcpy(lpProcessorType,  "Intel Pentium");
    		}
    		else 
    			strcpy(lpProcessorType, "알 수 없는 시스템");
    
    		// 프로세서의 갯수를 검사한다.
    		wsprintf(lpNumProcessors, "%d", sysInfo.dwNumberOfProcessors);
    	}
    
    
    다음으로 현재 사용 중인 운영체제의 종류를 알아내는 코드는 다음과 같습니다.
    	//===============================================================
    	// GetOSVersion : OS의 버전을 얻어온다.
    	// --------------------------------------------------------------
    	// lpstInfo
    	// lpstBuildNumber
    	// lpstServicePack
    	//===============================================================
    	void GetOSVersion (LPSTR lpstInfo, LPSTR lpstBuildNumber, LPSTR lpstServicePack)
    	{
    		int stat = 0;
    		TCHAR data [64];
    		DWORD dataSize;
    		DWORD win95Info;
    		OSVERSIONINFO versionInfo;
    		HKEY hKey;
    		LONG result;
    
    		lpstServicePack[0] = 0;
    		versionInfo.dwOSVersionInfoSize = sizeof (OSVERSIONINFO);
    
    		// 버전 정보를 얻어낸다.
    		if (!::GetVersionEx (&versionInfo)) 
    		{
    			strcpy(lpstInfo, "운영체제 정보를 얻을 수 없습니다.");
    			return;
    		}
    
    		// NT이면 서버인지 웍스테이션인지 검사한다. 이는 레지스트리를 보고 검사한다.
    		if (versionInfo.dwPlatformId == VER_PLATFORM_WIN32_NT) 
    		{
    			strcpy(lpstInfo, "Windows NT");
    			dataSize = sizeof (data);		
    			result = ::RegOpenKeyEx (HKEY_LOCAL_MACHINE,
    				"System\\CurrentControlSet\\Control\\ProductOptions", 0, KEY_QUERY_VALUE, &hKey);
    			if (result != ERROR_SUCCESS) 
    				return;
    
    			result = ::RegQueryValueEx (hKey, "ProductType", NULL, NULL, (LPBYTE) data, &dataSize);
    			RegCloseKey (hKey);
    
    			if (result != ERROR_SUCCESS) 
    				return;
    
    			if (lstrcmpi (data, "WinNT") == 0) 
    				strcpy(lpstInfo, "Windows NT Workstation");
    			else if (lstrcmpi (data, "ServerNT") == 0) 
    				strcpy(lpstInfo, "Windows NT Server");
    			else 
    				strcpy(lpstInfo, "Windows NT Server - Domain Controller");
    
    			// NT 버전을 알아낸다.
    			if (versionInfo.dwMajorVersion == 3 || versionInfo.dwMinorVersion == 51) 
    				strcat(lpstInfo, " 3.51");
    			else if (versionInfo.dwMajorVersion == 5) // 윈도우 2000의 경우
    				strcat(lpstInfo, " 5.0");
    			else 
    				strcat(lpstInfo, " 4.0");
    
    			// Build 번호를 알아낸다.
    			wsprintf(lpstBuildNumber, "%d", versionInfo.dwBuildNumber);
    		}
    		else if (versionInfo.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) 
    		{
    			strcpy(lpstInfo, "Windows 95");
    			if ((versionInfo.dwMajorVersion > 4) || ((versionInfo.dwMajorVersion == 4)
    				&& (versionInfo.dwMinorVersion > 0))) 
    			{
    				strcpy(lpstInfo, "Windows 98");
    			}
    			// 윈도우 95는 Build 번호가 하위 워드에 들어간다.
    			win95Info = (DWORD)(LOBYTE(LOWORD(versionInfo.dwBuildNumber)));
    			wsprintf(lpstBuildNumber, "%d", win95Info);
    		}
    		else 
    			wsprintf(lpstInfo, "Windows 3.1");
    
    		// 서비스 팩 정보를 얻어낸다.
    		strcpy(lpstServicePack, versionInfo.szCSDVersion);
    	}
    

    12. IE의 설치 여부와 버전 확인

    현재 시스템에 IE가 설치되었는지 여부와 그 버전을 알려면 어떻게 해야합니까 ?

    사실 동작시켜보지 않고서는 IE가 제대로 설치되어있는지 알아내는 방법은 없지만 레지스트리를 통해 IE가 설치되었는지 여부와 버전을 확인할 수 있습니다. 그 함수는 다음과 같습니다.

    	//===========================================================================
    	// GetIEVersion : IE의 버전을 얻는다. 정보를 찾을 수 없으면 FALSE를 리턴한다.
    	//===========================================================================
    	BOOL GetIEVersion(LPSTR lpVer)
    	{	
    		LONG result;
    		HKEY hKey;
    		DWORD dwType; 
    		char data[65];
    		DWORD dataSize = 64;
    
    		// --------------------
    		// IE의 버전을 얻는다.
    		// --------------------
    		result = ::RegOpenKeyEx (HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Internet Explorer", 0, KEY_QUERY_VALUE, &hKey);
    		if (result == ERROR_SUCCESS) 
    		{
    			result = ::RegQueryValueEx (hKey, "Version", NULL, &dwType, (unsigned char *)data, &dataSize);
    			strcpy(lpVer, data);
    		}
    		else
    			return FALSE;
    
    		RegCloseKey (hKey);
    		return TRUE;
    	}
    

    13. IE의 보안 설정 보기

    IE에 보면 네 단계의 보안 영역이 있습니다. 그 영역별로 설정되어있는 보안 설정값을 읽으려면 어떻게 해야합니까 ?

    IE에는 다음과 같은 네 가지 보안 영역이 존재합니다.

    • 인터넷 영역
    • 로컬 인터넷 영역
    • 신뢰할 수 있는 사이트 영역
    • 제한된 사이트 영역
    IE는 보안 영역 설정과 관련하여 Internet Security Manager와 Internet Zone Manager라는 인터페이스가 존재합니다. 이를 이용해 보안 영역의 보안을 설정하고 특정 IP나 도메인 이름을 등록할 수 있습니다. 자세한 사항은 레퍼런스를 찾아보기 바랍니다.
     
    	#include "objbase.h"
    	#include "urlmon.h"
    
    	char szTemp1[256];
    	char szTemp2[256];
    	HRESULT hr;
    	IInternetSecurityManager *pSecurityMgr;
    	IInternetZoneManager *pZoneMgr;
    	DWORD dwEnum, dwZoneCount;
    	// --- 변수 선언부
    	DWORD dwZone;
    	ZONEATTRIBUTES zoneAttr;
    	int nLevel = 2;
    
    	// COM 라이브러리를 초기화한다.
    	CoInitialize(NULL);
    	dwEnum = 0;
    	pSecurityMgr = NULL;
    	pZoneMgr = NULL;
    
    	// Internet Security 인터페이스 초기화
    	hr = CoCreateInstance(CLSID_InternetSecurityManager, NULL, CLSCTX_ALL, //INPROC_SERVER,
    			IID_IInternetSecurityManager, (void**)&pSecurityMgr);
    	if (hr != S_OK)
    	{
    		return;
    	}
    	hr = CoCreateInstance(CLSID_InternetZoneManager, NULL, CLSCTX_ALL, //INPROC_SERVER,
    			IID_IInternetZoneManager, (void**)&pZoneMgr);	
    	if (hr != S_OK)
    	{
    		return;
    	}
    	dwEnum = 0;
    
    	// 보안 영역 열거자(Zone Enumerator)를 초기화한다.
    	pZoneMgr->CreateZoneEnumerator(&dwEnum, &dwZoneCount, 0);
    	for(DWORD i = 1;i < dwZoneCount;i++)
    	{
    		pZoneMgr->GetZoneAt(dwEnum, i, &dwZone);
    		pZoneMgr->GetZoneAttributes(dwZone, &zoneAttr);
    
    		// zoneAttr.szDisplayName에 보안 영역의 이름이 들어오는데 유니코드이다. 이를 변환한다.
    		WideCharToMultiByte(CP_ACP, 0, zoneAttr.szDisplayName, -1, szTemp1, 255, NULL, NULL);	
    		// zoneAttr.dwTemplateCurrentLevel에는 보안 영역의 보안값 설정이 들어온다.
    		wsprintf(szTemp2, "%x", zoneAttr.dwTemplateCurrentLevel);
    	}
    
    	// 보안 영역 열거자(Zone Enumerator)를 제거한다.
    	if (dwEnum != 0)
    		pZoneMgr->DestroyZoneEnumerator(dwEnum);
    
    	pSecurityMgr->Release();
    	pZoneMgr->Release();
    	// COM 라이브러리를 메모리에서 내린다.
    	CoUninitialize();
    }
    

    14. ActiveX 컨트롤의 등록 방법

    Regsvr32 같은 유틸리티를 이용하지 않고 프로그램 내에서 컨트롤을 레지스트리에 등록하려면 어떻게 해야합니까 ?

    모든 AcitveX 컨트롤은 자신을 레지스트리에 등록하기위한 목적으로 DllRegisterServer라는 함수를 갖고 있습니다. ActiveX 컨트롤을 메모리로 로드한 다음에 이 함수를 불러주면 원하는 일을 수행할 수 있습니다. 반대로 ActiveX 컨트롤을 레지스트리에서 제거하기 위한 용도로 DllUnRegisterServer라는 함 수도 존재합니다.

    	// ==============================================================
    	// RegisterOCX     지정된 ActiveX 컨트롤을 레지스트리에 등록한다.
    	// --------------------------------------------------------------
    	// LPSTR pszString 등록하고자 하는 ActiveX 컨트롤의 절대 경로명
    	// ==============================================================
    	BOOL WINAPI RegisterOCX(LPSTR pszString)
    	{
    		int iReturn = 0;
    		HRESULT (STDAPICALLTYPE * lpDllEntryPoint)();
    		HINSTANCE hLib;
    
    		// OLE 라이브러리를 초기화한다.				
    		if (FAILED(OleInitialize(NULL)))
    		{
    			MessageBox(GetFocus(), "OLE 초기화 실패", "에러", MB_OK);
    			return FALSE;
    		}
    
    		// 지정된 activeX 컨트롤을 메모리로 로드한다.
    		hLib = LoadLibrary(pszString);
    		if (hLib <= NULL)
    		{
    			MessageBox(GetFocus(), "파일을 로드하는데 실패했습니다.", "에러", MB_OK);
    			OleUninitialize();
    			return FALSE;
    		}
    
    		// "DllRegisterServer" 함수의 위치를 찾는다.
    		lpDllEntryPoint = (long (__stdcall *)(void))GetProcAddress(hLib, "DllRegisterServer");
    	
    		// 이 함수를 호출합니다.
    		if (lpDllEntryPoint)
    		{
    			if (FAILED((*lpDllEntryPoint)()))
    			{
    				DWORD dwRet;
    				char szTemp[128];
    
    				dwRet = GetLastError();
    				wsprintf(szTemp, "에러 번호 : %lx", dwRet);
    				MessageBox(GetFocus(), szTemp, "DllRegisterServer 에러", MB_OK);
    				FreeLibrary(hLib);
    				OleUninitialize();
    				return FALSE;
    			}
    		}
    		else
    		{
    			MessageBox(GetFocus(), "DllRegisterServer를 찾을 수 없습니다.", "에러", MB_OK);
    			FreeLibrary(hLib);
    			OleUninitialize();
    			return FALSE;
    		}
    
    		FreeLibrary(hLib);
    		OleUninitialize();
    		return TRUE;
    	}
    

    15. 데이터 파일의 실행

    탐색기에서 실행 파일이 아닌 데이터 파일을 더블클릭하면 그 데이터 파일과 연결된 실행 파일이 실행되면서 그 파일을 물고 올라갑니다. 이를 구현하는 방법을 알려주세요.

    ShellExecuteEx라는 API를 사용하면 됩니다. 다음 함수는 인자로 데이터 파일 혹은 실행 파일의 경로를 받아서 실행해줍니다. 데이터 파일의 경우에는 연결된 실행 파일을 찾아서 그걸 실행해줍니다.

    	BOOL Execute(LPSTR lpPath)
    	{
    		char FilePath[255];
    		SHELLEXECUTEINFO  ExecInfo;
    	
    		// lpPath를 나누어 본다.
    		char drive[_MAX_DRIVE];
    		char dir[_MAX_DIR];
    		char fname[_MAX_FNAME];
    		char ext[_MAX_EXT];
    
    		_splitpath(lpPath, drive, dir, fname, ext);
    		// 디렉토리 경로를 얻는다.
    		strcpy(FilePath, drive);
    		strcat(FilePath, dir);
    
    		// 파일 이름을 얻는다.
    		strcat(fname, ".");
    		strcat(fname, ext);
    
    		SHELLEXECUTEINFO  ExecInfo;
    		ExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); 
    		ExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS; 
    		ExecInfo.hwnd = hWnd; 
    		ExecInfo.lpVerb = "open"; 
    		ExecInfo.lpFile = fname; 
    		ExecInfo.lpParameters = NULL; 
    		ExecInfo.lpDirectory = FilePath; 
    		ExecInfo.nShow = SW_SHOW; 
    		ExecInfo.hInstApp = g_hInstance; // g_hInstance는 프로그램의 인스턴스 핸들
    
    		return ShellExecuteEx(&ExecInfo);
    	}
    

    16. 파일의 존재 여부 테스트

    어떤 파일이 실제로 존재하는 것인지 간단히 테스트해보는 방법은 무엇인가요 ?

    CreateFile API를 사용하면 됩니다. 파일을 열때 플래그 중의 하나로 OPEN_EXISTING이라는 것이 있는데 이를 사용하면 됩니다. 다음 코드를 예로 보시기 바랍니다.

    	hFile = CreateFile("C:\\TEMP\\test.txt", GENERIC_READ, 0, NULL,  
    			OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    	if (hFile != INVALID_HANDLE_VALUE)
    	{
    		// 파일이 존재하는 경우
    		CloseHandle(hFile);
    	}
    

    17. API를 이용한 파일 I/O

    API를 이용한 파일 입출력에 대한 예제 코드가 없습니까 ?

    프로그래밍을 하다보면 간혹 파일 I/O를 API를 이용해 수행해야 할 경우가 있습니다. 이 때 다음의 예제 코드를 복사해다가 사용하면 편리할 것입니다. MFC의 CFile을 이용한 파일 I/O에 대해 알고 싶으시면 요기를 클릭하세요.

  • 파일 쓰기의 경우
    	HANDLE fp;
    	DWORD NumberOfBytesWritten;
    	char lpBuffer[1024];
    
    	// FileName이 지정한 파일의 이름이 있으면 그걸 열고 없으면 그 이름으로 하나 생성한다.
    	if ((fp=CreateFile((LPCTSTR)FileName, GENERIC_WRITE | GENERIC_READ,	0, NULL, 
    						OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
    	{
    		// 파일 열기 에러 발생
    	}  
    	// 필요한 만큼 WriteFile의 호출을 반복한다. 파일 포인터의 이동시에는 SetFilePointer API를 이용한다.
    	WriteFile(fp, lpBuffer, 1024, &NumberOfBytesWritten, NULL);
    	if (NumberOfBytesWritten != 1024)
    	{
    		// 파일 쓰기 에러 발생 
    		CloseHandle(fp);		
    	}
    	// 작업이 다 끝났으면 파일을 닫는다.
    	CloseHandle(fp);		
    
  • 파일 읽기의 경우
    	HANDLE fp;
    	DWORD NumberOfBytesRead;
    	char lpBuffer[1024];
    
    	if ((fp=CreateFile((LPCTSTR)FileName, GENERIC_READ,	
    		FILE_SHARE_READ, NULL, OPEN_EXISTING, 
    		FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
    	{
    		// 파일 열기 에러 발생
    	}  
    	// 필요한 만큼 ReadFile의 호출을 반복한다.
    	ReadFile(fp, lpBuffer, 1024, &NumberOfBytesRead, NULL);
    	if (NumberOfBytesRead != 1024)
    	{
    		// 파일 읽기 에러 발생 
    		CloseHandle(fp);		
    	}
    	// 작업이 다 끝났으면 파일을 닫는다.
    	CloseHandle(fp);		
    

    파일 포인터의 이동시에는 SetFilePointer라는 API를 사용하고 파일의 크기를 알고 싶을 때는 GetFileSize라는 API를 사용한다.

    18. GetParent API의 리턴값

    다이얼로그 박스에서 GetParent API를 호출했을 때 리턴되는 값이 이상합니다.

    GetParent API의 리턴값은 보통의 윈도우에서는 윈도우 생성시 지정한 부모/자식 윈도우 간의 관계에 따라 달라집니다. 하지만 다이얼로그 박스의 경우에는 다릅니다. 다이얼로그 박스는 GetParent를 호출하면 무조건 그 것이 속한 응용프로그램의 메인 윈도우 핸들이 리턴됩니다.

    19. 특정 프로그램을 실행하고 종료를 기다리기

    특정 프로그램을 실행한 다음에 그 프로그램이 종료될 때 다른 일을 하고 싶습니다. 어떻게 해야합니까 ?

    다음은 CreateProcess를 이용해서 특정 프로그램의 실행이 끝나기를 기다리는 코드입니다.

    	// buffer에 실행하고자하는 파일이름이 들어온다.
    	void DoCreate(HWND hWnd, LPSTR buffer)
    	{
    		STARTUPINFO            sui;
    		PROCESS_INFORMATION    pi;
    		DWORD                  ret;
    
    		memset(&sui, 0x00, sizeof(STARTUPINFO));
    		sui.cb = sizeof(STARTUPINFO);
        
    		ret = CreateProcess(buffer, NULL, NULL, NULL, FALSE, 
    			0, NULL, NULL,&sui, &pi);
    		if (ret == TRUE) // 제대로 실행되었으면
    		{
    			hProcess = pi.hProcess;
    			// 실행이 끝나기를 대기한다.
    			WaitForSingleObject(hProcess, 0xffffffff);
    			CloseHandle(hProcess);
    		}
    	}
    

    20. 파일 열기 다이얼로그 띄우기

    API를 이용해 파일 오픈 다이얼로그를 띄우고 싶습니다.

    API를 이용해 파일 오픈 다이얼로그를 띄우는 방법은 다음과 같습니다.

    	#define MAXCHARS   255
    	OPENFILENAME       ofn;
    	char               buffer[MAXCHARS];
    
    	memset(&ofn, 0x00, sizeof(OPENFILENAME));
    	ofn.lStructSize = sizeof(OPENFILENAME);
    	ofn.hwndOwner = hWnd;
    	ofn.lpstrFilter = "모든 파일\000*.*\000\000";
    	ofn.lpstrInitialDir = m_szTemp;
    	ofn.nFilterIndex = 1;
    	ofn.lpstrFile = buffer;
    	ofn.nMaxFile = MAXCHARS;
    	ofn.lpstrTitle = "파일 선택하기";
    
    	if (GetOpenFileName(&ofn))  // 사용자가 파일을 제대로 선택한 경우
    	{
    		// ....
    	}
    

    21. 긴 파일 이름과 짧은 파일 이름간의 변환 방법

    주어진 긴 파일 이름에 해당하는 짧은 파일 이름을 알려면 어떻게 해야하나요 ?

  • 긴 파일 이름에서 짧은 파일 이름으로의 변환 : GetShortPathName API 사용
  • 짧은 파일 이름에서 긴 파일 이름으로의 변환 : GetFullPathName API 사용

    22. 시스템 이미지 리스트 얻어내기

    탐색기 등에서 사용되는 시스템 이미지 리스트를 사용할 수 있는 방법이 있는지 알고 싶습니다.

    물론 있습니다. SHGetFileInfo라는 API를 이용하면 됩니다. 이를 이용하면 16X16이나 32X32 크기의 이미지 리스트를 얻어낼 수 있습니다.

    	SHFILEINFO sfi;
    	HIMAGELIST sysSmallList, sysLargeList;
    
    	sysSmallList = (HIMAGELIST)SHGetFileInfo(TEXT("C:\\"), 0, &sfi,   sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_SMALLICON);
    	sysLargeList = (HIMAGELIST)SHGetFileInfo(TEXT("C:\\"), 0, &sfi,   sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_ICON);
    

    23. 리스트뷰 컨트롤에서 스타일 동적 변경하기

    리스트뷰 컨트롤에서 리스트(LVS_ICON) 스타일을 사용 중인데 이를 실행 중에 리포트(LVS_REPORT) 스타일로 변경하고 싶습니다. 어떻게 해야할까요 ?

    윈도우의 스타일은 윈도우 엑스트라 바이트라는 영역에 저장됩니다. 이 곳의 데이터를 읽고 쓰는데 GetWindowLong, SetWindowLong 같은 API를 사용하는데 다음 코드를 예로 보시기 바랍니다.

  • 리스트 스타일에서 리포트 스타일로
    	LONG lStyle = GetWindowLong(hListWnd, GWL_STYLE);
    	SetWindowLong(hListWnd, GWL_STYLE, (lStyle & ~LVS_LIST) | LVS_REPORT);
    
  • 리포트 스타일에서 리스트 스타일로
    	LONG lStyle = GetWindowLong(hListWnd, GWL_STYLE);
    	SetWindowLong(hListWnd, GWL_STYLE, (lStyle & ~LVS_REPORT) | LVS_LIST);
    

    24. 윈도우와 다이얼로그에서 클래스 포인터의 사용

    제가 만든 C++ 클래스내에서 윈도우를 생성합니다. 생성한 윈도우의 윈도우 프로시저는 그 클래스의 정적 멤버 함수로 선언되어 있습니다. 정적 함수의 경우에는 데이터 멤버를 접근하지 못하기 때문에 접근하게 하려고 윈도우를 생성한 그 클래스의 객체에 대한 포인터를 전역 변수로 유지하여 사용하고 있습니다. 별로 깨끗한 방법도 아닌 것 같고 또 동시에 여러 개의 객체가 동작할 경우에는 에러가 날 수밖에 없는데 해결 방법이 없을까요 ?

    이는 다이얼로그의 경우에도 마찬가지입니다. 윈도우 생성시 사용하는 CreateWindow나 CreateWindowEx 같은 함수를 보면 마지막 인자로 윈도우 생성 데이터라는 것을 지정하게 되어있습니다. 다이얼로그 생성시에는 DialogBox라는 API말고 DialogBoxParam이라는 API가 있어서 마지막 인자로 초기화 데이터를 넘겨줄 수 있도록 되어있는데 이것과 윈도우 엑스트라 바이트를 같이 사용하면 정적함수로 선언된 윈도우 프로시저나 다이얼로그박스 프로시저내에서 객체에 대한 포인터를 사용할 수 있습니다. 예를 들어 살펴보겠습니다.

  • 윈도우의 경우

    다음과 같이 CExplorerBar라는 클래스내에서 윈도우를 하나 생성합니다. CreateWindowEx 함수나 CreateWindow 함수의 마지막 인자로 this 포인터를 지정합니다.

    	BOOL CExplorerBar::RegisterAndCreateWindow(void)
    	{
    		....
    		CreateWindowEx( 0,  EB_CLASS_NAME, NULL,
                         WS_CHILD | WS_CLIPSIBLINGS | WS_BORDER,
                         rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
                         m_hwndParent, NULL, g_hInst, (LPVOID)this);
          
       }
    

    위에서 생성한 윈도우의 윈도우 프로시저는 WndProc이고 CExplorerBar 클래스의 정적 멤버 함수로 존재한다고 가정하겠습니다.

    	LRESULT CALLBACK CExplorerBar::WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam)
    	{
    		CExplorerBar  *pThis = (CExplorerBar*)GetWindowLong(hWnd, GWL_USERDATA);
    
    		switch (uMessage)
    		{
    			case WM_NCCREATE:
    			{
    				LPCREATESTRUCT lpcs = (LPCREATESTRUCT)lParam;
    				pThis = (CExplorerBar*)(lpcs->lpCreateParams);
    				SetWindowLong(hWnd, GWL_USERDATA, (LONG)pThis);
    			}
    			break;
    			case WM_CREATE :
    				return pThis->OnCreate();
    

    WM_NCCREATE 메시지에서 lParam 인자로 넘어오는 CExplorerBar 객체에 대한 포인터를 받아서 윈도우 엑스트라 바이트로 저장하고 있습니다. 윈도우 엑스트라 바이트는 윈도우 마다 할당되는 고유의 영역으로 사용자 정의 영역으로 GWL_USERDATA가 정의되어 있습니다. 여기에다 CExplorerBar 객체에 대한 포인터를 저장해두고 윈도우 프로시저에 진입할 때마다 이 값을 pThis라는 변수에 대입해 놓고 사용합니다. 참고로 WM_NCCREATE는 WM_CREATE 메시지보다 먼저 발생하는 메시지입니다.

  • 다이얼로그의 경우

    예를 들어 CKTree라는 클래스내의 한 멤버 함수에서 다이얼로그 박스를 띄운다고 가정하겠습니다.

    	BOOL CKTree::SelectFolder(short sTypes, long dwFolderID, short  bCreationEnabled)
    	{
    		// ......
    		if (DialogBoxParam(hInst, MAKEINTRESOURCE(IDD_FOLDER_SELECT), hWnd, (DLGPROC)SelectFolderDlg, (LPARAM)this))
    

    DialogBoxParam 함수는 DialogBox 함수보다 인자가 하나 더 있는데 그것이 바로 다이얼로그 프로시저의 WM_INITDIALOG 메시지의 lParam 인자로 넘어갑니다. 여기에 CKTree 객체에 대한 포인터를 넘깁니다. 그리고나서 다이얼로그 박스 프로시저의 WM_INITDIALOG 메시지에서 이를 받아서

    	LRESULT CALLBACK SelectFolderDlg(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
    	{
    		switch (message) 
    		{	
    	        case WM_INITDIALOG:
    			{
    				// lParam으로 넘어온 값을 KPointer라는 윈도우 프로퍼티에 저장한다.
    				SetProp(hDlg, "KPointer", (HANDLE)lParam);
    				.......
    			case  WM_NOTIFY :
    			{
    				// CKTree 객체에 대한 포인터가 필요하면 KPointer 윈도우 프로퍼티 값을 읽어들인다.
    				CKTree *pKTree = (CKMartTree *)GetProp(hDlg, "KPointer");
    				if (pKTree->m_bWait)
    				{
    					.....
    

    여기서는 앞서 윈도우와는 달리 윈도우 프로퍼티라는 것을 이용해서 넘어온 포인터 정보를 저장하고 필요할 때 읽어옵니다. 여기서 앞서의 윈도우 엑스트라 바이트를 사용해도 무방합니다.

    25. 특정 프린터로 출력하기

    제 PC에는 두 대의 프린터가 붙어있습니다. 다이얼로그를 띄우지 않고 상황에 따라 다른 프린터로 출력하고 싶은데 동적으로 HDC를 생성하는 방법을 모르겠습니다.

    CreateDC를 이용하면 됩니다. 시스템 디렉토리의 WIN.INI를 보면 [Devices]라는 섹션이 있는데 이 아래로 이 시스템에 설치되어 있는 모든 프린터 드라이버의 목록이 나옵니다. 예를 들어 다음과 같이 나옵니다.

    	[Devices]
    	HUNFAX=HUNFAX,FaxModem
    	삼성 SLB-6216H PCL5=SSMPCL5,\\영업팀\볼륨프린터
    	......
    

    프린터별로 DeviceName=DriverName,OutputName와 같은 구조로 구성되어 있습니다. 이 값들을 CreateDC의 인자로 사용하면 됩니다. 예를 들어 위에서 두 번째 프린터의 DC를 생성하려면 다음과 같이 CreateDC를 호출합니다.

    	// hDC = CreateDC(DriverName, DeviceName, OutputName, NULL);
    	hDC = CreateDC ("SSMPCL5", "삼성 SLB-6216H PCL5", "\\\\영업팀\\볼륨프린터", NULL) ;
    

    CreateDC의 마지막 인자로는 프린터의 설정값을 변경할 수 있습니다. 예를 들어 가로로 찍는다든지 2장을 찍는다든지 하는 설정을 변경하는데 사용됩니다. NULL을 주면 디폴트 값을 사용합니다. 다른 설정을 사용하고 싶다면 다음과 같이 합니다. CreateDC 호출 앞에서 DocumentProperties라는 함수를 호출하여 프린터의 기본 설정을 읽어온 다음에 이를 변경합니다. 다음 예는 출력 방향을 가로로 변경하는 예제입니다.

    	// 프린터의 디폴트 설정을 읽어온다.
    	LPDEVMODE lpoutDevMode;
    	HANDLE hPrinter;
    	HDC hPrnDC;
    
    	// 프린터의 핸들을 얻는다.
    	if (OpenPrinter( lpDeviceName, &hPrinter,  NULL))
    	{
    		// OpenPrinter로 얻은 프린터의 초기 설정을 DocumentProperties API로 얻어온다.
    		// 먼저 마지막 인자를 0으로 해서 DocumentProperties를 호출하여 필요한 버퍼의 크기를 알아옵니다.
    		long lSize = DocumentProperties(GetFocus(), hPrinter, lpPrinterName, NULL, NULL, 0);
    		lpoutDevMode = (LPDEVMODE)malloc(lSize);
    		long lRet = DocumentProperties(GetFocus(), hPrinter, lpPrinterName, lpoutDevMode, NULL, DM_OUT_BUFFER);
    		if (lRet == IDOK)
    		{
    			// 프린터의 인쇄 방향 설정을 변경한다.
    			// 여기서 원하는 변환을 수행한다.
    			lpoutDevMode->dmOrientation = DMORIENT_LANDSCAPE;
    		}
    		hPrnDC = CreateDC (lpDriverName, lpDeviceName, lpOutputName, lpoutDevMode) ;
    		free(lpoutDevMode);
    		return hPrnDC;
    	}
    

    26. 메뉴 관련 함수

    메뉴 항목을 하나 추가하려고 합니다. InsertMenuItem API를 사용하는데 윈도우 3.1에서와 사용법이 다른 것 같습니다.

    사용법이 달라졌습니다.

    	MENUITEMINFO mii;
    
    	memset(&mii, 0x00, sizeof(MENUITEMINFO));
    	mii.cbSize = sizeof(MENUITEMINFO);
    	mii.fMask = MIIM_TYPE;
    	mii.fType = MFT_SEPARATOR;
    	InsertMenuItem(hSubMenu, GetMenuItemCount(hSubMenu), TRUE,  &mii);
    							
    	mii.cbSize = sizeof(MENUITEMINFO);
    	mii.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID;
    	mii.fType = MFT_STRING;
    	mii.fState = MFS_DEFAULT | MFS_UNHILITE;
    	mii.wID = ID_WORKPLACE_REMOVE;
    	mii.dwTypeData = "바구니에서 제거";
    	InsertMenuItem(hSubMenu, GetMenuItemCount(hSubMenu), TRUE,  &mii);
    

    27. 코드 실행 중에 다른 윈도우 메시지 처리하기

    하나의 함수 내에서 시간이 오래 걸리는 작업을 하고 있습니다. 이 때 작업 취소를 위한 별도의 다이얼로그를 하나 띄워 두었는데 이 쪽의 버튼이 눌리지 않습니다. 어떻게 해야할까요 ?

    시간이 오래 걸리는 작업을 별도의 스레드로 만들어 처리하던지 아니면 시간이 오래 걸리는 작업을 수행하는 함수 안에서 다음 코드를 가끔 호출해주면 됩니다. 만일 루프를 돌고 있다면 루프내에서 한번씩 호출해주면 됩니다.

    	MSG       msg;
    
    	while (PeekMessage(&msg, NULL, NULL, NULL, TRUE))
    	{
    		TranslateMessage(&msg);
    		DispatchMessage(&msg);
    	}
    

    VB에서라면 DoEvents라는 메소드를 호출해주면 됩니다.

    28. 메인 윈도우에서 캡션을 제거하고 싶습니다. 어떻게 해야 합니까 ?

    윈도우를 생성할 때 WS_CAPTION이란 스타일을 지정하지 않아도 항상 윈도우의 캡션이 보입니다. 이를 제거하려면 어떻게 해야 하나요 ?

    캡션을 제거하려는 윈도우의 WM_NCCREATE 메시지를 처리해야 합니다. 이 메시지는 WM_CREATE 메시지보다 앞서 발생하는 메시지입니다. NC는 Non-Client를 나타냅니다. GetWindowLong과 SetWindowLong API를 이용해서 WS_CAPTION 스타일을 제거합니다. 이 두 API는 앞서 리스트뷰 컨트롤에서 스타일 동적 변경하기에서 이미 사용해본 바 있습니다.

    		case WM_NCCREATE :
    		{
    			long lStyle;
        
    			lStyle = GetWindowLong(hWnd, GWL_STYLE);
    			lStyle = (lStyle & (~WS_CAPTION)); 
    			SetWindowLong (hWnd, GWL_STYLE, lStyle);
    			return TRUE;
    		}
    

    29. 사각형 형태 이외의 모양을 갖는 윈도우를 띄우고 싶습니다.

    윈도우는 기본 모양이 사각형인데 다른 형태의 모양을 갖는 윈도우를 띄우려면 어떻게 해야합니까 ?

    원하는 모양을 Region이란 것으로 만들어야 합니다. 만든 다음에 이것을 원하는 시점에 SetWindowRgn라는 API를 이용해 윈도우에 설정해주면 됩니다. 예를 들어 타원 모양의 윈도우를 띄우고 싶다면 다음과 같이 해주면 됩니다.

    	HRGN g_hRgn;
    
    	g_hRgn = CreateEllipticRgn(0, 0, 700, 600);
    	SetWindowRgn(hWnd, g_hRgn, FALSE);		
    

    이렇게 했을 경우 윈도우의 위치 이동이 문제가 됩니다. 보통 캡션을 잡고 이동시켜야 하는데 캡션이 없으니까 문제가 됩니다. 이에 관한 것은 31. 윈도우의 이동 처리하기를 참고하기 바랍니다.

    30. 시스템에 설치되어 있는 모든 프린터 드라이버 알아내기

    현재 시스템에 설치되어 있는 모든 프린터 드라이버의 종류를 알아내고 싶습니다.

    EnumPrinters라는 API를 사용하면 됩니다. 다음 코드는 현재 시스템에 설치되어 있는 모든 프린터 드라이버(로컬과 네트웍 프린터 포함)의 이름을 메시지박스로 보여주는 예제입니다.

    	BOOL bSuccess;
    	DWORD cbRequired, cbBuffer, nEntries;
    	PRINTER_INFO_1 *lpBuffer = NULL;
    
    	// 버퍼의 크기를 알아낸다. cbRequired로 들어온다.
    	EnumPrinters(PRINTER_ENUM_CONNECTIONS | PRINTER_ENUM_LOCAL, NULL, 1, (unsigned char *)lpBuffer, 0, &cbRequired, &nEntries);
    	cbBuffer = cbRequired;
    	// 버퍼를 다시 버퍼를 잡는다.
    	lpBuffer = (PRINTER_INFO_1 *)malloc(cbBuffer);
    	// 프린터의 종류를 알아낸다.
    	bSuccess =	EnumPrinters(PRINTER_ENUM_CONNECTIONS | PRINTER_ENUM_LOCAL, NULL, 1, (unsigned char *)lpBuffer, cbRequired, &cbRequired, &nEntries);
    	if (bSuccess == FALSE)
    	{
    		free(lpBuffer);
    		// 다른 이유로 에러가 난 경우
    		return;
    	}
    	// 알아낸 프린터를 하나씩 enumerate한다.
    	for (int i = 0;i < nEntries; i++)
    	{
    		::MessageBox(NULL, lpBuffer[i].pName, "프린터 이름", MB_OK);
      	}
    	free(lpBuffer);
    

    31. 윈도우의 이동 처리하기

    보통 윈도우는 캡션 영역을 잡고 위치 이동을 수행하게 되는데 윈도우의 특정 영역을 잡고 이동할 수 있게 하려면 어떻게 해야합니까 ?

    WM_NCHITTEST라는 메시지를 잘(?) 처리하면 어느 영역이든 윈도우 이동을 처리할 수 있습니다. 마우스로 윈도우 위를 이동하면 WM_NCHITTEST, WM_SETCURSOR, WM_MOUSEMOVE 같은 메시지들이 발생합니다. WM_NCHITTEST는 현재 마우스가 윈도우의 어느 영역위에 있는지 알아내기 위해 사용됩니다. 이 메시지의 처리부에서 HTCAPTION이란 값을 리턴해주면 윈도우 운영체제는 지금 마우스 포인터가 윈도우의 캡션 부분에 와있다고 생각해서 여기서 드래그 작업이 시작될 경우에 윈도우의 위치를 이동시켜 버립니다. 다음 코드는 WM_NCHITTEST 메시지를 처리하여 현재 마우스 좌표가 정해진 영역에 있으면 HTCAPTION을 돌려주는 예제입니다.

    	case WM_NCHITTEST:
    	{
    		// 현재 마우스 위치를 바탕으로 pt 변수를 채운다.
    		POINT pt(LOWORD(lParam), HIWORD(lParam));
    
    		// 마우스 좌표를 윈도우의 좌측 상단 기준의 좌표로 변경한다.
    		ScreenToClient(hWnd, &pt);
    		// 지정된 사각형 안에 포함되는 점인지 검사한다. 
    		// g_TitleRect는 RECT 타입의 변수로 지정된 사각형의 좌표가 들어있다.
    		if (PtInRect(&g_TitleRect, pt))
    			return HTCAPTION;			
    		break;
    	}
    

    32. 바탕 화면 위의 모든 윈도우를 최소화하거나 모든 최소화 실행 취소

    바탕 화면 위의 모든 윈도우를 최소화하거나 모두 최소화 실행 취소를 프로그램으로 구현하는 방법을 알고 싶습니다.

    태스크바의 빈 공간을 오른쪽 마우스 버튼으로 클릭해보면 팝업 메뉴가 뜨는데 거기에 보면 "모든 창을 최소화(M)"와 "모두 최소화 실행 취소(U)" 명령이 존재하는데 그것을 대신 선택해주는 형식으로 프로그램을 작성해주면 됩니다.

    다음은 "모든 창을 최소화"해주는 루틴입니다. keybd_event API를 이용해서 사용자가 키입력한 것처럼 흉내내줍니다.

    	void IconizeAllWindow()
    	{ 
    		keybd_event(0x5b, 0, 0, 0);
    		keybd_event(77, 0, 0, 0);    // 'M' key
    		keybd_event(0x5b, 0, 2, 0);
    	}
    

    다음은 "모두 최소화 실행 취소" 루틴입니다.

    	void RestoreWindowState()
    	{
    		keybd_event(0x5b, 0, 0, 0);
    		keybd_event(84, 0, 0, 0);    // 'U' key
    		keybd_event(0x5b, 0, 2, 0);
    	}
    

    33. VxD 드라이버 호출하기

    Vxd 드라이버를 동적으로 로드해서 호출하고 싶습니다. 어떻게 해야합니까 ?

    먼저 해당하는 VxD 드라이버의 이름과 위치와 호출하려는 작업의 작업 코드명을 알아야 합니다. 드라이버의 로드는 CreateFile API를 이용합니다. VxD 드라이버의 호출은 DeviceIoControl API를 이용합니다. 자세한 설명은 DeviceIoControl API의 레퍼런스를 참고하기 바랍니다.

    	DWORD byteReturned;
    
    	// 먼저 VxD 드라이버를 오픈한다.
    	hDevice = CreateFile("\\\\.\\NMOUSE.VXD", 0, 0, 0, CREATE_NEW, FILE_FLAG_DELETE_ON_CLOSE, 0);
    	if (DeviceIoControl(hDevice, W32_SETHWND, &hWnd, 4, NULL, 0, &byteReturned, NULL))
    	{
    		// Success !!!
    	}
    

    Vxd 드라이버는 kernel 모드(이를 윈도우에서는 Ring 0라고 부릅니다)에서 동작하기 때문에 모든 하드웨어와 메모리를 바로 접근할 수 있습니다. VxD 드라이버를 작성하려면 DDK를 이용하거나 Numega의 DriverStudio나 KRFTech사의 WinDriver를 이용해야 합니다.

    34. Thread 실행시 에러

    CreateThread를 이용해 스레드를 만들어 생성하고 있습니다. 루틴에 이상은 없는 것 같은데 스레드가 많이 생성되어 시간이 좀 지나면 에러가 발생합니다. 이유가 무엇일까요 ?

    정말로 스레드 코드에 별 이상이 없다면 CreateThread API 대신에 beginthread나 beginthreadex를 사용해보기 바랍니다. 자세한 사항은 마이크로소프트의 Knowledge base를 참고하시기 바랍니다.

    35. 윈도우 운영체제 종료하기

    프로그램에서 특정 상황이 되면 윈도우 운영체제를 종료하고 싶습니다.

    ExitWindowsEx API를 사용하면 됩니다. 이 함수의 원형은 다음과 같습니다.

    	BOOL ExitWindowsEx(UINT uFlags, DWORD dwReserved);
    

    uFlags로 종료방법을 지정할 수 있습니다. 다음과 같은 값이 가능합니다.

    EWX_LOGOFF 현재 사용자를 로그오프한다.
    EWX_POWEROFF 시스템을 종료하고 파워오프한다. 파워오프는 이를 지원하는 하드웨어에서만 가능하다.
    EWX_REBOOT 시스템을 종료하고 시스템을 재시동한다.
    EWX_SHUTDOWN 시스템을 종료한다.
    EWX_FORCE WM_QUERYSESSION이나 WM_ENDQUERYSESSION을 보내지 않고 실행중인 모든 프로세스를 종료한다. 위의 네 가지 플래그들과 함께 사용할 수 있다.

    36. 디폴트 웹 브라우저 알아내기

    디폴트로 지정된 웹 브라우저를 실행하는 방법을 알고 싶습니다.

    디폴트로 지정된 웹 브라우저는 레지스트리에 자신을 등록합니다. .htm (혹은 .html) 파일의 편집기로 링크도 되지만 http, ftp, gopher 등의 프로토콜 연결 프로그램으로 등록됩니다. 제 생각에 가장 좋은 것은 http 프로토콜의 연결 프로그램을 찾아보는 것으로 생각됩니다. 다음 레지스트리 항목에 보면 연결된 웹 브라우저의 절대 경로를 알 수 있습니다.

        HKEY_CLASSES_ROOT\http\shell\open\command
    

    이 항목의 값을 읽는 방법은 3. 레지스트리 읽기/쓰기를 참고하고 프로그램의 실행에 관한 부분은 19. 특정 프로그램을 실행하고 종료를 기다리기를 참고하거나 ShellExecute API 혹은 WinExec API를 사용하면 됩니다. 이 기능을 수행하는 함수는 다음과 같습니다.

    void LaunchDefaultWebBrowser(HWND hWnd)
    {
        // HKEY_CLASSES_ROOT\http\shell\open\command
    	DWORD dwType, cbData;
    	HKEY hSubKey; 
    	long lRet;
    	LPSTR pszString, pszSrcPath;
    
    	// 키를 오픈한다.
    	if ((lRet = RegOpenKeyEx(HKEY_CLASSES_ROOT, "http\\shell\\open\\command",
    			0, KEY_READ | KEY_QUERY_VALUE , &hSubKey)) == ERROR_SUCCESS)
    	{
    		cbData = 255;	// 문자열 값을 읽어올 데이터의 크기를 준다.
            pszString = (LPSTR)malloc(255);
            pszSrcPath = pszString;
    		if ((lRet = RegQueryValueEx(hSubKey, "",
    			NULL, &dwType, (unsigned char *)pszString, &cbData)) == ERROR_SUCCESS)
    		{
    			// pszString에 디폴트 웹 브라우저의 경로가 들어온다.
                // pszString에서 "를 제거한다.
                RemoveChar(pszString, '"');
                WinExec(pszString, SW_SHOWNORMAL);            
    		}
            free(pszString);
    		RegCloseKey(hSubKey);
    	}
    }
    
    void RemoveChar(LPSTR lpSrc, char chRemove)
    {
    	LPTSTR pstrSource = lpSrc;
    	LPTSTR pstrDest = lpSrc;
    	LPTSTR pstrEnd = lpSrc + strlen(lpSrc);
    
    	while (pstrSource < pstrEnd)
    	{
    		if (*pstrSource != chRemove)
    		{
    			*pstrDest = *pstrSource;
    			pstrDest = _tcsinc(pstrDest);
    		}
    		pstrSource = _tcsinc(pstrSource);
    	}
    	*pstrDest = '\0';
    }
    
    Copyright 1999© 한기용 Last updated: 07/29/2004 12:27:59 Designed By 한기남

    37. 윈도우의 최대/최소 크기 설정

    윈도우의 캡션을 없앴을 경우 윈도우를 최대화했을 때 아래의 Task Bar가 가려져 버리는 현상이 생기는데.. 캡션이 있으면 Task Bar 위로만 최대화되는데 말입니다. 어떻게 해결할 수 있는 방법이 없나 궁금하네요..

    말씀하신 문제를 해결하려면 WM_GETMINMAXINFO 메시지를 처리해야 합니다. 이는 윈도우의 최대 크기 등을 설정하기 위해 사용되는 메시지입니다. 다음과 같이 처리합니다.

    case WM_GETMINMAXINFO :
    {
        LPMINMAXINFO lpmmi;
        RECT rc;
    
        SystemParametersInfo(SPI_GETWORKAREA, 0, &rc,0);
        lpmmi = (LPMINMAXINFO)lParam; 
        lpmmi->ptMaxSize.x = rc.right; 
        lpmmi->ptMaxSize.y = rc.bottom; 
        lpmmi->ptMaxPosition.x = 0; 
        lpmmi->ptMaxPosition.y = 0; 
        lpmmi->ptMinTrackSize.x = GetSystemMetrics(SM_CXMINTRACK); 
        lpmmi->ptMinTrackSize.y = GetSystemMetrics(SM_CYMINTRACK); 
        lpmmi->ptMaxTrackSize.x = GetSystemMetrics(SM_CXMAXTRACK); 
        lpmmi->ptMaxTrackSize.y = GetSystemMetrics(SM_CYMAXTRACK); 
        break;
    }
    

    38. Thread에서 Automation 메소드 호출시 에러 발생

    Thread를 생성하고 Automation 메소드를 호출했는데 에러가 발생합니다.

    App 클래스의 InitInstance 함수에서 AfxOleInit를 호출하는 부분을 CoInitializeEx(NULL, COINIT_MULTITHREADED)를 호출하는 것으로 변경하기 바랍니다. 그리고 App 클래스에 ExitInstance 함수를 추가하고 거기서 CoUninitialize를 호출하도록 하면 됩니다. MFC의 AfxOleInit는 기본적으로 STA(Single Threading Apartment) 모델을 사용합니다. Thread에서 자신이 생성하지 않는 COM 객체를 접근할 때는 MTA(Multiple Threading Apartment) 모델을 사용해야 합니다.

    39. 최상위 윈도우의 종료 방법

    현재 최상위 윈도우를 찾아서 종료하는 코드를 만들고 싶습니다.

    일단 현재 사용자가 작업 중인 최상위 윈도우의 핸들은 GetForegroundWindow API로 얻어냅니다. 그런데 그 윈도우가 자식 윈도우일 수 있기 때문에 GetParent API를 반복적으로 사용해서 최상위 탑 레벨 윈도우의 핸들을 알아냅니다. 종료하는 방법은 먼저 DestroyWindow를 호출해서 시도해보고 실패하면 시스템 메뉴의 "닫기" 명령을 이용해 처리합니다. 사실 이 것도 실패할 수 있는데 무조건 종료시키고 싶다면 아래 코드에서 주석 처리해 놓은 GetWindowThreadProcessId/Terminate API 부분을 사용하면 됩니다.

        HWND hTopWnd = GetForegroundWindow();
        if (hTopWnd == NULL)
        {
            return;
        }
    
        while(GetParent(hTopWnd))
        {
            hTopWnd = GetParent(hTopWnd);
        }
    
        if (DestroyWindow(hTopWnd) == FALSE)
        {
            SendMessage(hTopWnd, WM_SYSCOMMAND, SC_CLOSE, NULL);
            //GetWindowThreadProcessId(hTopWnd, &dwProcessId);
            //TerminateProcess(dwProcessId);
        }            
    
    

    40. 인터넷 익스플로러의 위치 경로 알아내기

    인터넷 익스플로러가 설치된 절대 경로를 알고 싶습니다.

    레지스트리의 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\App Paths\IExplore.exe 의 기본 값으로 인터넷 익스플로러의 설치 경로가 들어옵니다. 다음 함수를 호출하면 설치 경로를 얻어 줍니다. 인자로 넘어가는 lpPath는 적어도 256바이트 이상의 크기를 갖는 문자 배열이어야 합니다.

    BOOL GetIEPath(LPTSTR lpPath) 
    {
        long lRet;
        HKEY hKey;
    
        lRet = RegOpenKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\IExplore.exe", &hKey);
        if (lRet == ERROR_SUCCESS)
        {
            long cbData = 255;
    
            RegQueryValue(hKey, "", lpPath, &cbData);
            RegCloseKey(hKey);
        }
        else
            return FALSE;
        return TRUE;
    }
    
  • 반응형
    반응형

    Win32 MASM 프로그래밍 소개(Visual C++이용)

    Written by 권필진. 06'08 - aka Xeno

     


    Chapter 1   준비사항

    개요 : Win32 프로그래밍을 시작하기 전에

    Chapter 1 - 1   주의 사항

    MASM 프로그래밍을 하기 전에, 모든 책임은 자신이 진다는 것을 명심하기 바란다. MASM은 로우레벨의 명령어를 실행할 수 있기 때문에, 버그가 발생했을 경우 컴퓨터에 치명적인 에러 혹은 PC자체가 작동하지 않는일도 있을 수도 있다. 그러므로,

    이 문서로 인하여 발생한 손해 및 문제는 필자가 어떠한 책임과 보상도 지지 않음을 숙지하고 읽어나가기 바란다.

    (너무 겁먹을필요는 없다)

    주의:이 문서는 2006년경에 작성 했다. 테스트 환경으로서는 WindowsXP-SP2환경으로 테스트했다. 본 문서는 수시로 변경 될 수도 있다.

    Chapter 1 - 2   MASM 입수

    MASM 이란, 마이크로소프트사의 어셈블러 툴이다. 현재는 무상으로 제공되기 때문에, 어셈블러공부를 하고 싶다면 지금이 최적의 시기이다. 어셈블리를 익히게 된다면 컴퓨터에 대해 지금까지 몰랐던 것을 알 수있게 될지도 모른다. 하지만, MASM을 설명하는 곳은 드믈고 자료또한 거의 없기 때문에 이렇게 문서를 작성하고 있는 것이다. 또한, 본 문서는 간단한 소개정도에 그치는 수준이며 좀더 자세한 내용은 추후로 미루는 것 또한 알아두기 바란다.

    그렇다면 먼저, MASM을 구해야 한다. 이미 말했듯이, 현재는 무상으로 제공되기 때문에, 마이크로소프트의 홈페이지에서 다운로드 한다. 마이크로소프트의 DDK (Driver Development Kit) 라는 패키지 안에 포함되어있다. DDK 는, 20메가 이상의 크기일것이므로 다운로드하는데 시간이 걸릴지도 모른다.

    참고로, DDK 에 들어있는 MASM은 버젼이 낮고 MMX명령어를 인식하지 못하기 때문에, 강좌와 버젼을 맞출려면 「패치」를 해주어야 한다. 패치 또한 마이크로소프트의 홈페이지에서 구할 수가 있고, 다운로드한 후 압축을 해제하고 , 파일들을 모두 MASM이 있는 디렉토리안에 복사 한 후「패치」실행 파일을 실행한다.

    MASM 의 버젼이 최신상태로 되었다면, VC++ 에서 MASM를 사용할 수 있도록 설정한다. 이렇게 하면 VC환경에서 어셈블리환경을 사용할 수가 있게 되므로, 여러가지 편리한 점이 있다.

    가장 먼저 해야하는 것은, MASM 를 구해야 한다. 이것은 아래의 장소에서 구할 수가 있다.

    상기에서 설명했듯이 MASM을 구하고 패치를 해야하지만 번거롭다. 이럴경우 VC용 패키지가 존재한다. 본 강좌에서는

    이방법을 사용하기로 한다.

    Visual C++ 6.0 에 MASM를 추가하는 가장 간단한 방법은, ProcessorPack 의 추가이다. 이미 ML버전이 6.15가 포함되어 있기 때문에, 패치 단계가 필요없다. 하지만, 패키지가 SP4 ,SP5 전용이기 때문에, SP6 을 설치했다면, /C 옵션을 사용해서 압축을 해제한 후 직접 설치해 주어야한다. 참고로 VisualStudio.NET 이상 버전에는 ML.EXE 는 추가해 주지않아도 이미 포함 되어있다. 2002 는 버전 7.00 이며, 2003에는 7.10 이 포함 되어 있다. 아래의 주소에서 ProcessorPack을 다운로드 할 수있다.

    http://msdn.microsoft.com/vstudio/downloads/tools/ppack/default.aspx 에서 "Download Now" 부분을 클릭한다. 혹은 http://download.microsoft.com/download/vb60ent/update/6/w9x2kxp/en-us/vcpp5.exe 링크를 클릭해서 다운 받은 후 vcpp5.exe / c 옵션으로 압축을 해제해도 된다. WinZip과 같은 압축 프로그램으로도 압축을 해제할 수있다.

    압축해제한 후 해당 폴더를 VC의 설치 폴더에 통째로 복사해준 후 아래의 순서대로 경로를 잡아주면 된다.

    MASM를 구했는가? MASM의 프로그램 파일명은 ML.EXE 라는 이름이다. 이 프로그램은 소스파일을 어셈블링 해주고 링크까지 해주게 되며,원한다면, 오브젝트파일만 생성해 줄 수도 있다.(16bit링커사용시)

    추가로, 본강좌에 사용되는 Microsof Visual C++ 6.0은 제공해 줄 수가 없다. 각자 구하기 바란다.(-_-)

    사용된 소스는 복사해서 붙여넣기를 수행해도 작동하도록 되어있으며, 텍스트뷰어는 소스파일의 링크를 클릭해서

    해당 .dsw파일을 열어서 F5키를 눌러서 실행하면 된다.

     

     

    Chapter 1 - 3   VC++에서 MASM을 사용하도록 설정

    이제부터는, MASM를 VC++에서 사용할 수 있도록 해보자. MASM을 VC++ 에서 사용할려면, VC++의 「커스텀 빌드」라는 기능을 사용해야한다.

    먼저 File -> New 를 선택해서 Win32 Application을 선택한 후 "An Empty Project"를 선택한 후 다시 File->New를

    선택해서 Text File을 선택하고 파일명을 "VCMASM.ASM"이라고 정해주고 아래의 내용을 수행해 보기 바란다.

    VC++에서 ML.EXE 를 실행해야 하므로, 실행파일 경로를 설정해주어야 한다. 「Tools」-> 「Options」을 선택하고 아래와 같이 설정한다.

    Directories」탭으로 이동후에 반드시 「Executable Files」를 선택하기 바란다. 추가 버튼을 눌러서 압축해제한 폴더를 지정해준다. 참고로, 가장 하단에 추가하기 바란다. 본 강좌에서는 C:\VC60\VCPP5 라는 폴더이다.

    이제 실제로 간단한 프로그램을 만들어 보자.

    File -> New로, Win32 application 의 시작 프로젝트를 작성한다. 그런 다음에, MASM의 소스코드를 작성합니다. 이것은 File -> New -> Files탭에서, 확장자(extension)가 ASM 이 되게끔 하고 신규파일을 작성한다.

    그런 다음, 커스텀 빌드의 설정을 한다. Project -> Setting을 선택하고, 어셈블리 소스 파일을 선택한다. 커스텀 빌드 탭을 선택한 후, 아래와 같이 설정한다.

    Commands 텍스트 박스에는,

    ml /c /coff /Cx /nologo /Fo$(OutDir)\ /Fl$(InputName) /Zi $(InputPath)

    로 설정한다. 이는 디버그 모드의 경우지만, 릴리즈 모드로 할 경우, 커맨드에 있는 /Zi 플래그를 제거하면 된다.

    Outputs의 텍스트박스에는,

    $(OutDir)\$(InputName).obj

    로 설정해 준다. 이제부터는 VC에서, Build-> Run 을 선택하면, 자동으로 어셈블해주고 링크도 VC++가 알아서 해주게 된다. 하지만, 조금 번거로운것은, 파일을 작성할 때 마다 커스텀 빌드를 설정해 줘야 한다는 것이다.

    좀 더 자세한 자료는 다음부분을 참고하기 바란다 → Microsoft Support Q106399  (영어)

    커스텀 빌드에 대해, 좀 더 자세하게 알고 싶다면, MSDN에 자료가 아주 상세하게 설명되어 있기 때문에, 그부분을 참조하기 바란다.

    Chapter 1 - 4   최초의 MASM 프로그램

    모든언어의 표준 입문 방식인 「Hello World」프로그램을, MASM으로 간단하게 만들어 보자. 물론 무슨 내용인지 몰라도 상관없다. 모르는것이 당연한 것이며, 그래서 본 문서가 있는것이다.

    ;**********************************************************************
    ; 최초 MASM 프로그래밍
    ; 주석은 세미콜론으로
    ;**********************************************************************
    
    .586
    .model flat, stdcall
    
    NULL            EQU     0
    
    MessageBoxA     proto :dword, :dword, :dword, :dword
    ExitProcess     proto :dword
    
    .data
    
    TITLE1          DB '어셈블리 테스트', 0
    MESSAGE         DB '~Hello World!!~', 0
    
    .code
    WinMainCRTStartup   proc
    
        invoke MessageBoxA, NULL, offset MESSAGE, offset TITLE1, 0
        invoke ExitProcess, 0
        ret
    WinMainCRTStartup   endp
    end
    

    이 내용을 어셈블(assemble) 한 후 실행하면(F5), "~Hello World!!~"라고 표시되는 메시지 박스가 화면에 표시된다.

    (안된다면 십중 팔구 오타이니 복사해서 붙여넣기 바란다)

    (주의) 위의 소스에는 편의상 색을 다르게 표시했지만, 실제로는 변하지 않는다. (전용 통합환경을 사용하면 모든기능을 사용할 수가 있다. 많은것을 바라지는 말기 바란다. WinASM IDE를 사용하라.)


    Chapter 2   하드웨어의 구조

    개요 : 컴퓨터에 대해서

    Chapter 2 - 1   하드웨어

    이번에는, 간단하게 하드웨어의 구조에 대해 설명한다. 하드웨어의 구조라 하더라도, USB의 규격이나, 메인보드는 어떻게 구성되어 있는가, 그런 것을 다루는 것이 아니다. 대부분을 CPU 의 구조에 대해 설명을 할 것이다. 「최소한, 이것만은 반드시 알아야 한다」라는 부분을 위주로 해서 설명해 나갈것이다.

    다음의 순서로 설명 할 것이다.

    1. 어셈블러에서 가능한 것
    2. 저장장치(레지스터/메모리/스택)
    3. 외부 입력과 외부 출력(I/O)·인터럽트
    4. BIOS·시스템 콜·VRAM

    Chapter 2 - 2   어셈블러에서 가능한 것

    먼저, 왜 어셈블러를 사용 할까? 그 이유는, 몇가지가 있다. 대부분의 프로그래밍은, C언어로도 충분하다. 일반적으로 말해, 언어가 고급으로 될수록 , 인터프리터성이 높아져서 , 언어가 사람이 이해하기 쉬워지지만, 그 언어로 실제 수행되는 부분은 오히려 감소하게 된다. 즉, Basic과 C언어의 예를 들면, Basic에서 할수 있는 일은 C언어로도 수행 할 수 있다.하지만 반대로, C언어로 수행할 수 있는 것이 반드시 Basic 에서 수행된다는 보장은 없다. 같은 맥락으로 C언어와 어셈블러의 관계에서도 존재한다. 예를 들어 C언어에서는, 하드웨어를 직접조작은 할 수 없다(물론 인라인어셈블리를 사용할 수는 있지만). 단지 예를 든것이다. 그렇기 때문에, 직접 하드웨어를 조작하고 싶은 경우 등에 어셈블러를 사용한다. 직접 하드 디스크의 트랙이나 섹터를 조작하고 싶은 경우에 어셈블러를 사용한다라고 생각하면 이해하기 쉬울 것이다.

    프로그램이 고속성을 요구하는 경우, C언어보다 어셈블러 쪽이 일반적으로 빠르기 때문에, 이런 경우는 어셈블러에서 프로그램을 주로 짠다. 그렇지만, 어셈블러는 너무 로우레벨이므로, 프로그래밍에는 상당한 노력을 필요로 하고 코딩의 양도 많아진다. 그러므로, 어셈블러에서만 프로그래밍을 하는 것은, 현실적이지는 않다. 많은 경우, C언어등의 고급(중급) 언어와 함께 조합해서 사용하게 된다.

    Chapter 2 - 3   저장장치(레지스터/메모리/스택)

    기억장치에는, 3 종류가 있다. 레지스터와 메모리와 스택이다. 이 밖에도, 보조기억장치로 하드 디스크나 플로피 디스크등이 있다. 어셈블러에서는, 주로 레지스터와 스택을 이용한다.

    레지스터는, CPU의 내부에 있는 기억장치이다. CPU의 내부에 존재하므로, 액세스 속도 또한 아주 빠르다. 그렇지만, 레지스터는, 무한정으로 사용할 수 있는 자원이 아니다. 레지스터의 종류에는, 3개 있다. 범용 레지스터,세그먼트 레지스터,플래그 레지스터.

    레지스터 종류 해설
    범용 레지스터
    8개가 있다. 프로그램에서 사용된다.
    세그먼트 레지스터
    6개가 있다. 세그먼트(segment)의 관리에 사용된다.
    상태 레지스터
    1개가 있다. CPU 상태를 기억한다. 일반적으로 변경할 수 없다.

    프로그래밍에서는, 거의 대부분 범용 레지스터를 사용한다. 범용 레지스터에는, 다음의 8가지가 있다. 모두 32 비트이다.

    레지스터명 해설
    EAX
    어큐물레이터. 수치계산에 사용되는 레지스터.
    EBX
    베이스 레지스터. 보조 계산이나, 그 외의 용도에 사용되는 레지스터.
    ECX
    카운터 레지스터. 보조 계산이나, 카운터, 루프에서 사용되는 레지스터.
    EDX
    데이터 레지스터. 보조 계산이나, 데이터용 조작용의 레지스터.
    ESI
    소스 인덱스. 보조 계산이나, 데이터 처리 인스트럭션으로 사용되는 레지스터.
    EDI
    목적지 인덱스. 보조 계산이나, 데이터 처리 인스트럭션으로 사용되는 레지스터.
    ESP
    스택 포인터. 스택 처리로 사용되는 레지스터.
    EBP
    베이스 포인터. 스택 처리로 사용되는 레지스터.

    C언어에서 변수의 수식자에 register 를 지정할 수 있다.(레지스터변수) 이런 경우 실제로는, 변수에 ESI(소스 인덱스)와 EDI(목적지 인덱스)를 사용하게 된다.또한, 계산용의 레지스터로서 ESP(스택 포인터)와 EBP (베이스 포인터) 를 사용할 수 없다. 이런 레지스터 들은, 스택관리라고 하는 중요한 임무가 있기 때문 이다. 수치계산은, 실질적으로 6개의 레지스터로 하게 된다. 각각의 레지스터에는, 고유한 역할도 있으므로, 거기에 맞춰서 프로그램을 짜지 않으면 안된다. 왜 이런 엄격한 룰이 존재하는지를 탓하지 말기 바란다. 컴퓨터는 복잡한 장치이다. 하지만 이런 기초적인 룰만 익히면 어떤언어보다 강력함을 얻을수 있다. 대충 아는 언어와는 다른 것이다.

     

    32 비트의 경우, 누산기는 EAX 로 취급하지만 하위 16 비트를 AX 로서 다룰 수도 있다. AX 는 또 다시, 상위 8 비트를 AH , 하위 8 비트를 AL 로 나타낸다. 이것은, EBX ECX EDX 의 경우도 같다.

    그렇다면, 이제 레지스터를 사용한 프로그램을 살펴보자.

    ;*****************************************************************
    ;  레지스터를 사용한 프로그램
    ;*****************************************************************
    
    .586                   ; Pentium 용의 코드를 쓰는 경우 필요
    .model flat, stdcall
    
    NUM         EQU   1
    
    wsprintfA   proto c :dword, :dword, :dword, :dword, :dword
    MessageBoxA proto   :dword, :dword, :dword, :dword
    ExitProcess proto   :dword
    
    .data
    
    BUFFER      DB 64 DUP(0)
    STRINGS     DB '%d + %d = %d', 0
    TITLENAME   DB '결과는? ', 0
    
    .code
    
    WinMainCRTStartup proc
    
        mov eax, NUM     ; 누산기에 대입
        mov ebx, NUM + 1 ; 베이스 레지스터에 대입
        add eax, ebx     ; 누산기와 베이스 레지스터를 더한다
    
        invoke wsprintfA, offset BUFFER, offset STRINGS, NUM, ebx, eax
        invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
        invoke ExitProcess, 0
    
        ret
    WinMainCRTStartup endp
    
    end
    

    덧셈을 한 후, 메시지 박스로 표시하는 프로그램이다("1+2=3"). 레지스터는, EAX(누산기)와 EBX(베이스 레지스터)를 사용하고 있다. 프로그램에서 mov 나 add 등을 니모닉이라 한다. 니모닉은, 기계어의 비트 표현을 문자표현으로 나타낸 것이다. 예를 들어 mov 라면, MOVe 의 약자로 「대입」을 의미한다. add (은)는 ADD 로 「가산」을 의미한다. 프로그램에서는 누산기와 베이스 레지스터에 수를 대입해서 ,덧셈을 수행하고 있다.

    MASM에서, 함수를 프로시저라고 부른다. 프로시저의 정의는, proc - endp 로 한다. 위의 예에서는 , WinMainCRTStartup 라는 프로시저가 정의되어 있고, 그 사이의 기계어 인스트럭션이 실행되고 있다고 생각하면 된다.

    이 함수는 스타트 업 코드(Startup Code)로 불리고 프로그램에서 가장 먼저 호출되는 함수다. 보통 C로 프로그램을 짜는 경우, 스타트 업 코드는 자동으로 생성되어 링크 된다. 또한 스타트 업 코드는, 프로그램을 초기화하는데 사용된다. VC++ 로 Windows 프로그래밍을 하는 경우, 스타트 업 코드는 WinMainCRTStartup 함수지만, Console 용의 프로그램의 경우는 mainCRTStartup 함수가 스타트 업 코드가 된다. 통합 환경에 의해, 스타트 업 코드 함수의 이름은 각각 다르다.반드시 정해진것은 아니다. 개발자 마음인것이다. main이란것을 찾지만 말기 바란다. 이것은 C언어의 룰일 뿐이다.실제로는 이런것은 없다.

    Chapter 2 - 4   그 외 레지스터

    상태 레지스터(flag register)는, 하나 밖에 없다. 레지스터의 내부는, CPU 상태를 보존하고 있다. 일반적으로 상태 레지스터 전체를 변경할 수 없다(스택을 사용하면 변경할 수 있지만, 권하는 방법은 아니다 pushf / popf). 플래그의 종류는,상태 플래그,제어 플래그,시스템 플래그의 3개가 있다. 상태 플래그는, 연산의 결과 상태가 보존된다. 상태 플래그의 종류는 다음과 같다.

    플래그명 비트 해설
    CF (Carry Flag) 0 carry flag. 자리수가 올라가면 세트.
    PF (Parity Flag) 2 패리티플래그. 연산 결과가 짝수일 경우 세트.
    AF (Adjust Flag) 4 조절 플래그. 2 진화 10진(BCD) 연산에서 사용.
    ZF (Zero Flag) 6 제로 플래그. 연산 결과가 제로일 경우 세트.
    SF (Sign Flag) 7 부호 플래그. 연산 결과가 부호가 있는 경우 세트.
    OF (Overflow Flag) 11 오버플로우 플래그. 오버플로우가 발생하면 세트.

    일반적으로 상태 플래그는 변경할 수 없지만, carry flag는 변경 가능하다. 니모닉 STC (SeT Carry flag), CLC (CLear Carry flag) 인스트럭션으로 변경할 수 있다.

    제어 플래그는 DF (Direction Flag ; 방향 플래그) 밖에 없다. string 인스트럭션(메모리 전송 인스트럭션)에서, 메모리 전송 방향을 지정한다. 니모닉으로는, STD (SeT Direction flag) CLD (CLear Direction flag)를 사용한다.

    시스템 플래그는, 가상 8086 인스트럭션이나 인터럽트 제어 등에 사용되지만 본 문서에서는 설명하지 않는다.인텔의 아키텍쳐 메뉴얼을 참조하기 바란다.

    그러면, 상태 레지스터를 사용한 간단한 프로그램을 만들어 보자.

    ;*****************************************************************
    ;  상태 레지스터를 사용한 프로그램
    ;*****************************************************************
    
    .586
    .model flat, stdcall   ; 메모리모델은 플랫으로…
    
    NUM         equ   1
    
    wsprintfA   proto c :dword, :dword, :dword
    MessageBoxA proto   :dword, :dword, :dword, :dword
    ExitProcess proto   :dword
    
    .data
    
    BUFFER      db 64 DUP(0)
    STRINGS     db '2의 10승은 %d 입니다', 0
    TITLENAME   db '2의 10승을 푸는 문제', 0
    
    .code
    
    WinMainCRTStartup proc
    
        mov eax, NUM
        mov ecx, 10
    
    MYLOOP:
        mov ebx, eax
        add eax, ebx
        dec ecx       ; ecx 의 값을 1 감소
        jnz MYLOOP    ; 제로 플래그가 세트 되지않으면 루프
    
        invoke wsprintfA, offset BUFFER, offset STRINGS, eax
        invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
        invoke ExitProcess, 0
    
        ret
    WinMainCRTStartup endp
    
    end
    
    

    먼저 WinMainCRTStartup 함수의 내용보자. 이번에는 2의 10승을 구하는 문제이다("2의 10승은 1024입니다"). ECX 레지스터의 내용을 1씩 감소해 가면서, ECX 레지스터가 0이 되면 종료하는 루프를 만들었다. dec (DECrement)라는 니모닉은, 레지스터의 내용을 1 감소시키는 인스트럭션이다(C에서 abc--). jnz (Jump if Non-Zero flag)란, 제로 플래그가 세트되지 않는 동안, 지정된 주소에 점프 하는 조건 점프 인스트럭션이다. 여기에서는 MYLOOP 라고 하는 주소로 점프 한다. MYLOOP 와 같이, 이름의 뒤에 구두점을(콜론) 붙여서 주소에 이름을 지정할 수가 있다.(레이블)

    이번 예제에서는 상태 플래그의 사용법을 보기 위해서, 귀찮은 방법으로 프로그래밍했지만, 루프를 할 경우는 좀 더 편리한 니모닉이 준비되어 있다바로 loop (LOOP if ecx is non-zero) 라는 것이 있다. 위의 내용을 다시 작성하면 아래와 같이 된다.

    mov ecx, 10    ; 루프 시키고 싶은 횟수만 ECX 에 대입
    
    MYLOOP:
        mov ebx, eax
        mov eax, ebx
        loop MYLOOP    ; ECX 를 1줄여 0이 될 때까지 반복한다
    
    

    또한 실제로 , 2의 계승을 구하는 좀 더 간단한 방법도 있다. 쉬프트를 사용하는 방법이다.

    세그먼트(segment) 레지스터의 종류에는 다음과 같은 종류가 있다. 모두 16 비트이다.

    레지스터 해설
    CS 코드 세그먼트 실행 코드가 존재하는 세그먼트
    DS ES FS GS 데이터 세그먼트 데이터가 존하는 세그먼트
    SS 스택 세그먼트 스택이 존재하는 세그먼트

    16 비트 시절에는 DS와 ES가 있었지만, 현재에는 FS DS 가 더해져서, 모두 「데이터 세그먼트」라고 불려진다.

    세그먼트(segment)란, 연속된 메모리를 필요에 따라서 분할하고, 그 하나하나를 가리키는 의미이다. 예를 들어, 1개의 프로그램에는, 실행 코드와 데이터가 있다. 이것을 분할한 영역에 두는것이 바로, 세그먼트(segment)의 발상이다. 예를 들면, 실행 코드는 CS 세그먼트(segment)에 놓여지고, 데이터는 DS 세그먼트(segment)에 놓여지게 된다. 세그먼트(segment)는 같은 연속적인 메모리에 할당할 수 있고, 세그먼트(segment)끼리 서로 겹쳐 지는 경우도 있다.

    예제 프로그램을 보면 .code 라고 하는 code segment를 정의하고 있다. 또한 .data 라는 데이터 세그먼트를 정의하고 있다. 그 밖에도 .stack 등도 있어서, 스택 세그먼트도 정의할 수가 있다. 하나의 세그먼트(segment)에는, 4 G바이트까지(!) 사용할 수 있다(2의 32승 : 그래서 32비트). 예를 들어 CS 세그먼트(segment)라면 CS:00000000h ~ CS:FFFFFFFFh 까지의 영역이 존재한다. 물론, 실제 메모리는 이렇게 사용되지는 않는다. 단지 예를 든 것이다.

     

    Chapter 2 - 5   메모리

    기억장치이다.「메모리」에 값을 대입할 수도 있다. 우선, 프로그램을 보자.

    ;*****************************************************************
    ;  메모리를 사용하는 프로그램
    ;*****************************************************************
    
    .586
    .model flat, stdcall  
    ; stdcall 은 함수의 호출 방법을 정의!
    
    NUM         equ   32
    
    MessageBoxA proto   :dword, :dword, :dword, :dword
    ExitProcess proto   :dword
    
    .data
    
    BUFFER      db 128 dup(? )
    TITLENAME   db '메모리에 대입하는 프로그램', 0
    
    .code
    
    WinMainCRTStartup proc
    
        mov    ecx, NUM            ; 카운터의 회수
        mov    edx, offset BUFFER  ; 버퍼의 포인터
        mov    ah, 20h
        ;
    
     _LOOP0:
        mov    byte ptr [edx], ah
        inc    ah
        inc    edx
        loop   _LOOP0
    
        invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
        invoke ExitProcess, 0
    
        ret
    
    WinMainCRTStartup endp
    
    end
    

    이번 프로그램은, 128바이트의 정적 버퍼를 확보하고, 그 영역에 문자를 차례차례 대입하는 프로그램이다. 실제로 「 ! "#$%&'()*+,-. /0123456789:<=>?」라는 문자가 대입된다. ASCII 코드의 20h 부터 3Fh 까지이다. 메모리에 대입하는 경우에는 ptr 연산자를 사용한다. [edx]란 edx 가 가리키는 메모리를 나타냅니다. byte ptr [edx] 란, 「edx 가 가리키는 메모리에 바이트값(8 비트)으로 값을 대입하라」라는 의미이다. 워드값으로 값을 대입하는 경우에는, word ptr [edx] 가 된다. C에서 어렵다고 하는 "포인터"이다. 어셈블리에서는 기본이 포인터이므로, 복잡하지 않다. 필자의 경우도

    처음에 C를 할때 괜히 헷갈렸다. 오히려.......

    Chapter 2 - 6   스택

    스택은, 후입선출 (Last-In First-Out) 형태의 데이터 구조이다. 어셈블러에서 스택은 중요한 역할을 한다. 레지스터의 내용을 일시적으로 저장하거나 프로시저 호출 시에 인수를 스택에 넣거나 한다. 특히 C언어나 Pascal 등은, 함수 호출시 인수를 스택에 넣어 호출을 수행한다.이것을 함수호출 규약이라고 부른다.

    그렇다면, 스택을 이용한 프로그램을 보자.

    ;*****************************************************************
    ;  스택을 사용한 프로그램
    ;*****************************************************************
    
    .586
    .model flat, stdcall
    
    NUM         equ   5
    
    wsprintfA   proto c :dword, :dword, :dword
    MessageBoxA proto   :dword, :dword, :dword, :dword
    ExitProcess proto   :dword
    
    .data
    
    BUFFER      db 64 dup(0)
    TITLENAME   db '스택을 사용하는 프로그램', 0
    STR1        db '카운트다운 %d', 0
    
    .code
    
    WinMainCRTStartup proc
    
        mov     ecx, NUM
    
     _LOOP0:
        push    ecx       ; SubFunc 로 ECX 를 스택에 보존
        call    SubFunc
        pop     ecx
        loop    _LOOP0
    
        invoke ExitProcess, 0
    
        ret
    
    WinMainCRTStartup endp
    
    SubFunc proc          ; 새로운 프로시저를 정의
    
        invoke wsprintfA, offset BUFFER, offset STR1, ecx
        invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
    
        mov     ecx, 99   ; 여기서 ECX 의 값을 바꿔도, 스택에 값이 보존되있기 때문에 문제없다
    
        ret
    
    SubFunc endp
    
    end
    

    이번에는, SubFunc 라는 새로운 프로시저(함수)를 만들어 보았다. 간단한 프로그램이므로, 특별히 문제는 없을 것이다. ("카운트다운 5")라는 결과가 나온다.이후 "카운트다운 4" "3"등으로 1까지 감소한다. push 니모닉은 레지스터의 내용을 스택에 대입한다. pop 은 스택으로부터 값을 레지스터에 대입한다.스택을 사용하는 MOVe라고 생각하면 쉽다. call 은 프로시저를 호출한다. 이 때, 현재 실행중인 주소를 스택에 대입하고 나서 프로시저로 점프 한다. 프로시저에서 돌아오는 경우에, ret 을 사용한다. ret 은 스택으로부터 값을 꺼내고, 호출시 원래의 주소로 돌아오게 된다.

    Chapter 2 - 7   I/0·인터럽트

    CPU 로 계산만 수행해서는, 컴퓨터의 진정한 능력을 사용 할 수가 없다. 컴퓨터의 기능을 활용하기 위해서는, 주변장치의 입력이나 출력을 제어할 수 있어야 한다. 예를 들어, 키보드로부터 입력을 받는다거나 디스플레이 장치에에 결과를 표시하거나. 이런것을 제어하기 위해서 I/O 공간을 사용한다. I/O 공간은 16 비트의 공간에서 16 진수로 말하면 0000h - FFFFh 의 공간이다 (16 진수는 수치뒤에[h] 를 붙인다). 이 공간으로 데이터를 전송 하거나(외부 출력), 데이터를 받거나(외부 입력) 하는 방식으로 , 주변장치를 액세스 할 수가 있다.

    윈도우즈에는, 「시스템 정보」를 표시하는 프로그램이 있다. 「시작 메뉴」의 「보조프로그램」안의 「시스템정보」 이다.이 프로그램으로, 어떻게 I/O 공간이 할당되어 있는지를 볼 수가 있다.

    인터럽트는 하드웨어 인터럽트와 소프트웨어 인터럽트의 2 종류가 있습니다. 하드웨어 인터럽트란, 외부로부터 입력이 있었을 경우, 현재의 프로그램을 일시중지 하고, 별도의 프로그램을 실행하는 기능입니다. 예를 들어, 프로그램 A를 실행중에 키보드가 눌렸다고 가정 합시다. 이 경우 키보드가 눌려진 키를 버퍼에 보존해 두어야만 합니다. 거기서 하드웨어 인터럽트가 발생해 키보드버퍼에 값이 저장됩니다. 물론 하드웨어 인터럽트가 발생하면 , 플래그와 레지스터의 값은 스택에 저장되므로, 원래의 프로그램에는 아무런 영향도 끼치지 않습니다.

    하드웨어 인터럽트의 예

    소프트웨어 인터럽트의 예

    이와는 달리, 소프트웨어 인터럽트는 프로그램에서 명시적으로 MS-DOS의 함수 호출이나, BIOS의 프로시저를 호출하는 것을 말합니다.(즉, 프로그래머가 발생시키는 인터럽트를 의미합니다)

    프로그램의 예를 보고 이해하시기 바랍니다.

    ;******************************************************************************
    ; 소프트웨어 인터럽트를 사용한 프로그램
    ;   이 프로그램은 MS-DOS용의 프로그램입니다
    ;   VC에서는 실행할 수 없습니다. 참고용으로 제작한 것입니다.
    ;******************************************************************************
    
    .model small, c   ; 모델에서 small문을 먼저 기록하게되면 세그먼트(segment)는 16 비트가 된다
    .286
    
    ;******************************************************************************
    ; 프로세스의 종료 매크로
    ;******************************************************************************
    end_process macro ret_value
    
                mov   al, ret_value
                mov   ah, 4ch
                int   21h
    
                endm
    ;******************************************************************************
    ; 캐릭터 라인의 표시 매크로
    ;******************************************************************************
    display     macro string
    
                mov   dx, offset string
                mov   ah, 09h
                int   21h
    
                endm
    
    ;******************************************************************************
    ; 1 캐릭터 입력(에코 없음) 매크로
    ;   AL = 키보드로부터 입력된 캐릭터
    ;******************************************************************************
    read_kbd    macro
    
                mov   ah, 08h
                int   21h
    
                endm
    
    ;******************************************************************************
    ; 데이터 정의
    ;******************************************************************************
    .data
    
    MSG         db '이 프로그램은 즉시 종료합니다', 0dh, 0ah, '$'
    
    ;******************************************************************************
    ; 실행 프로세스
    ;******************************************************************************
    .code
    
    START:
                mov   ax, _DATA
                mov   ds, ax
    
                display     MSG   ; 메세지를 표시
    
                read_kbd          ; 버튼 입력 대기
    
                end_process 0     ; 프로그램의 종료
    
    
    end START ; end 의사 인스트럭션의 뒤에는 스타트 지점을 기술
    
    

    주석에서도 밝혔듯이, VC에서는 실행할 수 없습니다. VC의 링커는 32 비트이므로, 이 프로그램은 16 비트의 링커로 링크 하지 않으면 안됩니다. 16 비트의 링커는 아래에서 다운로드 받아서 사용하기 바랍니다.

    ftp://ftp.microsoft.com/softlib/mslfiles/lnk563.exe

    압축해제하면 16 비트의 링커가 생성됩니다. 32 비트용의 링커가 아니므로 주의하기 바랍니다.

    이번 예에서는 매크로 정의하고 있습니다. 매크로는 함수와 달리, 호출된 곳에 치환됩니다. "복사" "붙여넣기"와 유사합니다. 호출이 아니고 대신 코드문이 바로 나타나게 됩니다. 이기능으로 매크로 어셈블리라고 불리게 됩니다. C++의 인 라인 함수와 같은 것입니다. 매크로는 보면 알 수 있듯이 macro - endm 로 정의되고 있습니다. 함수 호출에 대해서는, 16 비트 어셈블러 쪽을 참고 하시길 바랍니다.

    Chapter 2 - 8   BIOS/시스템 콜 /VRAM

    BIOS 는 메인보드에 장착되어 있는, 로우레벨로 하드웨어 조작을 하는 프로그램입니다. BIOS 는 소프트웨어라고 불리지 않고 펌 웨어라고 불리어 집니다. 직접 하드 디스크의 섹터에 기록하거나 읽어들이거나 하는 처리나, 플로피 디스크의 조작등의 로우레벨 함수들로 구성되어 있습니다.즉, 실제로 컴퓨터를 움직이는 가장 밑부분입니다.

    시스템 콜이란, BIOS 나 OS 의 로우레벨 함수를 호출하는 것을 말합니다.

    VRAM (Video RAM)에는, 캐릭터 VRAM 와 그래픽 VRAM 가 있습니다. 캐릭터 VRAM 란, 메모리상의 캐릭터를 디스플레이에 표시하는 RAM 입니다. 메모리상에 직접 캐릭터를 입력하는 것으로, 화면상에 캐릭터를 동기적으로 표시 할 수가 있습니다. 그래픽 VRAM 란, 메모리상의 데이터를 그래픽으로서 표시하는 RAM 입니다. 메모리와 화면의 그래픽은 서로 대응하고 있으므로, 메모리에 기록하는 것으로, 직접그림이나 곡선같은 객체등을 화면에 표시할 수가 있습니다. 그래픽 보드등에 있는 램에 해당합니다.

    대부분의 경우 그래픽 VRAM 는, A000 : 0000 에 할당되어 있습니다.

    우선 하드웨어의 구조는 이것이 다 입니다. 너무 간단하다고 생각 하실 것입니다. 윈도우 프로그래밍을 하게 될려면 먼저 도스나 시스템 프로그램에 대해서 알아야 합니다. 모든것에는 단계가 있습니다. 본 강좌는 상세 매뉴얼이 아닙니다. 이전에 한번쯤은 익혀보셨던 내용을 다시금 간단히 정리한 것입니다. 하지만 프로그래밍은 많은것을 요구하지는 않습니다. 간단한 명령어로도 훌륭한 프로그램을 작성 할 수 있습니다. 알고리즘을 언어의 문법에 맞게끔 작성하는 도구일 뿐입니다.이것을 이해하지 못하게되면 각종 언어를 섭렵해도 멀 했는지 모르게되는 딜레마에 빠지게 됩니다. 예를 들어서 영어의 문법만 잔뜩외우게 된후 실제 대화는 못하게 되는것이랑 같습니다. 미국의 꼬마는 문법을 배우지 않습니다. 말부터 배우고 자연히 문법을 익혀서 오류를 줄이고 멋지게 표현하는 법을 배우는것과 같은 이치입니다. 언어는 도구입니다.라는 말을 각종 개발자사이트에서 하는 이유는 이것입니다. 모든것을 외우려 하지 마시길 바랍니다. 기본적인것을 배우시고 활용을 하다가보면 막히는 부분도 발생하고 그런부분을 하나하나 익혀가시게 되면서 익히는 겁니다. 에 에 에 문법과 알고리즘을 보강하시게되고, 나중에는 자신만의 라이브러리와 개발방식을 타인에게 전파시킬 수 있는 것입니다.

    그래도 먼가 찜찜하다고 생각하시는 분들은 어셈블리 기초강좌를 참조하시기 바랍니다. 이역시 강좌를 할 것입니다.

    다음부터는 Windows 프로그래밍을 하게 됩니다. 기초지식으로서는, Windows API 에 대한 지식이 필요합니다만, 너무 자세히 알려하시기 전에 가볍게 체험 하신후에 별도의 서적등으로 공부하시기 바랍니다. MSDN도 훌륭합니다. 여기서는 자세하게 설명하지는 않습니다.


    Chapter 3   Windows 프로그래밍

    개요 : Windows 프로그래밍

    Chapter 3 - 1   Windows 프로그래밍

    MASM으로 Windows 프로그래밍을 시작해보자!

    기초지식으로 Windows API, C 언어,CPU 등의 기본지식이 필요하다. 이런 부분에 대해서는 본 문서에서 자세히 설명하지 않으므로, 공부하기 바란다.

     

    Chapter 3 - 2  최초의 프로그램

    MASM으로 Windows 프로그래밍을 하는 것이지만, 처음으로 작성하는 것이라서 주석을 달아 두겠다.

    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 어셈블러 윈도우프로그래밍
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    
    .486
    .model flat, stdcall
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 정의
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    NULL      =       0
    MB_OK     =       0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; prototype
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    MessageBoxA proto :dword, :dword, :dword, :dword
    ExitProcess proto :dword
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 데이터 세그먼트
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .data
    
    MSG1      db	'메세지 테스트',0
    TITLE1    db	'타이틀',  0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; code segment
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .code
    
    WinMainCRTStartup	proc
        invoke MessageBoxA, NULL, offset MSG1, offset TITLE1, MB_OK
        invoke ExitProcess, 0
        ret
    WinMainCRTStartup	endp
    
    end
    

    하나씩 설명하겠다. 먼저 .486 지시어이다. MASM에서, 486용의 인스트럭션을 생성하는 지시어이다. 예를 들어, .386으로 하게되면, 80386용의 인스트럭션을 생성하게 된다. .386p와 같이, CPU명 뒤에 p를 붙이게되면 특권 인스트럭션도 사용할 수 있게 된다. (p는 privilage→특권의 약자)

    다음으로 .model 지시어이다. 이 인스트럭션은 메모리모델이라는 메모리사용 규약을 정한다. Win32 프로그램의 경우에는 플랫메모리모델만 사용하기 대문에, 메모리모델에는항상 flat를 지정한다. 또한, 함수(프로시저)의 호출 규약에는, Windows 프로그램 라이브러리의 호출 규약인 stdcall을 지정한다.

    프로그램에서, WinMainCRTStartup라는 프로시저를 정의하고 있다. 프로시저를 정의하려면 , proc 지시어를 사용한다. 또, 프로시저 호출방법에는, 여러 가지가 있지만, 간략화된 호출 방법으로 invoke 지시어를 사용할 수가 있다. Invoke만 호출해줌으로써, MASM에서 프로시저 호출을 간단하게 수행할 수가 있게 된다. 복잡한 스택처리르 해주게된다. 이명령어와 다른 몇가지로 인해서 고급언어와 비슷하게 프로그램을 하게 될 수 있게 된다.

    Windows 프로그램의 종료에는, 반드시 ExitProcess를 호출해야 한다. VC++에서는, 내부적으로 ExitProcess 를 호출한다. 왜 반드시 호출해야하는가 하면 , ExitProcess를 호출하지 않으면 완전하게 프로세스가 종료하지 않기 때문이다. 만약 호출하지 않는 경우는, 메모리상에 쓰레기값이 남는 경우도 발생할 수도 있다.

    Chapter 3 - 3   인스턴스 핸들

    인스턴스 핸들이란, 각각의 모듈의 실체를 나타내는 핸들이다.선뜻 이해가 되지 않을것이다 . 좀 더 자세하게 설명을 하겠다.

    Windows에는, 모든 오브젝트를 "핸들" 로 다루게 된다. 그렇기 때문에 Windows에서, 실행 파일이 메모리상에 로드 되면, 어떤 형태로든 이"인스턴스"를 관리하지 않으면 안 되는 것이다. Windows는, 어플리케이션이 자신의 인스턴스에 대해서 조작을 수행할 경우 "인스턴스 핸들"을 그 값으로 넘겨주게 된다.(조작용 번호라고 생각하기 바란다)

    그런데, 왜 인스턴스 핸들이 필요한가 를 설명하면, 실행파일(혹은 모듈)에 속해있는 리소스를 얻을려고 하는 경우에는, 이 인스턴스 핸들을"키"로해서 Windows에 요청해야 하는 것이다. 인스턴스 핸들의 존재 의미는,어찌보면 당연한 것이다.

    먼저, 프로그램을 살펴보자.

    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 어셈블러 윈도우 프로그래밍
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .586
    .model flat, stdcall
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 정의
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    NULL	=	0
    MB_OK	=	0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; prototype
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    MessageBoxA       proto   :dword, :dword, :dword, :dword
    GetModuleHandleA  proto   :dword
    wsprintfA         proto c :dword, :dword, :dword
    ExitProcess       proto   :dword
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 데이터 세그먼트
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .data
    
    MSG1	db	'인스턴스 핸들: 0x%X', 0
    TITLE1	db	'타이틀 ', 0
    
    STR1	db	64 dup(0)
    hModule	dd	?
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; code segment
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .code
    
    WinMainCRTStartup	proc
        invoke GetModuleHandleA, NULL
        mov    hModule, eax
    
        invoke wsprintfA, offset STR1, offset MSG1, hModule
        invoke MessageBoxA, NULL, offset STR1, offset TITLE1, MB_OK
        invoke ExitProcess, 0
    
        ret
    WinMainCRTStartup	endp
    
    end
    

    실행(F5)해보면 모듈핸들 번호를 메세지 박스로 표시해준다. 컴퓨터마다 다를수 있으나 보통 0x400000형식이다.모듈 핸들을 얻기위해서는 GetModuleHandle 함수를 사용한다. 함수명 뒤에 붙어 있는 A의 의미는 문자열이 Ascii 코드라는 의미이다. 함수명 뒤가 W일 경우는, 문자열이 UNICODE라는 의미이다. 참고삼아 기억해 두기 바란다.

    (NT의 경우는 기본값으로 유니코드방식이다)

    WindowsAPI에 대해서는, MSDN 라이브러리를 참조하기 바란다.

    Chapter 3 - 4   로컬 변수

    다음에는, 로컬 변수에 대해서 알아보자.

    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 어셈블러 윈도우 프로그래밍
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    
    .586
    .model flat, stdcall
    option casemap:none
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 정의
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    NULL	=	0
    MB_OK	=	0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; prototype
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    MessageBoxA       proto   :dword, :dword, :dword, :dword
    GetModuleHandleA  proto   :dword
    wsprintfA         proto c :dword, :dword, :dword
    ExitProcess       proto   :dword
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 데이터 세그먼트
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .data
    
    MSG1	db	'인스턴스 핸들: 0x%X', 0
    TITLE1	db	'타이틀', 0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; code segment
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .code
    
    WinMainCRTStartup	proc
        ;---------------------------
        ; 로컬 변수
        ;---------------------------
        local pBuf[64] :byte
        local hModule  :dword
    
        ;---------------------------
        ; 프로그램 본체
        ;---------------------------
        invoke GetModuleHandleA, NULL
        mov    hModule, eax
    
        invoke wsprintfA, addr pBuf, offset MSG1, hModule
    
        invoke MessageBoxA, NULL, addr pBuf, offset TITLE1, MB_OK
        invoke ExitProcess, 0
    
        ret
    WinMainCRTStartup	endp
    
    end
    

    프로시저 내부에서 local 지시어를 사용함으로써, 로컬 변수를 사용할 수가 있다. 위를 예에서 알수 있듯이, 로컬 변수의 선언은,

    local  name[] :size

    와 같이 한다. 위의 예에서는 배열을 지정했다. " 인스턴스 핸들: 0x400000"라는 결과가 나오게 된다.

    Chapter 3 - 5   윈도우 생성

    이제부터는, 실제로 윈도우를 생성해 본다. 프로그램의 내용이 좀 길지만 천천히 살펴보자.

    ;******************************************************************************
    ; 어셈블러 윈도우 프로그래밍 windows.asm
    ;******************************************************************************
    
    .586
    .model flat, stdcall
    option casemap:none
    
    ;******************************************************************************
    ; 정의
    ;******************************************************************************
    
    NULL              =       0
    WM_DESTROY        =       2h
    IDI_APPLICATION   =       32512
    IDC_ARROW         =       32512
    WHITE_BRUSH       =       0
    WS_OVERLAPPED     =       00000000h
    WS_SYSMENU        =       00080000h
    WS_CAPTION        =       00C00000h
    WS_EX_APPWINDOW   =       00040000h
    SW_SHOWNORMAL     =       1
    CW_USEDEFAULT     =       80000000h
    
    HANDLE  typedef     dword
    LPSTR   typedef     dword
    
    POINT        struct
        x               dword   ?
        y               dword   ?
    POINT        ends
    
    WNDCLASS     struct
        style           dword   ?
        lpfnWndProc     dword   ?
        cbClsExtra      dword   ?
        cbWndExtra      dword   ?
        hInstance       HANDLE  ?
        hIcon           HANDLE  ?
        hCursor         HANDLE  ?
        hbrBackground   HANDLE  ?
        lpszMenuName    LPSTR   ?
        lpszClassName   LPSTR   ?
    WNDCLASS     ends
    
    MSG          struct
        hWnd            HANDLE  ?
        message         dword   ?
        wParam          dword   ?
        lParam          dword   ?
        time            dword   ?
        point           POINT   <?, ? >
    MSG          ends
    
    ;******************************************************************************
    ; 프로시저 정의
    ;******************************************************************************
    
    CreateWindowExA     proto   dwExStyle   :dword,
                                pClassName  :LPSTR,
                                pWindowName :LPSTR,
                                dwStyle     :dword,
                                x           :dword,
                                y           :dword,
                                nWidth      :dword,
                                nHeight     :dword,
                                hWndParent  :HANDLE,
                                hMenu       :HANDLE,
                                hInstance   :HANDLE,
                                pParam      :dword
    RegisterClassA      proto   pWndClass   :dword
    GetStockObject      proto   nObject     :dword
    LoadIconA           proto   hInstance   :HANDLE,
                                lpIconName  :LPSTR
    LoadCursorA         proto   hInstance   :HANDLE,
                                lpCurName   :LPSTR
    GetMessageA         proto   lpMsg       :dword,
                                hWnd        :HANDLE,
                                nMsgFilMin  :dword,
                                nMsgFilMax  :dword
    TranslateMessage    proto   lpMsg       :dword
    DispatchMessageA    proto   lpMsg       :dword
    PostQuitMessage     proto   nExitCode   :dword
    DefWindowProcA      proto   hWnd        :HANDLE,
                                Msg         :dword,
                                wParam      :dword,
                                lParam      :dword
    GetModuleHandleA    proto   lpstr       :LPSTR
    ShowWindow          proto   hWnd        :HANDLE,
                                nCmdShow    :dword
    UpdateWindow        proto   hWnd        :HANDLE
    ExitProcess         proto   nExitCode   :dword
    
    ;*****************사용자 정의***********************
    InitInstance        proto   hInstance   :HANDLE
    CreateMyWindow      proto   hInstance   :HANDLE
    
    ;******************************************************************************
    ; 데이터 세그먼트
    ;******************************************************************************
        .data
    
    ClassName   db  'FirstWnd', 0
    WindowName  db  'First Application', 0
    
    ;******************************************************************************
    ; code segment
    ;******************************************************************************
        .code
    
    ;******************************************************************************
    ; 메인 함수
    ;******************************************************************************
    WinMainCRTStartup    proc
        ;*************************
        ; 로컬 변수
        ;*************************
        local    msg     :MSG
        local    hWnd    :HANDLE
        local    hInst   :HANDLE
    
        ;*************************
        ; 프로그램
        ;*************************
        invoke GetModuleHandleA, NULL
        mov     hInst, eax
    
        invoke InitInstance, hInst
        cmp     eax, NULL
        je      ExitProg
    
        invoke CreateMyWindow, hInst
        cmp     eax, NULL
        je      ExitProg
        mov     hWnd, eax
    
        invoke ShowWindow, hWnd, SW_SHOWNORMAL
        invoke UpdateWindow, hWnd
    
    MSGLOOP:
        invoke GetMessageA, addr msg, NULL, 0, 0
        or      eax, eax
        je      ExitProg
    
        invoke TranslateMessage, addr msg
        invoke DispatchMessageA, addr msg
        jmp     MSGLOOP
    
    ExitProg:
        invoke ExitProcess, msg.wParam
    
        ret
    WinMainCRTStartup    endp
    
    ;******************************************************************************
    ; 윈도우 클래스의 등록
    ;******************************************************************************
    InitInstance    proc    hInstance:HANDLE
        ;*************************
        ; 로컬 변수
        ;*************************
        local    wc    :WNDCLASS
    
        ;*************************
        ; 프로그램
        ;*************************
    
        mov     wc.style, 0
        mov     wc.lpfnWndProc, offset MainWndProc
        mov     wc.cbClsExtra, 0
        mov     wc.cbWndExtra, 0
    
        mov     eax, hInstance
        mov     wc.hInstance, eax
    	
        invoke LoadIconA, NULL, IDI_APPLICATION
        mov     wc.hIcon, eax
    
        invoke LoadCursorA, NULL, IDC_ARROW
        mov     wc.hCursor, eax
    
        invoke GetStockObject, WHITE_BRUSH
        mov     wc.hbrBackground, eax
    
        mov     wc.lpszMenuName, NULL
        mov     wc.lpszClassName, offset ClassName
    
        invoke RegisterClassA, addr wc
    
        ret
    InitInstance    endp
    
    ;******************************************************************************
    ; 윈도우의 작성
    ;******************************************************************************
    CreateMyWindow    proc    hInstance:HANDLE
        ;*************************
        ; 프로그램
        ;*************************
    
        invoke CreateWindowExA, WS_EX_APPWINDOW, offset ClassName,
            offset WindowName, WS_SYSMENU or WS_CAPTION, CW_USEDEFAULT,
            CW_USEDEFAULT, 300, 400, NULL, NULL, hInstance, NULL
    
        ret
    CreateMyWindow    endp
    
    ;******************************************************************************
    ; 윈도우 프로시저
    ;******************************************************************************
    MainWndProc    proc    hWnd:HANDLE, Msg:dword, wParam:dword, lParam:dword
    
        .if Msg == WM_DESTROY
            invoke PostQuitMessage, 0
        .else
            invoke DefWindowProcA, hWnd, Msg, wParam, lParam
        .endif
    
        ret
    MainWndProc    endp
    
    end
    

    VC에서 실행(F5)하게 되면 "Line to long"이라고 에러가 나올것이다. 강좌에서는 편의상 여러줄로 했지만,

    CreateWindowExA proto dwExStyle :dword ~ 이부분을 모두 한줄에 빈칸을 줄여서 작성해줘야한다.

    프로그램을 보면 알 수 있듯이, Windows 프로그래밍은 코딩량이 많은것을 알 수가 있다. MASM32에는, 필요한 함수나 구조체의 정의를 정리한 파일을 제공해 주고있다. 그것을 사용하는것이 여러모로 편리할 것이다.

    위의 프로그램에서는, 편의상 데이터형으로 핸들(HANDLE)과 문자열(LPSTR)과 정수로 나누고 있다.

    MASM로 Windows 프로그래밍을 해 본 솔직한 의견으로는 MASM으로 Windows 프로그래밍을 할 정도라면, C 로하는 편이 훨씬 편하다. C와 비교해서 MASM으로 작성했다고 해서 특별히 빨라지는것도 아니기 때문에, 필요한 부분만 MASM 로 작성하는것이 주된 목적일 것이다. 좀 더 심하게 말해서 C의 인라인 어셈블러를 사용하는 것이 압도적으로 좋다.-_-;

     


    Chapter 4   리소스의 이용

    개요 : 리소스를 사용하는 경우

    Chapter 4 - 1   리소스의 사용법

    이번에는 리소스에 대해 설명한다. C언어에서 Windows 리소스를 사용하는것과 별다른 차이점은 없다.

    그런데, 리소스에는 2 종류가 있다. 내부 리소스/외부 리소스라는 것이다.(필자의 사견이다)

    내부 리소스는, 실행 파일에 포함되어있는 리소스이다. 실행 파일에 포함해 두면, 리소스 로딩을 빠르게 할 수가있다. 다만, 리소스가 실행 파일에 포함되므로, 실행파일의 크기가 커지게 되는 현상이 생긴다. 특히 비트맵과같은 것은 사이즈가 크므로 조심하기 바란다.

    외부 리소스는, 문자 그대로 실행 파일에 포함되지 않는 리소스를 의미한다. 그러므로 실행중에 다른 파일로부터 로드하거나 스스로 만들어주지 않으면 사용할 수가 없다. 그런 만큼, 로딩하는 시간이 소모되게 된다.하지만, 메모리의 효율성에서는 더 좋다.

     

    Chapter 4 - 2   인클루드 파일

    상기의 예에서와 같이, 함수를 하나하나식 정의해서 사용한다면 매우 번거롭게 된다. 그래서 MASM32에 포함되어있는 함수와 정수를 정의한 인클루드 파일을 사용하게 된다. 다음의 4개의 파일을 기억해 두면 편리하다.

    1. windows.inc 윈도우관련정수가 정의되어 있는 인클루드 파일
    2. kernel32.inc 커널함수(코어 함수)가 정의되어 있는 파일
    3. user32.inc 유저함수(유저 인터페이스 함수)가 정의되어 있는 파일
    4. gdi32.inc GDI 함수(그래픽 디바이스 인터페이스 함수)가 정의되어 있는 파일

    Chapter 4 - 3   아이콘과 다이얼로그 박스를 사용한 예

    실제로 프로그램을 보자. 이번 예에서 사용하는 리소스는 다이얼로그 박스와 아이콘이다.

     

    ;******************************************************************************
    ; 리소스 사용 프로그램 resource.asm
    ;******************************************************************************
    
    .486
    .model flat, stdcall
    option casemap:none
    
    include <windows.inc>
    include <kernel32.inc>
    include <user32.inc>
    include <gdi32.inc>
    
    include <resource.inc>  ;리소스를 사용하므로 반드시 선언
    
    ;******************************************************************************
    ; prototype
    ;******************************************************************************
    
    DlgProc             proto   hWnd    :HWND, 
                                Msg     :UINT, 
                                wParam  :WPARAM, 
                                lParam  :LPARAM
    WinMainCRTStartup   proto
    OnCommand           proto   hWnd    :HWND,
                                wParam  :WPARAM
    
    ;******************************************************************************
    ;  데이터 / 변수
    ;******************************************************************************
    
    .data
    MSG1    db          "정상적으로 표시되었는가? ", 0
    TITLE1  db          "리소스 테스트", 0
    hInst   HINSTANCE   ?
    
    ;******************************************************************************
    ;  코드
    ;******************************************************************************
    
    .code
    ;------------------------------------------------------------------------------
    ;  다이얼로그 프로시저
    ;------------------------------------------------------------------------------
    DlgProc proc \
        hWnd    : HWND,
        Msg     : UINT,
        wParam  : WPARAM,
        lParam  : LPARAM
    
        .if Msg == WM_INITDIALOG
            invoke LoadIconA, hInst, IDI_MAINICON
            invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax
        .elseif Msg == WM_CLOSE
            invoke EndDialog, hWnd, 0
        .elseif Msg == WM_COMMAND
            invoke OnCommand, hWnd, wParam
        .else
            mov     eax, FALSE
            ret
        .endif
    
        mov     eax, TRUE
        ret
    DlgProc endp
    
    ;------------------------------------------------------------------------------
    ;  커맨드 처리
    ;------------------------------------------------------------------------------
    OnCommand proc \
        hWnd    : HWND,
        wParam  : WPARAM
    
        mov     eax, wParam
        and     eax, 0ffffh
    
        .if eax == IDOK
            invoke SendMessage, hWnd, WM_CLOSE, 0, 0
        .elseif eax == IDCANCEL
            invoke MessageBox, hWnd, addr MSG1, addr TITLE1, MB_OK
        .endif
    
        ret
    OnCommand endp
    
    ;------------------------------------------------------------------------------
    ;  메인 프로시저
    ;------------------------------------------------------------------------------
    WinMainCRTStartup proc
        invoke GetModuleHandle, NULL
        mov     hInst, eax
        invoke DialogBoxParamA, hInst, IDD_TEST, NULL, addr DlgProc, NULL
        invoke ExitProcess, 0
        ret
    WinMainCRTStartup endp
    
    ;------------------------------------------------------------------------------
    ;  End of File
    ;------------------------------------------------------------------------------
    end
    

    위의 프로그램을 어셈블(assemble) 해서 실행해도 다이얼로그 박스는 표시되지 않는다. 왜냐하면, 아직 리소스를 만들지 않았기 때문이다. 다이얼로그 박스를 표시 하기 위해서는, 리소스로서 다이얼로그 박스를 만들어 주어야 한다. 아이콘을 사용한다면, 아이콘 리소스도 필요하다. 만든 리소스를 리소스 컴파일러로 컴파일 해서 실행 파일에 함께 링크 해주어야 리소스를 사용하는 것이 가능해진다. 다음과 같은 리소스를 만들어 보자.

    먼저, 다이얼알로그 박스이다.

    VC++로 작성하는 경우에는, 다음과 같이 프로퍼티를 설정한다.

    프롭퍼티 내용
    ID IDD_TEST = 101
    Style Overlapped
    Border Thin
    More Style 「Center」을 체크

    다음은, 아이콘이다, 하지만 이것은 마음대로 만들어도 좋다. 이 예제에서는, 다음과 같은 아이콘을 준비했다.

    생성한 후 ID는 다음과 같이 지정한다.

    ID IDI_MAINICON = 102

    이제는, resource.inc 파일을 만든다. 이 파일에는 리소스의 ID를 저장해 둔다.

    ;******************************************************************************
    ; 리소스 ID정의 
    ;****************************************************************************** 
    IDD_TEST     = 101 
    IDI_MAINICON = 102
    

    마쳤다면, 프로젝트를 빌드 하고 실행해준다. 이제는 화면에 다이얼로그박스가 표시되었을 것이다.


    Chapter 5   Tips

    개요 : 약간의 Tips

    Chapter 5 - 1   인클루드 파일의 종류

    지금까지, 대략이나마, MASM로 윈도우 프로그래밍을 접해 보았다.

    이번은 조금 다른 얘기로 화제를 돌려보겠다. MASM 는 어셈블러이니까, 어찌보면 어려울 것 같이 생각될 수도 있지만, 실제론 전혀 어렵지는 않다. 오히려 C 를 했던 적이 있는 사람이라면, 아주 쉽게 이해 할 수가 있을 것이다.

    MASM로 Windows 프로그래밍을 하는 경우, 함수를 반드시 정의하지 않으면 안된다. 그 함수를 매번 정의해서 사용하는 편보다는, 미리 헤더 파일에 모아 두고 사용하는 편이 더 좋을 것이다.

    하지만 프로그래머가 직접 만들어서 사용해도 되지만, MASM32 헤더 파일을 사용하는것이 더편리하다.핵심적인 헤더파일을 설명하겠다.

    코어 함수군
    Include File 해설
    windows.inc Windows 프로그래밍에서 사용하는 대부분의 정수의 정의.
    kernel32.inc 커널(코어) 함수. 프로세스 관리, 파일 조작등의 함수가 정의.
    user32.inc 유저 함수. 유저 인터페이스 함수의 정의.
    gdi32.inc GDI 함수. 그래픽 디바이스 인터페이스 함수의 정의.
    advapi32.inc 확장 함수. 암호화, 시큐리티, 레지스트리 조작등의 함수의 정의.
    서브 함수군
    Include File 해설
    comctl32.inc 커먼 컨트롤
    comdlg32.inc common dialog
    d3drm.inc DirectX 3D Retained Mode
    ddraw.inc DirectX DirectDraw
    dinput.inc DirectX DirectInput
    dplayx.inc DirectX DirectPlay
    dsetup.inc DirectX DirectSetup
    dsound.inc DirectX DirectSound
    glaux.inc OpenGL
    glu32.inc OpenGL
    imm32.inc IME API
    lsapi32.inc LS API
    lz32.inc LZ 압축
    netapi32.inc NetAPI
    odbc32.inc ODBC API
    opengl32.inc OpenGL
    scrnsavew.inc Screen Saver API
    setupapi.inc Setup API
    shell32.inc Shell API
    tapi32.inc Telephony API
    wininet.inc Internet API
    winmm.inc Multi Media API
    winspool.inc Printer API
    wsock32.inc WinSock API

    좀 더 있지만, 이 정도로 만족하기 바란다.

    Chapter 5 - 2   A와 W의 의미와 함수 호출 규약

    함수명의 끝에 A 나 W 가 붙어 있는데 이것의 의미는 무엇일까?

    실제로 문자열을 Unicode로 프로그래밍 하는 경우, W가 붙어 있는 함수를 사용한다. A가 붙어 있는 함수는, 문자열을 ASCII 코드로 프로그래밍 한다고 가정하는 것이다.

    W 는 Wide 의 W이며 , A 는 Ascii (혹은 ANSI )의 A 이다.(맞겠지?)

    MASM로 Windows 프로그래밍 하는 경우, 호출 규약에는 C 호출 규약과 STDCALL 의 호출 규약을 이용한다. 약간의 차이점이 존재한다.

    VC++의 경우, 함수는 C 규약으로 통일되어 있다. 함수 수식자로서 __cdecl가 자동적으로 추가된다. API 에서는, __stdcall 로 정의되고 있다.(언더바가 2개이다)

    실제로 WINAPI 나 CALLBACK 라고 하는 수식자는 __stdcall 이라고 정의되어 있다. API 로부터 호출되는 것을 생각하면 당연하다.

    C 호출 규약과 STDCALL 호출 규약의 차이는, 스택을 오픈하는 추최가 누구인가에 따라 다르다. C는 호출한 측에서 스택을 오픈하고, STDCALL 은 함수의 내부에서 스택을 오픈한다. 다음은 C 와 STDCALL 의 호출 규약의 요약이다.

    처리 C
    (__cdecl)
    STDCALL
    (__stdcall)
    함수명의 선두에 _ 를 추가
    인수를 역순으로 스택에 푸쉬
    인수 스택을 호출한 측에서 오픈 ×
    인수 스택을 호출된 측에서 오픈 ×
    VARARG 사용가능

    자세한 부분은 MASM의 도움말을 참조하기 바란다.

    Chapter 5 - 3   프로시저의 prototype 선언

    함수(프로시저)의 호출을 수행할 경우, invoke 지시어를 사용하면 편하다.하지만 미리 prototype 선언을 수행해 주어야 한다. 또한 invoke 지시어는, 명시적으로 호출규약을 지정해 주어야만 사용할 수가 있다.

    invoke 지시어는, 매크로와 비슷한 것으로, invoke를 사용하는 편이 함수호출이 더 수월해진다. 스택 처리를 자동화 해주기 때문이다.

    또한, addr 와 offset 는 모두 변수 혹은 정수의 주소를 반환한다.차이점으로는,

    offset 지시어만으로 프로그래밍할려면, 상당한 힘든경우가 있다. offset지시어는, 실행시의 스택주소를 얻을수 없기때문이다.이름 그대로, offset 은 식별자의 오프셋(offset)주소를 구해준다.

    addr를 사용함으로써 , 실행시의 스택변수의 주소를 얻을 수가 있다.

    Chapter 5 - 4   NULL와'\0'

    한가지 주의할 것이, NULL = 0 혹은, '\0' = 0 아니다. 확실히, 헤더파일을 보면, NULL = 0 으로 정의되어 있다. 그러나 그것은 편의상 정의된것이므로, 의미적으로는 다르다. 혹시 다른 개발환경에는 NULL = -1 라고 정의되어 있을지도 모른다.

    포인터나 핸들등이 정해지지 않았을 경우에는, 반드시 NULL 을 사용하도록 한다. Windows 로부터 구해지는 모든 핸들은, 실제로 (void *) 형태이므로 NULL을 사용하는 이유를 알 수 있을 것이다.

    또다시 , C 언어 이야기로 되었지만, '\0'는 문자열의 끝을 나타내는 기호상수이므로, 문자열 외에서 사용해서는 안된다. NULL도 같이 맥락으로 정해진 용도 이외에는 사용하지 않는 것이 좋다.

     

    Chapter 5 - 5   ExitProcess

    Windows 프로그램을 종료할 경우에는 반드시, ExitProcess를 호출하도록 하자. 물론, 사용하지 않아도 프로그램을 종료할 수 있다.

    하지만, DLL을 호출했을 경우, ExitProcess를 사용하지 않고 프로세스를 종료하면, 프로세스에서 로딩된 DLL이 메모리에 남게된다. ExitProcess를 호출하므로써, DLL에 프로그램이 종료하는 것을 전달되고, DLL도 메모리에서 해제된다.

     


    Chapter 6   텍스트뷰어

    개요 : 텍스트뷰어 작성

    Chapter 6 - 1   텍스트뷰어 프로그램

    지금까지의 단편된 지식을 이용해서, 이번에는 좀 더 실용적인(??) 프로그램을 만들어 보자. 실용적이라지만 단순히 텍스트 파일을 열고 화면에 내용을 표시하는 단순한 프로그램이다. API등의 지식은 MSDN이나 기타참조를 하기 바란다.

    만드는 방법은 아주 간단하다. 윈도우의 작업영역에 에디트박스를 생성하면 끝이다.

    실제로, WM_CREATE 메세지에서,작업영역에 에디트박스를 생성해주면 된다. 그림으로 표시하면 다음과 같다.

    파일을 여는 기능만 수행하여, 파일의 내용을 볼수 있도록만 한다.

    프로그램은 다음과 같다. → 프로젝트(VC++용)

     ; --------------------------------------------------------------------
     ;
     ;  Text Viewer main.asm
     ;
     ; --------------------------------------------------------------------
    .586
    .model flat, stdcall
    option casemap:none
    
    include <windows.inc>
    include <kernel32.inc>
    include <user32.inc>
    include <gdi32.inc>
    include <comdlg32.inc>
    
    include <resource.inc>
    
     ; --------------------------------------------------------------------
     ;
     ;  Procedure Definitions
     ;
     ; --------------------------------------------------------------------
    
    InitInstance        proto   hInstance   :HINSTANCE
    CreateMyWindow      proto   hInstance   :HINSTANCE
    OnCreate            proto   hWnd        :HWND
    OnSize              proto   hWnd        :HWND
    OnCommand           proto   hWnd        :HWND, 
                                id          :UINT
    OnFileOpen          proto   hWnd        :HWND
    
    
     ; --------------------------------------------------------------------
     ;
     ;  Data
     ;
     ; --------------------------------------------------------------------
    
    .data
    
    CLASSNAME   DB  'FirstWnd', 0
    TCLASSNAME  DB  'EDIT', 0
    APPNAME     DB  '텍스트뷰어', 0
    FILTER      DB  '텍스트 파일 (*. txt)', 0, '*. txt', 0
                DB  '워드 파일 (*. doc)', 0, '*. doc', 0, 0
    
    
    g_hEditWnd  HWND    NULL
    g_szFile    DB  260 DUP (0)
    
     
     ; --------------------------------------------------------------------
     ;
     ;  Code
     ;
     ; --------------------------------------------------------------------
    
    .code
    
     ;******************************************************************************
     ; 메인 함수
     ;******************************************************************************
    
    WinMainCRTStartup proc
        local    msg     :MSG
        local    hWnd    :HWND
        local    hInst   :HINSTANCE
    
        invoke GetModuleHandleA, NULL
        mov     hInst, eax
    
        invoke InitInstance, hInst
        cmp     eax, NULL
        je      _Exit
    
        invoke CreateMyWindow, hInst
        cmp     eax, NULL
        je      _Exit
        mov     hWnd, eax
    
        invoke ShowWindow, hWnd, SW_SHOWNORMAL
        invoke UpdateWindow, hWnd
    
     _MsgLoop:
        invoke GetMessageA, addr msg, NULL, 0, 0
        or      eax, eax
        je      _Exit
    
        invoke TranslateMessage, addr msg
        invoke DispatchMessageA, addr msg
        jmp     _MsgLoop
    
     _Exit:
        invoke ExitProcess, msg.wParam
        ret
    WinMainCRTStartup endp
    
     ;******************************************************************************
     ; 윈도우 클래스의 등록 
     ;
     ;   ATOM InitInstance hInstance:HISNTANCE
     ;******************************************************************************
    
    InitInstance proc \
        hInstance       :HINSTANCE
        local   wc      :WNDCLASS
    
        mov     wc.style, 0
        mov     wc.lpfnWndProc, offset MainWndProc
        mov     wc.cbClsExtra, 0
        mov     wc.cbWndExtra, 0
    
        mov     eax, hInstance
        mov     wc.hInstance, eax
    	
        invoke LoadIconA, hInstance, IDI_MAIN_ICON
        mov     wc.hIcon, eax
    
        invoke LoadCursorA, NULL, IDC_ARROW
        mov     wc.hCursor, eax
    
        invoke GetStockObject, WHITE_BRUSH
        mov     wc.hbrBackground, eax
    
        mov     wc.lpszMenuName, IDR_MAIN_MENU
        mov     wc.lpszClassName, offset CLASSNAME
    
        invoke RegisterClassA, addr wc
    
        ret
    InitInstance endp
    
     ;******************************************************************************
     ; 윈도우의 작성
     ;
     ;   HWND CreateMyWindow hInstance:HINSTANCE
     ;******************************************************************************
    
    CreateMyWindow proc \
        hInstance   :HINSTANCE
    
        invoke CreateWindowExA,
            WS_EX_APPWINDOW, 
            offset CLASSNAME,
            offset APPNAME, 
            WS_OVERLAPPEDWINDOW, 
            CW_USEDEFAULT,
            CW_USEDEFAULT, 
            CW_USEDEFAULT, 
            CW_USEDEFAULT, 
            NULL, 
            NULL, 
            hInstance, 
            NULL
    
        ret
    CreateMyWindow endp
    
     ;******************************************************************************
     ; 윈도우 프로시저
     ;   
     ;   LRESULT MainWndProc hWnd:HWND, Msg:DWORD, wParam:DWORD, lParam:DWORD
     ;******************************************************************************
    
    MainWndProc proc \
        hWnd    :HWND, 
        Msg     :DWORD, 
        wParam  :DWORD, 
        lParam  :DWORD
    
        .if Msg == WM_DESTROY
            invoke PostQuitMessage, 0
        .elseif Msg == WM_CREATE
            invoke OnCreate, hWnd
        .elseif Msg == WM_SIZE
            invoke OnSize, hWnd
        .elseif Msg == WM_COMMAND
            mov     eax, wParam
            and     eax, 0000FFFFh
            invoke OnCommand, hWnd, eax
        .else
            invoke DefWindowProcA, hWnd, Msg, wParam, lParam
            ret
        .endif
    
        mov     eax, 0
        ret
    MainWndProc endp
    
     ;******************************************************************************
     ; WM_CREATE 메세지의 처리
     ;
     ;   BOOL OnCreate hWnd : HWND
     ;******************************************************************************
    
    OnCreate proc \
        hParentWnd      :HWND
        local   hInst   :HINSTANCE
        local   rc      :RECT
    
        invoke GetModuleHandle, NULL
        mov     hInst, eax
    
        invoke GetClientRect, hParentWnd, addr rc
    
        invoke CreateWindowExA,
            WS_EX_CLIENTEDGE, 
            offset TCLASSNAME,
            NULL, 
            WS_VISIBLE or WS_CHILD or ES_MULTILINE or ES_AUTOHSCROLL \
            or ES_AUTOVSCROLL or WS_HSCROLL or WS_VSCROLL, 
            0,
            0, 
            rc.right, 
            rc.bottom, 
            hParentWnd, 
            NULL, 
            hInst, 
            NULL
        mov     g_hEditWnd, eax
    
        invoke SendMessage, g_hEditWnd, EM_SETLIMITTEXT, 0FFFFh, 0
    
        mov     eax, TRUE
        ret
    OnCreate endp
    
     ;******************************************************************************
     ; WM_SIZE 메세지의 처리
     ;
     ;   void OnSize hWnd :HWND
     ;******************************************************************************
    
    OnSize proc \
        hWnd        :HWND
        local   rc  :RECT
    
        invoke GetClientRect, hWnd, addr rc
    
        invoke SetWindowPos, g_hEditWnd, NULL, 0, 0, 
            rc.right, rc.bottom, SWP_NOMOVE or SWP_NOZORDER
        ret
    OnSize endp
    
     ;******************************************************************************
     ; WM_COMMAND 메세지의 처리
     ;
     ;   void OnCommand hWnd:HWND, id:UINT
     ;******************************************************************************
    
    OnCommand proc \
        hWnd    :HWND,
        id      :UINT
        
        .if id == IDM_EXIT
            invoke SendMessage, hWnd, WM_CLOSE, 0, 0
        .elseif id == IDM_FOPEN
            invoke OnFileOpen, hWnd
        .endif
    
        ret
    OnCommand endp
    
     ;******************************************************************************
     ; IDM_FOPEN 메뉴 메세지의 처리
     ;
     ;   void OnFileOpen hWnd:HWND
     ;******************************************************************************
    
    OnFileOpen proc \
        hWnd            :HWND
        local   ofn     :OPENFILENAME
        local   hFile   :HANDLE
        local   pBuffer :DWORD
        local   dwRead  :DWORD
    
        ;  파일 오픈 다이얼로그 표시
        invoke RtlZeroMemory, addr ofn, sizeof OPENFILENAME
    
        mov     ofn.lStructSize, sizeof OPENFILENAME
        mov     eax, hWnd
        mov     ofn.hWndOwner, eax
        mov     ofn.lpstrFile, offset g_szFile
        mov     ofn.nMaxFile, 260
        mov     ofn.lpstrFilter, offset FILTER
        mov     ofn.nFilterIndex, 1
        mov     ofn.lpstrFileTitle, NULL
        mov     ofn.nMaxFileTitle, 0
        mov     ofn.lpstrInitialDir, NULL
        mov     ofn.Flags, OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST \
                or OFN_HIDEREADONLY
    
        invoke GetOpenFileName, addr ofn
    
        or      eax, eax
        je      _OnFileExit
    
        ; 파일을 연다
        invoke CreateFile, ofn.lpstrFile, GENERIC_READ, 0, NULL, 
            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
        mov     hFile, eax
    
        ; 데이터용 버퍼의 확보
        invoke GlobalAlloc, GPTR, 0FFFFh
        mov     pBuffer, eax
    
        ; 데이터의 해석
        invoke ReadFile, hFile, pBuffer, 0FFFFh, addr dwRead, NULL
    
        ; 데이터의 기록
        invoke SetWindowText, g_hEditWnd, pBuffer
    
        ; 버퍼를 삭제
        invoke GlobalFree, pBuffer
    
        ; 파일 핸들을 닫는다
        invoke CloseHandle, hFile
    
     _OnFileExit:
        ret
    OnFileOpen endp
    
    end
    

    파일을 선택하는 기능은, common dialog의 파일 오픈 다이얼로그를 사용하고 있다. 이것을 사용하는 것으로, 아주 쉽게 파일선택을 할 수가 있다. 자세한 내용은 프로그램을 참조하기 바란다.

    빌드 할 때의 주의 사항으로는, common dialog 박스를 사용하므로, common dialog 박스를 사용하기 위해서는, common dialog 박스의 동적 링크 라이브러리(DLL)의 라이브러리를 링크 해주어야 한다. (commlib.lib ?부분 참조 )


    Chapter 7   FPU로 수치 연산

    개요 : 부동 소수점 연산에 대해

    Chapter 7 - 1   부동 소수점 연산에 대해

    FPU를(Float Point Unit) 이용한 부동 소수점 연산에 대해, 설명을 한다. 통합 환경으로는 VC++ 를 사용하지만,굳이 VC++가 아니여도 사용할 수 있다. 실제 계산 인스트럭션에 대해서도, 본 설명서에서 자세히 설명하지 않는다. 인텔 메뉴얼을 참조하기 바란다.

    이글을 보고 있는 사람은, FPU의 기능을 모르는 사람은 없다는 가정이다. FPU는, 부동 소수점 연산용을 하기 위한 전용 유닛이다. 80386 CPU 까지는 옵션사항이 였지만, 80486부터는 표준으로 CPU내부에 장착되어 있다.

    (80486SX는 예외)

    FPU에는, 몇개의 레지스터와 전용 인스트럭션이 존재한다. 계산용 레지스터로 8개의 스택타입의 레지스터가 장착되어있다. 계산 인스트럭션에는, 가감승제, 초월함수, 삼각함수 등 수치 계산에 필요한 기능은 대부분 제공되어 있다.

    Chapter 7 - 2   FPU용 레지스터

    부동 소수점 계산용의 레지스터로 8개의 레지스터가 준비되어 있다. 하지만, 왜 스택형식의 레지스터일까?

    실제로 계산을 하려면 스택을 사용하는 것이 여러모로 편하다. 이것은 알고리즘책등에서 스택을 이용해 계산하는것을 본사람도 있을것이다.

    실제로 역폴랜드 기법의 계산법으로 프로그래밍 할것이다. 그러므로 레지스터가 몇개 있는지를 의식하지 않아도, 프로그래밍을 해 나갈 수가 있다. (물론 8개를 넘겨서 프로그램을 작성하는 경우에는, 문제가 있다) 이런 방식은 , 스택에 독립적으로 데이터를 읽어들이고,, 역폴랜드식의 계산방법을 무시하고 계산하면, FPU를 사용하는 것기능에도 불구하고 늦어지는 일이 발생할 수 있으므로 주의가 필요하다.

    계산시에 특정스택을 다루고 싶은 경우는, st(n)라는 표기법을 사용한다. n 에는, 현재스택으로부터 n 번째의 스택을 지정한다. 예를 들면, 현재 스택 탑으로부터 한단계전의 스택내용을 참조하고 싶은 경우에는, st(1)로 사용한다. 2개전이라면 st(2)로 한다.

    다만, 미사용영역의 스택에 액세스 하게되면 에러가 된다.

    Chapter 7 - 3   프로그래밍의 방법

    실제로, 아래와 같은 계산을 수행해보자.

    ( 5.98 + 34.89 ) * (34.89 - 23.0)
    

    이 계산을 실제로 부동 소수점 인스트럭션을 이용해서 프로그래밍하면, 다음과 같다.

    ; _a = 5.98, _b = 34.89, _c = 23.0 으로 한다
    
    fld     _a  ; ①FPU스택에 _a 의 값을 푸쉬
    fadd    _b  ; ②현재스택의 값과 _b 의 값을 더한다
    
    fld     _b  ; ③FPU스택에 _b 의 값을 푸쉬
    fsub    _c  ; ④현재스택의 값으로부터 _c 의 값을 뺀다
    
    fmul        ; ⑤현재스택과 한단계전의 스택값을 곱해서 한단계전의 스택에 푸쉬
    

    이런 계산을 할 경우, 스택에 실제로 어떻게 데이터가 싸이는지 알아보자.스택은 다음과 같이 운용된다.

    Chapter 7 - 4   실제 프로그래밍

    그렇다면, 실제로 프로그램을 작성해보자. 수치 연산의 부분을 MASM로 작성하고, C언어로 그 연산을 호출하는 형태로 작성한다. 먼저 C언어의 프로그램은 다음과 같다.

    #include <stdio.h>
    
    #ifdef __cplusplus
    extern "C" double func(double a, double b, double c);
    #endif
    
    int main()
    {
        printf("func() = %f\n", func(5.98, 34.89, 23.0));
    
        return 0;
    }

    func라고 하는 함수는 MASM로 작성한다. func함수의 내용은 다음과 같다.

    .586
    .model flat, c
    
    .code
    
    func proc \
        _a: real8,
        _b: real8,
        _c: real8
    
        fld     _a
        fadd    _b
        fld     _b
        fsub    _c
        fmul
    
        ret
    func endp
    
    end
    

    함수의 반환값은, 스택의 탑에 넣어 둔다.

    예를 들어서 다음과 같은 수식을 프로그래밍해보자.

    (1) a * (b + c)
    (2) |a + b| * sin(c)
    (3) a * √(a + b) * cos(c)

    각각 다음과 같이 된다.

    ; (1) a * (b + c)
    
    fld     a
    fld     b
    fadd    c
    fmul
    
    ; (2) |a + b| * sin(c)
    
    fld     a
    fadd    b
    fabs
    fld     c
    fsin
    fmul
    
    ; (3) a * √(a + b) * cos(c)
    
    fld     a
    fld     a
    fadd    b
    fsqrt
    fld     c
    fcos
    fmul
    fmul

    부동 소수점 계산은 이런식이다. 크게 어렵지 않다. 부동 소수점 연산의 에러 처리는, 인텔 메뉴얼을 참조하기 바란다.


    Chapter 8   MMX의 SIMD 연산

    개요 : MMX를 사용

    Chapter 8 - 1   MMX 연산의 개요

    ■MMX란?

    먼저 MMX 명령에 대해 설명한다.

    MMX란, 단순한 데이터 요소로부터 대규모 배열에 대한 반복 연산을 수행한다. 용도로는, 동영상, 그래픽, 비디오의 결합, 화상 처리, 오디오 합성, 음성 합성, 압축등이 있다.

    말이 어려울 것이다. 실제로 샘플 프로그램을 VC++로 작성해서 알아보자.

    ■MMX 인스트럭션의 개요

    MMX 인스트럭션은, SIMD (Single Instruction , Multiple - Data) 실행 모델이라는 형태를 취한다. 무슨 말인가하면, 하나의 인스트럭션으로 복수의 데이터를 한번에 계산한다는 것이다.

    배열 char A[8] 과 char B[8] 라는 배열이 있었다고 가정하자.배열 한개의 요소를 MMX 인스트럭션을 사용하면 한번에 계산 할 수가 있다. 그림으로 설명하면 다음과 같다.

    위의 예에서 알 수 있듯이 하나의 인스트럭션으로 한번에 병렬 연산을 수행하게 된다. 이런식으로 화상 처리나 오디오 합성, 음성 합성의 처리 능력을 크게 향상시킬 수가 있다.

    MMX는, 8 개의 64 비트 MMX 레지스터 (MM0, MM1, ... MM7)를 가지고 있다. MMX레지스터는, 8개의 FPU 레지스터를 공유해서 사용하므로, 부동 소수점 연산과 MMX 연산을 혼합해서 사용할 경우에는 주의가 필요하다.

    MMX 연산에는, 4가지의 데이터형을 계산할 때에 이용한다. Packed Byte/Packed Word/Packed Double Word/Quad Word이다. Packed Byte라는 것은, 64 비트 레지스터를 8비트씩 8개의 영역으로 나눈 형태의 데이터형이다. 그림으로 설명하면, 아래의 그림과 같다. 상자로 둘러싸여 있기 때문에, 팩(Packed)이라고 부른다.

    Chapter 8 - 2   MMX를 사용할 수 있는지 체크

    MMX를 사용하기 전에 MMX 인스트럭션을 사용할 수가 있는지 체크 할 필요가 있다.

    체크루틴에는 CPUID 인스트럭션을 사용한다. 구체적인 프로그램은 다음과 같다.(인라인어셈 사용)

    /* CPUID의 존재 체크 */
    
    BOOL IsCPUID()
    {
        DWORD dwPreEFlags, dwPostEFlags;
    
        _asm {
            /* EFlags 의 값획득 */
            pushfd
            pop     eax
            mov     dwPreEFlags, eax
    
            /* CPUID 인스트럭션의 존재체크 (ID 플래그 체크) */
            xor     eax, 00200000h
            push    eax
            popfd
    
            /* EFlags 의 값의 획득 */
            pushfd
            pop     eax
            mov     dwPostEFlags, eax
        }
    
        if(dwPreEFlags == dwPostEFlags)
            return FALSE;
        return TRUE;
    }
    
    /* MMX 가능여부 체크 */
    
    BOOL IsMMX()
    {
        DWORD dwRetEDX;
    
        /* CPUID 인스트럭션 체크 */
        if(! IsCPUID()) return FALSE;
    
        /* MMX 가능여부 체크 */
        _asm {
            mov     eax, 1
            cpuid
            and     edx, 00800000h
            mov     dwRetEDX, edx
        }
        if(dwRetEDX) return TRUE;
        return FALSE;
    }

    Chapter 8 - 3   포화 연산·비포화 연산

    MMX 연산에는 포화 연산과 비포화 연산이 있다. 포화 연산은, 연산으로 오버플로우/언더 플로우(underflow)가 발생했을 경우 그 최대값/최소값을 자동으로 대입해 주는 연산이다. 비포화의 경우는 그것을 수행하지 않는다.

    구체적인 예를 들겠다. Packed Byte에서 MMX 무부호 연산을 생각해 보자. 한개의 요소가 240 + 23 = 263 으로, 오버플로우가 발생 했을 경우, 자동으로 최대값을 대입해 준다. 즉 이 경우는, 255 가 대입된다. 다음의 그림을 보면 이해할 수 있을 것이다.

    덧붙이면 PADDUSB 라는 것은, 무부호 포화 가산 연산을 수행하는 MMX 인스트럭션이다. (Packed Add Unsigned with Satuation Byte)

    MMX 인스트럭션에는 다음과 같은 것이 있다. 간단히 설명한다.

    데이터 전송 인스트럭션
    MOVD 32 비트 데이터 전송 인스트럭션
    MOVQ 64 비트 데이터 전송 인스트럭션
    산술 가감(상태) 연산 인스트럭션
    PADDx / PSUBx 비포화 연산 인스트럭션 x = B(BYTE) / W(WORD) / D(DWORD)
    PADDSx / PSUBSx 부호 첨부 포화 연산 인스트럭션 x = B(BYTE) / W(WORD)
    PADDUSx / PSUBUSx 무부호 포화 연산 인스트럭션 x = B(BYTE) / W(WORD)
    곱셈 인스트럭션
    PMULHx / PMULLx 곱셈 인스트럭션 x = W(WORD) / D(DWORD)
    논리 연산 인스트럭션
    PAND / PANDN / POR / PXOR  
    쉬프트 명령
    PSLLx / PSRLx 좌/우논리 쉬프트
    PSRAx 오른쪽 산술 쉬프트
    변환 인스트럭션
    PACKSSxx 부호화 포화 팩화 xx = WB / DW
    PACKUSxx 무부호 포화 팩화 xx = WB
    PUNPCKHxx 상위 unpack화 xx = BW / WD / DQ
    PUNPCKLxx 하위 unpack화 xx = BW / WD / DQ
    제어
    EMMS MMX 의 종료

    마지막의 EMMS 인스트럭션은, MMX의 사용을 종료했을 때에 반드시 수행해야 한다. MMX는 FPU의 레지스터를 공유해서 사용하므로, 명시적으로 레지스터를 EMMS 인스트럭션으로 초기화해 두지 않으면 안 되기 때문이다.

    Chapter 8 - 4   실제 프로그램·알파블렌딩

    실제로 MMX를 사용해서 연산해 보자. 이번에는 2개의 데이터의 덧셈을 실시한다. 만약 화상자료일 경우라면,「알파블렌딩(Add Alpha-Blending)」이라는 기술이다.(반투명 처리)

     /* Dest = Dest + Src 를 수행한다
     * 데이터의 길이는 8의 배수여야만 한다
     */
    
    void AddData(BYTE *lpDestBuf, BYTE *lpSrcBuf1, BYTE *lpSrcBuf2, DWORD nLength)
    {
        _asm {
            /* 초기화 */
            mov     ecx, nLength;
            shr     ecx, 3
            mov     edi, lpDestBuf
            mov     ebx, lpSrcBuf1
            mov     edx, lpSrcBuf2
    
            /* 루프 연산 */
        ADD_LOOP:
            movq    mm0, [ebx]
            movq    mm1, [edx]
    
            /* 데이터 가산 */
            paddusb mm0, mm1
    
            /* 결과를 저장 */
            movq    [edi], mm0
    
            /* 증감(increment) */
            add     edi, 8
            add     ebx, 8
            add     edx, 8
    
            dec     ecx
            jnz     ADD_LOOP
    
            /* MMX 종료 */
            emms
        }
    }

    이런 간단한 예제로, MMX에 대해서 안다는 것은 불가능하다. 이런 부분을 기초로 해서 응용해 나가면, 화상 처리와 같은 부분에서 처리능력을 크게 개선할 수가 있다. 자 이제부터는 스스로 여러가지 프로그램을 짜 보고 실감해 보기바란다.


    Win32 MASM 프로그래밍 소개(Visual C++이용)

    Written by 권필진. 06'08 - aka Xeno

     


    Chapter 1   준비사항

    개요 : Win32 프로그래밍을 시작하기 전에

    Chapter 1 - 1   주의 사항

    MASM 프로그래밍을 하기 전에, 모든 책임은 자신이 진다는 것을 명심하기 바란다. MASM은 로우레벨의 명령어를 실행할 수 있기 때문에, 버그가 발생했을 경우 컴퓨터에 치명적인 에러 혹은 PC자체가 작동하지 않는일도 있을 수도 있다. 그러므로,

    이 문서로 인하여 발생한 손해 및 문제는 필자가 어떠한 책임과 보상도 지지 않음을 숙지하고 읽어나가기 바란다.

    (너무 겁먹을필요는 없다)

    주의:이 문서는 2006년경에 작성 했다. 테스트 환경으로서는 WindowsXP-SP2환경으로 테스트했다. 본 문서는 수시로 변경 될 수도 있다.

    Chapter 1 - 2   MASM 입수

    MASM 이란, 마이크로소프트사의 어셈블러 툴이다. 현재는 무상으로 제공되기 때문에, 어셈블러공부를 하고 싶다면 지금이 최적의 시기이다. 어셈블리를 익히게 된다면 컴퓨터에 대해 지금까지 몰랐던 것을 알 수있게 될지도 모른다. 하지만, MASM을 설명하는 곳은 드믈고 자료또한 거의 없기 때문에 이렇게 문서를 작성하고 있는 것이다. 또한, 본 문서는 간단한 소개정도에 그치는 수준이며 좀더 자세한 내용은 추후로 미루는 것 또한 알아두기 바란다.

    그렇다면 먼저, MASM을 구해야 한다. 이미 말했듯이, 현재는 무상으로 제공되기 때문에, 마이크로소프트의 홈페이지에서 다운로드 한다. 마이크로소프트의 DDK (Driver Development Kit) 라는 패키지 안에 포함되어있다. DDK 는, 20메가 이상의 크기일것이므로 다운로드하는데 시간이 걸릴지도 모른다.

    참고로, DDK 에 들어있는 MASM은 버젼이 낮고 MMX명령어를 인식하지 못하기 때문에, 강좌와 버젼을 맞출려면 「패치」를 해주어야 한다. 패치 또한 마이크로소프트의 홈페이지에서 구할 수가 있고, 다운로드한 후 압축을 해제하고 , 파일들을 모두 MASM이 있는 디렉토리안에 복사 한 후「패치」실행 파일을 실행한다.

    MASM 의 버젼이 최신상태로 되었다면, VC++ 에서 MASM를 사용할 수 있도록 설정한다. 이렇게 하면 VC환경에서 어셈블리환경을 사용할 수가 있게 되므로, 여러가지 편리한 점이 있다.

    가장 먼저 해야하는 것은, MASM 를 구해야 한다. 이것은 아래의 장소에서 구할 수가 있다.

    상기에서 설명했듯이 MASM을 구하고 패치를 해야하지만 번거롭다. 이럴경우 VC용 패키지가 존재한다. 본 강좌에서는

    이방법을 사용하기로 한다.

    Visual C++ 6.0 에 MASM를 추가하는 가장 간단한 방법은, ProcessorPack 의 추가이다. 이미 ML버전이 6.15가 포함되어 있기 때문에, 패치 단계가 필요없다. 하지만, 패키지가 SP4 ,SP5 전용이기 때문에, SP6 을 설치했다면, /C 옵션을 사용해서 압축을 해제한 후 직접 설치해 주어야한다. 참고로 VisualStudio.NET 이상 버전에는 ML.EXE 는 추가해 주지않아도 이미 포함 되어있다. 2002 는 버전 7.00 이며, 2003에는 7.10 이 포함 되어 있다. 아래의 주소에서 ProcessorPack을 다운로드 할 수있다.

    http://msdn.microsoft.com/vstudio/downloads/tools/ppack/default.aspx 에서 "Download Now" 부분을 클릭한다. 혹은 http://download.microsoft.com/download/vb60ent/update/6/w9x2kxp/en-us/vcpp5.exe 링크를 클릭해서 다운 받은 후 vcpp5.exe / c 옵션으로 압축을 해제해도 된다. WinZip과 같은 압축 프로그램으로도 압축을 해제할 수있다.

    압축해제한 후 해당 폴더를 VC의 설치 폴더에 통째로 복사해준 후 아래의 순서대로 경로를 잡아주면 된다.

    MASM를 구했는가? MASM의 프로그램 파일명은 ML.EXE 라는 이름이다. 이 프로그램은 소스파일을 어셈블링 해주고 링크까지 해주게 되며,원한다면, 오브젝트파일만 생성해 줄 수도 있다.(16bit링커사용시)

    추가로, 본강좌에 사용되는 Microsof Visual C++ 6.0은 제공해 줄 수가 없다. 각자 구하기 바란다.(-_-)

    사용된 소스는 복사해서 붙여넣기를 수행해도 작동하도록 되어있으며, 텍스트뷰어는 소스파일의 링크를 클릭해서

    해당 .dsw파일을 열어서 F5키를 눌러서 실행하면 된다.

     

     

    Chapter 1 - 3   VC++에서 MASM을 사용하도록 설정

    이제부터는, MASM를 VC++에서 사용할 수 있도록 해보자. MASM을 VC++ 에서 사용할려면, VC++의 「커스텀 빌드」라는 기능을 사용해야한다.

    먼저 File -> New 를 선택해서 Win32 Application을 선택한 후 "An Empty Project"를 선택한 후 다시 File->New를

    선택해서 Text File을 선택하고 파일명을 "VCMASM.ASM"이라고 정해주고 아래의 내용을 수행해 보기 바란다.

    VC++에서 ML.EXE 를 실행해야 하므로, 실행파일 경로를 설정해주어야 한다. 「Tools」-> 「Options」을 선택하고 아래와 같이 설정한다.

    Directories」탭으로 이동후에 반드시 「Executable Files」를 선택하기 바란다. 추가 버튼을 눌러서 압축해제한 폴더를 지정해준다. 참고로, 가장 하단에 추가하기 바란다. 본 강좌에서는 C:\VC60\VCPP5 라는 폴더이다.

    이제 실제로 간단한 프로그램을 만들어 보자.

    File -> New로, Win32 application 의 시작 프로젝트를 작성한다. 그런 다음에, MASM의 소스코드를 작성합니다. 이것은 File -> New -> Files탭에서, 확장자(extension)가 ASM 이 되게끔 하고 신규파일을 작성한다.

    그런 다음, 커스텀 빌드의 설정을 한다. Project -> Setting을 선택하고, 어셈블리 소스 파일을 선택한다. 커스텀 빌드 탭을 선택한 후, 아래와 같이 설정한다.

    Commands 텍스트 박스에는,

    ml /c /coff /Cx /nologo /Fo$(OutDir)\ /Fl$(InputName) /Zi $(InputPath)

    로 설정한다. 이는 디버그 모드의 경우지만, 릴리즈 모드로 할 경우, 커맨드에 있는 /Zi 플래그를 제거하면 된다.

    Outputs의 텍스트박스에는,

    $(OutDir)\$(InputName).obj

    로 설정해 준다. 이제부터는 VC에서, Build-> Run 을 선택하면, 자동으로 어셈블해주고 링크도 VC++가 알아서 해주게 된다. 하지만, 조금 번거로운것은, 파일을 작성할 때 마다 커스텀 빌드를 설정해 줘야 한다는 것이다.

    좀 더 자세한 자료는 다음부분을 참고하기 바란다 → Microsoft Support Q106399  (영어)

    커스텀 빌드에 대해, 좀 더 자세하게 알고 싶다면, MSDN에 자료가 아주 상세하게 설명되어 있기 때문에, 그부분을 참조하기 바란다.

    Chapter 1 - 4   최초의 MASM 프로그램

    모든언어의 표준 입문 방식인 「Hello World」프로그램을, MASM으로 간단하게 만들어 보자. 물론 무슨 내용인지 몰라도 상관없다. 모르는것이 당연한 것이며, 그래서 본 문서가 있는것이다.

    ;**********************************************************************
    ; 최초 MASM 프로그래밍
    ; 주석은 세미콜론으로
    ;**********************************************************************
    
    .586
    .model flat, stdcall
    
    NULL            EQU     0
    
    MessageBoxA     proto :dword, :dword, :dword, :dword
    ExitProcess     proto :dword
    
    .data
    
    TITLE1          DB '어셈블리 테스트', 0
    MESSAGE         DB '~Hello World!!~', 0
    
    .code
    WinMainCRTStartup   proc
    
        invoke MessageBoxA, NULL, offset MESSAGE, offset TITLE1, 0
        invoke ExitProcess, 0
        ret
    WinMainCRTStartup   endp
    end
    

    이 내용을 어셈블(assemble) 한 후 실행하면(F5), "~Hello World!!~"라고 표시되는 메시지 박스가 화면에 표시된다.

    (안된다면 십중 팔구 오타이니 복사해서 붙여넣기 바란다)

    (주의) 위의 소스에는 편의상 색을 다르게 표시했지만, 실제로는 변하지 않는다. (전용 통합환경을 사용하면 모든기능을 사용할 수가 있다. 많은것을 바라지는 말기 바란다. WinASM IDE를 사용하라.)


    Chapter 2   하드웨어의 구조

    개요 : 컴퓨터에 대해서

    Chapter 2 - 1   하드웨어

    이번에는, 간단하게 하드웨어의 구조에 대해 설명한다. 하드웨어의 구조라 하더라도, USB의 규격이나, 메인보드는 어떻게 구성되어 있는가, 그런 것을 다루는 것이 아니다. 대부분을 CPU 의 구조에 대해 설명을 할 것이다. 「최소한, 이것만은 반드시 알아야 한다」라는 부분을 위주로 해서 설명해 나갈것이다.

    다음의 순서로 설명 할 것이다.

    1. 어셈블러에서 가능한 것
    2. 저장장치(레지스터/메모리/스택)
    3. 외부 입력과 외부 출력(I/O)·인터럽트
    4. BIOS·시스템 콜·VRAM

    Chapter 2 - 2   어셈블러에서 가능한 것

    먼저, 왜 어셈블러를 사용 할까? 그 이유는, 몇가지가 있다. 대부분의 프로그래밍은, C언어로도 충분하다. 일반적으로 말해, 언어가 고급으로 될수록 , 인터프리터성이 높아져서 , 언어가 사람이 이해하기 쉬워지지만, 그 언어로 실제 수행되는 부분은 오히려 감소하게 된다. 즉, Basic과 C언어의 예를 들면, Basic에서 할수 있는 일은 C언어로도 수행 할 수 있다.하지만 반대로, C언어로 수행할 수 있는 것이 반드시 Basic 에서 수행된다는 보장은 없다. 같은 맥락으로 C언어와 어셈블러의 관계에서도 존재한다. 예를 들어 C언어에서는, 하드웨어를 직접조작은 할 수 없다(물론 인라인어셈블리를 사용할 수는 있지만). 단지 예를 든것이다. 그렇기 때문에, 직접 하드웨어를 조작하고 싶은 경우 등에 어셈블러를 사용한다. 직접 하드 디스크의 트랙이나 섹터를 조작하고 싶은 경우에 어셈블러를 사용한다라고 생각하면 이해하기 쉬울 것이다.

    프로그램이 고속성을 요구하는 경우, C언어보다 어셈블러 쪽이 일반적으로 빠르기 때문에, 이런 경우는 어셈블러에서 프로그램을 주로 짠다. 그렇지만, 어셈블러는 너무 로우레벨이므로, 프로그래밍에는 상당한 노력을 필요로 하고 코딩의 양도 많아진다. 그러므로, 어셈블러에서만 프로그래밍을 하는 것은, 현실적이지는 않다. 많은 경우, C언어등의 고급(중급) 언어와 함께 조합해서 사용하게 된다.

    Chapter 2 - 3   저장장치(레지스터/메모리/스택)

    기억장치에는, 3 종류가 있다. 레지스터와 메모리와 스택이다. 이 밖에도, 보조기억장치로 하드 디스크나 플로피 디스크등이 있다. 어셈블러에서는, 주로 레지스터와 스택을 이용한다.

    레지스터는, CPU의 내부에 있는 기억장치이다. CPU의 내부에 존재하므로, 액세스 속도 또한 아주 빠르다. 그렇지만, 레지스터는, 무한정으로 사용할 수 있는 자원이 아니다. 레지스터의 종류에는, 3개 있다. 범용 레지스터,세그먼트 레지스터,플래그 레지스터.

    레지스터 종류 해설
    범용 레지스터
    8개가 있다. 프로그램에서 사용된다.
    세그먼트 레지스터
    6개가 있다. 세그먼트(segment)의 관리에 사용된다.
    상태 레지스터
    1개가 있다. CPU 상태를 기억한다. 일반적으로 변경할 수 없다.

    프로그래밍에서는, 거의 대부분 범용 레지스터를 사용한다. 범용 레지스터에는, 다음의 8가지가 있다. 모두 32 비트이다.

    레지스터명 해설
    EAX
    어큐물레이터. 수치계산에 사용되는 레지스터.
    EBX
    베이스 레지스터. 보조 계산이나, 그 외의 용도에 사용되는 레지스터.
    ECX
    카운터 레지스터. 보조 계산이나, 카운터, 루프에서 사용되는 레지스터.
    EDX
    데이터 레지스터. 보조 계산이나, 데이터용 조작용의 레지스터.
    ESI
    소스 인덱스. 보조 계산이나, 데이터 처리 인스트럭션으로 사용되는 레지스터.
    EDI
    목적지 인덱스. 보조 계산이나, 데이터 처리 인스트럭션으로 사용되는 레지스터.
    ESP
    스택 포인터. 스택 처리로 사용되는 레지스터.
    EBP
    베이스 포인터. 스택 처리로 사용되는 레지스터.

    C언어에서 변수의 수식자에 register 를 지정할 수 있다.(레지스터변수) 이런 경우 실제로는, 변수에 ESI(소스 인덱스)와 EDI(목적지 인덱스)를 사용하게 된다.또한, 계산용의 레지스터로서 ESP(스택 포인터)와 EBP (베이스 포인터) 를 사용할 수 없다. 이런 레지스터 들은, 스택관리라고 하는 중요한 임무가 있기 때문 이다. 수치계산은, 실질적으로 6개의 레지스터로 하게 된다. 각각의 레지스터에는, 고유한 역할도 있으므로, 거기에 맞춰서 프로그램을 짜지 않으면 안된다. 왜 이런 엄격한 룰이 존재하는지를 탓하지 말기 바란다. 컴퓨터는 복잡한 장치이다. 하지만 이런 기초적인 룰만 익히면 어떤언어보다 강력함을 얻을수 있다. 대충 아는 언어와는 다른 것이다.

     

    32 비트의 경우, 누산기는 EAX 로 취급하지만 하위 16 비트를 AX 로서 다룰 수도 있다. AX 는 또 다시, 상위 8 비트를 AH , 하위 8 비트를 AL 로 나타낸다. 이것은, EBX ECX EDX 의 경우도 같다.

    그렇다면, 이제 레지스터를 사용한 프로그램을 살펴보자.

    ;*****************************************************************
    ;  레지스터를 사용한 프로그램
    ;*****************************************************************
    
    .586                   ; Pentium 용의 코드를 쓰는 경우 필요
    .model flat, stdcall
    
    NUM         EQU   1
    
    wsprintfA   proto c :dword, :dword, :dword, :dword, :dword
    MessageBoxA proto   :dword, :dword, :dword, :dword
    ExitProcess proto   :dword
    
    .data
    
    BUFFER      DB 64 DUP(0)
    STRINGS     DB '%d + %d = %d', 0
    TITLENAME   DB '결과는? ', 0
    
    .code
    
    WinMainCRTStartup proc
    
        mov eax, NUM     ; 누산기에 대입
        mov ebx, NUM + 1 ; 베이스 레지스터에 대입
        add eax, ebx     ; 누산기와 베이스 레지스터를 더한다
    
        invoke wsprintfA, offset BUFFER, offset STRINGS, NUM, ebx, eax
        invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
        invoke ExitProcess, 0
    
        ret
    WinMainCRTStartup endp
    
    end
    

    덧셈을 한 후, 메시지 박스로 표시하는 프로그램이다("1+2=3"). 레지스터는, EAX(누산기)와 EBX(베이스 레지스터)를 사용하고 있다. 프로그램에서 mov 나 add 등을 니모닉이라 한다. 니모닉은, 기계어의 비트 표현을 문자표현으로 나타낸 것이다. 예를 들어 mov 라면, MOVe 의 약자로 「대입」을 의미한다. add (은)는 ADD 로 「가산」을 의미한다. 프로그램에서는 누산기와 베이스 레지스터에 수를 대입해서 ,덧셈을 수행하고 있다.

    MASM에서, 함수를 프로시저라고 부른다. 프로시저의 정의는, proc - endp 로 한다. 위의 예에서는 , WinMainCRTStartup 라는 프로시저가 정의되어 있고, 그 사이의 기계어 인스트럭션이 실행되고 있다고 생각하면 된다.

    이 함수는 스타트 업 코드(Startup Code)로 불리고 프로그램에서 가장 먼저 호출되는 함수다. 보통 C로 프로그램을 짜는 경우, 스타트 업 코드는 자동으로 생성되어 링크 된다. 또한 스타트 업 코드는, 프로그램을 초기화하는데 사용된다. VC++ 로 Windows 프로그래밍을 하는 경우, 스타트 업 코드는 WinMainCRTStartup 함수지만, Console 용의 프로그램의 경우는 mainCRTStartup 함수가 스타트 업 코드가 된다. 통합 환경에 의해, 스타트 업 코드 함수의 이름은 각각 다르다.반드시 정해진것은 아니다. 개발자 마음인것이다. main이란것을 찾지만 말기 바란다. 이것은 C언어의 룰일 뿐이다.실제로는 이런것은 없다.

    Chapter 2 - 4   그 외 레지스터

    상태 레지스터(flag register)는, 하나 밖에 없다. 레지스터의 내부는, CPU 상태를 보존하고 있다. 일반적으로 상태 레지스터 전체를 변경할 수 없다(스택을 사용하면 변경할 수 있지만, 권하는 방법은 아니다 pushf / popf). 플래그의 종류는,상태 플래그,제어 플래그,시스템 플래그의 3개가 있다. 상태 플래그는, 연산의 결과 상태가 보존된다. 상태 플래그의 종류는 다음과 같다.

    플래그명 비트 해설
    CF (Carry Flag) 0 carry flag. 자리수가 올라가면 세트.
    PF (Parity Flag) 2 패리티플래그. 연산 결과가 짝수일 경우 세트.
    AF (Adjust Flag) 4 조절 플래그. 2 진화 10진(BCD) 연산에서 사용.
    ZF (Zero Flag) 6 제로 플래그. 연산 결과가 제로일 경우 세트.
    SF (Sign Flag) 7 부호 플래그. 연산 결과가 부호가 있는 경우 세트.
    OF (Overflow Flag) 11 오버플로우 플래그. 오버플로우가 발생하면 세트.

    일반적으로 상태 플래그는 변경할 수 없지만, carry flag는 변경 가능하다. 니모닉 STC (SeT Carry flag), CLC (CLear Carry flag) 인스트럭션으로 변경할 수 있다.

    제어 플래그는 DF (Direction Flag ; 방향 플래그) 밖에 없다. string 인스트럭션(메모리 전송 인스트럭션)에서, 메모리 전송 방향을 지정한다. 니모닉으로는, STD (SeT Direction flag) CLD (CLear Direction flag)를 사용한다.

    시스템 플래그는, 가상 8086 인스트럭션이나 인터럽트 제어 등에 사용되지만 본 문서에서는 설명하지 않는다.인텔의 아키텍쳐 메뉴얼을 참조하기 바란다.

    그러면, 상태 레지스터를 사용한 간단한 프로그램을 만들어 보자.

    ;*****************************************************************
    ;  상태 레지스터를 사용한 프로그램
    ;*****************************************************************
    
    .586
    .model flat, stdcall   ; 메모리모델은 플랫으로…
    
    NUM         equ   1
    
    wsprintfA   proto c :dword, :dword, :dword
    MessageBoxA proto   :dword, :dword, :dword, :dword
    ExitProcess proto   :dword
    
    .data
    
    BUFFER      db 64 DUP(0)
    STRINGS     db '2의 10승은 %d 입니다', 0
    TITLENAME   db '2의 10승을 푸는 문제', 0
    
    .code
    
    WinMainCRTStartup proc
    
        mov eax, NUM
        mov ecx, 10
    
    MYLOOP:
        mov ebx, eax
        add eax, ebx
        dec ecx       ; ecx 의 값을 1 감소
        jnz MYLOOP    ; 제로 플래그가 세트 되지않으면 루프
    
        invoke wsprintfA, offset BUFFER, offset STRINGS, eax
        invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
        invoke ExitProcess, 0
    
        ret
    WinMainCRTStartup endp
    
    end
    
    

    먼저 WinMainCRTStartup 함수의 내용보자. 이번에는 2의 10승을 구하는 문제이다("2의 10승은 1024입니다"). ECX 레지스터의 내용을 1씩 감소해 가면서, ECX 레지스터가 0이 되면 종료하는 루프를 만들었다. dec (DECrement)라는 니모닉은, 레지스터의 내용을 1 감소시키는 인스트럭션이다(C에서 abc--). jnz (Jump if Non-Zero flag)란, 제로 플래그가 세트되지 않는 동안, 지정된 주소에 점프 하는 조건 점프 인스트럭션이다. 여기에서는 MYLOOP 라고 하는 주소로 점프 한다. MYLOOP 와 같이, 이름의 뒤에 구두점을(콜론) 붙여서 주소에 이름을 지정할 수가 있다.(레이블)

    이번 예제에서는 상태 플래그의 사용법을 보기 위해서, 귀찮은 방법으로 프로그래밍했지만, 루프를 할 경우는 좀 더 편리한 니모닉이 준비되어 있다바로 loop (LOOP if ecx is non-zero) 라는 것이 있다. 위의 내용을 다시 작성하면 아래와 같이 된다.

    mov ecx, 10    ; 루프 시키고 싶은 횟수만 ECX 에 대입
    
    MYLOOP:
        mov ebx, eax
        mov eax, ebx
        loop MYLOOP    ; ECX 를 1줄여 0이 될 때까지 반복한다
    
    

    또한 실제로 , 2의 계승을 구하는 좀 더 간단한 방법도 있다. 쉬프트를 사용하는 방법이다.

    세그먼트(segment) 레지스터의 종류에는 다음과 같은 종류가 있다. 모두 16 비트이다.

    레지스터 해설
    CS 코드 세그먼트 실행 코드가 존재하는 세그먼트
    DS ES FS GS 데이터 세그먼트 데이터가 존하는 세그먼트
    SS 스택 세그먼트 스택이 존재하는 세그먼트

    16 비트 시절에는 DS와 ES가 있었지만, 현재에는 FS DS 가 더해져서, 모두 「데이터 세그먼트」라고 불려진다.

    세그먼트(segment)란, 연속된 메모리를 필요에 따라서 분할하고, 그 하나하나를 가리키는 의미이다. 예를 들어, 1개의 프로그램에는, 실행 코드와 데이터가 있다. 이것을 분할한 영역에 두는것이 바로, 세그먼트(segment)의 발상이다. 예를 들면, 실행 코드는 CS 세그먼트(segment)에 놓여지고, 데이터는 DS 세그먼트(segment)에 놓여지게 된다. 세그먼트(segment)는 같은 연속적인 메모리에 할당할 수 있고, 세그먼트(segment)끼리 서로 겹쳐 지는 경우도 있다.

    예제 프로그램을 보면 .code 라고 하는 code segment를 정의하고 있다. 또한 .data 라는 데이터 세그먼트를 정의하고 있다. 그 밖에도 .stack 등도 있어서, 스택 세그먼트도 정의할 수가 있다. 하나의 세그먼트(segment)에는, 4 G바이트까지(!) 사용할 수 있다(2의 32승 : 그래서 32비트). 예를 들어 CS 세그먼트(segment)라면 CS:00000000h ~ CS:FFFFFFFFh 까지의 영역이 존재한다. 물론, 실제 메모리는 이렇게 사용되지는 않는다. 단지 예를 든 것이다.

     

    Chapter 2 - 5   메모리

    기억장치이다.「메모리」에 값을 대입할 수도 있다. 우선, 프로그램을 보자.

    ;*****************************************************************
    ;  메모리를 사용하는 프로그램
    ;*****************************************************************
    
    .586
    .model flat, stdcall  
    ; stdcall 은 함수의 호출 방법을 정의!
    
    NUM         equ   32
    
    MessageBoxA proto   :dword, :dword, :dword, :dword
    ExitProcess proto   :dword
    
    .data
    
    BUFFER      db 128 dup(? )
    TITLENAME   db '메모리에 대입하는 프로그램', 0
    
    .code
    
    WinMainCRTStartup proc
    
        mov    ecx, NUM            ; 카운터의 회수
        mov    edx, offset BUFFER  ; 버퍼의 포인터
        mov    ah, 20h
        ;
    
     _LOOP0:
        mov    byte ptr [edx], ah
        inc    ah
        inc    edx
        loop   _LOOP0
    
        invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
        invoke ExitProcess, 0
    
        ret
    
    WinMainCRTStartup endp
    
    end
    

    이번 프로그램은, 128바이트의 정적 버퍼를 확보하고, 그 영역에 문자를 차례차례 대입하는 프로그램이다. 실제로 「 ! "#$%&'()*+,-. /0123456789:<=>?」라는 문자가 대입된다. ASCII 코드의 20h 부터 3Fh 까지이다. 메모리에 대입하는 경우에는 ptr 연산자를 사용한다. [edx]란 edx 가 가리키는 메모리를 나타냅니다. byte ptr [edx] 란, 「edx 가 가리키는 메모리에 바이트값(8 비트)으로 값을 대입하라」라는 의미이다. 워드값으로 값을 대입하는 경우에는, word ptr [edx] 가 된다. C에서 어렵다고 하는 "포인터"이다. 어셈블리에서는 기본이 포인터이므로, 복잡하지 않다. 필자의 경우도

    처음에 C를 할때 괜히 헷갈렸다. 오히려.......

    Chapter 2 - 6   스택

    스택은, 후입선출 (Last-In First-Out) 형태의 데이터 구조이다. 어셈블러에서 스택은 중요한 역할을 한다. 레지스터의 내용을 일시적으로 저장하거나 프로시저 호출 시에 인수를 스택에 넣거나 한다. 특히 C언어나 Pascal 등은, 함수 호출시 인수를 스택에 넣어 호출을 수행한다.이것을 함수호출 규약이라고 부른다.

    그렇다면, 스택을 이용한 프로그램을 보자.

    ;*****************************************************************
    ;  스택을 사용한 프로그램
    ;*****************************************************************
    
    .586
    .model flat, stdcall
    
    NUM         equ   5
    
    wsprintfA   proto c :dword, :dword, :dword
    MessageBoxA proto   :dword, :dword, :dword, :dword
    ExitProcess proto   :dword
    
    .data
    
    BUFFER      db 64 dup(0)
    TITLENAME   db '스택을 사용하는 프로그램', 0
    STR1        db '카운트다운 %d', 0
    
    .code
    
    WinMainCRTStartup proc
    
        mov     ecx, NUM
    
     _LOOP0:
        push    ecx       ; SubFunc 로 ECX 를 스택에 보존
        call    SubFunc
        pop     ecx
        loop    _LOOP0
    
        invoke ExitProcess, 0
    
        ret
    
    WinMainCRTStartup endp
    
    SubFunc proc          ; 새로운 프로시저를 정의
    
        invoke wsprintfA, offset BUFFER, offset STR1, ecx
        invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
    
        mov     ecx, 99   ; 여기서 ECX 의 값을 바꿔도, 스택에 값이 보존되있기 때문에 문제없다
    
        ret
    
    SubFunc endp
    
    end
    

    이번에는, SubFunc 라는 새로운 프로시저(함수)를 만들어 보았다. 간단한 프로그램이므로, 특별히 문제는 없을 것이다. ("카운트다운 5")라는 결과가 나온다.이후 "카운트다운 4" "3"등으로 1까지 감소한다. push 니모닉은 레지스터의 내용을 스택에 대입한다. pop 은 스택으로부터 값을 레지스터에 대입한다.스택을 사용하는 MOVe라고 생각하면 쉽다. call 은 프로시저를 호출한다. 이 때, 현재 실행중인 주소를 스택에 대입하고 나서 프로시저로 점프 한다. 프로시저에서 돌아오는 경우에, ret 을 사용한다. ret 은 스택으로부터 값을 꺼내고, 호출시 원래의 주소로 돌아오게 된다.

    Chapter 2 - 7   I/0·인터럽트

    CPU 로 계산만 수행해서는, 컴퓨터의 진정한 능력을 사용 할 수가 없다. 컴퓨터의 기능을 활용하기 위해서는, 주변장치의 입력이나 출력을 제어할 수 있어야 한다. 예를 들어, 키보드로부터 입력을 받는다거나 디스플레이 장치에에 결과를 표시하거나. 이런것을 제어하기 위해서 I/O 공간을 사용한다. I/O 공간은 16 비트의 공간에서 16 진수로 말하면 0000h - FFFFh 의 공간이다 (16 진수는 수치뒤에[h] 를 붙인다). 이 공간으로 데이터를 전송 하거나(외부 출력), 데이터를 받거나(외부 입력) 하는 방식으로 , 주변장치를 액세스 할 수가 있다.

    윈도우즈에는, 「시스템 정보」를 표시하는 프로그램이 있다. 「시작 메뉴」의 「보조프로그램」안의 「시스템정보」 이다.이 프로그램으로, 어떻게 I/O 공간이 할당되어 있는지를 볼 수가 있다.

    인터럽트는 하드웨어 인터럽트와 소프트웨어 인터럽트의 2 종류가 있습니다. 하드웨어 인터럽트란, 외부로부터 입력이 있었을 경우, 현재의 프로그램을 일시중지 하고, 별도의 프로그램을 실행하는 기능입니다. 예를 들어, 프로그램 A를 실행중에 키보드가 눌렸다고 가정 합시다. 이 경우 키보드가 눌려진 키를 버퍼에 보존해 두어야만 합니다. 거기서 하드웨어 인터럽트가 발생해 키보드버퍼에 값이 저장됩니다. 물론 하드웨어 인터럽트가 발생하면 , 플래그와 레지스터의 값은 스택에 저장되므로, 원래의 프로그램에는 아무런 영향도 끼치지 않습니다.

    하드웨어 인터럽트의 예

    소프트웨어 인터럽트의 예

    이와는 달리, 소프트웨어 인터럽트는 프로그램에서 명시적으로 MS-DOS의 함수 호출이나, BIOS의 프로시저를 호출하는 것을 말합니다.(즉, 프로그래머가 발생시키는 인터럽트를 의미합니다)

    프로그램의 예를 보고 이해하시기 바랍니다.

    ;******************************************************************************
    ; 소프트웨어 인터럽트를 사용한 프로그램
    ;   이 프로그램은 MS-DOS용의 프로그램입니다
    ;   VC에서는 실행할 수 없습니다. 참고용으로 제작한 것입니다.
    ;******************************************************************************
    
    .model small, c   ; 모델에서 small문을 먼저 기록하게되면 세그먼트(segment)는 16 비트가 된다
    .286
    
    ;******************************************************************************
    ; 프로세스의 종료 매크로
    ;******************************************************************************
    end_process macro ret_value
    
                mov   al, ret_value
                mov   ah, 4ch
                int   21h
    
                endm
    ;******************************************************************************
    ; 캐릭터 라인의 표시 매크로
    ;******************************************************************************
    display     macro string
    
                mov   dx, offset string
                mov   ah, 09h
                int   21h
    
                endm
    
    ;******************************************************************************
    ; 1 캐릭터 입력(에코 없음) 매크로
    ;   AL = 키보드로부터 입력된 캐릭터
    ;******************************************************************************
    read_kbd    macro
    
                mov   ah, 08h
                int   21h
    
                endm
    
    ;******************************************************************************
    ; 데이터 정의
    ;******************************************************************************
    .data
    
    MSG         db '이 프로그램은 즉시 종료합니다', 0dh, 0ah, '$'
    
    ;******************************************************************************
    ; 실행 프로세스
    ;******************************************************************************
    .code
    
    START:
                mov   ax, _DATA
                mov   ds, ax
    
                display     MSG   ; 메세지를 표시
    
                read_kbd          ; 버튼 입력 대기
    
                end_process 0     ; 프로그램의 종료
    
    
    end START ; end 의사 인스트럭션의 뒤에는 스타트 지점을 기술
    
    

    주석에서도 밝혔듯이, VC에서는 실행할 수 없습니다. VC의 링커는 32 비트이므로, 이 프로그램은 16 비트의 링커로 링크 하지 않으면 안됩니다. 16 비트의 링커는 아래에서 다운로드 받아서 사용하기 바랍니다.

    ftp://ftp.microsoft.com/softlib/mslfiles/lnk563.exe

    압축해제하면 16 비트의 링커가 생성됩니다. 32 비트용의 링커가 아니므로 주의하기 바랍니다.

    이번 예에서는 매크로 정의하고 있습니다. 매크로는 함수와 달리, 호출된 곳에 치환됩니다. "복사" "붙여넣기"와 유사합니다. 호출이 아니고 대신 코드문이 바로 나타나게 됩니다. 이기능으로 매크로 어셈블리라고 불리게 됩니다. C++의 인 라인 함수와 같은 것입니다. 매크로는 보면 알 수 있듯이 macro - endm 로 정의되고 있습니다. 함수 호출에 대해서는, 16 비트 어셈블러 쪽을 참고 하시길 바랍니다.

    Chapter 2 - 8   BIOS/시스템 콜 /VRAM

    BIOS 는 메인보드에 장착되어 있는, 로우레벨로 하드웨어 조작을 하는 프로그램입니다. BIOS 는 소프트웨어라고 불리지 않고 펌 웨어라고 불리어 집니다. 직접 하드 디스크의 섹터에 기록하거나 읽어들이거나 하는 처리나, 플로피 디스크의 조작등의 로우레벨 함수들로 구성되어 있습니다.즉, 실제로 컴퓨터를 움직이는 가장 밑부분입니다.

    시스템 콜이란, BIOS 나 OS 의 로우레벨 함수를 호출하는 것을 말합니다.

    VRAM (Video RAM)에는, 캐릭터 VRAM 와 그래픽 VRAM 가 있습니다. 캐릭터 VRAM 란, 메모리상의 캐릭터를 디스플레이에 표시하는 RAM 입니다. 메모리상에 직접 캐릭터를 입력하는 것으로, 화면상에 캐릭터를 동기적으로 표시 할 수가 있습니다. 그래픽 VRAM 란, 메모리상의 데이터를 그래픽으로서 표시하는 RAM 입니다. 메모리와 화면의 그래픽은 서로 대응하고 있으므로, 메모리에 기록하는 것으로, 직접그림이나 곡선같은 객체등을 화면에 표시할 수가 있습니다. 그래픽 보드등에 있는 램에 해당합니다.

    대부분의 경우 그래픽 VRAM 는, A000 : 0000 에 할당되어 있습니다.

    우선 하드웨어의 구조는 이것이 다 입니다. 너무 간단하다고 생각 하실 것입니다. 윈도우 프로그래밍을 하게 될려면 먼저 도스나 시스템 프로그램에 대해서 알아야 합니다. 모든것에는 단계가 있습니다. 본 강좌는 상세 매뉴얼이 아닙니다. 이전에 한번쯤은 익혀보셨던 내용을 다시금 간단히 정리한 것입니다. 하지만 프로그래밍은 많은것을 요구하지는 않습니다. 간단한 명령어로도 훌륭한 프로그램을 작성 할 수 있습니다. 알고리즘을 언어의 문법에 맞게끔 작성하는 도구일 뿐입니다.이것을 이해하지 못하게되면 각종 언어를 섭렵해도 멀 했는지 모르게되는 딜레마에 빠지게 됩니다. 예를 들어서 영어의 문법만 잔뜩외우게 된후 실제 대화는 못하게 되는것이랑 같습니다. 미국의 꼬마는 문법을 배우지 않습니다. 말부터 배우고 자연히 문법을 익혀서 오류를 줄이고 멋지게 표현하는 법을 배우는것과 같은 이치입니다. 언어는 도구입니다.라는 말을 각종 개발자사이트에서 하는 이유는 이것입니다. 모든것을 외우려 하지 마시길 바랍니다. 기본적인것을 배우시고 활용을 하다가보면 막히는 부분도 발생하고 그런부분을 하나하나 익혀가시게 되면서 익히는 겁니다. 에 에 에 문법과 알고리즘을 보강하시게되고, 나중에는 자신만의 라이브러리와 개발방식을 타인에게 전파시킬 수 있는 것입니다.

    그래도 먼가 찜찜하다고 생각하시는 분들은 어셈블리 기초강좌를 참조하시기 바랍니다. 이역시 강좌를 할 것입니다.

    다음부터는 Windows 프로그래밍을 하게 됩니다. 기초지식으로서는, Windows API 에 대한 지식이 필요합니다만, 너무 자세히 알려하시기 전에 가볍게 체험 하신후에 별도의 서적등으로 공부하시기 바랍니다. MSDN도 훌륭합니다. 여기서는 자세하게 설명하지는 않습니다.


    Chapter 3   Windows 프로그래밍

    개요 : Windows 프로그래밍

    Chapter 3 - 1   Windows 프로그래밍

    MASM으로 Windows 프로그래밍을 시작해보자!

    기초지식으로 Windows API, C 언어,CPU 등의 기본지식이 필요하다. 이런 부분에 대해서는 본 문서에서 자세히 설명하지 않으므로, 공부하기 바란다.

     

    Chapter 3 - 2  최초의 프로그램

    MASM으로 Windows 프로그래밍을 하는 것이지만, 처음으로 작성하는 것이라서 주석을 달아 두겠다.

    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 어셈블러 윈도우프로그래밍
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    
    .486
    .model flat, stdcall
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 정의
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    NULL      =       0
    MB_OK     =       0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; prototype
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    MessageBoxA proto :dword, :dword, :dword, :dword
    ExitProcess proto :dword
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 데이터 세그먼트
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .data
    
    MSG1      db	'메세지 테스트',0
    TITLE1    db	'타이틀',  0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; code segment
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .code
    
    WinMainCRTStartup	proc
        invoke MessageBoxA, NULL, offset MSG1, offset TITLE1, MB_OK
        invoke ExitProcess, 0
        ret
    WinMainCRTStartup	endp
    
    end
    

    하나씩 설명하겠다. 먼저 .486 지시어이다. MASM에서, 486용의 인스트럭션을 생성하는 지시어이다. 예를 들어, .386으로 하게되면, 80386용의 인스트럭션을 생성하게 된다. .386p와 같이, CPU명 뒤에 p를 붙이게되면 특권 인스트럭션도 사용할 수 있게 된다. (p는 privilage→특권의 약자)

    다음으로 .model 지시어이다. 이 인스트럭션은 메모리모델이라는 메모리사용 규약을 정한다. Win32 프로그램의 경우에는 플랫메모리모델만 사용하기 대문에, 메모리모델에는항상 flat를 지정한다. 또한, 함수(프로시저)의 호출 규약에는, Windows 프로그램 라이브러리의 호출 규약인 stdcall을 지정한다.

    프로그램에서, WinMainCRTStartup라는 프로시저를 정의하고 있다. 프로시저를 정의하려면 , proc 지시어를 사용한다. 또, 프로시저 호출방법에는, 여러 가지가 있지만, 간략화된 호출 방법으로 invoke 지시어를 사용할 수가 있다. Invoke만 호출해줌으로써, MASM에서 프로시저 호출을 간단하게 수행할 수가 있게 된다. 복잡한 스택처리르 해주게된다. 이명령어와 다른 몇가지로 인해서 고급언어와 비슷하게 프로그램을 하게 될 수 있게 된다.

    Windows 프로그램의 종료에는, 반드시 ExitProcess를 호출해야 한다. VC++에서는, 내부적으로 ExitProcess 를 호출한다. 왜 반드시 호출해야하는가 하면 , ExitProcess를 호출하지 않으면 완전하게 프로세스가 종료하지 않기 때문이다. 만약 호출하지 않는 경우는, 메모리상에 쓰레기값이 남는 경우도 발생할 수도 있다.

    Chapter 3 - 3   인스턴스 핸들

    인스턴스 핸들이란, 각각의 모듈의 실체를 나타내는 핸들이다.선뜻 이해가 되지 않을것이다 . 좀 더 자세하게 설명을 하겠다.

    Windows에는, 모든 오브젝트를 "핸들" 로 다루게 된다. 그렇기 때문에 Windows에서, 실행 파일이 메모리상에 로드 되면, 어떤 형태로든 이"인스턴스"를 관리하지 않으면 안 되는 것이다. Windows는, 어플리케이션이 자신의 인스턴스에 대해서 조작을 수행할 경우 "인스턴스 핸들"을 그 값으로 넘겨주게 된다.(조작용 번호라고 생각하기 바란다)

    그런데, 왜 인스턴스 핸들이 필요한가 를 설명하면, 실행파일(혹은 모듈)에 속해있는 리소스를 얻을려고 하는 경우에는, 이 인스턴스 핸들을"키"로해서 Windows에 요청해야 하는 것이다. 인스턴스 핸들의 존재 의미는,어찌보면 당연한 것이다.

    먼저, 프로그램을 살펴보자.

    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 어셈블러 윈도우 프로그래밍
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .586
    .model flat, stdcall
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 정의
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    NULL	=	0
    MB_OK	=	0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; prototype
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    MessageBoxA       proto   :dword, :dword, :dword, :dword
    GetModuleHandleA  proto   :dword
    wsprintfA         proto c :dword, :dword, :dword
    ExitProcess       proto   :dword
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 데이터 세그먼트
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .data
    
    MSG1	db	'인스턴스 핸들: 0x%X', 0
    TITLE1	db	'타이틀 ', 0
    
    STR1	db	64 dup(0)
    hModule	dd	?
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; code segment
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .code
    
    WinMainCRTStartup	proc
        invoke GetModuleHandleA, NULL
        mov    hModule, eax
    
        invoke wsprintfA, offset STR1, offset MSG1, hModule
        invoke MessageBoxA, NULL, offset STR1, offset TITLE1, MB_OK
        invoke ExitProcess, 0
    
        ret
    WinMainCRTStartup	endp
    
    end
    

    실행(F5)해보면 모듈핸들 번호를 메세지 박스로 표시해준다. 컴퓨터마다 다를수 있으나 보통 0x400000형식이다.모듈 핸들을 얻기위해서는 GetModuleHandle 함수를 사용한다. 함수명 뒤에 붙어 있는 A의 의미는 문자열이 Ascii 코드라는 의미이다. 함수명 뒤가 W일 경우는, 문자열이 UNICODE라는 의미이다. 참고삼아 기억해 두기 바란다.

    (NT의 경우는 기본값으로 유니코드방식이다)

    WindowsAPI에 대해서는, MSDN 라이브러리를 참조하기 바란다.

    Chapter 3 - 4   로컬 변수

    다음에는, 로컬 변수에 대해서 알아보자.

    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 어셈블러 윈도우 프로그래밍
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    
    .586
    .model flat, stdcall
    option casemap:none
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 정의
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    NULL	=	0
    MB_OK	=	0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; prototype
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    MessageBoxA       proto   :dword, :dword, :dword, :dword
    GetModuleHandleA  proto   :dword
    wsprintfA         proto c :dword, :dword, :dword
    ExitProcess       proto   :dword
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; 데이터 세그먼트
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .data
    
    MSG1	db	'인스턴스 핸들: 0x%X', 0
    TITLE1	db	'타이틀', 0
    
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    ; code segment
    ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    .code
    
    WinMainCRTStartup	proc
        ;---------------------------
        ; 로컬 변수
        ;---------------------------
        local pBuf[64] :byte
        local hModule  :dword
    
        ;---------------------------
        ; 프로그램 본체
        ;---------------------------
        invoke GetModuleHandleA, NULL
        mov    hModule, eax
    
        invoke wsprintfA, addr pBuf, offset MSG1, hModule
    
        invoke MessageBoxA, NULL, addr pBuf, offset TITLE1, MB_OK
        invoke ExitProcess, 0
    
        ret
    WinMainCRTStartup	endp
    
    end
    

    프로시저 내부에서 local 지시어를 사용함으로써, 로컬 변수를 사용할 수가 있다. 위를 예에서 알수 있듯이, 로컬 변수의 선언은,

    local  name[] :size

    와 같이 한다. 위의 예에서는 배열을 지정했다. " 인스턴스 핸들: 0x400000"라는 결과가 나오게 된다.

    Chapter 3 - 5   윈도우 생성

    이제부터는, 실제로 윈도우를 생성해 본다. 프로그램의 내용이 좀 길지만 천천히 살펴보자.

    ;******************************************************************************
    ; 어셈블러 윈도우 프로그래밍 windows.asm
    ;******************************************************************************
    
    .586
    .model flat, stdcall
    option casemap:none
    
    ;******************************************************************************
    ; 정의
    ;******************************************************************************
    
    NULL              =       0
    WM_DESTROY        =       2h
    IDI_APPLICATION   =       32512
    IDC_ARROW         =       32512
    WHITE_BRUSH       =       0
    WS_OVERLAPPED     =       00000000h
    WS_SYSMENU        =       00080000h
    WS_CAPTION        =       00C00000h
    WS_EX_APPWINDOW   =       00040000h
    SW_SHOWNORMAL     =       1
    CW_USEDEFAULT     =       80000000h
    
    HANDLE  typedef     dword
    LPSTR   typedef     dword
    
    POINT        struct
        x               dword   ?
        y               dword   ?
    POINT        ends
    
    WNDCLASS     struct
        style           dword   ?
        lpfnWndProc     dword   ?
        cbClsExtra      dword   ?
        cbWndExtra      dword   ?
        hInstance       HANDLE  ?
        hIcon           HANDLE  ?
        hCursor         HANDLE  ?
        hbrBackground   HANDLE  ?
        lpszMenuName    LPSTR   ?
        lpszClassName   LPSTR   ?
    WNDCLASS     ends
    
    MSG          struct
        hWnd            HANDLE  ?
        message         dword   ?
        wParam          dword   ?
        lParam          dword   ?
        time            dword   ?
        point           POINT   <?, ? >
    MSG          ends
    
    ;******************************************************************************
    ; 프로시저 정의
    ;******************************************************************************
    
    CreateWindowExA     proto   dwExStyle   :dword,
                                pClassName  :LPSTR,
                                pWindowName :LPSTR,
                                dwStyle     :dword,
                                x           :dword,
                                y           :dword,
                                nWidth      :dword,
                                nHeight     :dword,
                                hWndParent  :HANDLE,
                                hMenu       :HANDLE,
                                hInstance   :HANDLE,
                                pParam      :dword
    RegisterClassA      proto   pWndClass   :dword
    GetStockObject      proto   nObject     :dword
    LoadIconA           proto   hInstance   :HANDLE,
                                lpIconName  :LPSTR
    LoadCursorA         proto   hInstance   :HANDLE,
                                lpCurName   :LPSTR
    GetMessageA         proto   lpMsg       :dword,
                                hWnd        :HANDLE,
                                nMsgFilMin  :dword,
                                nMsgFilMax  :dword
    TranslateMessage    proto   lpMsg       :dword
    DispatchMessageA    proto   lpMsg       :dword
    PostQuitMessage     proto   nExitCode   :dword
    DefWindowProcA      proto   hWnd        :HANDLE,
                                Msg         :dword,
                                wParam      :dword,
                                lParam      :dword
    GetModuleHandleA    proto   lpstr       :LPSTR
    ShowWindow          proto   hWnd        :HANDLE,
                                nCmdShow    :dword
    UpdateWindow        proto   hWnd        :HANDLE
    ExitProcess         proto   nExitCode   :dword
    
    ;*****************사용자 정의***********************
    InitInstance        proto   hInstance   :HANDLE
    CreateMyWindow      proto   hInstance   :HANDLE
    
    ;******************************************************************************
    ; 데이터 세그먼트
    ;******************************************************************************
        .data
    
    ClassName   db  'FirstWnd', 0
    WindowName  db  'First Application', 0
    
    ;******************************************************************************
    ; code segment
    ;******************************************************************************
        .code
    
    ;******************************************************************************
    ; 메인 함수
    ;******************************************************************************
    WinMainCRTStartup    proc
        ;*************************
        ; 로컬 변수
        ;*************************
        local    msg     :MSG
        local    hWnd    :HANDLE
        local    hInst   :HANDLE
    
        ;*************************
        ; 프로그램
        ;*************************
        invoke GetModuleHandleA, NULL
        mov     hInst, eax
    
        invoke InitInstance, hInst
        cmp     eax, NULL
        je      ExitProg
    
        invoke CreateMyWindow, hInst
        cmp     eax, NULL
        je      ExitProg
        mov     hWnd, eax
    
        invoke ShowWindow, hWnd, SW_SHOWNORMAL
        invoke UpdateWindow, hWnd
    
    MSGLOOP:
        invoke GetMessageA, addr msg, NULL, 0, 0
        or      eax, eax
        je      ExitProg
    
        invoke TranslateMessage, addr msg
        invoke DispatchMessageA, addr msg
        jmp     MSGLOOP
    
    ExitProg:
        invoke ExitProcess, msg.wParam
    
        ret
    WinMainCRTStartup    endp
    
    ;******************************************************************************
    ; 윈도우 클래스의 등록
    ;******************************************************************************
    InitInstance    proc    hInstance:HANDLE
        ;*************************
        ; 로컬 변수
        ;*************************
        local    wc    :WNDCLASS
    
        ;*************************
        ; 프로그램
        ;*************************
    
        mov     wc.style, 0
        mov     wc.lpfnWndProc, offset MainWndProc
        mov     wc.cbClsExtra, 0
        mov     wc.cbWndExtra, 0
    
        mov     eax, hInstance
        mov     wc.hInstance, eax
    	
        invoke LoadIconA, NULL, IDI_APPLICATION
        mov     wc.hIcon, eax
    
        invoke LoadCursorA, NULL, IDC_ARROW
        mov     wc.hCursor, eax
    
        invoke GetStockObject, WHITE_BRUSH
        mov     wc.hbrBackground, eax
    
        mov     wc.lpszMenuName, NULL
        mov     wc.lpszClassName, offset ClassName
    
        invoke RegisterClassA, addr wc
    
        ret
    InitInstance    endp
    
    ;******************************************************************************
    ; 윈도우의 작성
    ;******************************************************************************
    CreateMyWindow    proc    hInstance:HANDLE
        ;*************************
        ; 프로그램
        ;*************************
    
        invoke CreateWindowExA, WS_EX_APPWINDOW, offset ClassName,
            offset WindowName, WS_SYSMENU or WS_CAPTION, CW_USEDEFAULT,
            CW_USEDEFAULT, 300, 400, NULL, NULL, hInstance, NULL
    
        ret
    CreateMyWindow    endp
    
    ;******************************************************************************
    ; 윈도우 프로시저
    ;******************************************************************************
    MainWndProc    proc    hWnd:HANDLE, Msg:dword, wParam:dword, lParam:dword
    
        .if Msg == WM_DESTROY
            invoke PostQuitMessage, 0
        .else
            invoke DefWindowProcA, hWnd, Msg, wParam, lParam
        .endif
    
        ret
    MainWndProc    endp
    
    end
    

    VC에서 실행(F5)하게 되면 "Line to long"이라고 에러가 나올것이다. 강좌에서는 편의상 여러줄로 했지만,

    CreateWindowExA proto dwExStyle :dword ~ 이부분을 모두 한줄에 빈칸을 줄여서 작성해줘야한다.

    프로그램을 보면 알 수 있듯이, Windows 프로그래밍은 코딩량이 많은것을 알 수가 있다. MASM32에는, 필요한 함수나 구조체의 정의를 정리한 파일을 제공해 주고있다. 그것을 사용하는것이 여러모로 편리할 것이다.

    위의 프로그램에서는, 편의상 데이터형으로 핸들(HANDLE)과 문자열(LPSTR)과 정수로 나누고 있다.

    MASM로 Windows 프로그래밍을 해 본 솔직한 의견으로는 MASM으로 Windows 프로그래밍을 할 정도라면, C 로하는 편이 훨씬 편하다. C와 비교해서 MASM으로 작성했다고 해서 특별히 빨라지는것도 아니기 때문에, 필요한 부분만 MASM 로 작성하는것이 주된 목적일 것이다. 좀 더 심하게 말해서 C의 인라인 어셈블러를 사용하는 것이 압도적으로 좋다.-_-;

     


    Chapter 4   리소스의 이용

    개요 : 리소스를 사용하는 경우

    Chapter 4 - 1   리소스의 사용법

    이번에는 리소스에 대해 설명한다. C언어에서 Windows 리소스를 사용하는것과 별다른 차이점은 없다.

    그런데, 리소스에는 2 종류가 있다. 내부 리소스/외부 리소스라는 것이다.(필자의 사견이다)

    내부 리소스는, 실행 파일에 포함되어있는 리소스이다. 실행 파일에 포함해 두면, 리소스 로딩을 빠르게 할 수가있다. 다만, 리소스가 실행 파일에 포함되므로, 실행파일의 크기가 커지게 되는 현상이 생긴다. 특히 비트맵과같은 것은 사이즈가 크므로 조심하기 바란다.

    외부 리소스는, 문자 그대로 실행 파일에 포함되지 않는 리소스를 의미한다. 그러므로 실행중에 다른 파일로부터 로드하거나 스스로 만들어주지 않으면 사용할 수가 없다. 그런 만큼, 로딩하는 시간이 소모되게 된다.하지만, 메모리의 효율성에서는 더 좋다.

     

    Chapter 4 - 2   인클루드 파일

    상기의 예에서와 같이, 함수를 하나하나식 정의해서 사용한다면 매우 번거롭게 된다. 그래서 MASM32에 포함되어있는 함수와 정수를 정의한 인클루드 파일을 사용하게 된다. 다음의 4개의 파일을 기억해 두면 편리하다.

    1. windows.inc 윈도우관련정수가 정의되어 있는 인클루드 파일
    2. kernel32.inc 커널함수(코어 함수)가 정의되어 있는 파일
    3. user32.inc 유저함수(유저 인터페이스 함수)가 정의되어 있는 파일
    4. gdi32.inc GDI 함수(그래픽 디바이스 인터페이스 함수)가 정의되어 있는 파일

    Chapter 4 - 3   아이콘과 다이얼로그 박스를 사용한 예

    실제로 프로그램을 보자. 이번 예에서 사용하는 리소스는 다이얼로그 박스와 아이콘이다.

     

    ;******************************************************************************
    ; 리소스 사용 프로그램 resource.asm
    ;******************************************************************************
    
    .486
    .model flat, stdcall
    option casemap:none
    
    include <windows.inc>
    include <kernel32.inc>
    include <user32.inc>
    include <gdi32.inc>
    
    include <resource.inc>  ;리소스를 사용하므로 반드시 선언
    
    ;******************************************************************************
    ; prototype
    ;******************************************************************************
    
    DlgProc             proto   hWnd    :HWND, 
                                Msg     :UINT, 
                                wParam  :WPARAM, 
                                lParam  :LPARAM
    WinMainCRTStartup   proto
    OnCommand           proto   hWnd    :HWND,
                                wParam  :WPARAM
    
    ;******************************************************************************
    ;  데이터 / 변수
    ;******************************************************************************
    
    .data
    MSG1    db          "정상적으로 표시되었는가? ", 0
    TITLE1  db          "리소스 테스트", 0
    hInst   HINSTANCE   ?
    
    ;******************************************************************************
    ;  코드
    ;******************************************************************************
    
    .code
    ;------------------------------------------------------------------------------
    ;  다이얼로그 프로시저
    ;------------------------------------------------------------------------------
    DlgProc proc \
        hWnd    : HWND,
        Msg     : UINT,
        wParam  : WPARAM,
        lParam  : LPARAM
    
        .if Msg == WM_INITDIALOG
            invoke LoadIconA, hInst, IDI_MAINICON
            invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax
        .elseif Msg == WM_CLOSE
            invoke EndDialog, hWnd, 0
        .elseif Msg == WM_COMMAND
            invoke OnCommand, hWnd, wParam
        .else
            mov     eax, FALSE
            ret
        .endif
    
        mov     eax, TRUE
        ret
    DlgProc endp
    
    ;------------------------------------------------------------------------------
    ;  커맨드 처리
    ;------------------------------------------------------------------------------
    OnCommand proc \
        hWnd    : HWND,
        wParam  : WPARAM
    
        mov     eax, wParam
        and     eax, 0ffffh
    
        .if eax == IDOK
            invoke SendMessage, hWnd, WM_CLOSE, 0, 0
        .elseif eax == IDCANCEL
            invoke MessageBox, hWnd, addr MSG1, addr TITLE1, MB_OK
        .endif
    
        ret
    OnCommand endp
    
    ;------------------------------------------------------------------------------
    ;  메인 프로시저
    ;------------------------------------------------------------------------------
    WinMainCRTStartup proc
        invoke GetModuleHandle, NULL
        mov     hInst, eax
        invoke DialogBoxParamA, hInst, IDD_TEST, NULL, addr DlgProc, NULL
        invoke ExitProcess, 0
        ret
    WinMainCRTStartup endp
    
    ;------------------------------------------------------------------------------
    ;  End of File
    ;------------------------------------------------------------------------------
    end
    

    위의 프로그램을 어셈블(assemble) 해서 실행해도 다이얼로그 박스는 표시되지 않는다. 왜냐하면, 아직 리소스를 만들지 않았기 때문이다. 다이얼로그 박스를 표시 하기 위해서는, 리소스로서 다이얼로그 박스를 만들어 주어야 한다. 아이콘을 사용한다면, 아이콘 리소스도 필요하다. 만든 리소스를 리소스 컴파일러로 컴파일 해서 실행 파일에 함께 링크 해주어야 리소스를 사용하는 것이 가능해진다. 다음과 같은 리소스를 만들어 보자.

    먼저, 다이얼알로그 박스이다.

    VC++로 작성하는 경우에는, 다음과 같이 프로퍼티를 설정한다.

    프롭퍼티 내용
    ID IDD_TEST = 101
    Style Overlapped
    Border Thin
    More Style 「Center」을 체크

    다음은, 아이콘이다, 하지만 이것은 마음대로 만들어도 좋다. 이 예제에서는, 다음과 같은 아이콘을 준비했다.

    생성한 후 ID는 다음과 같이 지정한다.

    ID IDI_MAINICON = 102

    이제는, resource.inc 파일을 만든다. 이 파일에는 리소스의 ID를 저장해 둔다.

    ;******************************************************************************
    ; 리소스 ID정의 
    ;****************************************************************************** 
    IDD_TEST     = 101 
    IDI_MAINICON = 102
    

    마쳤다면, 프로젝트를 빌드 하고 실행해준다. 이제는 화면에 다이얼로그박스가 표시되었을 것이다.


    Chapter 5   Tips

    개요 : 약간의 Tips

    Chapter 5 - 1   인클루드 파일의 종류

    지금까지, 대략이나마, MASM로 윈도우 프로그래밍을 접해 보았다.

    이번은 조금 다른 얘기로 화제를 돌려보겠다. MASM 는 어셈블러이니까, 어찌보면 어려울 것 같이 생각될 수도 있지만, 실제론 전혀 어렵지는 않다. 오히려 C 를 했던 적이 있는 사람이라면, 아주 쉽게 이해 할 수가 있을 것이다.

    MASM로 Windows 프로그래밍을 하는 경우, 함수를 반드시 정의하지 않으면 안된다. 그 함수를 매번 정의해서 사용하는 편보다는, 미리 헤더 파일에 모아 두고 사용하는 편이 더 좋을 것이다.

    하지만 프로그래머가 직접 만들어서 사용해도 되지만, MASM32 헤더 파일을 사용하는것이 더편리하다.핵심적인 헤더파일을 설명하겠다.

    코어 함수군
    Include File 해설
    windows.inc Windows 프로그래밍에서 사용하는 대부분의 정수의 정의.
    kernel32.inc 커널(코어) 함수. 프로세스 관리, 파일 조작등의 함수가 정의.
    user32.inc 유저 함수. 유저 인터페이스 함수의 정의.
    gdi32.inc GDI 함수. 그래픽 디바이스 인터페이스 함수의 정의.
    advapi32.inc 확장 함수. 암호화, 시큐리티, 레지스트리 조작등의 함수의 정의.
    서브 함수군
    Include File 해설
    comctl32.inc 커먼 컨트롤
    comdlg32.inc common dialog
    d3drm.inc DirectX 3D Retained Mode
    ddraw.inc DirectX DirectDraw
    dinput.inc DirectX DirectInput
    dplayx.inc DirectX DirectPlay
    dsetup.inc DirectX DirectSetup
    dsound.inc DirectX DirectSound
    glaux.inc OpenGL
    glu32.inc OpenGL
    imm32.inc IME API
    lsapi32.inc LS API
    lz32.inc LZ 압축
    netapi32.inc NetAPI
    odbc32.inc ODBC API
    opengl32.inc OpenGL
    scrnsavew.inc Screen Saver API
    setupapi.inc Setup API
    shell32.inc Shell API
    tapi32.inc Telephony API
    wininet.inc Internet API
    winmm.inc Multi Media API
    winspool.inc Printer API
    wsock32.inc WinSock API

    좀 더 있지만, 이 정도로 만족하기 바란다.

    Chapter 5 - 2   A와 W의 의미와 함수 호출 규약

    함수명의 끝에 A 나 W 가 붙어 있는데 이것의 의미는 무엇일까?

    실제로 문자열을 Unicode로 프로그래밍 하는 경우, W가 붙어 있는 함수를 사용한다. A가 붙어 있는 함수는, 문자열을 ASCII 코드로 프로그래밍 한다고 가정하는 것이다.

    W 는 Wide 의 W이며 , A 는 Ascii (혹은 ANSI )의 A 이다.(맞겠지?)

    MASM로 Windows 프로그래밍 하는 경우, 호출 규약에는 C 호출 규약과 STDCALL 의 호출 규약을 이용한다. 약간의 차이점이 존재한다.

    VC++의 경우, 함수는 C 규약으로 통일되어 있다. 함수 수식자로서 __cdecl가 자동적으로 추가된다. API 에서는, __stdcall 로 정의되고 있다.(언더바가 2개이다)

    실제로 WINAPI 나 CALLBACK 라고 하는 수식자는 __stdcall 이라고 정의되어 있다. API 로부터 호출되는 것을 생각하면 당연하다.

    C 호출 규약과 STDCALL 호출 규약의 차이는, 스택을 오픈하는 추최가 누구인가에 따라 다르다. C는 호출한 측에서 스택을 오픈하고, STDCALL 은 함수의 내부에서 스택을 오픈한다. 다음은 C 와 STDCALL 의 호출 규약의 요약이다.

    처리 C
    (__cdecl)
    STDCALL
    (__stdcall)
    함수명의 선두에 _ 를 추가
    인수를 역순으로 스택에 푸쉬
    인수 스택을 호출한 측에서 오픈 ×
    인수 스택을 호출된 측에서 오픈 ×
    VARARG 사용가능

    자세한 부분은 MASM의 도움말을 참조하기 바란다.

    Chapter 5 - 3   프로시저의 prototype 선언

    함수(프로시저)의 호출을 수행할 경우, invoke 지시어를 사용하면 편하다.하지만 미리 prototype 선언을 수행해 주어야 한다. 또한 invoke 지시어는, 명시적으로 호출규약을 지정해 주어야만 사용할 수가 있다.

    invoke 지시어는, 매크로와 비슷한 것으로, invoke를 사용하는 편이 함수호출이 더 수월해진다. 스택 처리를 자동화 해주기 때문이다.

    또한, addr 와 offset 는 모두 변수 혹은 정수의 주소를 반환한다.차이점으로는,

    offset 지시어만으로 프로그래밍할려면, 상당한 힘든경우가 있다. offset지시어는, 실행시의 스택주소를 얻을수 없기때문이다.이름 그대로, offset 은 식별자의 오프셋(offset)주소를 구해준다.

    addr를 사용함으로써 , 실행시의 스택변수의 주소를 얻을 수가 있다.

    Chapter 5 - 4   NULL와'\0'

    한가지 주의할 것이, NULL = 0 혹은, '\0' = 0 아니다. 확실히, 헤더파일을 보면, NULL = 0 으로 정의되어 있다. 그러나 그것은 편의상 정의된것이므로, 의미적으로는 다르다. 혹시 다른 개발환경에는 NULL = -1 라고 정의되어 있을지도 모른다.

    포인터나 핸들등이 정해지지 않았을 경우에는, 반드시 NULL 을 사용하도록 한다. Windows 로부터 구해지는 모든 핸들은, 실제로 (void *) 형태이므로 NULL을 사용하는 이유를 알 수 있을 것이다.

    또다시 , C 언어 이야기로 되었지만, '\0'는 문자열의 끝을 나타내는 기호상수이므로, 문자열 외에서 사용해서는 안된다. NULL도 같이 맥락으로 정해진 용도 이외에는 사용하지 않는 것이 좋다.

     

    Chapter 5 - 5   ExitProcess

    Windows 프로그램을 종료할 경우에는 반드시, ExitProcess를 호출하도록 하자. 물론, 사용하지 않아도 프로그램을 종료할 수 있다.

    하지만, DLL을 호출했을 경우, ExitProcess를 사용하지 않고 프로세스를 종료하면, 프로세스에서 로딩된 DLL이 메모리에 남게된다. ExitProcess를 호출하므로써, DLL에 프로그램이 종료하는 것을 전달되고, DLL도 메모리에서 해제된다.

     


    Chapter 6   텍스트뷰어

    개요 : 텍스트뷰어 작성

    Chapter 6 - 1   텍스트뷰어 프로그램

    지금까지의 단편된 지식을 이용해서, 이번에는 좀 더 실용적인(??) 프로그램을 만들어 보자. 실용적이라지만 단순히 텍스트 파일을 열고 화면에 내용을 표시하는 단순한 프로그램이다. API등의 지식은 MSDN이나 기타참조를 하기 바란다.

    만드는 방법은 아주 간단하다. 윈도우의 작업영역에 에디트박스를 생성하면 끝이다.

    실제로, WM_CREATE 메세지에서,작업영역에 에디트박스를 생성해주면 된다. 그림으로 표시하면 다음과 같다.

    파일을 여는 기능만 수행하여, 파일의 내용을 볼수 있도록만 한다.

    프로그램은 다음과 같다. → 프로젝트(VC++용)

     ; --------------------------------------------------------------------
     ;
     ;  Text Viewer main.asm
     ;
     ; --------------------------------------------------------------------
    .586
    .model flat, stdcall
    option casemap:none
    
    include <windows.inc>
    include <kernel32.inc>
    include <user32.inc>
    include <gdi32.inc>
    include <comdlg32.inc>
    
    include <resource.inc>
    
     ; --------------------------------------------------------------------
     ;
     ;  Procedure Definitions
     ;
     ; --------------------------------------------------------------------
    
    InitInstance        proto   hInstance   :HINSTANCE
    CreateMyWindow      proto   hInstance   :HINSTANCE
    OnCreate            proto   hWnd        :HWND
    OnSize              proto   hWnd        :HWND
    OnCommand           proto   hWnd        :HWND, 
                                id          :UINT
    OnFileOpen          proto   hWnd        :HWND
    
    
     ; --------------------------------------------------------------------
     ;
     ;  Data
     ;
     ; --------------------------------------------------------------------
    
    .data
    
    CLASSNAME   DB  'FirstWnd', 0
    TCLASSNAME  DB  'EDIT', 0
    APPNAME     DB  '텍스트뷰어', 0
    FILTER      DB  '텍스트 파일 (*. txt)', 0, '*. txt', 0
                DB  '워드 파일 (*. doc)', 0, '*. doc', 0, 0
    
    
    g_hEditWnd  HWND    NULL
    g_szFile    DB  260 DUP (0)
    
     
     ; --------------------------------------------------------------------
     ;
     ;  Code
     ;
     ; --------------------------------------------------------------------
    
    .code
    
     ;******************************************************************************
     ; 메인 함수
     ;******************************************************************************
    
    WinMainCRTStartup proc
        local    msg     :MSG
        local    hWnd    :HWND
        local    hInst   :HINSTANCE
    
        invoke GetModuleHandleA, NULL
        mov     hInst, eax
    
        invoke InitInstance, hInst
        cmp     eax, NULL
        je      _Exit
    
        invoke CreateMyWindow, hInst
        cmp     eax, NULL
        je      _Exit
        mov     hWnd, eax
    
        invoke ShowWindow, hWnd, SW_SHOWNORMAL
        invoke UpdateWindow, hWnd
    
     _MsgLoop:
        invoke GetMessageA, addr msg, NULL, 0, 0
        or      eax, eax
        je      _Exit
    
        invoke TranslateMessage, addr msg
        invoke DispatchMessageA, addr msg
        jmp     _MsgLoop
    
     _Exit:
        invoke ExitProcess, msg.wParam
        ret
    WinMainCRTStartup endp
    
     ;******************************************************************************
     ; 윈도우 클래스의 등록 
     ;
     ;   ATOM InitInstance hInstance:HISNTANCE
     ;******************************************************************************
    
    InitInstance proc \
        hInstance       :HINSTANCE
        local   wc      :WNDCLASS
    
        mov     wc.style, 0
        mov     wc.lpfnWndProc, offset MainWndProc
        mov     wc.cbClsExtra, 0
        mov     wc.cbWndExtra, 0
    
        mov     eax, hInstance
        mov     wc.hInstance, eax
    	
        invoke LoadIconA, hInstance, IDI_MAIN_ICON
        mov     wc.hIcon, eax
    
        invoke LoadCursorA, NULL, IDC_ARROW
        mov     wc.hCursor, eax
    
        invoke GetStockObject, WHITE_BRUSH
        mov     wc.hbrBackground, eax
    
        mov     wc.lpszMenuName, IDR_MAIN_MENU
        mov     wc.lpszClassName, offset CLASSNAME
    
        invoke RegisterClassA, addr wc
    
        ret
    InitInstance endp
    
     ;******************************************************************************
     ; 윈도우의 작성
     ;
     ;   HWND CreateMyWindow hInstance:HINSTANCE
     ;******************************************************************************
    
    CreateMyWindow proc \
        hInstance   :HINSTANCE
    
        invoke CreateWindowExA,
            WS_EX_APPWINDOW, 
            offset CLASSNAME,
            offset APPNAME, 
            WS_OVERLAPPEDWINDOW, 
            CW_USEDEFAULT,
            CW_USEDEFAULT, 
            CW_USEDEFAULT, 
            CW_USEDEFAULT, 
            NULL, 
            NULL, 
            hInstance, 
            NULL
    
        ret
    CreateMyWindow endp
    
     ;******************************************************************************
     ; 윈도우 프로시저
     ;   
     ;   LRESULT MainWndProc hWnd:HWND, Msg:DWORD, wParam:DWORD, lParam:DWORD
     ;******************************************************************************
    
    MainWndProc proc \
        hWnd    :HWND, 
        Msg     :DWORD, 
        wParam  :DWORD, 
        lParam  :DWORD
    
        .if Msg == WM_DESTROY
            invoke PostQuitMessage, 0
        .elseif Msg == WM_CREATE
            invoke OnCreate, hWnd
        .elseif Msg == WM_SIZE
            invoke OnSize, hWnd
        .elseif Msg == WM_COMMAND
            mov     eax, wParam
            and     eax, 0000FFFFh
            invoke OnCommand, hWnd, eax
        .else
            invoke DefWindowProcA, hWnd, Msg, wParam, lParam
            ret
        .endif
    
        mov     eax, 0
        ret
    MainWndProc endp
    
     ;******************************************************************************
     ; WM_CREATE 메세지의 처리
     ;
     ;   BOOL OnCreate hWnd : HWND
     ;******************************************************************************
    
    OnCreate proc \
        hParentWnd      :HWND
        local   hInst   :HINSTANCE
        local   rc      :RECT
    
        invoke GetModuleHandle, NULL
        mov     hInst, eax
    
        invoke GetClientRect, hParentWnd, addr rc
    
        invoke CreateWindowExA,
            WS_EX_CLIENTEDGE, 
            offset TCLASSNAME,
            NULL, 
            WS_VISIBLE or WS_CHILD or ES_MULTILINE or ES_AUTOHSCROLL \
            or ES_AUTOVSCROLL or WS_HSCROLL or WS_VSCROLL, 
            0,
            0, 
            rc.right, 
            rc.bottom, 
            hParentWnd, 
            NULL, 
            hInst, 
            NULL
        mov     g_hEditWnd, eax
    
        invoke SendMessage, g_hEditWnd, EM_SETLIMITTEXT, 0FFFFh, 0
    
        mov     eax, TRUE
        ret
    OnCreate endp
    
     ;******************************************************************************
     ; WM_SIZE 메세지의 처리
     ;
     ;   void OnSize hWnd :HWND
     ;******************************************************************************
    
    OnSize proc \
        hWnd        :HWND
        local   rc  :RECT
    
        invoke GetClientRect, hWnd, addr rc
    
        invoke SetWindowPos, g_hEditWnd, NULL, 0, 0, 
            rc.right, rc.bottom, SWP_NOMOVE or SWP_NOZORDER
        ret
    OnSize endp
    
     ;******************************************************************************
     ; WM_COMMAND 메세지의 처리
     ;
     ;   void OnCommand hWnd:HWND, id:UINT
     ;******************************************************************************
    
    OnCommand proc \
        hWnd    :HWND,
        id      :UINT
        
        .if id == IDM_EXIT
            invoke SendMessage, hWnd, WM_CLOSE, 0, 0
        .elseif id == IDM_FOPEN
            invoke OnFileOpen, hWnd
        .endif
    
        ret
    OnCommand endp
    
     ;******************************************************************************
     ; IDM_FOPEN 메뉴 메세지의 처리
     ;
     ;   void OnFileOpen hWnd:HWND
     ;******************************************************************************
    
    OnFileOpen proc \
        hWnd            :HWND
        local   ofn     :OPENFILENAME
        local   hFile   :HANDLE
        local   pBuffer :DWORD
        local   dwRead  :DWORD
    
        ;  파일 오픈 다이얼로그 표시
        invoke RtlZeroMemory, addr ofn, sizeof OPENFILENAME
    
        mov     ofn.lStructSize, sizeof OPENFILENAME
        mov     eax, hWnd
        mov     ofn.hWndOwner, eax
        mov     ofn.lpstrFile, offset g_szFile
        mov     ofn.nMaxFile, 260
        mov     ofn.lpstrFilter, offset FILTER
        mov     ofn.nFilterIndex, 1
        mov     ofn.lpstrFileTitle, NULL
        mov     ofn.nMaxFileTitle, 0
        mov     ofn.lpstrInitialDir, NULL
        mov     ofn.Flags, OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST \
                or OFN_HIDEREADONLY
    
        invoke GetOpenFileName, addr ofn
    
        or      eax, eax
        je      _OnFileExit
    
        ; 파일을 연다
        invoke CreateFile, ofn.lpstrFile, GENERIC_READ, 0, NULL, 
            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
        mov     hFile, eax
    
        ; 데이터용 버퍼의 확보
        invoke GlobalAlloc, GPTR, 0FFFFh
        mov     pBuffer, eax
    
        ; 데이터의 해석
        invoke ReadFile, hFile, pBuffer, 0FFFFh, addr dwRead, NULL
    
        ; 데이터의 기록
        invoke SetWindowText, g_hEditWnd, pBuffer
    
        ; 버퍼를 삭제
        invoke GlobalFree, pBuffer
    
        ; 파일 핸들을 닫는다
        invoke CloseHandle, hFile
    
     _OnFileExit:
        ret
    OnFileOpen endp
    
    end
    

    파일을 선택하는 기능은, common dialog의 파일 오픈 다이얼로그를 사용하고 있다. 이것을 사용하는 것으로, 아주 쉽게 파일선택을 할 수가 있다. 자세한 내용은 프로그램을 참조하기 바란다.

    빌드 할 때의 주의 사항으로는, common dialog 박스를 사용하므로, common dialog 박스를 사용하기 위해서는, common dialog 박스의 동적 링크 라이브러리(DLL)의 라이브러리를 링크 해주어야 한다. (commlib.lib ?부분 참조 )


    Chapter 7   FPU로 수치 연산

    개요 : 부동 소수점 연산에 대해

    Chapter 7 - 1   부동 소수점 연산에 대해

    FPU를(Float Point Unit) 이용한 부동 소수점 연산에 대해, 설명을 한다. 통합 환경으로는 VC++ 를 사용하지만,굳이 VC++가 아니여도 사용할 수 있다. 실제 계산 인스트럭션에 대해서도, 본 설명서에서 자세히 설명하지 않는다. 인텔 메뉴얼을 참조하기 바란다.

    이글을 보고 있는 사람은, FPU의 기능을 모르는 사람은 없다는 가정이다. FPU는, 부동 소수점 연산용을 하기 위한 전용 유닛이다. 80386 CPU 까지는 옵션사항이 였지만, 80486부터는 표준으로 CPU내부에 장착되어 있다.

    (80486SX는 예외)

    FPU에는, 몇개의 레지스터와 전용 인스트럭션이 존재한다. 계산용 레지스터로 8개의 스택타입의 레지스터가 장착되어있다. 계산 인스트럭션에는, 가감승제, 초월함수, 삼각함수 등 수치 계산에 필요한 기능은 대부분 제공되어 있다.

    Chapter 7 - 2   FPU용 레지스터

    부동 소수점 계산용의 레지스터로 8개의 레지스터가 준비되어 있다. 하지만, 왜 스택형식의 레지스터일까?

    실제로 계산을 하려면 스택을 사용하는 것이 여러모로 편하다. 이것은 알고리즘책등에서 스택을 이용해 계산하는것을 본사람도 있을것이다.

    실제로 역폴랜드 기법의 계산법으로 프로그래밍 할것이다. 그러므로 레지스터가 몇개 있는지를 의식하지 않아도, 프로그래밍을 해 나갈 수가 있다. (물론 8개를 넘겨서 프로그램을 작성하는 경우에는, 문제가 있다) 이런 방식은 , 스택에 독립적으로 데이터를 읽어들이고,, 역폴랜드식의 계산방법을 무시하고 계산하면, FPU를 사용하는 것기능에도 불구하고 늦어지는 일이 발생할 수 있으므로 주의가 필요하다.

    계산시에 특정스택을 다루고 싶은 경우는, st(n)라는 표기법을 사용한다. n 에는, 현재스택으로부터 n 번째의 스택을 지정한다. 예를 들면, 현재 스택 탑으로부터 한단계전의 스택내용을 참조하고 싶은 경우에는, st(1)로 사용한다. 2개전이라면 st(2)로 한다.

    다만, 미사용영역의 스택에 액세스 하게되면 에러가 된다.

    Chapter 7 - 3   프로그래밍의 방법

    실제로, 아래와 같은 계산을 수행해보자.

    ( 5.98 + 34.89 ) * (34.89 - 23.0)
    

    이 계산을 실제로 부동 소수점 인스트럭션을 이용해서 프로그래밍하면, 다음과 같다.

    ; _a = 5.98, _b = 34.89, _c = 23.0 으로 한다
    
    fld     _a  ; ①FPU스택에 _a 의 값을 푸쉬
    fadd    _b  ; ②현재스택의 값과 _b 의 값을 더한다
    
    fld     _b  ; ③FPU스택에 _b 의 값을 푸쉬
    fsub    _c  ; ④현재스택의 값으로부터 _c 의 값을 뺀다
    
    fmul        ; ⑤현재스택과 한단계전의 스택값을 곱해서 한단계전의 스택에 푸쉬
    

    이런 계산을 할 경우, 스택에 실제로 어떻게 데이터가 싸이는지 알아보자.스택은 다음과 같이 운용된다.

    Chapter 7 - 4   실제 프로그래밍

    그렇다면, 실제로 프로그램을 작성해보자. 수치 연산의 부분을 MASM로 작성하고, C언어로 그 연산을 호출하는 형태로 작성한다. 먼저 C언어의 프로그램은 다음과 같다.

    #include <stdio.h>
    
    #ifdef __cplusplus
    extern "C" double func(double a, double b, double c);
    #endif
    
    int main()
    {
        printf("func() = %f\n", func(5.98, 34.89, 23.0));
    
        return 0;
    }

    func라고 하는 함수는 MASM로 작성한다. func함수의 내용은 다음과 같다.

    .586
    .model flat, c
    
    .code
    
    func proc \
        _a: real8,
        _b: real8,
        _c: real8
    
        fld     _a
        fadd    _b
        fld     _b
        fsub    _c
        fmul
    
        ret
    func endp
    
    end
    

    함수의 반환값은, 스택의 탑에 넣어 둔다.

    예를 들어서 다음과 같은 수식을 프로그래밍해보자.

    (1) a * (b + c)
    (2) |a + b| * sin(c)
    (3) a * √(a + b) * cos(c)

    각각 다음과 같이 된다.

    ; (1) a * (b + c)
    
    fld     a
    fld     b
    fadd    c
    fmul
    
    ; (2) |a + b| * sin(c)
    
    fld     a
    fadd    b
    fabs
    fld     c
    fsin
    fmul
    
    ; (3) a * √(a + b) * cos(c)
    
    fld     a
    fld     a
    fadd    b
    fsqrt
    fld     c
    fcos
    fmul
    fmul

    부동 소수점 계산은 이런식이다. 크게 어렵지 않다. 부동 소수점 연산의 에러 처리는, 인텔 메뉴얼을 참조하기 바란다.


    Chapter 8   MMX의 SIMD 연산

    개요 : MMX를 사용

    Chapter 8 - 1   MMX 연산의 개요

    ■MMX란?

    먼저 MMX 명령에 대해 설명한다.

    MMX란, 단순한 데이터 요소로부터 대규모 배열에 대한 반복 연산을 수행한다. 용도로는, 동영상, 그래픽, 비디오의 결합, 화상 처리, 오디오 합성, 음성 합성, 압축등이 있다.

    말이 어려울 것이다. 실제로 샘플 프로그램을 VC++로 작성해서 알아보자.

    ■MMX 인스트럭션의 개요

    MMX 인스트럭션은, SIMD (Single Instruction , Multiple - Data) 실행 모델이라는 형태를 취한다. 무슨 말인가하면, 하나의 인스트럭션으로 복수의 데이터를 한번에 계산한다는 것이다.

    배열 char A[8] 과 char B[8] 라는 배열이 있었다고 가정하자.배열 한개의 요소를 MMX 인스트럭션을 사용하면 한번에 계산 할 수가 있다. 그림으로 설명하면 다음과 같다.

    위의 예에서 알 수 있듯이 하나의 인스트럭션으로 한번에 병렬 연산을 수행하게 된다. 이런식으로 화상 처리나 오디오 합성, 음성 합성의 처리 능력을 크게 향상시킬 수가 있다.

    MMX는, 8 개의 64 비트 MMX 레지스터 (MM0, MM1, ... MM7)를 가지고 있다. MMX레지스터는, 8개의 FPU 레지스터를 공유해서 사용하므로, 부동 소수점 연산과 MMX 연산을 혼합해서 사용할 경우에는 주의가 필요하다.

    MMX 연산에는, 4가지의 데이터형을 계산할 때에 이용한다. Packed Byte/Packed Word/Packed Double Word/Quad Word이다. Packed Byte라는 것은, 64 비트 레지스터를 8비트씩 8개의 영역으로 나눈 형태의 데이터형이다. 그림으로 설명하면, 아래의 그림과 같다. 상자로 둘러싸여 있기 때문에, 팩(Packed)이라고 부른다.

    Chapter 8 - 2   MMX를 사용할 수 있는지 체크

    MMX를 사용하기 전에 MMX 인스트럭션을 사용할 수가 있는지 체크 할 필요가 있다.

    체크루틴에는 CPUID 인스트럭션을 사용한다. 구체적인 프로그램은 다음과 같다.(인라인어셈 사용)

    /* CPUID의 존재 체크 */
    
    BOOL IsCPUID()
    {
        DWORD dwPreEFlags, dwPostEFlags;
    
        _asm {
            /* EFlags 의 값획득 */
            pushfd
            pop     eax
            mov     dwPreEFlags, eax
    
            /* CPUID 인스트럭션의 존재체크 (ID 플래그 체크) */
            xor     eax, 00200000h
            push    eax
            popfd
    
            /* EFlags 의 값의 획득 */
            pushfd
            pop     eax
            mov     dwPostEFlags, eax
        }
    
        if(dwPreEFlags == dwPostEFlags)
            return FALSE;
        return TRUE;
    }
    
    /* MMX 가능여부 체크 */
    
    BOOL IsMMX()
    {
        DWORD dwRetEDX;
    
        /* CPUID 인스트럭션 체크 */
        if(! IsCPUID()) return FALSE;
    
        /* MMX 가능여부 체크 */
        _asm {
            mov     eax, 1
            cpuid
            and     edx, 00800000h
            mov     dwRetEDX, edx
        }
        if(dwRetEDX) return TRUE;
        return FALSE;
    }

    Chapter 8 - 3   포화 연산·비포화 연산

    MMX 연산에는 포화 연산과 비포화 연산이 있다. 포화 연산은, 연산으로 오버플로우/언더 플로우(underflow)가 발생했을 경우 그 최대값/최소값을 자동으로 대입해 주는 연산이다. 비포화의 경우는 그것을 수행하지 않는다.

    구체적인 예를 들겠다. Packed Byte에서 MMX 무부호 연산을 생각해 보자. 한개의 요소가 240 + 23 = 263 으로, 오버플로우가 발생 했을 경우, 자동으로 최대값을 대입해 준다. 즉 이 경우는, 255 가 대입된다. 다음의 그림을 보면 이해할 수 있을 것이다.

    덧붙이면 PADDUSB 라는 것은, 무부호 포화 가산 연산을 수행하는 MMX 인스트럭션이다. (Packed Add Unsigned with Satuation Byte)

    MMX 인스트럭션에는 다음과 같은 것이 있다. 간단히 설명한다.

    데이터 전송 인스트럭션
    MOVD 32 비트 데이터 전송 인스트럭션
    MOVQ 64 비트 데이터 전송 인스트럭션
    산술 가감(상태) 연산 인스트럭션
    PADDx / PSUBx 비포화 연산 인스트럭션 x = B(BYTE) / W(WORD) / D(DWORD)
    PADDSx / PSUBSx 부호 첨부 포화 연산 인스트럭션 x = B(BYTE) / W(WORD)
    PADDUSx / PSUBUSx 무부호 포화 연산 인스트럭션 x = B(BYTE) / W(WORD)
    곱셈 인스트럭션
    PMULHx / PMULLx 곱셈 인스트럭션 x = W(WORD) / D(DWORD)
    논리 연산 인스트럭션
    PAND / PANDN / POR / PXOR  
    쉬프트 명령
    PSLLx / PSRLx 좌/우논리 쉬프트
    PSRAx 오른쪽 산술 쉬프트
    변환 인스트럭션
    PACKSSxx 부호화 포화 팩화 xx = WB / DW
    PACKUSxx 무부호 포화 팩화 xx = WB
    PUNPCKHxx 상위 unpack화 xx = BW / WD / DQ
    PUNPCKLxx 하위 unpack화 xx = BW / WD / DQ
    제어
    EMMS MMX 의 종료

    마지막의 EMMS 인스트럭션은, MMX의 사용을 종료했을 때에 반드시 수행해야 한다. MMX는 FPU의 레지스터를 공유해서 사용하므로, 명시적으로 레지스터를 EMMS 인스트럭션으로 초기화해 두지 않으면 안 되기 때문이다.

    Chapter 8 - 4   실제 프로그램·알파블렌딩

    실제로 MMX를 사용해서 연산해 보자. 이번에는 2개의 데이터의 덧셈을 실시한다. 만약 화상자료일 경우라면,「알파블렌딩(Add Alpha-Blending)」이라는 기술이다.(반투명 처리)

     /* Dest = Dest + Src 를 수행한다
     * 데이터의 길이는 8의 배수여야만 한다
     */
    
    void AddData(BYTE *lpDestBuf, BYTE *lpSrcBuf1, BYTE *lpSrcBuf2, DWORD nLength)
    {
        _asm {
            /* 초기화 */
            mov     ecx, nLength;
            shr     ecx, 3
            mov     edi, lpDestBuf
            mov     ebx, lpSrcBuf1
            mov     edx, lpSrcBuf2
    
            /* 루프 연산 */
        ADD_LOOP:
            movq    mm0, [ebx]
            movq    mm1, [edx]
    
            /* 데이터 가산 */
            paddusb mm0, mm1
    
            /* 결과를 저장 */
            movq    [edi], mm0
    
            /* 증감(increment) */
            add     edi, 8
            add     ebx, 8
            add     edx, 8
    
            dec     ecx
            jnz     ADD_LOOP
    
            /* MMX 종료 */
            emms
        }
    }

    이런 간단한 예제로, MMX에 대해서 안다는 것은 불가능하다. 이런 부분을 기초로 해서 응용해 나가면, 화상 처리와 같은 부분에서 처리능력을 크게 개선할 수가 있다. 자 이제부터는 스스로 여러가지 프로그램을 짜 보고 실감해 보기바란다.



    반응형
    반응형