Windows 管道的原理剖析

寵の児 提交于 2019-12-26 14:41:33

Windows 管道的原理剖析

今天有朋友问我,管道是否可以支持多个连接?其实是可以的,但是和socket使用起来有点不同,下面是msdn中管道服务端的例子:

#include <windows.h> 
#include <stdio.h> 
#include <tchar.h>
#include <strsafe.h>

#define BUFSIZE 512
 
DWORD WINAPI InstanceThread(LPVOID); 
VOID GetAnswerToRequest(LPTSTR, LPTSTR, LPDWORD); 
 
int _tmain(VOID) 
{ 
   BOOL   fConnected = FALSE; 
   DWORD  dwThreadId = 0; 
   HANDLE hPipe = INVALID_HANDLE_VALUE, hThread = NULL; 
   LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe"); 
 
   for (;;) 
   { 
      _tprintf( TEXT("\nPipe Server: Main thread awaiting client connection on %s\n"), lpszPipename);
      hPipe = CreateNamedPipe( 
          lpszPipename,             // pipe name 
          PIPE_ACCESS_DUPLEX,       // read/write access 
          PIPE_TYPE_MESSAGE |       // message type pipe 
          PIPE_READMODE_MESSAGE |   // message-read mode 
          PIPE_WAIT,                // blocking mode 
          PIPE_UNLIMITED_INSTANCES, // max. instances  
          BUFSIZE,                  // output buffer size 
          BUFSIZE,                  // input buffer size 
          0,                        // client time-out 
          NULL);                    // default security attribute 

      if (hPipe == INVALID_HANDLE_VALUE) 
      {
          _tprintf(TEXT("CreateNamedPipe failed, GLE=%d.\n"), GetLastError()); 
          return -1;
      }
 
      fConnected = ConnectNamedPipe(hPipe, NULL) ? 
         TRUE : (GetLastError() == ERROR_PIPE_CONNECTED); 
 
      if (fConnected) 
      { 
         printf("Client connected, creating a processing thread.\n"); 
      
         // Create a thread for this client. 
         hThread = CreateThread( 
            NULL,              // no security attribute 
            0,                 // default stack size 
            InstanceThread,    // thread proc
            (LPVOID) hPipe,    // thread parameter 
            0,                 // not suspended 
            &dwThreadId);      // returns thread ID 

         if (hThread == NULL) 
         {
            _tprintf(TEXT("CreateThread failed, GLE=%d.\n"), GetLastError()); 
            return -1;
         }
         else CloseHandle(hThread); 
       } 
      else 
        // The client could not connect, so close the pipe. 
         CloseHandle(hPipe); 
   } 

   return 0; 
} 
 
DWORD WINAPI InstanceThread(LPVOID lpvParam)
{ 
   HANDLE hHeap      = GetProcessHeap();
   TCHAR* pchRequest = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR));
   TCHAR* pchReply   = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR));

   DWORD cbBytesRead = 0, cbReplyBytes = 0, cbWritten = 0; 
   BOOL fSuccess = FALSE;
   HANDLE hPipe  = NULL;

   if (lpvParam == NULL)
   {
       printf( "\nERROR - Pipe Server Failure:\n");
       printf( "   InstanceThread got an unexpected NULL value in lpvParam.\n");
       printf( "   InstanceThread exitting.\n");
       if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
       if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
       return (DWORD)-1;
   }

   if (pchRequest == NULL)
   {
       printf( "\nERROR - Pipe Server Failure:\n");
       printf( "   InstanceThread got an unexpected NULL heap allocation.\n");
       printf( "   InstanceThread exitting.\n");
       if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
       return (DWORD)-1;
   }

   if (pchReply == NULL)
   {
       printf( "\nERROR - Pipe Server Failure:\n");
       printf( "   InstanceThread got an unexpected NULL heap allocation.\n");
       printf( "   InstanceThread exitting.\n");
       if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
       return (DWORD)-1;
   }

   // Print verbose messages. In production code, this should be for debugging only.
   printf("InstanceThread created, receiving and processing messages.\n");

// The thread's parameter is a handle to a pipe object instance. 
 
   hPipe = (HANDLE) lpvParam; 

