반응형
출처 : http://www.microsoft.com/whdc/devtools/debugging/debugstart.mspx

Debugging Tools and Symbols: Getting Started

Debugging Tools for Windows is a set of extensible tools for debugging device drivers for the Microsoft Windows family of operating systems. Debugging Tools for Windows supports debugging of:

Applications, services, drivers, and the Windows kernel.

Native 32-bit x86, native Intel Itanium, and native x64 platforms.

Microsoft Windows NT 4.0, Windows 2000, Windows XP, Microsoft Windows Server 2003, Windows Vista and Windows Server 2008.

User-mode programs and kernel-mode programs.

Live targets and dump files.

Local and remote targets.

In addition to Debugging Tools for Windows, effective debugging also requires:

Special debugging routines, macros, and global variables in the Windows Driver Development Kit (DDK). You can use these routines in your driver code to send messages to a debugger and set breakpoints to aid in debugging.

Access to Windows symbol files. If you have access to the Internet while debugging, you can set your debugger's symbol path to point to the Windows symbol server. If you do not have access to the Internet while debugging, you can download symbols in advance from the Microsoft website.

Debugging Tools for Windows includes WinDbg, a powerful debugger with a graphical interface and a console interface, as well as the console-based debuggers NTSD, CDB, and KD.

About WinDbg
WinDbg provides source-level debugging through a graphical user interface and a text-based interface.

WinDbg uses the Microsoft Visual Studio debug symbol formats for source-level debugging. It can access any public function's names and variables exposed by modules that were compiled with Codeview (.pdb) symbol files.

WinDbg can view source code, set breakpoints, view variables (including C++ objects), stack traces, and memory. It includes a command window to issue a wide variety of commands not available through the drop-down menus. For kernel-mode debugging, WinDbg typically requires two computers (the host machine and the target machine). It also allows you to remotely debug user-mode code.

To Prepare for a Debugging Session

1.

If you want to debug kernel-mode code, set up a host computer to run the debugger and a target computer to run the code being debugged. Connect the two computers using either an IEEE 1394 cable or a null-modem cable between two serial ports.

2.

Download and install the latest version of Debugging Tools for Windows on the host computer. The tools can also be installed from the Windows DDK, the Platform SDK, or the Customer Support Diagnostics CD.

3.

Use Windows Symbol Server to access the symbol files from the host computer.

About the Microsoft Symbol Server

Symbol server technology is built into Debugging Tools for Windows. Microsoft provides access to an Internet symbol server that contains symbol files for the Microsoft Windows Server 2003, Windows XP, and Windows 2000 operating systems, as well as other Microsoft products.

The Internet symbol server is populated with a variety of Windows symbols for Microsoft Windows operating systems, including hot fixes, Service Packs, Security Rollup Packages, and retail releases. Symbols are also available on the server for current Betas and Release Candidates for Windows products, plus a variety of other Microsoft products, such as Microsoft Internet Explorer.

If you have access to the Internet during debugging, you can configure the debugger to download symbols as needed during a debugging session, rather than downloading symbol files separately before a debugging session. The symbols are downloaded to a directory location that you specify and then the debugger loads them from there.

To use the Microsoft Symbol Server

1.

Make sure you have installed the latest version of Debugging Tools for Windows.

2.

Start a debugging session.

3.

Decide where to store the downloaded symbols (the "downstream store"). This can be a local drive or a UNC path.

4.

Set the debugger symbol path as follows, substituting your downstream store path for DownstreamStore.

SRV*DownstreamStore*http://msdl.microsoft.com/download/symbols

For example, to download symbols to c:\websymbols, you would add the following to your symbol path:
SRV*c:\websymbols*http://msdl.microsoft.com/download/symbols

Note: http://msdl.microsoft.com/download/symbols is not browsable and is intended only for access by the debugger. For more information on setting symbol paths and using symbol servers, see the documentation accompanying Debugging Tools for Windows.

Debugger and Symbols Downloads

Tools Download Windows Symbol Packages
Tools Install Debugging Tools for Windows 32-bit Version  
Tools Install Debugging Tools for Windows 64-bit Versions  

반응형
반응형
http://msdn.microsoft.com/en-us/library/ms810467.aspx

Serial Communications in Win32
 

Allen Denver
Microsoft Windows Developer Support

December 11, 1995

Applies to:
   Microsoft® Win32®
   Microsoft Windows®


Summary: Learn how serial communications in Microsoft Win32 is significantly different from serial communications in 16-bit Microsoft Windows. This article assumes a familiarity with the fundamentals of multiple threading and synchronization in Win32. In addition, a basic understanding of the Win32 heap functions is useful to fully comprehend the memory management methods used by the Multithreaded TTY (MTTTY) sample included with this article. (35 printed pages)

Download the MTTTY sample (4918.exe) for this technical article.

Contents

Overview
Introduction
Opening a Port
Reading and Writing
Serial Status
Serial Settings
Conclusion
Bibliography

Overview

Serial communications in Microsoft® Win32® is significantly different from serial communications in 16-bit Microsoft Windows®. Those familiar with 16-bit serial communications functions will have to relearn many parts of the system to program serial communications properly. This article will help to accomplish this. Those unfamiliar with serial communications will find this article a helpful foundation for development efforts.

This article assumes you are familiar with the fundamentals of multiple threading and synchronization in Win32. In addition, a basic familiarity of the Win32 heap functions is useful to fully comprehend the memory management methods used by the sample, MTTTY, included with this article.

For more information regarding these functions, consult the Platform SDK documentation, the Microsoft Win32 SDK Knowledge Base, or the Microsoft Developer Network Library. Application programming interfaces (APIs) that control user interface features of windows and dialog boxes, though not discussed here, are useful to know in order to fully comprehend the sample provided with this article. Readers unfamiliar with general Windows programming practices should learn some of the fundamentals of general Windows programming before taking on serial communications. In other words, get your feet wet before diving in head first. (36 printed pages)

Introduction

The focus of this article is on application programming interfaces (APIs) and methods that are compatible with Microsoft Windows NTand Windows 95; therefore, APIs supported on both platforms are the only ones discussed. Windows 95 supports the Win32 Telephony API (TAPI) and Windows NT 3.x does not; therefore, this discussion will not include TAPI. TAPI does deserve mention, however, in that it very nicely implements modem interfacing and call controlling. A production application that works with modems and makes telephone calls should implement these features using the TAPI interface. This will allow seamless integration with the other TAPI-enabled applications that a user may have. Furthermore, this article does not discuss some of the configuration functions in Win32, such as GetCommProperties.

The sample included with this article, MTTTY: Multithreaded TTY (4918.exe), implements many of the features discussed here. It uses three threads in its implementation: a user interface thread that does memory management, a writer thread that controls all writing, and a reader/status thread that reads data and handles status changes on the port. The sample employs a few different data heaps for memory management. It also makes extensive use of synchronization methods to facilitate communication between threads.

Opening a Port

The CreateFile function opens a communications port. There are two ways to call CreateFile to open the communications port: overlapped and nonoverlapped. The following is the proper way to open a communications resource for overlapped operation:

HANDLE hComm;
hComm = CreateFile( gszPort,  
                    GENERIC_READ | GENERIC_WRITE, 
                    0, 
                    0, 
                    OPEN_EXISTING,
                    FILE_FLAG_OVERLAPPED,
                    0);
if (hComm == INVALID_HANDLE_VALUE)
   // error opening port; abort

Removal of the FILE_FLAG_OVERLAPPED flag from the call to CreateFile specifies nonoverlapped operation. The next section discusses overlapped and nonoverlapped operations.

The Platform SDK documentation states that when opening a communications port, the call to CreateFile has the following requirements:

  • fdwShareMode must be zero. Communications ports cannot be shared in the same manner that files are shared. Applications using TAPI can use the TAPI functions to facilitate sharing resources between applications. For Win32 applications not using TAPI, handle inheritance or duplication is necessary to share the communications port. Handle duplication is beyond the scope of this article; please refer to the Platform SDK documentation for more information.
  • fdwCreate must specify the OPEN_EXISTING flag.
  • hTemplateFile parameter must be NULL.

One thing to note about port names is that traditionally they have been COM1, COM2, COM3, or COM4. The Win32 API does not provide any mechanism for determining what ports exist on a system. Windows NT and Windows 95 keep track of installed ports differently from one another, so any one method would not be portable across all Win32 platforms. Some systems even have more ports than the traditional maximum of four. Hardware vendors and serial-device-driver writers are free to name the ports anything they like. For this reason, it is best that users have the ability to specify the port name they want to use. If a port does not exist, an error will occur (ERROR_FILE_NOT_FOUND) after attempting to open the port, and the user should be notified that the port isn't available.

Reading and Writing

Reading from and writing to communications ports in Win32 is very similar to file input/output (I/O) in Win32. In fact, the functions that accomplish file I/O are the same functions used for serial I/O. I/O in Win32 can be done either of two ways: overlapped or nonoverlapped. The Platform SDK documentation uses the terms asynchronous and synchronous to connote these types of I/O operations. This article, however, uses the terms overlapped and nonoverlapped.

Nonoverlapped I/O is familiar to most developers because this is the traditional form of I/O, where an operation is requested and is assumed to be complete when the function returns. In the case of overlapped I/O, the system may return to the caller immediately even when an operation is not finished and will signal the caller when the operation completes. The program may use the time between the I/O request and its completion to perform some "background" work.

Reading and writing in Win32 is significantly different from reading and writing serial communications ports in 16-bit Windows. 16-bit Windows only has the ReadComm and WriteComm functions. Win32 reading and writing can involve many more functions and choices. These issues are discussed below.

Nonoverlapped I/O

Nonoverlapped I/O is very straightforward, though it has limitations. An operation takes place while the calling thread is blocked. Once the operation is complete, the function returns and the thread can continue its work. This type of I/O is useful for multithreaded applications because while one thread is blocked on an I/O operation, other threads can still perform work. It is the responsibility of the application to serialize access to the port correctly. If one thread is blocked waiting for its I/O operation to complete, all other threads that subsequently call a communications API will be blocked until the original operation completes. For instance, if one thread were waiting for a ReadFile function to return, any other thread that issued a WriteFile function would be blocked.

One of the many factors to consider when choosing between nonoverlapped and overlapped operations is portability. Overlapped operation is not a good choice because most operating systems do not support it. Most operating systems support some form of multithreading, however, so multithreaded nonoverlapped I/O may be the best choice for portability reasons.

Overlapped I/O

Overlapped I/O is not as straightforward as nonoverlapped I/O, but allows more flexibility and efficiency. A port open for overlapped operations allows multiple threads to do I/O operations at the same time and perform other work while the operations are pending. Furthermore, the behavior of overlapped operations allows a single thread to issue many different requests and do work in the background while the operations are pending.

In both single-threaded and multithreaded applications, some synchronization must take place between issuing requests and processing the results. One thread will have to be blocked until the result of an operation is available. The advantage is that overlapped I/O allows a thread to do some work between the time of the request and its completion. If no work can be done, then the only case for overlapped I/O is that it allows for better user responsiveness.

Overlapped I/O is the type of operation that the MTTTY sample uses. It creates a thread that is responsible for reading the port's data and reading the port's status. It also performs periodic background work. The program creates another thread exclusively for writing data out the port.

Note   Applications sometimes abuse multithreading systems by creating too many threads. Although using multiple threads can resolve many difficult problems, creating excessive threads is not the most efficient use of them in an application. Threads are less a strain on the system than processes but still require system resources such as CPU time and memory. An application that creates excessive threads may adversely affect the performance of the entire system. A better use of threads is to create a different request queue for each type of job and to have a worker thread issue an I/O request for each entry in the request queue. This method is used by the MTTTY sample provided with this article.

An overlapped I/O operation has two parts: the creation of the operation and the detection of its completion. Creating the operation entails setting up an OVERLAPPED structure, creating a manual-reset event for synchronization, and calling the appropriate function (ReadFile or WriteFile). The I/O operation may or may not be completed immediately. It is an error for an application to assume that a request for an overlapped operation always yields an overlapped operation. If an operation is completed immediately, an application needs to be ready to continue processing normally. The second part of an overlapped operation is to detect its completion. Detecting completion of the operation involves waiting for the event handle, checking the overlapped result, and then handling the data. The reason that there is more work involved with an overlapped operation is that there are more points of failure. If a nonoverlapped operation fails, the function just returns an error-return result. If an overlapped operation fails, it can fail in the creation of the operation or while the operation is pending. You may also have a time-out of the operation or a time-out waiting for the signal that the operation is complete.

Reading

The ReadFile function issues a read operation. ReadFileEx also issues a read operation, but since it is not available on Windows 95, it is not discussed in this article. Here is a code snippet that details how to issue a read request. Notice that the function calls a function to process the data if the ReadFile returns TRUE. This is the same function called if the operation becomes overlapped. Note the fWaitingOnRead flag that is defined by the code; it indicates whether or not a read operation is overlapped. It is used to prevent the creation of a new read operation if one is outstanding.

DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader = {0};

// Create the overlapped event. Must be closed before exiting
// to avoid a handle leak.
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

if (osReader.hEvent == NULL)
   // Error creating overlapped event; abort.

if (!fWaitingOnRead) {
   // Issue read operation.
   if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
      if (GetLastError() != ERROR_IO_PENDING)     // read not delayed?
         // Error in communications; report it.
      else
         fWaitingOnRead = TRUE;
   }
   else {    
      // read completed immediately
      HandleASuccessfulRead(lpBuf, dwRead);
    }
}

The second part of the overlapped operation is the detection of its completion. The event handle in the OVERLAPPED structure is passed to the WaitForSingleObject function, which will wait until the object is signaled. Once the event is signaled, the operation is complete. This does not mean that it was completed successfully, just that it was completed. The GetOverlappedResult function reports the result of the operation. If an error occurred, GetOverlappedResult returns FALSE and GetLastError returns the error code. If the operation was completed successfully, GetOverlappedResult will return TRUE.

Note   GetOverlappedResult can detect completion of the operation, as well as return the operation's failure status.GetOverlappedResult returns FALSE and GetLastError returns ERROR_IO_INCOMPLETE when the operation is not completed. In addition, GetOverlappedResult can be made to block until the operation completes. This effectively turns the overlapped operation into a nonoverlapped operation and is accomplished by passing TRUE as the bWaitparameter.

Here is a code snippet that shows one way to detect the completion of an overlapped read operation. Note that the code calls the same function to process the data that was called when the operation completed immediately. Also note the use of the fWaitingOnRead flag. Here it controls entry into the detection code, since it should be called only when an operation is outstanding.