// Loop until done reading
   while (1) 
   { 
      fSuccess = ReadFile( 
         hPipe,        // handle to pipe 
         pchRequest,    // buffer to receive data 
         BUFSIZE*sizeof(TCHAR), // size of buffer 
         &cbBytesRead, // number of bytes read 
         NULL);        // not overlapped I/O 

      if (!fSuccess || cbBytesRead == 0)
      {   
          if (GetLastError() == ERROR_BROKEN_PIPE)
          {
              _tprintf(TEXT("InstanceThread: client disconnected.\n"), GetLastError()); 
          }
          else
          {
              _tprintf(TEXT("InstanceThread ReadFile failed, GLE=%d.\n"), GetLastError()); 
          }
          break;
      }

   // Process the incoming message.
      GetAnswerToRequest(pchRequest, pchReply, &cbReplyBytes); 
 
   // Write the reply to the pipe. 
      fSuccess = WriteFile( 
         hPipe,        // handle to pipe 
         pchReply,     // buffer to write from 
         cbReplyBytes, // number of bytes to write 
         &cbWritten,   // number of bytes written 
         NULL);        // not overlapped I/O 

      if (!fSuccess || cbReplyBytes != cbWritten)
      {   
          _tprintf(TEXT("InstanceThread WriteFile failed, GLE=%d.\n"), GetLastError()); 
          break;
      }
  }
 
   FlushFileBuffers(hPipe); 
   DisconnectNamedPipe(hPipe); 
   CloseHandle(hPipe); 

   HeapFree(hHeap, 0, pchRequest);
   HeapFree(hHeap, 0, pchReply);

   printf("InstanceThread exitting.\n");
   return 1;
}

VOID GetAnswerToRequest( LPTSTR pchRequest, 
                         LPTSTR pchReply, 
                         LPDWORD pchBytes )
{
    _tprintf( TEXT("Client Request String:\"%s\"\n"), pchRequest );

    // Check the outgoing message to make sure it's not too long for the buffer.
    if (FAILED(StringCchCopy( pchReply, BUFSIZE, TEXT("default answer from server") )))
    {
        *pchBytes = 0;
        pchReply[0] = 0;
        printf("StringCchCopy failed, no outgoing message.\n");
        return;
    }
    *pchBytes = (lstrlen(pchReply)+1)*sizeof(TCHAR);
}

在这个例子中:

  1. 循环使用CreateNamedPipeConnectNamedPipe来建立一个实例连接。
  2. 使用线程针对这个实例进行读写控制。

本文我们就来探讨一下命名管道多个实例的原理。

1. 管道的使用

1.1 服务端

  1. 创建管道