#define READ_TIMEOUT      500      // milliseconds

DWORD dwRes;

if (fWaitingOnRead) {
   dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
   switch(dwRes)
   {
      // Read completed.
      case WAIT_OBJECT_0:
          if (!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE))
             // Error in communications; report it.
          else
             // Read completed successfully.
             HandleASuccessfulRead(lpBuf, dwRead);

          //  Reset flag so that another opertion can be issued.
          fWaitingOnRead = FALSE;
          break;

      case WAIT_TIMEOUT:
          // Operation isn't complete yet. fWaitingOnRead flag isn't
          // changed since I'll loop back around, and I don't want
          // to issue another read until the first one finishes.
          //
          // This is a good time to do some background work.
          break;                       

      default:
          // Error in the WaitForSingleObject; abort.
          // This indicates a problem with the OVERLAPPED structure's
          // event handle.
          break;
   }
}

Writing

Transmitting data out the communications port is very similar to reading in that it uses a lot of the same APIs. The code snippet below demonstrates how to issue and wait for a write operation to be completed.

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   DWORD dwRes;
   BOOL fRes;

   // Create this write operation's OVERLAPPED structure's hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // error creating overlapped event handle
      return FALSE;

   // Issue write.
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) { 
         // WriteFile failed, but isn't delayed. Report error and abort.
         fRes = FALSE;
      }
      else
         // Write is pending.
         dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
         switch(dwRes)
         {
            // OVERLAPPED structure's event has been signaled. 
            case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE))
                       fRes = FALSE;
                 else
                  // Write operation completed successfully.
                  fRes = TRUE;
                 break;
            
            default:
                 // An error has occurred in WaitForSingleObject.
                 // This usually indicates a problem with the
                // OVERLAPPED structure's event handle.
                 fRes = FALSE;
                 break;
         }
      }
   }
   else
      // WriteFile completed immediately.
      fRes = TRUE;

   CloseHandle(osWrite.hEvent);
   return fRes;
}

Notice that the code above uses the WaitForSingleObject function with a time-out value of INFINITE. This causes theWaitForSingleObject function to wait forever until the operation is completed; this may make the thread or program appear to be "hung" when, in fact, the write operation is simply taking a long time to complete or flow control has blocked the transmission. Status checking, discussed later, can detect this condition, but doesn't cause the WaitForSingleObject to return. Three things can alleviate this condition:

  • Place the code in a separate thread. This allows other threads to execute any functions they desire while our writer thread waits for the write to be completed. This is what the MTTTY sample does.
  • Use COMMTIMEOUTS to cause the write to be completed after a time-out period has passed. This is discussed more fully in the "Communications Time-outs" section later in this article. This is also what the MTTTY sample allows.
  • Change the WaitForSingleObject call to include a real time-out value. This causes more problems because if the program issues another operation while an older operation is still pending, new OVERLAPPED structures and overlapped events need to be allocated. This type of recordkeeping is difficult, particularly when compared to using a "job queue" design for the operations. The "job queue" method is used in the MTTTY sample.

    Note: The time-out values in synchronization functions are not communications time-outs. Synchronization time-outs causeWaitForSingleObject or WaitForMultipleObjects to return WAIT_TIMEOUT. This is not the same as a read or write operation timing out. Communications time-outs are described later in this article.

Because the WaitForSingleObject function in the above code snippet uses an INFINITE time-out, it is equivalent to usingGetOverlappedResult with TRUE for the fWait parameter. Here is equivalent code in a much simplified form:

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   BOOL fRes;

   // Create this writes OVERLAPPED structure hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // Error creating overlapped event handle.
      return FALSE;

   // Issue write.
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) { 
         // WriteFile failed, but it isn't delayed. Report error and abort.
         fRes = FALSE;
      }
      else {
         // Write is pending.
         if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, TRUE))
            fRes = FALSE;
         else
            // Write operation completed successfully.
            fRes = TRUE;
      }
   }
   else
      // WriteFile completed immediately.
      fRes = TRUE;

   CloseHandle(osWrite.hEvent);
   return fRes;
}

GetOverlappedResult is not always the best way to wait for an overlapped operation to be completed. For example, if an application needs to wait on another event handle, the first code snippet serves as a better model than the second. The call toWaitForSingleObject is easy to change to WaitForMultipleObjects to include the additional handles on which to wait. This is what the MTTTY sample application does.

A common mistake in overlapped I/O is to reuse an OVERLAPPED structure before the previous overlapped operation is completed. If a new overlapped operation is issued before a previous operation is completed, a new OVERLAPPED structure must be allocated for it. A new manual-reset event for the hEvent member of the OVERLAPPED structure must also be created. Once an overlapped operation is complete, the OVERLAPPED structure and its event are free for reuse.

The only member of the OVERLAPPED structure that needs modifying for serial communications is the hEvent member. The other members of the OVERLAPPED structure should be initialized to zero and left alone. Modifying the other members of the OVERLAPPEDstructure is not necessary for serial communications devices. The documentation for ReadFile and WriteFile state that the Offsetand OffsetHigh members of the OVERLAPPED structure must be updated by the application, or else results are unpredictable. This guideline should be applied to OVERLAPPED structures used for other types of resources, such as files.

Serial Status

There are two methods to retrieve the status of a communications port. The first is to set an event mask that causes notification of the application when the desired events occur. The SetCommMask function sets this event mask, and the WaitCommEvent function waits for the desired events to occur. These functions are similar to the 16-bit functions SetCommEventMask andEnableCommNotification, except that the Win32 functions do not post WM_COMMNOTIFY messages. In fact, the WM_COMMNOTIFY message is not even part of the Win32 API. The second method for retrieving the status of the communications port is to periodically call a few different status functions. Polling is, of course, neither efficient nor recommended.

Communications Events

Communications events can occur at any time in the course of using a communications port. The two steps involved in receiving notification of communications events are as follows:

  • SetCommMask sets the desired events that cause a notification.
  • WaitCommEvent issues a status check. The status check can be an overlapped or nonoverlapped operation, just as the read and write operations can be.

    Note: The word event in this context refers to communications events only. It does not refer to an event object used for synchronization.

Here is an example of the SetCommMask function:

DWORD dwStoredFlags;

dwStoredFlags = EV_BREAK | EV_CTS   | EV_DSR | EV_ERR | EV_RING |\
                EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
if (!SetCommMask(hComm, dwStoredFlags))
   // error setting communications mask

A description of each type of event is in Table 1.

Table 1. Communications Event Flags

Event FlagDescription
EV_BREAK A break was detected on input.
EV_CTS The CTS (clear-to-send) signal changed state. To get the actual state of the CTS line,GetCommModemStatus should be called.
EV_DSR The DSR (data-set-ready) signal changed state. To get the actual state of the DSR line,GetCommModemStatus should be called.
EV_ERR A line-status error occurred. Line-status errors are CE_FRAME, CE_OVERRUN, and CE_RXPARITY. To find the cause of the error, ClearCommError should be called.
EV_RING A ring indicator was detected.
EV_RLSD The RLSD (receive-line-signal-detect) signal changed state. To get the actual state of the RLSD line,GetCommModemStatus should be called. Note that this is commonly referred to as the CD (carrier detect) line.
EV_RXCHAR A new character was received and placed in the input buffer. See the "Caveat" section below for a discussion of this flag.
EV_RXFLAG The event character was received and placed in the input buffer. The event character is specified in theEvtChar member of the DCB structure discussed later. The "Caveat" section below also applies to this flag.
EV_TXEMPTY The last character in the output buffer was sent to the serial port device. If a hardware buffer is used, this flag only indicates that all data has been sent to the hardware. There is no way to detect when the hardware buffer is empty without talking directly to the hardware with a device driver.

After specifying the event mask, the WaitCommEvent function detects the occurrence of the events. If the port is open for nonoverlapped operation, then the WaitCommEvent function does not contain an OVERLAPPED structure. The function blocks the calling thread until the occurrence of one of the events. If an event never occurs, the thread may block indefinitely.

Here is a code snippet that shows how to wait for an EV_RING event when the port is open for nonoverlapped operation:

   DWORD dwCommEvent;

   if (!SetCommMask(hComm, EV_RING))
      // Error setting communications mask
      return FALSE;

   if (!WaitCommEvent(hComm, &dwCommEvent, NULL))
      // An error occurred waiting for the event.
      return FALSE;
   else
      // Event has occurred.
      return TRUE;
Note   The Microsoft Win32 SDK Knowledge Base documents a problem with Windows 95 and the EV_RING flag. The above code never returns in Windows 95 because the EV_RING event is not detected by the system; Windows NT properly reports the EV_RING event. Please see the Win32 SDK Knowledge Base for more information on this bug.

As noted, the code above can be blocked forever if an event never occurs. A better solution would be to open the port for overlapped operation and wait for a status event in the following manner:

   #define STATUS_CHECK_TIMEOUT      500   // Milliseconds

   DWORD      dwRes;
   DWORD      dwCommEvent;
   DWORD      dwStoredFlags;
   BOOL      fWaitingOnStat = FALSE;
   OVERLAPPED osStatus = {0};

   dwStoredFlags = EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING |\
                  EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
   if (!SetCommMask(comHandle, dwStoredFlags))
      // error setting communications mask; abort
      return 0;

   osStatus.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osStatus.hEvent == NULL)
      // error creating event; abort
      return 0;

   for ( ; ; ) {
      // Issue a status event check if one hasn't been issued already.
      if (!fWaitingOnStat) {
         if (!WaitCommEvent(hComm, &dwCommEvent, &osStatus)) {
            if (GetLastError() == ERROR_IO_PENDING)
               bWaitingOnStatusHandle = TRUE;
            else
               // error in WaitCommEvent; abort
               break;
         }
         else
            // WaitCommEvent returned immediately.
            // Deal with status event as appropriate.
            ReportStatusEvent(dwCommEvent); 
      }

      // Check on overlapped operation.
      if (fWaitingOnStat) {
         // Wait a little while for an event to occur.
         dwRes = WaitForSingleObject(osStatus.hEvent, STATUS_CHECK_TIMEOUT);
         switch(dwRes)
         {
             // Event occurred.
             case WAIT_OBJECT_0: 
                 if (!GetOverlappedResult(hComm, &osStatus, &dwOvRes, FALSE))
                    // An error occurred in the overlapped operation;
                    // call GetLastError to find out what it was
                    // and abort if it is fatal.
                 else
                    // Status event is stored in the event flag
                    // specified in the original WaitCommEvent call.
                    // Deal with the status event as appropriate.
                    ReportStatusEvent(dwCommEvent);

                 // Set fWaitingOnStat flag to indicate that a new
                 // WaitCommEvent is to be issued.
                 fWaitingOnStat = FALSE;
                 break;

             case WAIT_TIMEOUT:
                 // Operation isn't complete yet. fWaitingOnStatusHandle flag 
                 // isn't changed since I'll loop back around and I don't want
                 // to issue another WaitCommEvent until the first one finishes.
                 //
                 // This is a good time to do some background work.
                DoBackgroundWork();
                 break;                       

             default:
                 // Error in the WaitForSingleObject; abort
                 // This indicates a problem with the OVERLAPPED structure's
                 // event handle.
                CloseHandle(osStatus.hEvent);
                return 0;
         }
      }
   }

   CloseHandle(osStatus.hEvent);

The code above very closely resembles the code for overlapped reading. In fact, the MTTTY sample implements its reading and status checking in the same thread using WaitForMultipleObjects to wait for either the read event or the status event to become signaled.

There are two interesting side effects of SetCommMask and WaitCommEvent. First, if the communications port is open for nonoverlapped operation, WaitCommEvent will be blocked until an event occurs. If another thread calls SetCommMask to set a new event mask, that thread will be blocked on the call to SetCommMask. The reason is that the original call to WaitCommEvent in the first thread is still executing. The call to SetCommMask blocks the thread until the WaitCommEvent function returns in the first thread. This side effect is universal for ports open for nonoverlapped I/O. If a thread is blocked on any communications function and another thread calls a communications function, the second thread is blocked until the communications function returns in the first thread. The second interesting note about these functions is their use on a port open for overlapped operation. If SetCommMask sets a new event mask, any pending WaitCommEvent will complete successfully, and the event mask produced by the operation is NULL.

Caveat

Using the EV_RXCHAR flag will notify the thread that a byte arrived at the port. This event, used in combination with the ReadFilefunction, enables a program to read data only after it is in the receive buffer, as opposed to issuing a read that waits for the data to arrive. This is particularly useful when a port is open for nonoverlapped operation because the program does not need to poll for incoming data; the program is notified of the incoming data by the occurrence of the EV_RXCHAR event. Initial attempts to code this solution often produce the following pseudocode, including one oversight covered later in this section:

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;

if (!SetCommMask(hComm, EV_RXCHAR))
   // Error setting communications event mask.

for ( ; ; ) {
   if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
      if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
         // A byte has been read; process it.
      else
         // An error occurred in the ReadFile call.
         break;
   }
   else
      // Error in WaitCommEvent.
      break;
}

The above code waits for an EV_RXCHAR event to occur. When this happens, the code calls ReadFile to read the one byte received. The loop starts again, and the code waits for another EV_RXCHAR event. This code works fine when one or two bytes arrive in quick succession. The byte reception causes the EV_RXCHAR event to occur. The code reads the byte. If no other byte arrives before the code calls WaitCommEvent again, then all is fine; the next byte to arrive will cause the WaitCommEvent function to indicate the occurrence of the EV_RXCHAR event. If another single byte arrives before the code has a chance to reach the WaitCommEventfunction, then all is fine, too. The first byte is read as before; the arrival of the second byte causes the EV_RXCHAR flag to be set internally. When the code returns to the WaitCommEvent function, it indicates the occurrence of the EV_RXCHAR event and the second byte is read from the port in the ReadFile call.

The problem with the above code occurs when three or more bytes arrive in quick succession. The first byte causes the EV_RXCHAR event to occur. The second byte causes the EV_RXCHAR flag to be set internally. The next time the code calls WaitCommEvent, it indicates the EV_RXCHAR event. Now, a third byte arrives at the communications port. This third byte causes the system to attempt to set the EV_RXCHAR flag internally. Because this has already occurred when the second byte arrived, the arrival of the third byte goes unnoticed. The code eventually will read the first byte without a problem. After this, the code will call WaitCommEvent, and it indicates the occurrence of the EV_RXCHAR event (from the arrival of the second byte). The second byte is read, and the code returns to the WaitCommEvent function. The third byte waits in the system's internal receive buffer. The code and the system are now out of sync. When a fourth byte finally arrives, the EV_RXCHAR event occurs, and the code reads a single byte. It reads the third byte. This will continue indefinitely.

The solution to this problem seems as easy as increasing the number of bytes requested in the read operation. Instead of requesting a single byte, the code could request two, ten, or some other number of bytes. The problem with this idea is that it still fails when two or more extra bytes above the size of the read request arrive at the port in quick succession. So, if two bytes are read, then four bytes arriving in quick succession would cause the problem. Ten bytes requested would still fail if twelve bytes arrived in quick succession.

The real solution to this problem is to read from the port until no bytes are remaining. The following pseudocode solves the problem by reading in a loop until zero characters are read. Another possible method would be to call ClearCommError to determine the number of bytes in the buffer and read them all in one read operation. This method requires more sophisticated buffer management, but it reduces the number of reads when a lot of data arrives at once.

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;

if (!SetCommMask(hComm, EV_RXCHAR))
   // Error setting communications event mask

for ( ; ; ) {
   if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
      do {
         if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
            // A byte has been read; process it.
         else
            // An error occurred in the ReadFile call.
            break;
      } while (dwRead);
   }
   else
      // Error in WaitCommEvent
      break;
}

The above code does not work correctly without setting the proper time-outs. Communications time-outs, discussed later, affect the behavior of the ReadFile operation in order to cause it to return without waiting for bytes to arrive. Discussion of this topic occurs later in the "Communications Time-outs" section of this article.

The above caveat regarding EV_RXCHAR also applies to EV_RXFLAG. If flag characters arrive in quick succession, EV_RXFLAG events may not occur for all of them. Once again, the best solution is to read all bytes until none remain.

The above caveat also applies to other events not related to character reception. If other events occur in quick succession some of the notifications will be lost. For instance, if the CTS line voltage starts high, then goes low, high, and low again, an EV_CTS event occurs. There is no guarantee of how many EV_CTS events will actually be detected with WaitCommEvent if the changes in the CTS line happen quickly. For this reason, WaitCommEvent cannot be used to keep track of the state of the line. Line status is covered in the "Modem Status" section later in this article.

Error Handling and Communications Status

One of the communications event flags specified in the call to SetCommMask is possibly EV_ERR. The occurrence of the EV_ERR event indicates that an error condition exists in the communications port. Other errors can occur in the port that do not cause the EV_ERR event to occur. In either case, errors associated with the communications port cause all I/O operations to be suspended until removal of the error condition. ClearCommError is the function to call to detect errors and clear the error condition.

ClearCommError also provides communications status indicating why transmission has stopped; it also indicates the number of bytes waiting in the transmit and receive buffers. The reason why transmission may stop is because of errors or to flow control. The discussion of flow control occurs later in this article.

Here is some code that demonstrates how to call ClearCommError:

    COMSTAT comStat;
    DWORD   dwErrors;
    BOOL    fOOP, fOVERRUN, fPTO, fRXOVER, fRXPARITY, fTXFULL;
    BOOL    fBREAK, fDNS, fFRAME, fIOE, fMODE;

    // Get and clear current errors on the port.
    if (!ClearCommError(hComm, &dwErrors, &comStat))
        // Report error in ClearCommError.
        return;

    // Get error flags.
    fDNS = dwErrors & CE_DNS;
    fIOE = dwErrors & CE_IOE;
    fOOP = dwErrors & CE_OOP;
    fPTO = dwErrors & CE_PTO;
    fMODE = dwErrors & CE_MODE;
    fBREAK = dwErrors & CE_BREAK;
    fFRAME = dwErrors & CE_FRAME;
    fRXOVER = dwErrors & CE_RXOVER;
    fTXFULL = dwErrors & CE_TXFULL;
    fOVERRUN = dwErrors & CE_OVERRUN;
    fRXPARITY = dwErrors & CE_RXPARITY;

    // COMSTAT structure contains information regarding
    // communications status.
    if (comStat.fCtsHold)
        // Tx waiting for CTS signal

    if (comStat.fDsrHold)
        // Tx waiting for DSR signal

    if (comStat.fRlsdHold)
        // Tx waiting for RLSD signal

    if (comStat.fXoffHold)
        // Tx waiting, XOFF char rec'd

    if (comStat.fXoffSent)
        // Tx waiting, XOFF char sent
    
    if (comStat.fEof)
        // EOF character received
    
    if (comStat.fTxim)
        // Character waiting for Tx; char queued with TransmitCommChar

    if (comStat.cbInQue)
        // comStat.cbInQue bytes have been received, but not read

    if (comStat.cbOutQue)
        // comStat.cbOutQue bytes are awaiting transfer

Modem Status (a.k.a. Line Status)

The call to SetCommMask may include the flags EV_CTS, EV_DSR, EV_RING, and EV_RLSD. These flags indicate changes in the voltage on the lines of the serial port. There is no indication of the actual status of these lines, just that a change occurred. TheGetCommModemStatus function retrieves the actual state of these status lines by returning a bit mask indicating a 0 for low or no voltage and 1 for high voltage for each of the lines.

Please note that the term RLSD (Receive Line Signal Detect) is commonly referred to as the CD (Carrier Detect) line.

Note   The EV_RING flag does not work in Windows 95 as mentioned earlier. The GetCommModemStatus function, however, does detect the state of the RING line.

Changes in these lines may also cause a flow-control event. The ClearCommError function reports whether transmission is suspended because of flow control. If necessary, a thread may call ClearCommError to detect whether the event is the cause of a flow-control action. Flow control is covered in the "Flow Control" section later in this article.

Here is some code that demonstrates how to call GetCommModemStatus:

   DWORD dwModemStatus;
   BOOL  fCTS, fDSR, fRING, fRLSD;

   if (!GetCommModemStatus(hComm, &dwModemStatus))
      // Error in GetCommModemStatus;
      return;

   fCTS = MS_CTS_ON & dwModemStatus;
   fDSR = MS_DSR_ON & dwModemStatus;
   fRING = MS_RING_ON & dwModemStatus;
   fRLSD = MS_RLSD_ON & dwModemStatus;

   // Do something with the flags.

Extended Functions

The driver will automatically change the state of control lines as necessary. Generally speaking, changing status lines is under the control of a driver. If a device uses communications port control lines in a manner different from RS-232 standards, the standard serial communications driver will not work to control the device. If the standard serial communications driver will not control the device, a custom device driver is necessary.

There are occasions when standard control lines are under the control of the application instead of the serial communications driver. For instance, an application may wish to implement its own flow control. The application would be responsible for changing the status of the RTS and DTR lines. EscapeCommFunction directs a communications driver to perform such extended operations.EscapeCommFunction can make the driver perform some other function, such as setting or clearing a BREAK condition. For more information on this function, consult the Platform SDK documentation, the Microsoft Win32 SDK Knowledge Base, or the Microsoft Developer Network (MSDN) Library.

Serial Settings

DCB Settings

The most crucial aspect of programming serial communications applications is the settings in the Device-Control Block (DCB) structure. The most common errors in serial communications programming occur in initializing the DCB structure improperly. When the serial communications functions do not behave as expected, a close examination of the DCB structure usually reveals the problem.

There are three ways to initialize a DCB structure. The first method is to use the function GetCommState. This function returns the current DCB in use for the communications port. The following code shows how to use the GetCommState function:

   DCB dcb = {0};

   if (!GetCommState(hComm, &dcb))
      // Error getting current DCB settings
   else
      // DCB is ready for use.

The second method to initialize a DCB is to use a function called BuildCommDCB. This function fills in the baud, parity type, number of stop bits, and number of data bits members of the DCB. The function also sets the flow-control members to default values. Consult the documentation of the BuildCommDCB function for details on which default values it uses for flow-control members. Other members of the DCB are unaffected by this function. It is the program's duty to make sure the other members of the DCB do not cause errors. The simplest thing to do in this regard is to initialize the DCB structure with zeros and then set the size member to the size, in bytes, of the structure. If the zero initialization of the DCB structure does not occur, then there may be nonzero values in the reserved members; this produces an error when trying to use the DCB later. The following function shows how to properly use this method:

   DCB dcb;

   FillMemory(&dcb, sizeof(dcb), 0);
   dcb.DCBlength = sizeof(dcb);
   if (!BuildCommDCB("9600,n,8,1", &dcb)) {   
      // Couldn't build the DCB. Usually a problem
      // with the communications specification string.
      return FALSE;
   }
   else
      // DCB is ready for use.

The third method to initialize a DCB structure is to do it manually. The program allocates the DCB structure and sets each member with any value desired. This method does not deal well with changes to the DCB in future implementations of Win32 and is not recommended.

An application usually needs to set some of the DCB members differently than the defaults or may need to modify settings in the middle of execution. Once proper initialization of the DCB occurs, modification of individual members is possible. The changes to the DCB structure do not have any effect on the behavior of the port until execution of the SetCommState function. Here is a section of code that retrieves the current DCB, changes the baud, and then attempts to set the configuration:

   DCB dcb;

   FillMemory(&dcb, sizeof(dcb), 0);
   if (!GetCommState(hComm, &dcb))     // get current DCB
      // Error in GetCommState
      return FALSE;

   // Update DCB rate.
   dcb.BaudRate = CBR_9600 ;

   // Set new state.
   if (!SetCommState(hComm, &dcb))
      // Error in SetCommState. Possibly a problem with the communications 
      // port handle or a problem with the DCB structure itself.

Here is an explanation of each of the members of the DCB and how they affect other parts of the serial communications functions.

Note   Most of this information is from the Platform SDK documentation. Because documentation is the official word in what the members actually are and what they mean, this table may not be completely accurate if changes occur in the operating system.

Table 2. The DCB Structure Members

Member Description
DCBlength Size, in bytes, of the structure. Should be set before calling SetCommState to update the settings.
BaudRate Specifies the baud at which the communications device operates. This member can be an actual baud value, or a baud index.
fBinary Specifies whether binary mode is enabled. The Win32 API does not support nonbinary mode transfers, so this member should be TRUE. Trying to use FALSE will not work.
fParity Specifies whether parity checking is enabled. If this member is TRUE, parity checking is performed and parity errors are reported. This should not be confused with the Parity member, which controls the type of parity used in communications.
fOutxCtsFlow Specifies whether the CTS (clear-to-send) signal is monitored for output flow control. If this member is TRUE and CTS is low, output is suspended until CTS is high again. The CTS signal is under control of the DCE (usually a modem), the DTE (usually the PC) simply monitors the status of this signal, the DTE does not change it.
fOutxDsrFlow Specifies whether the DSR (data-set-ready) signal is monitored for output flow control. If this member is TRUE and DSR is low, output is suspended until DSR is high again. Once again, this signal is under the control of the DCE; the DTE only monitors this signal.
fDtrControl Specifies the DTR (data-terminal-ready) input flow control. This member can be one of the following values:
  Value Meaning
  DTR_CONTROL_DISABLE Lowers the DTR line when the device is opened. The application can adjust the state of the line withEscapeCommFunction.
  DTR_CONTROL_ENABLE Raises the DTR line when the device is opened. The application can adjust the state of the line withEscapeCommFunction.
  DTR_CONTROL_HANDSHAKE Enables DTR flow-control handshaking. If this value is used, it is an error for the application to adjust the line with EscapeCommFunction.
fDsrSensitivity Specifies whether the communications driver is sensitive to the state of the DSR signal. If this member is TRUE, the driver ignores any bytes received, unless the DSR modem input line is high.
fTXContinueOnXoff Specifies whether transmission stops when the input buffer is full and the driver has transmitted the XOFF character. If this member is TRUE, transmission continues after the XOFF character has been sent. If this member is FALSE, transmission does not continue until the input buffer is within XonLim bytes of being empty and the driver has transmitted the XON character.
fOutX Specifies whether XON/XOFF flow control is used during transmission. If this member is TRUE, transmission stops when the XOFF character is received and starts again when the XON character is received.
fInX Specifies whether XON/XOFF flow control is used during reception. If this member is TRUE, the XOFF character is sent when the input buffer comes within XoffLim bytes of being full, and the XON character is sent when the input buffer comes within XonLim bytes of being empty.
fErrorChar Specifies whether bytes received with parity errors are replaced with the character specified by theErrorChar member. If this member is TRUE and the fParity member is TRUE, replacement occurs.
fNull Specifies whether null bytes are discarded. If this member is TRUE, null bytes are discarded when received.
fRtsControl Specifies the RTS (request-to-send) input flow control. If this value is zero, the default is RTS_CONTROL_HANDSHAKE. This member can be one of the following values:
  Value Meaning
  RTS_CONTROL_DISABLE Lowers the RTS line when the device is opened. The application can use EscapeCommFunction to change the state of the line.
  RTS_CONTROL_ENABLE Raises the RTS line when the device is opened. The application can use EscapeCommFunction to change the state of the line.
  RTS_CONTROL_HANDSHAKE Enables RTS flow-control handshaking. The driver raises the RTS line, enabling the DCE to send, when the input buffer has enough room to receive data. The driver lowers the RTS line, preventing the DCE to send, when the input buffer does not have enough room to receive data. If this value is used, it is an error for the application to adjust the line withEscapeCommFunction.
  RTS_CONTROL_TOGGLE Specifies that the RTS line will be high if bytes are available for transmission. After all buffered bytes have been sent, the RTS line will be low. If this value is set, it would be an error for an application to adjust the line with EscapeCommFunction. This value is ignored in Windows 95; it causes the driver to act as if RTS_CONTROL_ENABLE were specified.
fAbortOnError Specifies whether read and write operations are terminated if an error occurs. If this member is TRUE, the driver terminates all read and write operations with an error status (ERROR_IO_ABORTED) if an error occurs. The driver will not accept any further communications operations until the application has acknowledged the error by calling the ClearCommError function.
fDummy2 Reserved; do not use.
wReserved Not used; must be set to zero.
XonLim Specifies the minimum number of bytes allowed in the input buffer before the XON character is sent.
XoffLim Specifies the maximum number of bytes allowed in the input buffer before the XOFF character is sent. The maximum number of bytes allowed is calculated by subtracting this value from the size, in bytes, of the input buffer.
Parity Specifies the parity scheme to be used. This member can be one of the following values:
  Value Meaning
  EVENPARITY Even
  MARKPARITY Mark
  NOPARITY No parity
  ODDPARITY Odd