HANDLE CreateNamedPipeA(
  LPCSTR                lpName,
  DWORD                 dwOpenMode,
  DWORD                 dwPipeMode,
  DWORD                 nMaxInstances,
  DWORD                 nOutBufferSize,
  DWORD                 nInBufferSize,
  DWORD                 nDefaultTimeOut,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
  1. 等待客户端连接
BOOL ConnectNamedPipe(
  HANDLE       hNamedPipe,
  LPOVERLAPPED lpOverlapped
);
  1. 读取管道
BOOL ReadFile(
  HANDLE       hFile,
  LPVOID       lpBuffer,
  DWORD        nNumberOfBytesToRead,
  LPDWORD      lpNumberOfBytesRead,
  LPOVERLAPPED lpOverlapped
);
  1. 写入管道
BOOL WriteFile(
  HANDLE       hFile,
  LPCVOID      lpBuffer,
  DWORD        nNumberOfBytesToWrite,
  LPDWORD      lpNumberOfBytesWritten,
  LPOVERLAPPED lpOverlapped
);

其中,可以通过CreateNamedPipeA创建一个管道的多个实例。

1.2 客户端

  1. 等待服务端管道可以连接
BOOL WaitNamedPipeA(
  LPCSTR lpNamedPipeName,
  DWORD  nTimeOut
);
  1. 连接服务端
HANDLE CreateFileW(
  LPCWSTR               lpFileName,
  DWORD                 dwDesiredAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreationDisposition,
  DWORD                 dwFlagsAndAttributes,
  HANDLE                hTemplateFile
);

//或者

BOOL WINAPI CallNamedPipe(
  _In_  LPCTSTR lpNamedPipeName,
  _In_  LPVOID  lpInBuffer,
  _In_  DWORD   nInBufferSize,
  _Out_ LPVOID  lpOutBuffer,
  _In_  DWORD   nOutBufferSize,
  _Out_ LPDWORD lpBytesRead,
  _In_  DWORD   nTimeOut
);
  1. 读取管道
BOOL ReadFile(
  HANDLE       hFile,
  LPVOID       lpBuffer,
  DWORD        nNumberOfBytesToRead,
  LPDWORD      lpNumberOfBytesRead,
  LPOVERLAPPED lpOverlapped
);
  1. 写入管道
BOOL WriteFile(
  HANDLE       hFile,
  LPCVOID      lpBuffer,
  DWORD        nNumberOfBytesToWrite,
  LPDWORD      lpNumberOfBytesWritten,
  LPOVERLAPPED lpOverlapped
);

2. 实现

管道也是采用设备驱动来实现的,原理如下分析。

2.1 调用堆栈

kd> kb 10
 # ChildEBP RetAddr  Args to Child              
00 9397b970 83e48593 86310c10 8756ab60 8757d8ec Npfs!NpFsdCreate
01 9397b988 840582a9 87cf852f 9397bb30 00000000 nt!IofCallDriver+0x63
02 9397ba60 84037ac5 86310c10 857ea488 857ea770 nt!IopParseDevice+0xed7
03 9397badc 84047ed6 00000000 9397bb30 00000040 nt!ObpLookupObjectName+0x4fa
04 9397bb38 8403e9b4 0204ebac 857ea488 00008401 nt!ObOpenObjectByName+0x165
05 9397bbb4 84062218 0204ec08 c0100080 0204ebac nt!IopCreateFile+0x673
06 9397bc00 83e4f1ea 0204ec08 c0100080 0204ebac nt!NtCreateFile+0x34
07 9397bc00 771770b4 0204ec08 c0100080 0204ebac nt!KiFastCallEntry+0x12a
08 0204eb68 771755d4 753faa21 0204ec08 c0100080 ntdll!KiFastSystemCallRet
09 0204eb6c 753faa21 0204ec08 c0100080 0204ebac ntdll!NtCreateFile+0xc

采用标准的设备通信来实现,打开的路径如下:

kd> dS 008add48 
0039c618  "\??\PIPE\browser"

其中\??\PIPE是指向\device\NamedPipe的符号链接。

2.2 驱动实现

管道是通过npfs.sys来实现的,其中回调函数信息如下:

NTSTATUS
NTAPI
DriverEntry(IN PDRIVER_OBJECT DriverObject,
            IN PUNICODE_STRING RegistryPath)
{
    DriverObject->MajorFunction[IRP_MJ_CREATE] = NpFsdCreate;
    DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = NpFsdCreateNamedPipe;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = NpFsdClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = NpFsdRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = NpFsdWrite;
    DriverObject->MajorFunction[IRP_MJ_QUERY_INFORMATION] = NpFsdQueryInformation;
    DriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = NpFsdSetInformation;
    DriverObject->MajorFunction[IRP_MJ_QUERY_VOLUME_INFORMATION] = NpFsdQueryVolumeInformation;
    DriverObject->MajorFunction[IRP_MJ_CLEANUP] = NpFsdCleanup;
    DriverObject->MajorFunction[IRP_MJ_FLUSH_BUFFERS] = NpFsdFlushBuffers;
    DriverObject->MajorFunction[IRP_MJ_DIRECTORY_CONTROL] = NpFsdDirectoryControl;
    DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = NpFsdFileSystemControl;
    DriverObject->MajorFunction[IRP_MJ_QUERY_SECURITY] = NpFsdQuerySecurityInfo;
    DriverObject->MajorFunction[IRP_MJ_SET_SECURITY] = NpFsdSetSecurityInfo;

    DriverObject->DriverUnload = NULL;

    DriverObject->FastIoDispatch = &NpFastIoDispatch;

    RtlInitUnicodeString(&DeviceName, L"\\Device\\NamedPipe");
    Status = IoCreateDevice(DriverObject,
                            sizeof(NP_VCB),
                            &DeviceName,
                            FILE_DEVICE_NAMED_PIPE,
                            0,
                            FALSE,
                            &DeviceObject);
}

2.3 CreateNamedPipeA原理

  1. 首先在文件对象的上下文中会存在如下结构体,这个结构体中CcbList代表创建的实例的个数;并且有一个CurrentInstances表示当前创建的实例的个数。
typedef struct _NP_CB_HEADER
{
    NODE_TYPE_CODE NodeType;
    LIST_ENTRY DcbEntry;
    PVOID ParentDcb;
    ULONG CurrentInstances;
    ULONG ServerOpenCount;
    PSECURITY_DESCRIPTOR SecurityDescriptor;
} NP_CB_HEADER, *PNP_CB_HEADER;

typedef struct _NP_FCB
{
    /* Common Header */
    NP_CB_HEADER;

    /* FCB-specific fields */
    ULONG MaximumInstances;
    USHORT NamedPipeConfiguration;
    USHORT NamedPipeType;
    LARGE_INTEGER Timeout;
    LIST_ENTRY CcbList;
#ifdef _WIN64
    PVOID Pad[2];
#endif

    /* Common Footer */
    NP_CB_FOOTER;
} NP_FCB, *PNP_FCB;

管理代码如下,我们知道,对于每个文件,文件对象的上下文FsContext是一样的,所以管道的管理信息都是放在文件对象上下文中的,管理信息如下:

Fcb = (PNP_FCB)((ULONG_PTR)RelatedFileObject->FsContext & ~1);
  1. 实例结构如下
typedef struct _NP_CCB
{
    NODE_TYPE_CODE NodeType;
    UCHAR NamedPipeState;
    UCHAR ReadMode[2];
    UCHAR CompletionMode[2];
    SECURITY_QUALITY_OF_SERVICE ClientQos;
    LIST_ENTRY CcbEntry;
    PNP_FCB Fcb;
    PFILE_OBJECT FileObject[2];
    PEPROCESS Process;
    PVOID ClientSession;
    PNP_NONPAGED_CCB NonPagedCcb;
    NP_DATA_QUEUE DataQueue[2];
    PSECURITY_CLIENT_CONTEXT ClientContext;
    LIST_ENTRY IrpList;
} NP_CCB, *PNP_CCB;
  1. 当每次创建一个管道的时候(已经存在的管道),那么就会比较当前存在的实例数目是否大于最大的实例数目,如果大于,直接不创建。
if (Fcb->CurrentInstances >= Fcb->MaximumInstances)
{
    IoStatus.Status = STATUS_INSTANCE_NOT_AVAILABLE;
    TRACE("Leaving, IoStatus.Status = %lx\n", IoStatus.Status);
    return IoStatus;
}
  1. 否则创建一个实例,将其插入到队列中
InsertTailList(&Fcb->CcbList, &Ccb->CcbEntry);

2.4 客户端CreateFile的原理

服务端创建好了管道之后,客户端就会连接上来了,连接的主要代码是:

IO_STATUS_BLOCK
NTAPI
NpCreateClientEnd(IN PNP_FCB Fcb,
                  IN PFILE_OBJECT FileObject,
                  IN ACCESS_MASK DesiredAccess,
                  IN PSECURITY_QUALITY_OF_SERVICE SecurityQos,
                  IN PACCESS_STATE AccessState,
                  IN KPROCESSOR_MODE PreviousMode,
                  IN PETHREAD Thread,
                  IN PLIST_ENTRY List)
{
    ListHead = &Fcb->CcbList;
    NextEntry = ListHead->Flink;
    while (NextEntry != ListHead)
    {
        Ccb = CONTAINING_RECORD(NextEntry, NP_CCB, CcbEntry);
        if (Ccb->NamedPipeState == FILE_PIPE_LISTENING_STATE) break;

        NextEntry = NextEntry->Flink;
    }

    if (NextEntry == ListHead)
    {
        IoStatus.Status = STATUS_PIPE_NOT_AVAILABLE;
        TRACE("Leaving, IoStatus.Status = %lx\n", IoStatus.Status);
        return IoStatus;
    }

    IoStatus.Status = NpInitializeSecurity(Ccb, SecurityQos, Thread);
    if (!NT_SUCCESS(IoStatus.Status)) return IoStatus;

    IoStatus.Status = NpSetConnectedPipeState(Ccb, FileObject, List);
    if (!NT_SUCCESS(IoStatus.Status))
    {
        NpUninitializeSecurity(Ccb);
        TRACE("Leaving, IoStatus.Status = %lx\n", IoStatus.Status);
        return IoStatus;
    }

    Ccb->ClientSession = NULL;
    Ccb->Process = IoThreadToProcess(Thread);
}

这里主要是从服务端的实例中,取出一个可用的实例,标记为被连接的状态。

以后就可以根据这个文件对象来同行了。

3. 总结

3.1 多实例

从上面分析可以发现,一个命名管道可以有多个实例,因为在驱动中,第一次创建和后面创建实例走的流程不同,并且创建的实例都通过链接连接起来。每次调用CreateNamedPipe都会创建一个管道的实例。

3.2 客户端和服务端

从上面我们也可以发现,客户端和服务端使用的API打开或者创建管道的方式也不一样,因为对于管道响应的函数也不同:

DriverObject->MajorFunction[IRP_MJ_CREATE] = NpFsdCreate;
DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = NpFsdCreateNamedPipe;

其中CreateNamedPipe使用如下方式创建:

return IoCreateFile(FileHandle,
                        DesiredAccess,
                        ObjectAttributes,
                        IoStatusBlock,
                        NULL,
                        0,
                        ShareAccess,
                        CreateDisposition,
                        CreateOptions,
                        NULL,
                        0,
                        CreateFileTypeNamedPipe,
                        (PVOID)&Buffer,
                        0);

针对CreateFileTypeNamedPipe参数的IoCreateFile调用会发送IRP_MJ_CREATE_NAMED_PIPE的IRP请求,从而进入驱动的管道创建流程。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!