StopBits Specifies the number of stop bits to be used. This member can be one of the following values:
  Value Meaning
  ONESTOPBIT 1 stop bit
  ONE5STOPBITS 1.5 stop bits
  TWOSTOPBITS 2 stop bits
XonChar Specifies the value of the XON character for both transmission and reception.
XoffChar Specifies the value of the XOFF character for both transmission and reception.
ErrorChar Specifies the value of the character used to replace bytes received with a parity error.
EofChar Specifies the value of the character used to signal the end of data.
EvtChar Specifies the value of the character used to cause the EV_RXFLAG event. This setting does not actually cause anything to happen without the use of EV_RXFLAG in the SetCommMask function and the use of WaitCommEvent.
wReserved1 Reserved; do not use.

Flow Control

Flow control in serial communications provides a mechanism for suspending communications while one of the devices is busy or for some reason cannot do any communication. There are traditionally two types of flow control: hardware and software.

A common problem with serial communications is write operations that actually do not write the data to the device. Often, the problem lies in flow control being used when the program did not specify it. A close examination of the DCB structure reveals that one or more of the following members may be TRUE: fOutxCtsFlow, fOutxDsrFlow, or fOutX. Another mechanism to reveal that flow control is enabled is to call ClearCommError and examine the COMSTAT structure. It will reveal when transmission is suspended because of flow control.

Before discussing the types of flow control, a good understanding of some terms is in order. Serial communications takes place between two devices. Traditionally, there is a PC and a modem or printer. The PC is labeled the Data Terminal Equipment (DTE). The DTE is sometimes called the host. The modem, printer, or other peripheral equipment is identified as the Data Communications Equipment (DCE). The DCE is sometimes referred to as the device.

Hardware flow control

Hardware flow control uses voltage signals on control lines of the serial cable to control whether sending or receiving is enabled. The DTE and the DCE must agree on the types of flow control used for a communications session. Setting the DCB structure to enable flow control just configures the DTE. The DCE also needs configuration to make certain the DTE and DCE use the same type of flow control. There is no mechanism provided by Win32 to set the flow control of the DCE. DIP switches on the device, or commands sent to it typically establish its configuration. The following table describes the control lines, the direction of the flow control, and the line's effect on the DTE and DCE.

Table 3. Hardware Flow-control Lines

Line and DirectionEffect on DTE/DCE
CTS
(Clear To Send)
Output flow control
DCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data.

If the fOutxCtsFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.

If the fOutxCtsFlow member of the DCB is FALSE, then the state of the line does not affect transmission.

DSR
(Data Set Ready)
Output flow control
DCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data.

If the fOutxDsrFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.

If the fOutxDsrFlow member of the DCB is FALSE, then the state of the line does not affect transmission.

DSR
(Data Set Ready)
Input flow control
If the DSR line is low, then data that arrives at the port is ignored. If the DSR line is high, data that arrives at the port is received.

This behavior occurs if the fDsrSensitivity member of the DCB is set to TRUE. If it is FALSE, then the state of the line does not affect reception.

RTS 
(Ready To Send)
Input flow control
The RTS line is controlled by the DTE.

If the fRtsControl member of the DCB is set to RTS_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the RTS line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the RTS line low.

If the fRtsControl member of the DCB is set to RTS_CONTROL_TOGGLE, the driver sets the RTS line high when data is available for sending. The driver sets the line low when no data is available for sending. Windows 95 ignores this value and treats it the same as RTS_CONTROL_ENABLE.

If the fRtsControl member of the DCB is set to RTS_CONTROL_ENABLE or RTS_CONTROL_DISABLE, the application is free to change the state of the line as it needs. Note that in this case, the state of the line does not affect reception.

The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.

DTR
(Data Terminal Ready)
Input flow control
The DTR line is controlled by the DTE.

If the fDtrControl member of the DCB is set to DTR_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the DTR line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the DTR line low.

If the fDtrControl member of the DCB is set to DTR_CONTROL_ENABLE or DTR_CONTROL_DISABLE, the application is free to change the state of the line as it needs. In this case, the state of the line does not affect reception.

The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.

The need for flow control is easy to recognize when the CE_RXOVER error occurs. This error indicates an overflow of the receive buffer and data loss. If data arrives at the port faster than it is read, CE_RXOVER can occur. Increasing the input buffer size may cause the error to occur less frequently, but it does not completely solve the problem. Input flow control is necessary to completely alleviate this problem. When the driver detects that the input buffer is nearly full, it will lower the input flow-control lines. This should cause the DCE to stop transmitting, which gives the DTE enough time to read the data from the input buffer. When the input buffer has more room available, the voltage on flow-control lines is set high, and the DCE resumes sending data.

A similar error is CE_OVERRUN. This error occurs when new data arrives before the communications hardware and serial communications driver completely receives old data. This can occur when the transmission speed is too high for the type of communications hardware or CPU. This can also occur when the operating system is not free to service the communications hardware. The only way to alleviate this problem is to apply some combination of decreasing the transmission speed, replacing the communications hardware, and increasing the CPU speed. Sometimes third-party hardware drivers that are not very efficient with CPU resources cause this error. Flow control cannot completely solve the problems that cause the CE_OVERRUN error, although it may help to reduce the frequency of the error.

Software flow control

Software flow control uses data in the communications stream to control the transmission and reception of data. Because software flow control uses two special characters, XOFF and XON, binary transfers cannot use software flow control; the XON or XOFF character may appear in the binary data and would interfere with data transfer. Software flow control befits text-based communications or data being transferred that does not contain the XON and XOFF characters.

In order to enable software flow control, the fOutX and fInX members of the DCB must be set to TRUE. The fOutX member controls output flow control. The fInX member controls input flow control.

One thing to note is that the DCB allows the program to dynamically assign the values the system recognizes as flow-control characters. The XoffChar member of the DCB dictates the XOFF character for both input and output flow control. The XonCharmember of the DCB similarly dictates the XON character.

For input flow control, the XoffLim member of the DCB specifies the minimum amount of free space allowed in the input buffer before the XOFF character is sent. If the amount of free space in the input buffer drops below this amount, then the XOFF character is sent. For input flow control, the XonLim member of the DCB specifies the minimum number of bytes allowed in the input buffer before the XON character is sent. If the amount of data in the input buffer drops below this value, then the XON character is sent.

Table 4 lists the behavior of the DTE when using XOFF/XON flow control.

Table 4. Software flow-control behavior

Flow-control characterBehavior
XOFF received by DTE DTE transmission is suspended until XON is received. DTE reception continues. The fOutXmember of the DCB controls this behavior.
XON received by DTE If DTE transmission is suspended because of a previous XOFF character being received, DTE transmission is resumed. The fOutX member of the DCB controls this behavior.
XOFF sent from DTE XOFF is automatically sent by the DTE when the receive buffer approaches full. The actual limit is dictated by the XoffLim member of the DCB. The fInX member of the DCB controls this behavior. DTE transmission is controlled by the fTXContinueOnXoff member of the DCB as described below.
XON sent from the DTE XON is automatically sent by the DTE when the receive buffer approaches empty. The actual limit is dictated by the XonLim member of the DCB. The fInX member of the DCB controls this behavior.

If software flow control is enabled for input control, then the fTXContinueOnXoff member of the DCB takes effect. ThefTXContinueOnXoff member controls whether transmission is suspended after the XOFF character is automatically sent by the system. If fTXContinueOnXoff is TRUE, then transmission continues after the XOFF is sent when the receive buffer is full. IffTXContinueOnXoff is FALSE, then transmission is suspended until the system automatically sends the XON character. DCE devices using software flow control will suspend their sending after the XOFF character is received. Some equipment will resume sending when the XON character is sent by the DTE. On the other hand, some DCE devices will resume sending after any character arrives. ThefTXContinueOnXoff member should be set to FALSE when communicating with a DCE device that resumes sending after any character arrives. If the DTE continued transmission after it automatically sent the XOFF, the resumption of transmission would cause the DCE to continue sending, defeating the XOFF.

There is no mechanism available in the Win32 API to cause the DTE to behave the same way as these devices. The DCB structure contains no members for specifying suspended transmission to resume when any character is received. The XON character is the only character that causes transmission to resume.

One other interesting note about software flow control is that reception of XON and XOFF characters causes pending read operations to complete with zero bytes read. The XON and XOFF characters cannot be read by the application, since they are not placed in the input buffer.

A lot of programs on the market, including the Terminal program that comes with Windows, give the user three choices for flow control: Hardware, Software, or None. The Windows operating system itself does not limit an application in this way. The settings of the DCB allow for Software and Hardware flow control simultaneously. In fact, it is possible to separately configure each member of the DCB that affects flow control, which allows for several different flow-control configurations. The limits placed on flow-control choices are there for ease-of-use reasons to reduce confusion for end users. The limits placed on flow-control choices may also be because devices used for communications may not support all types of flow control.

Communications Time-outs

Another major topic affecting the behavior of read and write operations is time-outs. Time-outs affect read and write operations in the following way. If an operation takes longer than the computed time-out period, the operation is completed. There is no error code that is returned by ReadFileWriteFileGetOverlappedResult, or WaitForSingleObject. All indicators used to monitor the operation indicate that it completed successfully. The only way to tell that the operation timed out is that the number of bytes actually transferred are fewer than the number of bytes requested. So, if ReadFile returns TRUE, but fewer bytes were read than were requested, the operation timed out. If an overlapped write operation times out, the overlapped event handle is signaled and WaitForSingleObjectreturns WAIT_OBJECT_O. GetOverlappedResult returns TRUE, but dwBytesTransferred contains the number of bytes that were transferred before the time-out. The following code demonstrates how to handle this in an overlapped write operation:

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   DWORD dwRes;
   BOOL  fRes;

   // Create this write operation's OVERLAPPED structure hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // Error creating overlapped event handle.
      return FALSE;

   // Issue write
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) { 
         // WriteFile failed, but it isn't delayed. Report error.
         fRes = FALSE;
      }
      else
         // Write is pending.
         dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
         switch(dwRes)
         {
            // Overlapped event has been signaled. 
            case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE))
                       fRes = FALSE;
                 else {
                  if (dwWritten != dwToWrite) {
                     // The write operation timed out. I now need to 
                     // decide if I want to abort or retry. If I retry, 
                     // I need to send only the bytes that weren't sent. 
                     // If I want to abort, I would just set fRes to 
                     // FALSE and return.
                     fRes = FALSE;
                  }
                  else
                     // Write operation completed successfully.
                     fRes = TRUE;
                }
                 break;
            
            default:
                 // An error has occurred in WaitForSingleObject. This usually 
                // indicates a problem with the overlapped event handle.
                 fRes = FALSE;
                 break;
         }
      }
   }
   else {
      // WriteFile completed immediately.

      if (dwWritten != dwToWrite) {
          // The write operation timed out. I now need to 
          // decide if I want to abort or retry. If I retry, 
          // I need to send only the bytes that weren't sent. 
          // If I want to abort, then I would just set fRes to 
          // FALSE and return.
          fRes = FALSE;
      }
      else
          fRes = TRUE;
   }

   CloseHandle(osWrite.hEvent);
   return fRes;
}

The SetCommTimeouts function specifies the communications time-outs for a port. To retrieve the current time-outs for a port, a program calls the GetCommTimeouts function. An applications should retrieve the communications time-outs before modifying them. This allows the application to set time-outs back to their original settings when it finishes with the port. Following is an example of setting new time-outs using SetCommTimeouts:

COMMTIMEOUTS timeouts;

timeouts.ReadIntervalTimeout = 20; 
timeouts.ReadTotalTimeoutMultiplier = 10;
timeouts.ReadTotalTimeoutConstant = 100;
timeouts.WriteTotalTimeoutMultiplier = 10;
timeouts.WriteTotalTimeoutConstant = 100;

if (!SetCommTimeouts(hComm, &timeouts))
   // Error setting time-outs.
Note   Once again, communications time-outs are not the same as time-out values supplied in synchronization functions.WaitForSingleObject, for instance, uses a time-out value to wait for an object to become signaled; this is not the same as a communications time-out.

Setting the members of the COMMTIMEOUTS structure to all zeros causes no time-outs to occur. Nonoverlapped operations will block until all the requested bytes are transferred. The ReadFile function is blocked until all the requested characters arrive at the port. TheWriteFile function is blocked until all requested characters are sent out. On the other hand, an overlapped operation will not finish until all the characters are transferred or the operation is aborted. The following conditions occur until the operation is completed:

  • WaitForSingleObject always returns WAIT_TIMEOUT if a synchronization time-out is supplied. WaitForSingleObject will block forever if an INFINITE synchronization time-out is used.
  • GetOverlappedResult always returns FALSE and GetLastError returns ERROR_IO_INCOMPLETE if called directly after the call to GetOverlappedResult.

Setting the members of the COMMTIMEOUTS structure in the following manner causes read operations to complete immediately without waiting for any new data to arrive:

COMMTIMEOUTS timeouts;

timeouts.ReadIntervalTimeout = MAXDWORD; 
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 0;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 0;

if (!SetCommTimeouts(hComm, &timeouts))
   // Error setting time-outs.

These settings are necessary when used with an event-based read described in the "Caveat" section earlier. In order for ReadFile to return 0 bytes read, the ReadIntervalTimeout member of the COMMTIMEOUTS structure is set to MAXDWORD, and theReadTimeoutMultiplier and ReadTimeoutConstant are both set to zero.

An application must always specifically set communications time-outs when it uses a communications port. The behavior of read and write operations is affected by communications time-outs. When a port is initially open, it uses default time-outs supplied by the driver or time-outs left over from a previous communications application. If an application assumes that time-outs are set a certain way, while the time-outs are actually different, then read and write operations may never complete or may complete too often.

Conclusion

This article serves as a discussion of some of the common pitfalls and questions that arise when developing a serial communications application. The Multithreaded TTY sample that comes with this article is designed using many of the techniques discussed here. Download it and try it out. Learning how it works will provide a thorough understanding of the Win32 serial communications functions.

Bibliography

Brain, Marshall. Win32 System Services: The Heart of Windows NT. Englewood Cliffs, NJ: Prentice Hall, 1994.

Campbell, Joe. C Programmer's Guide to Serial Communications. 2d ed. Indianapolis, IN: Howard W. Sams & Company, 1994.

Mirho, Charles, and Andy Terrice. "Create Communications Programs for Windows 95 with the Win32 Comm API." Microsoft Systems Journal 12 (December 1994). (MSDN Library, Books and Periodicals)

반응형
반응형

Mouse wheel events do not work in the Visual Basic 6.0 IDE

SYMPTOMS

You cannot scroll by using the mouse wheel in the Microsoft Visual Basic 6.0 IDE.

CAUSE

This problem occurs because the Visual Basic 6.0 IDE does not have built-in support for scrolling by using the mouse wheel.

WORKAROUND

To work around this problem, use one of the following methods:

Method 1

Download the VB6 Mouse Wheel.exe file that includes the add-in DLL and the code that is used to create the add-in DLL.
  1. Download the VB6 Mouse Wheel.exe file. The following file is available for download from the Microsoft Download Center:
    Download
    Download the VB6MouseWheel.EXE package now.

    For more information about how to download Microsoft support files, click the following article number to view the article in the Microsoft Knowledge Base:
    119591  How to obtain Microsoft support files from online services
    Microsoft scanned this file for viruses. Microsoft used the most current virus-detection software that was available on the date that the file was posted. The file is stored on security-enhanced servers that help prevent any unauthorized changes to the file.
  2. Click Start, click Run, type regsvr32 <path>\VB6IDEMouseWheelAddin.dll, and then click OK.
  3. Start Visual Basic 6.0.
  4. Click Add-Ins, and then click Add-in Manager.
  5. In the Add-in Manager list, click MouseWheel Fix.
  6. Click to select the Loaded/Unloaded check box, and then click to select the Load on Startup check box.
  7. Click OK.
You can also build the add-in DLL in Visual Basic 6.0. If you do this, the add-in DLL is automatically registered. You can then follow steps 4 through 7 to enable the add-in DLL. To build the add-in DLL, click Make VB6IDEMouseWheelAddin.dll on the File menu.

Method 2

Return to an earlier version of Microsoft IntelliPoint software. To do this, follow these steps:
  1. If the IntelliPoint software that is installed on your computer is version 4.9 or a later version, remove the IntelliPoint software from your computer.
  2. Install IntelliPoint software version 4.12. The following file is available for download from the Microsoft Download Center:
    Download
    Download the IntelliPoint 4.12 package now.

    For more information about how to download Microsoft support files, click the following article number to view the article in the Microsoft Knowledge Base:
    119591  How to obtain Microsoft support files from online services
    Microsoft scanned this file for viruses. Microsoft used the most current virus-detection software that was available on the date that the file was posted. The file is stored on security-enhanced servers that help prevent any unauthorized changes to the file.
Note You can also use this add-in in most VBA environments. Install the add-in as described earlier, create a .reg file with the following values, and merge it with your registry.

Windows Registry Editor Version 5.00

HKEY_CURRENT_USER\Software\Microsoft\VBA\VBE\6.0\Addins\VB6IDEMouseWheelAddin.Connect
  • "FriendlyName"="MouseWheel Fix"
  • "CommandLineSafe"=dword:00000000
  • "LoadBehavior"=dword:00000000
Note These keys may be ignored if you put them under HKEY_LOCAL_MACHINE.

STATUS

This behavior is by design.

MORE INFORMATION

Mouse wheel support in Visual Basic 6.0 is a function of the mouse driver. The WM_MOUSEWHEEL message is sent to the Focus window when you rotate the mouse wheel. Because the Visual Basic 6.0 IDE does not have built-in support for scrolling by using the mouse wheel, the IDE ignores the WM_MOUSEWHEEL message. However, IntelliPoint software version 4.12 provides mouse wheel support and converts the WM_MOUSEWHEEL message to WM_SCROLL. IntelliPoint software version 4.9 and later versions do not have this feature. Therefore, if you want to use the mouse wheel to scroll in the Visual Basic 6.0 IDE, you must use IntelliPoint software version 4.12.

REFERENCES

For additional information about WM_MOUSEWHEEL notification, visit the following Microsoft Developer Network (MSDN) Web site: For more information, click the following article number to view the article in the Microsoft Knowledge Base:
828675  The wheel on the mouse scrolls at a slower rate after you install the Microsoft Desktop Elite keyboard

APPLIES TO
  • Microsoft Visual Basic 6.0 Enterprise Edition
  • Microsoft Visual Basic 6.0 Enterprise Edition Service Pack 3
  • Microsoft Visual Basic 6.0 Enterprise Edition Service Pack 4
  • Microsoft Visual Basic 6.0 Enterprise Edition Service Pack 5
  • Microsoft Visual Basic for Applications 6.0
  • Microsoft Visual Basic for Applications 5.0
  • Microsoft Visual Basic 5.0 Enterprise Edition
Keywords: 
kbevent kbideproject kbdriver kbdevstudio kbprb KB837910
반응형
반응형
출처 : http://www.microsoft.com/korea/msdn/msdnmag/issues/06/05/x64/default.aspx#top

X64 프라이머

64비트 윈도우를 프로그래밍 하기 위해서 알아야 할 모든 것들



이 문서 내용:
  • 64 비트 윈도우의 기초
  • X64 아키텍트의 간략한 내용
  • Visual C++ 2005로 x64용 소프트웨어 개발
  • X64용 빌드를 위한 디버깅 기술
이 문서에는 다음 기술이 사용됩니다:
Windows, Win64, Visual Studio 2005




Matt Pietrek (영문)

목차
x64 운영체제
x64의 간략한 내부 구조
Visual C++로 x64용 어플리케이션 개발
Win64 호환되는 코드 만들기
디버깅
매니지드 코드는 어떻게 하나요?
최종 정리

로운 64비트 윈도우에서 일했던 경험의 좋았던 점 중의 하나는, 새로운 기술이 어떻게 동작하는지 눈으로 확인할 수 있다는 것이었습니다. 저 자신은 특히 어떤 운영체제 밑바닥에 대해서 조금이라도 알기 전까지는, 그 운영체제에 대해서 그렇게 편안함을 느끼지 못하는 편입니다. 그래서, 64비트 Windows XP와 Windows Server™ 2003이 나타났을 때, 저는 아주 열심히 그 운영체제에 대해서 연구하였습니다.

Win64와 x64 CPU의 좋은 점은, 그 전의 CPU와 조금 다른 구조를 가지고 있지만, 그 차이를 배우는데 그렇게 많은 시간이 요구되지 않는 다는 점입니다. 저희 같은 개발자들에게는, x64로의 이동이 단지 컴파일만 다시 하면 끝나는 그런 작업이었으면 좋겠지만, 그렇게 생각하고 작업을 하신다면, 앞으로 디버거에서 너무 많은 시간을 보내셔야 할 것 같습니다.

이 글에서, 저는 Win64와 x64의 내부구조에 대한 저의 지식을 종합해서 유능한 Win32® 프로그래머가 x64로 이동하기 위해서 꼭 필요한 지식들 제공하겠습니다. 저는 여러분이 이미 Win32의 개념과 기본 x86개념, 그리고 왜 여러분의 코드가 Win64에서 동작 해야 하는지에 대해서 이미 알고 계신다고 가정하겠습니다. 그렇게 해야지 제가 좀 더 핵심적인 것들에 집중할 수 있을것 같습니다. 여기서의 이 요약이 x86 내부구조와 Win32에 대한 알고 있는 여러분의 지식에 관련된 상대적으로 중요한 x64와의 차이점에 대한 고찰이라고 생각하시기 바랍니다.

x64 시스템에서 한 가지 좋은 점은, 아이템니윰(Itanium)기반 시스템과는 다르게 여러 분이 심각한 효율 저하에 대한 고민을 하지 않고, Win32 혹은 Win64를 동일한 기계에서 이용하실 수 있다는 점입니다. 그리고 인텔과 AMD의 x64 구현에 조금 불명확한 몇 가지의 차이가 존재함에도 불구하고, x64용 윈도우는 둘 중 어느 곳에서나 동작합니다. 여러분이 AMD x64와 Intel x64 시스템을 위해서 각각 다른 버젼의 윈도우가 필요하지 않습니다.

저는 여기서 운영체제의 구현의 몇 가지 자세한 내용들, x64 CPU 내부 구조에 대한 개괄적인 설명, 그리고 Visual C++로 x64용 프로그램 개발 영역으로 이 글을 나누어서 진행하도록 하겠습니다:.


페이지 맨 위로페이지 맨 위로


x64 운영체제

저는 어떤 윈도우 내부구조를 설명하더라도, 처음에는 메모리와 주소 공간에서부터 시작합니다. 비록 64비트 프로세서가 이론적으로는 16 엑사 바이트(exabytes) 의 메모리에 접근할 수 있다고 하더라고, Win64는 현재 16 테라바이트(terabytes), 44비트 만을 지원하고 있습니다. 왜 64비트 전부를 사용해서, 16 엑사바이트 전부를 쓸 수 없었을 까요? 거기에는 몇 가지 이유가 존재합니다.

맨 처음 이유로는, 현재 x64 CPU들이 물리적인 메모리 공간에 접근할 때, 오직 40비트(1테라바이트)만을 허용합니다. 그 제한이 없어진다고 하더라고, 지금 현재의 하드웨어가 아닌, 향후에 나올 수 있는 CPU들의 내부구조들이 오직 52비트 (4페타바이트)만큼만 확장될 수 있습니다.그 정도만 해도, 그 많은 메모리를 매핑하기 위한 페이지 테이블의 사이즈는 어마 어마 할 것입니다.

Win32에서와 같이, 접근할 수 있는 주소 공간은, 사용자와 커널의 영역으로 나누어 집니다. 커널 모드의 코드가 8 테라바이트 상위에서 모든 프로세스에 의해 영역되고, 각각의 프로세서는 8 테라바이트 이하에 자신의 고유 영역을 가집니다. 64비트 윈도우의 각 버젼들은 그림 1과 그림 2에서 보여지는 것처럼, 서로 다른 물리적인 메모리 한계를 가지고 있습니다.

Win32에서와 같이, x64의 페이지 사이즈는 4KB 입니다. 처음의 64KB 공간은 절대로 매핑 되지 않기 때문에, 여러분이 볼 수 있는 맵핑 주소중 가장 낮은 번지는 0x10000입니다. Win32에서는 다르게, 시스템 DLL들은 사용자 모드 주소 공간의 제일 위 부분과 근접해 있는, 기본 로드 어드레스라는 것이 없습니다. 그 대신, 시스템 DLL들은 4GB 위의 공간에 적재 됩니다. 통상적으로, 그 주소는 0x7FF00000000 (8 테라바이트) 근처입니다.

새로운 x64 프로세서의 좋은 기능 중에 하나는, 윈도우가 하드웨어적으로 데이터 실행 보호(Data Execution Protection, 이하 DEP)를 할 수 있게 지원해 준다는 것입니다. x86 플랫폼에서는, 많은 버그와 바이러스가 CPU가 데이터를 올바른 코드 바이트로 인식하고 실행했기 때문에 존재할 수 있었습니다. 실수이든 혹은 고의적인 버퍼 오버런(buffer overrun)이 원래는 데이터 저장을 목적으로 했던 메모리 블락을 CPU에서 명령어로 인식하고 실행해 버리는 결과가 발생하곤 했었습니다. 이 DEP의 도움으로, 운영체제는, 의도한 코드 영역의 경계를 명료하게 설정할 수 있고, 이 의도된 경계를 벗어나는 코드 실행에 대해서는 CPU가 일종의 덫을 놓을 수 있게 되었습니다. 이 기능은 윈도우를 악의적인 공격에 덜 취약하게 만드는데 큰 도움을 줄 수 있습니다.

에러들을 잡기 위해서 고안된 장치들 중에 하나가, x64 링커가 실행파일에 대한 기본 적재 주소를 32비트 (4GB) 이상으로 설정한 것입니다. 이것은 코드가 Win64로 포팅된 후, 기존에 있던 코드에서 위에서 이야기한 보안에 문제를 일으키는 부분을 찾기 쉽게 만들어 줍니다. 특히, 만약 포인터가 32비트 값(예를 들어, DWORD)값으로 저장되어 있으면, 그러면, 그 값은 Win64 빌드를 동작시킬 때, 값이 일부분이 짤림 으로서, 포인터를 무효화 시키고, 접근위반(access violation)을 일으킵니다. 이러한 잔기술은 지저분한 포인터 버그를 찾기 쉽게 만들어 줍니다.

포인터와 DWORD에 대한 주제들도 Win64의 타입을 이야기하는데 빠질 수 없습니다. Win64에서 포인터의 크기가 얼마나 될까요? LONG형의 길이는요? 그리고, 핸들과 HWND의 크기는 얼마나 될까요? 다행스럽게도, 마이크로 소프트가 Win16에서 Win32로의 좀 지저분한 변환을 하면서, 새로운 자료형의 모델에 대해서는 64비트 까지의 확장이 쉽게 될 수 있도록 만들었습니다. 일반적으로 이야기 해서, 몇 가지 예외를 제외하고, 새로운 64비트 세계에서도, 포인터와 size_t를 제외한 모든 나머지 자료형 들은 Win32에서와 동일한 길이를 가지고 있습니다. 즉, 64비트 포인터는 8바이트인 반면에, int,long, DWORD는 아직도 4바이트입니다. 후반부에 Win64 개발에 대해서 이야기 하면서, 이 자료형에 대해서 좀 더 이야기 하도록 하겠습니다. [Editor's Update - 5/2/2006: 핸들은 포인터 값으로 선언되었습니다. 그래서, Win64에서는 4바이트가 아닌 8바이트 값입니다.]

Win64의 포맷은 PE32+라고 불립니다. 거의 모든 관점에서 이 포맷은 Win32PE 파일과 거의 구조적으로 동일합니다. ImageBase 같은 몇 몇의 포맷은 더 크기가 커졌고, 한 필드는 없어졌고, 그리고 다른 하나의 필드는 다른 CPU 타입을 반영할 수 있도록 변경되었습니다. 그림 3 은 바뀐 필드들을 보여 줍니다.

PE 헤더를 말고는, 그렇게 많이 바뀐 부분이 없습니다. IMAGE_LOAD_CONFIG, IMAGE_THUNK_DATA같은 몇 몇 구조체들은 단순히 필드들을 64비트로의 확장을 했을 뿐입니다. PDATA 섹션의 추가는 Win32와 Win64 구현의 주된 차이점중의 하나를 두드러지게 한다는 점에서 아주 흥미롭습니다: 그 차이는 바로 예외 처리(exception handling) 입니다.

x86 세계에서는, 예외 처리는 스택에 기반했었습니다. Win32 함수가 try/catch 혹은 try/finally 코드를 가지고 있을 때, 컴파일러는 스택에 작은 데이타 블락을 만들어 놓는 코드를 생성했었습니다. 추가로, 각각의 데이타 블락은 이전 try 데이타 구조체를 가리켰었습니다. 그래서, 최근에 추가된 구조체가 리스트의 헤드가 되는 링크드 리스트를 생성했었습니다. 함수가 불려지고, 종료될 때, 링크드 리스트의 헤더는 계속 갱신이 되었었습니다. 그러다, 예외가 발생하는 경우, 운영체제가 스택에 있는 링크드 리스트의 블락을 살펴서, 적절한 핸들러를 찾는 방식으로 이루어져 있었습니다. 저의 1997년 MSJ article (영문) 에서 좀 더 자세한 내용을 찾아 보실 수 있습니다.

Win32 예외처리와는 반대로, Win64 (x64와 아이템니움(Itanium) 버젼 둘 다 해당됩니다.)에서는 테이블 기반의 예외 처리를 사영합니다. 더 이상 스택 위에 있는 try 데이타 블락의 링크드 리스트는 없습니다. 대신, 각각의 Win64 실행파일은 런타임 함수 테이블을 가지고 있습니다. 각각의 함수 테이블은 함수의 시작 주소와 끝 주소를 가지고 있을 뿐만 아니라, 함수의 스택 프레임 레이아웃과 예외 처리 코드에 관련된 데이타들의 위치 역시 가지고 있습니다. 이 구조체들의 핵심을 보기 위해서 WINNT.H안에 들어 있는 x64 SDK안에 WINNT.H 안에 있는 IMAGE_RUNTIME_FUNCTION_ENTRY 구조를 살펴 보시기 바랍니다.

예외가 발생했을 때, 운영체제는 쓰레드 스택을 하나씩 탐색합니다. 스택을 탐색하면서 각각의 프레임을 탐색하고, 저장된 인스트럭션 포인터를 찾아서, 운영체제가 어떤 실행 모듈안에 인스트럭션 포인터가 있는지를 결정합니다. 그리고 나서, 운영체제는 런타임 함수 테이블을 그 모듈에서 찾아서, 적절한 런타임 함수를 찾아서, 데이타로 부터 적절한 예외 처리 결정을 내려 줍니다.

만약, 여러분이 로켓 과학자이고, PE32+ 모듈 없이 직접적으로 메모리에서 코드를 생성했으면 어떻게 될까요? RtlAddFunctionTable API를 이용하여, 운영체제에게 여러분이 동적으로 생성한 코드에 대해서 알려 줄 수 있습니다.

테이블 기반의 예외 핸들링의 단점은 (x86 스택 기반 모델에 비해 상대적으로) 함수 테이블을 찾아 보는 것이, 링크드 리스트에서 값을 찾는 것 보다 훨씬 시간이 많이 걸린다는 점입니다. 장점은, 함수를 실행시킬 때 마다 매 번 try 데이타 블락을 생성시키는 오버헤드가 없다는 점입니다.

꼭 기억하세요! 이 글이 아무리 재미있고, 흥미로워도, 이 글은 x64 예외 핸들링에 대한 자세한 설명이라기 보다, 간단한 소개에 불과합니다. x64의 예외 핸들러에 대한 좀 더 깊은 지식을 알고 싶으시다면, Kevin Frei의 블로그 (영문) 을 꼭 한 번 읽어 보시기 바랍니다.

x64에 호환되는 윈도우에 새로운 API가 그렇게 많지는 않습니다; 거의 모든 새로운 Win64 API들은 아이템니움(Itanium) 프로세서를 위한 윈도우 출시 때 이미 추가되었던 것들 입니다. 간단하게, 그 API들 중 가장 중요한 두 개의 API는 IsWow64Process와 GetNativeSystemInfo 입니다. 이 함수들은 Win32 어플리케이션이 자기 자신들이 Win64에서 돌고 있는지의 여부를 알려 줍니다. 그래서, 만약 64비트 환경에서 동작하고 있다면, 시스템의 진짜 사양(capability)를 올바르게 결정할 수 있게 해 줍니다. 반면에, 32비트 프로세스는 GetSystemInfo 함수를 호출하고, 오직 32비트 시스템인 것처럼, 시스템의 사양(capability)를 볼 수 있습니다. 예를 들어, GetSystemInfo는 32비트 프로세스 주소 영역만을 보고 합니다. 그림 4 는 x86에서는 사용할 수 없고, 오직 x64에서 쓸 수 있는 API들을 보여 주고 있습니다.

전부 다 64비트로 동작하는 윈도우 시스템이 아주 멋지게 들리겠지만, 현실적으로 여러분은 잠시 동안 Win32 코드를 필요로 하게 될 것 같습니다. 그러한 작업을 위해서, x64 버젼의 윈도우는 Win32와 Win64 프로세스를 동시에 동일한 시스템에서 동작시킬 수 있는 WOW64 서브시스템이 포함되어 있습니다. 그러나, 여러분의 32비트 DLL을 64비트 프로세스로 올리거나 혹은 반대의 일들은 지원되지 않습니다. (저를 믿으세요, 아주 좋은 일입니다.) 그리고 마침내 여러분은 구닥다리 16비트 코드에게 잘 가라고 인사를 할 수 있게 되었습니다!

x64 버젼의 윈도우에서는, 프로세서는 오직 Win64 DLL들만 로딩할 수 있는, Explorer.exe 같은 64비트 실행 파일에서부터 시작될 수 있습니다. 반면에, 32비트 실행 파일에서 시작한 프로세스는 오직 Win32 DLL 들만 로딩할 수 있습니다. Win32 프로세스가 커널 모드의 함수를 호출할 때-예를 들어서 파일을 읽는다든지-WOW64는 그 함수를 조용히 가로채서, 올바른 x64 코드의 주소를 주어서 호출하게 합니다.

물론, 서로 다른 종족(32비트와 64비트) 프로세서들끼리 통신할 일도 생길 수 있습니다. 운좋게도, Win32에서 여러분이 사랑하고 좋아했던 모든 프로세스간 통신 방법은 Win64에서도 동작합니다. 쉐어드 메모리(shared memory), 네임드 파이프(named pipe), 그리고, 기타 이름이 있는 동기화 객체들을 포함해서 말입니다.

여러분이 혹시 "그럼 시스템 디렉토리도 Win32와 Win64가 동일한가?"라고 생각하실 지도 모르겠습니다. 동일한 디렉토리가 32 비트와 64 비트 KERNEL32 나 USER32 등과 같은 동일한 이름의 시스템 DLL들을 동시에 가질 수 없습니다, 그렇지요? WOW64는 요술같이 파일 시스템의 리다이렉션(redirection)을 통해서 이 문제를 해결합니다. Win32 프로세스에서의 파일에 대한 쓰기 혹은 읽기 등이 발생하면, SysWow64라는 디렉토리에 있는 커널의 함수를 호출하는 것이 아닌, System32에 있는 커널의 함수를 호출하게 합니다. WOW64가 안보이게 SysWow64 디레토리로 요청한 것을 조용히 바꾸어 주는 것입니다. 그래서, Win64 시스템이 효과적으로 두 개의 시스템 디렉토리, 하나는 x64 용 바이너리들과 또 하나는 Win32용의 바이너리를 가지는 것 입니다.

약간 혼란스러울 수 있지만, 이러한 내부적인 처리는 상당히 부드러운 것처럼 보입니다. 제가 System32 디렉토리의 Kernel32.dll에서 Dir을 실행했을 때, SysWow64 디렉토리에서 했던 것과 정확히 똑같은 결과를 볼 수 있었습니다. 파일 시스템의 리다이렉션이 이런 방식으로 동작하는 것을 정확히 이해하기 까지, 제 자신은 머리가 좀 많이 아팠었습니다. 여러분이 x64 어플리케이션에서 32비트 WindowsSystem32 폴더를 알기를 정말로 원하신다면, GetSystemWow64Directory 라는 API가 여러분께 정확한 경로를 전달해 줄 것 입니다. 그래도, 전체 내용을 알기 위해서 MSDN 문서를 꼭 읽어 보시기 바랍니다.

파일 시스템의 리다이렉션이외에도, WOW64가 해주는 또 다른 마법 중의 하나가 레지스트리 리다이렉션입니다. 제가 아까 Win64 프로세스에서는 Win32 DLL들을 불러오지 않는다고 했던 말을 생각해 보시고, 그리고 COM 과 in-process 서버 DLL을 불러올 때, 레지스트리를 이용하는 것을 생각해 보시기 바랍니다. 만약, Win32 DLL에 구현되어 있는 COM 오브젝트를 64비트 어플리케이션이 CoCreateInstance를 이용해서, 생성하려고 하면 어떻게 될까요? DLL이 올라올 수 없습니다, 맞지요? WOW64는 Win32 어플리케이션으로부터의 접근을 SoftwareClasses 레지스트리 노드로 리다이렉션 해 줍니다. 결과적으로 Win32 어플리케이션에서 보는 레지스트리 구조는 x64 어플리케이션에서 보는 것과 서로 다르게 됩니다. 그리고, 여러분이 기대하시는 대로, 운영체제는 32비트 어플리케이션이 RegOpenKey와 그 계열 함수군을 이용하여, 실제로는 64비트인 레지스트리에 접근하려고 할 때, 내부적으로 새로운 플래그 값을 주어서, 그 값들에 접근할 수 있게 합니다.

약간만 더 깊숙이 들어가서, 쓰레드 로컬 데이타 영역도 살펴 보아야 합니다. x86 버젼의 윈도우에서는, FS 레지스터가 각 쓰레드의 메모리 영역과 가장 마지막 에러(GetLastError로 확인할 수 있는 에러 값), 그리고 쓰레드의 지역 저장 영역(TLS: Thread Local Storage, TlsGetValue로 값을 얻을 수 있는) 에 사용되었습니다. x64 버젼의 윈도우에서는, FS 레지스터는, GS 레지스터로 교체되었습니다. 그 외에는 거의 동일한 방식으로 x32와 x64의 운영체제가 동작합니다.

비록, 이 글이 x64의 사용자 입장에 초점을 두고 있기는 하지만, 커널 모드의 내부 구조에서 한 가지 추가된 중요한 점이 있습니다. PatchGuard라고 불리는 새로운 기술이 x64 윈도우에 추가되었습니다. 이 기술은 보안과 견고함을 위한 목적으로 추가되었습니다. 작게는 syscall 테이블이나 인터럽트 디스패치 테이블(interrupt dispatch table-IDT)를 변경하는 사용자 프로그램이나 드라이버들은 보안상의 문제와 잠재적인 안정성의 문제를 일으켜 왔었습니다. x64의 내부에서는, 그러한 방식으로 커널의 메모리를 지원되지 않는 방식으로 바꾸는 방식이 허용되지 않습니다. 이러한 것을 강화시키는 기술이 PatchGuard 입니다. 이 기술은 중요한 커널 메모리의 위치가 바뀌는 것은 커널 모드의 쓰레드에서 항상 감시합니다. 그리고 메모리가 바뀌면, 시스템은 버그체크를 통하여 멈춰 버립니다.

모든 것을 고려해 보아도, 만약 여러분이 Win32의 내부 구조에 어느 정도 알고 있고, 코드를 쓸 줄 알고, 동작하는지를 알고 있으면, Win64로의 이동에 있어서 크게 놀라지 않으실 겁니다. 거의 대부분은 좀 더 넓은 환경으로의 이동이라고 간주하셔 됩니다.


페이지 맨 위로페이지 맨 위로


x64의 간략한 내부 구조

자 이제, CPU의 구조 자체에 대해서 조금 살펴 보기로 하겠습니다. 왜냐하면, 기본적인 CPU의 명령어(instructions)에 대해서 알고 있는 것이, 개발(특히 디버깅!)을 훨씬 쉽게 만들기 때문입니다. 처음에 여러분이 알아 차릴 수 있는 것은, 컴파일러가 생성한 x64 코드가 여러분이 알고 있고, 사랑하는 x86 코드와 거의 흡사하다는 점입니다. IA64 코딩의 경우는 그렇게 유사하지 않았었습니다.

그리고, 두번째로 여러분이 알아차릴 수 있는 것은, 레지스터 이름이 여러분이 사용하던 것들과 조금씩 다르고, 레지스터 자체도 조금 많다는 점 입니다. 일반적인 용도의 x64 레지스턷들의 이름은 R로 시작합니다. 예를 들어서, RAX, RBX, 이런 것들이 있습니다. 이것들은 E이름을 가지고 있는 32비트 x86 레지스터들의 확장입니다. 아주 오래 전에, 16비트 AX 레지스터가 32비트 EAX가 되고, 16비트 BX 레지스터가 32비트 EBX가 되었던 것 처럼 말입니다. 32비트로 전이가 될 때 생겨난 E 레지스터들은 64비트로 이동하면서는 R 레지스터들이 된 것이죠. 그래서, RAX는 EAX의 계승자이고, RBX는 EBX의 계승, RSI는 ESI, 그런 식으로 확장되었습니다.

추가로, 8개의 일반적인 용도의 레지스터 (R8-R15)가 추가되었습니다. 64비트에서 주로 쓰이는 일반적인 용도의 레지스터들은 그림 5 와 같습니다.

물론 32비트 EIP 레지스터도 RIP 레지스터가 되었습니다. 그리고 32비트 명령어도 여전히 계속 동작하고 32비트 레지스터는 물론이고16비트 레지스터도 (EAX, AX, AL, AH등과 같은) 여전히 유효합니다.

그래픽 작업을 하거나 혹은 과학적인 연산이 필요한 고수들이 사용할 수 있도록, x64 CPU는 여전히 XMM0에서 XMM15로 명명된 16개의 128비트 SSE2 레지스터를 가지고 있습니다. 그 외 여기서 이야기하지 않는 다른 x64 레지스터들에 대한 모든 정보들은 WINNT.H 안에서 _CONTEXT로 적적하게 #ifdef된 구조체에서 찾아 보실 수 있습니다.

아뭏튼, x64 CPU는 언제라도 구형의 32비트 모드 혹은 64비트 모드 둘 다 에서 동작할 수 있습니다. 32비트 모드에서, CPU는 다른 x86 CPU처럼 명령어를 해석하고, 이에 기반하여 동작합니다. 64비트 모드에서는, CPU는 새로운 레지스터와 명령어를 지원하기 위해서 어떤 특정 명령어 인코딩에 대해서 약간의 사소한 조정을 하였습니다.

만약 여러분이 CPU 오피코드 인코딩 다이아그램(opcode encoding diagram)에 익숙하시다면, 아마도, 새로운 명령어 인코딩을 위한 공간은 빨리 없어진다는 것과 새로운 명령을 위해 여덟 개의 새로운 레지스터를 쥐어짜는 것은 쉬운 일은 아니라는 것을 기억하실 겁니다. 새로운 명령어를 추가하는 방법 중의 하나는, 거의 쓰이지 않는 명령어를 삭제하는 것 입니다. 그래서, x64에서는 기존의 CPU에서 사용되던 몇 개의 명령어가 삭제되었고, 지금까지, 제가 오직 그리워 하는 명령어는 스택의 일반용도 레지스터의 값을 모두 저장했다가, 다시 복원해 주는, 64비트 PUSHAD와 POPAD 입니다. 또 다른 방법 명령어 인코딩 공간을 확보하는 방법은, 64비트에서 더 이상 쓰이지 않는 세그먼트관련 레지스터들을 전부 제거해 버리는 것 입니다. 그래서, CS, DS, ES, SS, FS, 그리고 GS 레지스터가 더 이상을 쓰지 않습니다. 그렇게 많은 사람이 이 레지스터들을 그리워할 것 같지는 않군요.

64비트 주소가 사용됨에 따라, 여러분이 코드 사이즈에 대해서 궁금해 할지도 모르겠습니다. 예를 들어서, 아래의 경우는 흔한 32비트 명령어 입니다:

CALL DWORD PTR [XXXXXXXX]
위에서, X가 된 부분이 바로 32비트 주소입니다. 64비트 모드에서는, 위에서 X가 된 부분이 64비트 주소가 됩니다. 그래서, 5바이트 명령어가 9바이트로 되겠지요? 운 좋게도, 그렇지는 않습니다. 명령어는 여전히 똑같은 사이즈를 유지합니다. 64비트 모드에서는, 명령어의 32비트 오퍼랜드 부분은 현재 명령어에 상대적인 데이터의 오프셋으로 취급됩니다. 예를 하나 들어 보겠습니다. 32비트 모드에서는, 아래의 명령어가 00020000h번지에 있는 32비트 포인터 값을 부릅니다.
00401000: CALL DWORD PTR [00020000h]
64비트에서는, 똑같은 명령어가 0042100h(4010000h + 20000h)에 있는 64비트 포인터 값을 호출합니다. 만약 여러분이 여러분이 스스로 코드를 생성하고 있다면, 조금만 생각해 보아도 이러한 상대 주소 모드가 의미를 가지고 있다는 것을 알 수 있습니다. 여러분은 명령어에 8 바이트 포인트 값을 제공할 수 없습니다. 그 대신에, 여러분은 실제의 64비트 대상 주소가 존재하는 곳의 32비트 상대 경로를 제공해줘야 합니다. 그래서, 64비트 대상 포인터는 반드시 이 포인터를 이용하는 명령어와 2GB 내외로(앞뒤로 각각 2GB씩) 존재해야 한다는 말하지 않아도 알 수 있는 가정이 생깁니다. 거의 모든 사람들에게는 이것이 문제가 될 이유가 별로 없겠지만, 동적으로 코드를 생성한다든지 혹은 기존 코드를 메모리에서 변경하는 경우라면, 문제가 생길 수도 있습니다.

x64 레지스터의 가장 큰 장점 중의 하나는, 컴파일러가 스택 보다 모든 파라미터들을 레지스터에 전달하는 코드를 마침내 생성할 수 있다는 점 입니다. 스택에 파라미터를 구겨 넣는 것은 메모리 억세스를 필요로 합니다. 그리고, 우리는 CPU 캐쉬에 없는 메모리 억세스는 RMA이 그 내용을 가져올 때까지, 몇 사이클 동안 CPU를 잠시 서있게 한다는 사실도 알고 있습니다.

함수 호출 방식(Calling Convention) 의 경우, x64 내부구조를 이용하여 _stdcall, _cdecl, _fastcall, _thiscall 같은 기존에 존재하는 Win32 함수 호출 방식을 모조리 정리할 수 있는 기회를 가졌습니다. Win64의 경우는, 딱 하나의 함수 호출 방식이 존재합니다. _cdecl 같은 방식은 그냥 컴파일러에서 무시됩니다. 이러한 함수 호출 방식의 단일화는 무엇보다도 디버깅을 원활하게 하는데 큰 혜택입니다.

x64의 함수 호출 방식은 fastcall 방법과 유사합니다. x64 호출 방식에서는, 처음 네 개의 정수 인자가 이 목적을 위해 디자인된 레지스터에 전달됩니다:

 RCX: 1번째 정수 인자  
 RDX: 2번째 정수 인자  
 R8: 3 번째 정수 인자  
 R9: 4번째 정수 인자

네 개 이상의 정수 인자는 스택을 통해서 전달됩니다. 그리고 this 포인터는 정수 인자로 간주되어 항상 RCX 레지스터에서 발견될 수 있습니다. 부동 소수점 인자들에 대해서는, 처음 네 개의 인자들은 XMM0에서부터 XMM3 레지스터를 통해서 전달되고, 나머지 부동 인자들은 쓰레드 스택을 통해서 전달 됩니다.

함수 호출 방식에 대해서 조그만 더 깊숙이 들어가면, 인자들이 레지스터를 통해서 전달될 수 있음에도 불구하고, 컴파일러는 RSP 레지스터를 감소시키면서, 여전히 스택에 공간을 예약해 놓습니다. 최소한, 각각의 함수는 반드시 32바이트 (네 개의 64비트 값)을 예약해 놓아야 합니다. 이 공간은 레지스터들이 함수에 전달되어, 잘 알려진 스택 위치에 쉽게 복사되도록 합니다. 물론, 불리는 함수 측에서 함수의 인자를 채우지는 않습니다. 하지만, 필요한 경우에 이러한 스택 공간의 예약은, 함수 인자들이 레지스터에서 쉽게 스택으로의 복사를 가능하게 합니다. 물론, 네 개 이상의 인자가 전달된다면, 적절한 스택의 추가 공간이 반드시 예약되어야 합니다.

예를 한 번 들어 보겠습니다. 어떤 함수가 자식 함수에게 두 개의 정수 인자를 전달하는 경우가 있다고 가정을 한 번 해보겠습니다. 컴파일러는 두 개의 인자를 각각 RCX와 RDX에 각각 전달할 뿐만 아니라, RSP 스택 포인터 레지스터에서 32바이트를 빼놓습니다. 불리는 함수 입장에서는, 파라미터의 값을 RCX와 RDX 레지스터를 통해서 접근 가능합니다. 만약, 불리는 함수 코드가 레지스터가 다른 이유에서 필요할 경우, 이 값들은 예약된 32바이트 스택 영역에 복사됩니다. 그림 6은 6개의 정수 인자가 전달된 뒤의 레지스터와 스택을 보여 주고 있습니다. 그림 6 shows the registers and stack after six integer parameters have been passed.

Figure 6 정수의 전달
Figure 6 정수의 전달

x64 시스템에서의 파라미터 스택 정리는 약간 재밌는 모습을 보여 주고 있습니다. 기술적으로는, 불리는 함수(callee)가 아닌, 부르는 함수(caller)가 스택의 정리를 책임지고 있습니다. 그러나, 여러분은 프로롤그와 에필로그 코드를 제외하고 다른 부분에서 RSP를 조정하는 모습을 거의 보기가 힘들 것 입니다. PUSH와 POP 명령어로 스택에서 인자를 더하거나 빼주는 x86 컴파일러와 다르게, x64 코드 생성기는 (파라미터의 입장에서 보면) 얼마든지 큰 대상 함수에서도 쓸 수 있을 만큼 충분한 스택을 예약해 놓았습니다. 그래서, 자식 함수를 호출 시에, 파라미터를 설정하기 위해 똑같은 스택 영역을 계속 반복해서 씁니다.

짧게 말해서, RSP 레지스터는 거의 변하지 않습니다. 이 점은 ESP 레지스터 값이 파라미터가 스택에 추가되거나 정리되면서, 계속 변하는 x86 코드와 상당히 틀립니다.

예를 하나 들어 보겠습니다. 세 개의 다른 함수를 호출하는 x64 함수가 있다고 생각해 보시기 바랍니다. 처음 함수는 네 개의 인자(0x20 바이트=32바이트)를 받습니다. 두번째 인자는 열 두개의 인자(0x60바이트=96바이트)를 받습니다. 세번째 함수는 여덟 개의 인자(0x40=64바이트)를 받습니다. 프롤로그에서는, 생성된 코드는, 스택에 단지 96바이트만 예약해서 대상 함수가 인자들을 찾을 수 있도록, 96 바이트 안의 적절한 위치에 인자들을 복사해 놓습니다.

x86 함수 호출 방식에 대한 좀 더 자세한 내부 구조는 Raymond Chen's blog (영문) 에서 찾을 수 있습니다. 이것에 대해서 더 자세히 설명하지는 않겠습니다만, 몇 가지만 중요한 점을 더 말씀 드리자면. 첫 번째, 함수의 인자들 중, 처음 네 개의 인자들 중에서, 64비트 보다 적은 정수 인자들은 부호 확장(sign extended)이 일어나고, 적절한 레지스터를 통하여 전달할 수 있습니다. 두 번째로, 64비트 얼라인을 지키기 위해서, 절대로 8바이트의 정수배가 아닌 함수 인자가 스택에 존재해서는 안됩니다. 구조체를 포함해서, 1, 2, 4, 혹은 8 바이트가 아닌 인자들은 래퍼런스를 통해서 전달됩니다. 그리고 마지막으로, 8,16,32, 64비트의 구조체와 고용체는 동일한 크기의 정수 인 것 처럼, 전달됩니다.

함수 결과 값은 RAX 레지스터에 저장됩니다. 부동 소수점 형식의 값은 예외적으로 XMM0으로 돌려 받습니다. 함수 호출을 통하여, 제가 말씀드리는 레지스터들은 예약되어 있어야 합니다: RBX, RBP, RDI, RSI, R12, R13, R14, 그리고 R15. 그리고 지금 말씀드리는 레지스터들은 휘발성이고, 값이 없어질 수 있습니다:RAX, RCX, RDX, R8, R9, R10, 그리고 R11.

위에서 제가 예외 처리 메커니즘의 일환으로, 운영체제가 스택 프레임을 검사한다고 말씀 드렸습니다. 여러분이 한 번이라도, 스택을 검사하는 코드를 써보신 적이 있다면, 거의 임시적인 Win32 프레임의 레이아웃이 프로세스를 다루기 힘들게 한다는 것을 아실 겁니다. 이러한 상황이 x64에서는 더 좋아졌습니다. 만약 함수가 스택 공간을 할당하고, 다른 함수를 호출하고, 어떤 레지스터를 예약하거나, 예외 처리를 이용하다면, 그 함수는 반드시 표준화된 프롤로그와 에필로그를 생성하기 위하여 잘 정의된 명령어 집합(well-defined set of instructions)을 써야 합니다.

표준화된 함수의 스택 프레임을 사용하도록 강제하는 것은, 운영체제가 스택을 언제든지 탐색할 수 있는 것을 보장하는 한 방법입니다. 이러한 일관성에, 표준화된 프롤로그를 이용하여 컴파일러와 링커는 관련된 테이블에 데이터를 생성해야 합니다. 궁금하신 분들을 위해서, 좀 더 자세히 설명하면, 테이블의 모든 함수 정보들은 winnt.h에 정의되어 있는 IMAGE_FUNCTION_ENTRY64의 배열 테이블에 저장되어야 합니다. 어떻게 그 테이블을 찾는지 궁금하시다구요? 그 테이블은 PE헤더의 데이터 디렉토리 영역 안에 IMAGE_DIRECTORY_ENTRY_EXCEPTION의 엔트리가 지정하고 있습니다.

상당히 짧은 분량이지만, 내부구조의 많은 부분을 다루었습니다. 그러나, x86에 대한 큰 개념과 32비트 어셈블리 언어에 대한 지식이 있으신 분들은, 상당히 짧은 시간 안에 x64 명령어를 이해하실 수 있으실 겁니다.


페이지 맨 위로페이지 맨 위로


Visual C++로 x64용 어플리케이션 개발

Visual Studio® 2005 이전에도 마이크로 소프트에서 나온 C++ 컴파일러로 x64용 코드를 생성하는 게 가능했지만, IDE와 완벽히 통합은 되지 않았었습니다. 이 글에서는, 저는 여러분이 Visual Studio® 2005를 가지고 있고, 여러분이 x64용 도구 (기본 설치 옵션이 아닙니다.)을 선택했다고 가정하고 이 글을 진행하겠습니다. 그리고, 여러분이 기존에 Win32 사용자 모드에서 C++를 이용한 프로젝트 경험이 있다고 가정하겠습니다.

x64를 위한 첫 번째 단계는, 64비트 빌드 환경을 구축하는 것 입니다. 이미 다 아시겠지만, 여러분 프로젝트에는 기본으로 두 개의 환경이 있습니다. Debug와 Retail이 그것입니다. 여러분이 여기에서 더 해야 하는 것은, 두 개의 환경을 더 생성하는 것 뿐입니다. x64를 위한 Debug와 Retail 를 추가하는 것 입니다.

기존의 프로젝트 혹은 솔루션을 한 번 열어 보시기 바랍니다. 빌드 메뉴에서, Configuration Manager를 선택해 보십시오. Configuration Manager 다이얼로그 박스에서, Active Solution Plaftfrom 콤보 박스를 눌러서, New를 선택하시기 바랍니다(그림 7 ). 그러면, 여러분은 New Solution Plaftform이라고 명명된 다른 다이얼로그를 보실 수 있을 것 입니다.

그림7 새로운 빌드 환경의 생성
그림7 새로운 빌드 환경의 생성

x64를 새 플랫폼(그림 8)로 선택하고, 다른 설정은 그냥 놔두시기 바랍니다; 그리고 OK를 클릭하세요. 그게 전부랍니다! 여러분은 이제 네 가지의 가능한 빌드 환경을 구축하셨습니다: Win32 Debug, Win32 Retail, x64 Debug, x64 Retail. Configuration Manager 를 이용해서, 쉽게 다른 환경으로의 변경도 가능합니다.

자, 이제 여러분의 코드가 어떻게 64비트에 적용 가능한지 한 번 살펴 보기로 하겠습니다. x64 Debug 설정을 기본으로 놓으시고, 프로젝트를 빌드해 보시기 바랍니다. 프로젝트 자체가 아주 가볍지 않은 이상, Win32 환경에서 볼 수 없었던 컴파일 에러들을 보실 수 있을 겁니다. 여러분이 포팅이 불가능할 정도의 C++코드를 쓰지 않는 이상, 상대적으로 쉬운 에러 몇 개만 발생해서, 여러분은 쉽게 Win32와 x64에 대응하는 코드를 가지게 될 것 입니다. 특별히 조건부 컴파일 같은 것을 이용하지 않아도 말이죠.

그림 8 빌드 플랫폼의 선택
그림 8 빌드 플랫폼의 선택


페이지 맨 위로페이지 맨 위로


Win64 호환되는 코드 만들기

아마도, Win32 코드에서 x64로 컨번팅할 때 가장 문제가 되는 부분은, 타입 정의를 변경하는 일이 될 것 같습니다. 제가 혹시 앞에서 Win64의 자료형에 대해서 이야기 했던 것에 대해서 기억하시나요? C++ 컴파일러의 원래 자료형 (int, long 기타 등등) 을 쓰는 것 보다, 윈도우에서 정의한 typedef로 정의된 자료형을 쓰는 편이 깨끗한 Win32 x64 코드를 생성하는데 쉽습니다. 그리고 Win32의 자료형을 쓰실 때, 일관성있게 쓰셔야 할 필요가 있습니다. 예를 들어서, 윈도우가 HWND을 넘겨줄 때, 단지 편하고 쉽다고 해서, FARPROC 형식의 변수에 이 값을 저장하지 마십시요.

많은 코드를 업그레이드 하면서, 아마도 제가 흔히 그리고 쉽게 보는 에러는, 포인터 값이 32비트 데이타 타입인 int 혹은 long, 그리고 DWORD에 저장되어 있는 것 이었습니다. Win32와 Win64에서의 포인터는 사이즈는 틀리지만, 정수형은 동일한 크기를 유지합니다. 그러나, 컴파일러에게 포인터 값을 정수형에 저장하는 것은 금지하는 것 역시 가능하지 않습니다.

이런 상황을 해결하기 위해서, 윈도우 헤더에는 _PTR 타입이 선언되어 있습니다. 예를 들면, DWORD_PTR, INT_PTR, 그리고 LONG_PTR 같은 타입들이 대상 플랫폼에 따라서, 안전하게 포인터 변수를 이용하게 해줍니다. 예를 들어서, DWORD_PTR타입은 Win32에서 컴파일 되었을 때는, 32비트지만, Win64에서는 64비트입니다. 저의 경우는, 그동안의 연습으로, 어떤 자료형을 선언할 때, "내가 여기서 DWORD를 선언해야 할까? 아니면 DWORD_PTR를 선언해야 할까?"라고 물어보는 것이 버릇이 되어 버렸답니다.

기대하시는 대로, 정수형에서 얼마나 많은 바이트를 원하는지 계산하는데, 약간의 문제가 있을 수 있습니다. DWORD_PTR를 정의하는 헤더파일(basetsd.h)에서 역시 INT32, INT64, INT16, UINT32, DWORD64 같은 여러 형태의 정수를 선언해 놓고 있기 때문입니다.

자료형의 크기에 관련된 또 다른 문제는 printf와 sprintf의 포맷팅(formatting)입니다. 저는 확실히 과거에 %X 혹은 %08X등을 포인터 값을 나타내는데 사용했다는 점에서 약간의 죄책감(?)을 느끼고 있고, 그 코드는 x64 시스템에서 문제를 일으키고 있습니다. 옳은 방식은 대상 플랫폼의 포인터의 크기를 자동으로 계산해 주는 %p를 사용하는 것 입니다. 추가로, printf와 sprintf는 사이즈에 의존하지 않는 타입인 'I' 프리픽스(prefix)를 가지고 있습니다. 예를 들어서, 여러분은 %Iu를 UINT_PTR 변수를 출력하기 위해서 사용할 수 있습니다. 이와 동일한 방식으로, 여러분이 특정 변수가 언제 64비트 부호 있는 값이 될 것이라는 알고 있다면, %I64d를 쓰실 수 있습니다.

자료형의 불일치에서 일어난 에러들을 정리하는 것만으로는 Win64가 준비되었다고 말할 수 없습니다. 여러분은 아직은 아마도 x86에서만 돌아갈 수 있는 소스코드를 가지고 계실겁니다. Win64로의 포팅을 위해서, 특정 코드에서는 여러분이 Win32와 x64를 위한 두 가지 버젼의 함수를 쓰실 수도 있습니다. 이 때야말로, 전처리자(preprocessor)가 아주 유용하게 쓰입니다:

_M_IX86 
_M_AMD64 
_WIN64
전절한 전처리자의 사용이야 말로, 여러 플랫폼에서 동작하는 소프트웨어를 만들기 위해서는 필수적입니다. _M_IX86과 _M_AMD64는 특정 프로세서를 위해서 컴파일할 때를 위해서 정의되어 있습니다.

전처리자 매크로를 사용할 때, 뭘 원하는지 한 번 열심히 생각해 보시기 바랍니다. 예를 들어서, 이 코드가 정말로 x64 프로세서만을 위한 것이라면, 아래와 같이 매크로를 쓰시기 바랍니다:

#ifdef _M_AMD64
반면에, 동일한 코드가 x64와 아이템니움(Itanium)에서 동작하기를 원한다면, 여러분은 아래와 같이 쓰시는게 좋을 것 같습니다:
#ifdef _WIN64

이러한 매크로를 쓸 때 제가 유용하게 쓰는 버릇이 하나 있습니다. 바로 제가 무엇을 잊어 버렸을 때를 대비해서, 명시적으로 모든 경우에 대해서 #else 케이스문을 써주는 것입니다. 아래에 코드를 한 번 살펴봐 주시기 바랍니다:

#ifdef _M_AMD64 
// My x64 code here 
#else 
// My x86 code here 
#endif
또 다른 세번째 CPU가 나타나면? 저의 x86 코드가 저의 의도와는 다르게 컴파일 됩니다. 위의 코드를 더 좋은 방법으로 구성하는 방법은 아래와 같습니다:
#ifdef _M_AMD64 
// My x64 code here 
#elif defined (_M_IX86) 
// My x86 code here 
#else 
#error !!! Need to write code for this architecture 
#endif

그리고, 마지막으로 저의 경우는 Win32코드 중에서 x64로 쉽게 포팅되지 않는 부분 중의 하나는 인라인 어셈블러 부분이었습니다. Visual C++가 x64 를 위한 인라인 어셈블러를 지원하지 않거든요. 대신에 64비트 MASM이 제공되고, MSDN에 문서화가 되어 있습니다. ML64.exe와 다른 x64툴들은 (CL.EXE와 LINK.EXE를 포함해서) 커멘드라인에서도 쓸 수 있습니다. 단지 VCVARS64.BAT를 실행시키세요. 그러면, 여러분의 패스에 이 파일들의 경로를 포함해 줄 것 입니다.


페이지 맨 위로페이지 맨 위로


디버깅

여러분은 마침내, 여러분의 코드를 Win32와 x64 양쪽에서 깨끗하게 컴파일 할 수 있게 되었습니다. 이 대장정의 마지막은 "어떻게 디버깅을 하느냐?" 입니다. 여러분이 x64 에서 x64 버젼을 빌드 했는지의 여부에 상관없이, 여러분은 x64 모드에서 디버깅 하기 위해서는, Visual Studio의 리모트 디버깅 기능이 필요합니다. 운좋게도, 여러분이 64비트 컴퓨터에서 Visual Studio를 동작시키면, IDE가 내부적으로 이 단계를 여러분을 위해서 해줍니다. 어떤 이유로, 리모트 디버깅을 할 수 없을 때, 여러분의 또 다른 옵션은 x64용 WinDbg (영문)를 사용하시는 방법도 있습니다. 그러나, 그렇게 되면, Visual Studio 디버그의 많은 훌륭한 기능들을 포기하셔야 합니다.

여러분이 리모트 디버깅을 한 번도 해보신 적이 없다고 하더라도, 걱정하실 필요 없습니다. 한 번만 설치 되면, 리모트 디버깅도 로컬처럼 쉼없이 잘 동작합니다.

첫 번째 단계는 64비트 MSVSMON을 대상 컴퓨터에 설치하는 것 입니다. 이것은 일반적으로 VisualStudio와 같이 오는 RdbgSetup을 동작시키면, 알아서 해 줍니다. 일단 MSVSMON이 설치되면, Tools 메뉴에서 적절한 보안 설정(혹은 lack)등을 설정할 수 있습니다.

다음에는, Visual Studio 안에서, 여러분이 x64코드를 위해서 리모트 디버깅을 위한 설정을 하셔야 합니다. 이것은 프로젝트의 프로퍼티( 그림 9 )에서 할 수 있습니다.

그림 9 디버그 속성
그림 9 디버그 속성

여러분의 64비트 구성을 선택해서, Configuration 프로퍼티 아래에 있는 Debugging을 선택하시기 바랍니다. 위쪽에 Debugger to launch라고 되어 있는 부분이 있습니다. 일반적으로 이 부분이 Local Windows Debugger로 설정되어 있습니다. 이 설정을 Remote Windows Debugger로 변경하시기 바랍니다. 그 아래에서, 여러분이 디버깅을 시작할 때의 리모트 명령(예를 들어서, 프로그램 이름)과 리모트 시스템의 이름과 연결 속성을 선택해 줄 수 있습니다.

모든 것이 다 제대로 설정되었다면, 여러분의 x64 어플리케이션을 여러분이 Win32 프로그램에서 하듯이 디버깅할 수 있습니다. 여러분은 MSVSMON이 매 번 성공적으로 디버그와 연결이 될 때, "connected"라는 메세지를 보내 주기 때문에, 연결이 되었는지의 여부를 판별할 수 있습니다. 여기서 부터는, 여러분이 알고 있고, 사랑하는 Visual Studio 디버거와 거의 모든 것이 동일합니다. 64비트 레지스터를 보기 위해서, 그리고 비슷하지만 조금 다른 x64 어셈블리 코드를 보기 위해서 레지스터 창과 디스어셈블러 창을 띄우는 것을 잊지 마세요.

주의: 64비트 미니덤프는 32비트 덤프처럼 Visual Studio에서 직접 불러 올 수 없습니다. 대신, 여러분은 리모트 디버깅을 이용해야 합니다. 네이티브(native)와 매니지드(managed) 64비트 코드의 상호호환은 현재로서 Visual Studio 2005에서 지원되지 않습니다.


페이지 맨 위로페이지 맨 위로


매니지드 코드는 어떻게 하나요?

마이크로 소프트 .NET 프레임 웍에서 코딩 하는 것의 장점 중의 하나는, 깔려있는 운영제제의 많은 부분이 일반적인 목적으로 코드로 추상화 되어 사라져 버렸다는 점입니다. 추가로 IL 명령어 포맷은 CPU에 의존적이지 않습니다. 그래서, 이론적으로는, Win32에서 제작된 .NET에 기반을 둔 바이너리 파일은 x64시스템에서 수정하지 않고 동작해야 합니다. 그러나, 현실은 아주 복잡합니다.

x64버젼의 .NET 프레임웍 2.0을 제 x64시스템에 설치한 뒤에, 저는 제가 Win32에서 실행했던 똑같은.NET 실행파일을 동작시킬 수 있었습니다. 정말 좋지요? 물론, 모든 .NET에 기반을 둔 프로그램이 Win32와 x64에서 컴파일 하지 않고, 동일하게 동작 한다는 보장은 없습니다만, 다만 합리적인 범위의 시간 안에 그냥 동작합니다.

만약 여러분의 코드가 명시적으로 native code를 호출한다면 (예를 들어서, C# 혹은 Visual Basic 에서의 P/Invoke) 64비트 CLR에서 실행 시에 문제에 부딫힐 확률이 커집니다. 그러나, 다행스럽게도 컴파일러 스위치에 여러분의 코드가 어떤 플랫폼에서 동작할지를 명시하는 부분이 있습니다. 예를 들어서, 여러분은 64비트 CLR이 존재함에도 불구하고, 여러분의 코드가 WOW64에서 동작하기를 원한다고 명시할 수도 있습니다.


페이지 맨 위로페이지 맨 위로


최종 정리

모든 것을 고려해 보아도, x64 버젼의 윈도우로 이동하는 것은, 비교적 저에게는 어렵지 않은 과정이었습니다. 일단, 여러분이 운영체제 구조와 툴의 상대적으로 미미한 차이점을 한 번 훑어 보기만 하면, 하나의 코드를 바탕으로 해서 두 개의 플랫폼에서 동작하는 것이 어렵지 않습니다. Visual Studio 2005가 이 노력의 과정을 훨씬 쉽게 만들어 주고 있고, x64에 특화된 디바이스 드라이버나 툴, 예를 들면 Sysinternals.com의 Process Explorer은 매일 새롭게 나타나고 있습니다, 그러니 이 쪽으로 뛰어 들지 않을 이유가 없습니다!


페이지 맨 위로페이지 맨 위로



  • 이 문서는 한국 개발자를 위하여 Microsoft MVP 가 번역하여 제공한 문서입니다.
  • Microsoft 는 이 문서를 번역자의 의도대로 제공해 드리며 더 정확한 표현 또는 여러분의 의견을 환영합니다.
  • Microsoft 는 커뮤니티를 위해 번역자의 의도대로 이 문서를 제공합니다.



  • Matt Pietrek는 MSDN 매거진의 “Under the Hood Column” 뿐만 아니라 윈도우 시스템 레벨의 프로그래밍에 대해서 다수의 서적을 저술해 왔습니다. 과거에는 NuMega/Compuware의 BoundsChecker 시리즈의 제품을 담당하였고, 현재는 마이크포 소프트의 Visual Studio 팀에서 일하고 있습니다. 


    반응형
    반응형
    Windows XP사용시에 (다른 OS는 모르겠네요)
    VC++로 Thread Debug시에 OS가 멈추는 현상이 자주 발생합니다.
    IME쪽에 버그가 있어서 그렇다는데

    아래의 option을 Check해서 꺼주면 됩니다. 
    제어판 -> Data, Time, Language, and Regional Option ->Regional and Language option -> Languages Tab -> Detail ->Advanced Tab -> Check  the Turn off advanced text services

    영문윈도라서 영문윈도 경로로 적었으니 적절히 따라가서 고치시면 됩니다.



    Format하고나면 항상 잊어버려 정리해둡니다.
    반응형

    + Recent posts