LPC 函数

微软设计本地过程调用是为了能有效地和 Windows NT 子系统进行通讯。 尽管在理解 LPC 机制之前并不需要知道关于子系统的东西,当然能知道一点还是会觉得有意思的,也是我们所建议的。在这一章里,我们讨论了子系统,之后讲了一点未公开的 LPC 机制。


THE ORIGIN OF THE SUBSYSTEMS

尽管微软从未指出“NT”到底是什么意思,一个普遍的说法就是它是“New Technology”的缩写。但并不是说在 Windows NT 里什么都是新的。Windows NT 借用了很多以前的操作系统的概念。例如,NTFS(New Technology File System)从 OS/2 的 HPFS (High Performance File System)那里接来了许多理念。Win32 API 本身是Windows 16位 API 的扩展。Windows NT 3.51 的用户界面来自于 Windows 3.1,而 Windows NT 4.0 由继承了Windows 95 的用户界面。 Windows 2000(Beta 3)的用户界面与 Windows NT 4.0 的或多或少也有些相似。在这一节里,我们来讨论 Windows NT 的总体的体系,这种体系结构是微软从 MACH 操作系统那里借用来的,而 MACH 操作系统是由卡内基梅隆大学开发的。

在 80 年代,DOS 和各种 Unix 主宰着操作系统世界。 DOS 是一个单核的体系,内核就是一大块代码。Unix 使用的是分层的结构,操作系统分成了几个层次,每个层次只能使用下一层提供的接口。 MACH 操作系统使用的则是一种新型的客户机-服务器的方法。MACH 最初的版本是基于 BSD Unix 4.3 的。

MACH 小组主要有两个设计目标。第一,他们想有一个比 BSD 4.3 更为结构化的代码。第二,他们想支持各种 Unix API 的不同变体。 他们实现了这两个目标,办法是将内核代码的执行放到用户进程中,用户进程作服务器。MACH 非常小,只提供了所有 Unix API 所共有的基本系统服务。因此,我们叫它微内核。服务进程运行在用户模式下,提供更复杂的 API 接口。一般的应用程序进程是这些服务进程的客户。通过与 RPC(remote procedure call)类似的技术可以实现这种机制。在完成所需的处理之后,服务进程将结果返回给客户。

要在 MACH 环境中支持一种新 API,就需要编写服务进程和模拟库来对新 API 进行支持。 并非所有的服务进程都提供不同的 API。一些也会提供内存管理或 TTY 管理等一般功能 。

Windows NT 设计小组的目标与 MACH 开发者们的类似。他们想要支持 Win32、OS/2 和 POSIX 的 APIs,并为以后的 API 保留空间。Client-server 体系就成了很自然的选择。

在 Windows NT 下,服务器程序被称为受保护的子系统。子系统是运行在本地系统安全上下文中的用户进程。我们称其受保护的子系统是因为他们都是运行在单独的地址空间的独立进程,因此受到保护,以防止被访问/修改。 子系统可分为两类:

.Integral 系统(Integral subsystems)
.环境子系统(Environment subsystems)

Integral Subsystems

整体子系统完成一些基本的操作系统的工作。对于 Windows NT,这包括 Local Security Authority (lsass.exe)、Security Accounts Manager、Session Manager (smss.exe) 和 network server。Local Security Authority (LSA) 子系统位用户管理安全访问令牌;Security Accounts Manager (SAM) 子系统维护着用户权限信息,包括密码、用户所属的权限组、每位用户的访问权限以及某一用户的特别权限。Session Manager 子系统启动并记录 NT 的登陆会话而且是受保护子系统的中介。


Environment Subsystems

环境子系统是服务器进程,通过调用系统服务来为该子系统的应用程序完成操作系统功能。环境子系统运行在用户模式,它的与最终用户之间的接口在 Windows NT 的顶层模拟了另外的操作系统,比如说 OS/2 或 POSIX。在Windows NT 3.51 下,甚至连 Win32 API 也是通过子系统进程来实现的。

注:并非所有客户端 DLLs 中的API 函数都需要将调用传递给子系统进程。比如,大多数的 KERNEL32.DLL 调用可以直接映射为内核提供的系统服务。一些 API 函数通过 NTDLL.DLL 调用系统服务。大多数 USER32.DLL 和 GDI32.DLL 的函数都将调用传递给子系统进程 (在 Windows NT 4.0 里,出于性能上的考虑,微软将 Win32 子系统移进了内核)。

由 Windows NT 提供的系统调用内核叫做 native API。 Win32 子系统使用 native API 来实现 Win32 API。一般来讲,用户程序调用子系统提供的 API,避免了使用 native API 的繁琐。我们将用户程序称为子系统的客户,子系统提供程序使用的 API。

客户进程与子系统间的通信是通过一种叫 local procedure call (LPC) 的机制来完成的,是微软为其特别设计的。出于未知的原因,微软没有公开 LPC 的接口。 并没有理由说 LPC 不能作为进程间通讯(IPC)的机制。微软为机器间的客户机服务器通讯提供了一套 RPC。 Windows NT 优化了 RPC,将其转为了 LPC,只不过是客户机服务器全在同一台机器上罢了。然而,RPC 还是有多余的地方。LPC 在纯形式(raw form)下最为高效,子系统也只使用这种形式的 LPC。 除此之外,RPC 并未提供对 LPC 最快形式——Quick LPC 的访问机制。出于这些原因,我们提供了 LPC 接口的信息。


LOCAL PROCEDURE CALL

在 Windows NT 中,客户-子进程通讯的方式与 MACH 操作系统中的类似。每种子系统都有一个可户端 DLL 用来与客户可执行程序链接。 DLL 中有子系统 API 的 stub 函数。只要客户进程——使用子系统接口的应用程序——发出 API 调用,DLL 中相应的 stub 函数就将调用传递给子系统进程。 在处理完成后,子系统进程将结果返回给客户 DLL。DLL 中的 stub 函数等待子系统返回结果,再转回来将结果传递给调用者。客户进程只会觉得像是在调用自己代码中的函数一样。在 RPC 下,客户机确实是通过网络来调用某远程主机,因此叫做远程过程调用。Windows NT 的服务器与客户机运行在同一台机器上,因此此机制叫本地过程调用。

LPC 共有三种类型。第一类最多只能发送394字节的小消息。第二类能发送大一些的消息。第三类 LPC 叫做 Quick LPC,使用在 Windows NT 3.51 的 Win32 子系统上。

前两类 LPC 使用端口对象进行通讯。端口类似于 Unix 上的套接字或命名管道,是进程间的双向通讯通道。然而,与套接字不同的是,通过端口传递的数据并不是用流的形式。端口明确了消息的边界。简单讲,可以使用端口发送和接收消息。子系统用众所周知的名字来创建端口。需要调用子系统服务的客户进程使用这个名字打开相应的端口。打开端口后,客户程序就可以通过端口与服务器程序通讯了。


Short Message Communication

客户程序-子系统之间的建立在端口上的通讯过程如下。服务器程序/子系统使用 NtCreatePort() 函数创建一个端口。 端口的名字是公开的且是客户程序(或客户端 DLL)已知的。NtCreatePort() 返回一个端口句柄,子系统就使用此句柄调用 NtListenPort() 函数来等待和接受请求。任何进程都可以通过此端口发送连接请求并得到一个用于通讯的端口句柄。子系统接收请求消息、处理消息然后将回复经端口送回客户程序。

客户程序使用 NtConnectPort() 函数向正处于等待中子系统发送一个连接请求。 当子系统接收到连接请求后,跳出 NtListenPort() 函数并使用 NtAcceptConnectPort() 函数接受请求。 NtAcceptConnectPort 为客户所请求的连接返回一个新的端口句柄。服务器程序可以通过关闭句柄来断开与某一客户程序的连接。子系统使用 NtCompleteConnectPort() 函数完成连接协议。 现在,客户程序也从 NtConnectPort() 函数返回并得到了通讯端口的句柄这个句柄是此客户程序私有的。子进程不能继承端口句柄,所以需要再次打开子系统端口。


在完成通讯握手后,客户程序和子系统就可以通过这个端口开始通讯了。 客户使用NtRequestPort() 函数向子系统发送请求。当 NtRequestPort() 函数向子系统发送数据报消息时,客户并不会收到对发送消息的任何确认信息。若客户希望请求能得到回复,可以使用 NtRequestWaitReplyPort() 函数向子系统发送请求并等待子系统的回复。子系统使用 NtReplyWaitReceive() 函数接收请求的消息并使用 NtReplyPort() 函数发送回复消息。子系统可以使用 NtReplyWaitReceivePort() 函数回复上一消息并等待下一请求。Figure 8-1 为通讯的整个过程。

一个子系统可以用同一个端口接受/回复多于一个的客户。消息中包含着域来标识客户进程和线程。内核将进程 ID 和线程 ID 填入消息中。因此,子系统可以信赖此信息,LPC 也成为一种安全可靠的通讯机制,因为消息的发送方可以被正确地辨明。


Shared Section Communication

通过端口只能发送短消息——最多304字节。 大一些的消息可以通过一个共享内存区来发送。若客户想通过共享内存来传递消息,就必须在调用 NtConnectPort() 之前做一些额外的处理。客户需要创建一个 section 对象,这要用到 CreateFileMapping()——一个公开的函数。消息的大小只受限于 section 的大小。客户不需要将 section 映射到地址空间,端口连接程序会来完成。但是客户必须将 section 句柄传递给 NtConnectPort() 调用。函数返回 section 在客户和服务器程序中的映射地址。现在只要客户想要调用服务,就只需要将参数拷贝到共享 section 并通过端口发送消息。这个消息只用做客户请求的通知因为实际的参数是通过共享 section 传递的。

一般来说,作为短消息的一部分,客户指定服务器的共享 section 的地址空间和参数在共享 section 中的偏移。如果服务器用到此信息,就应该先进行有效性检查。 处理完请求后,服务器程序还要通过共享 section 将结果送回。 除了额外的处理,共享 section 的 LPC 与断消息通讯使用相同的端口 APIs。其操作序列也与短消息通讯的类似,有一点例外就是除处理消息端口外,客户必须创建共享 section 并完成参数传递。Figure 8-1 中的操作序列也适用于共享 section 的 LPC.


PORT-RELATED FUNCTIONS

在这一节里,我们来详细讨论端口相关的函数以及调用时传递的参数。我们还准备了样例程序来演示短消息传递和共享 section 的消息传递。下面我们来看程序。

NtCreatePort 
int _stdcall
NtCreatePort(
PHANDLE PortHandle,
POBJECT_ATTRIBUTES ObjectAttributes,
DWORD MaxConnectInfoLength,
DWORD MaxDataLength,
DWORD Unknown);

此函数为通讯创建一个新端口。端口的名字和在对象层级中的父目录通过 ObjectAttributes 参数传递。参数 MaxConnectInfoLength 指定可以向连接请求传递的最大信息字节数(在本节后面我们会讨论连接信息)。MaxDataLength 参数是可以通过端口传递的最大消息字节数。这两个参数都被忽略掉了。操作系统总是将连接信息的长度设为 260 字节,数据长度设为328字节,都是这些参数的最大值。传递的值一定要小于这些最大值,否则函数会返回错误。第五个参数未知,可以传递 0。函数将新创建的端口句柄返回到 PortHandle。服务器进程使用这个句柄来接受客户的请求。

NtConnectPort 
int _stdcall
NtConnectPort(
PHANDLE PortHandle,
PUNICODE_STRING PortName,
PVOID Unknown1,
LPCSECTIONINFO sectionInfo,
PLPCSECTIONMAPINFO mapInfo,
PVOID Unknown2,
PVOID ConnectInfo,
PDWORD pConnectInfoLength);

客户使用此程序来建立与服务器程序的 LPC 通讯。用于连接的端口名是用 Unicode 字符串在 PortName 参数中指定的。第二个参数,目前还未知,并不能传 NULL,否则函数的有效性检查会失败。第三个参数仅在使用共享 section 的 LPC 中使用,它是一个结构体指针,此结构体定义如下:

typedef struct LpcSectionInfo {
DWORD Length;
HANDLE SectionHandle;
DWORD Param1;
DWORD SectionSize;
DWORD ClientBaseAddress;
DWORD ServerBaseAddress;
} LPCSECTIONINFO, *PLPCSECTIONINFO;

结构体的 Length 域指定了结构体的大小,值总是24。此函数的调用者——客户程序——除了要填 Length 外还要填 SectionHandle 和 SectionSize 域。CreateFileMapping() 可以创建一个所需大小的共享 section。从 NtConnectPort() 函数返回时, LPCSECTIONINFO 结构体的 ClientBaseAddress 和 ServerBaseAddress 域分别包含着 section 在客户地址空间和服务器地址空间中的映射地址。

下一个传给 NtConnectPort() 函数的参数——mapInfo——也只用于共享 section 的 LPC, 此参数是一个指向如下结构体的指针:

typedef struct LpcSectionMapInfo
{
DWORD Length;
DWORD SectionSize;
DWORD ServerBaseAddress;
} LPCSECTIONMAPINFO, *PLPCSECTIONMAPINFO;

此结构重复了 LPCSECTIONINFO 结构体中的信息。客户需要填的只有 Length 域,即此结构体的大小——12字节。我们尚未搞清楚将此结构体传递给 NtConnectPort() 函数的重要意义。 同样,此函数也需要传递一个有效的结构体。如果传递 NULL,函数就会失败。我们注意到此结构体的两个成员,即 SectionSize 和 ServerBaseAddress 在函数返回时被清零。

我们还不知道传给 NtConnectPort() 函数的下一个参数,故设为 NULL。

客户程序可以通过连接请求向服务器发送一些信息。服务器通过 LPC 消息接收此信息,而 LPC 消息又是从接收连接请求的 NtReplyWaitReceivePort() 函数得到的。ConnectInfo 参数指向这条连接信息。连接信息的大小通过 pConnectInfoLength 传递,此参数是一个指向双字的指针。连接时服务器还要向客户发回一些信息。 此信息返回相同的 ConnectInfo 缓冲区,pConnectInfoLength 被设置为所返回的连接信息的大小。


NtReplyWaitReceivePort 
int _stdcall
NtReplyWaitReceivePort(
HANDLE PortHandle,
PDWORD Unknown,
PLPCMESSAGE pLpcMessageOut,
PLPCMESSAGE pLpcMessageIn);

此函数被 LPC 的服务器端用来接收客户请求并向客户回复。第一个参数是从 NtCreatePort() 函数得到的端口号。第二个参数,当前未知,可以传 NULL。 第三个参数是对前一请求的回复消息。此参数为 NULL 时函数仅接受从客户来的请求。第四个参数,一个指向 LpcMessage 的结构体,在函数返回时保存着所请求的信息。第三和第四个参数都是指向 LpcMessage 结构体的指针,我们列在这里。

typedef struct LpcMessage {

/* LPC Message Header */

WORD ActualMessageLength;
WORD TotalMessageLength;
DWORD MessageType;
DWORD ClientProcessId;
DWORD ClientThreadId;
DWORD MessageId;
DWORD SharedSectionSize;
BYTE MessageData[MAX_MESSAGE_DATA];
} LPCMESSAGE, *PLPCMESSAGE;

ActualMessageLength 被设为保存在 MessageData 域中实际消息的大小,而 TotalMessageLength 被设为整个 LpcMessage 和 MessageData 的大小。系统,而非客户机-服务器,设置 MessageType 域。消息有几种类型,我们详细介绍几个重要的:


LPC_REQUEST 当客户使用 NtRequestWaitReplyPort() 函数发送求请时,服务器接收此消息。服务器应该用  NtReplyPort() 函数或 NtReplyWaitReceivePort() 函数回复此消息。除了 LPC_REQUEST 消息外,其它消息服务器一概不应回复。NtRequestWaitReplyPort()  一直等到收到服务器的回复才再将回复消息返回给客户。有效的办法是,若服务全不发送回复消息,调用NtRequestWaitReplyPort() 函数的客户线程就挂起

LPC_REPLY 当服务器回复此请求时,客户从 NtRequestWaitReplyPort() 函数接收此类消
息。 

LPC_DATAGRAM 当客户使用 NtRequestPort() 函数发送请求时,服务器接收此类消息。正如此类型名所示,客户并不得到服务器对此类消息的回复。若服务器尝试使用NtReplyPort() 函数或 NtReplyWaitReceivePort() 函数回复此消息,则函数失败并返回错误。

LPC_PORT_CLOSED 当客户关闭端口句柄时,服务器接收此消息。若客户未关闭端口便结束,则操
作系统代替客户将其关闭。因此,服务器总能收到 LPC_PORT_CLOSED 消息,并可用它来释放为各客户所分配的资源。

LPC_CLIENT_DIED 当客户退出时,服务器接受此程序。更多信息请参看对  NtRegisterThreadTerminatePort() 函数的描述。

LPC_CONNECTION_REQUEST 当客户试图用 NtConnectPort() 函数连接端口时,相应的服务器接收此消息。

LpcMessage 结构体接下来的两个域分别由系统设置为客户进程的进程 ID 和线程 ID。再下一个域由系统设为系统生成的唯一的消息 ID。 服务器程序可以信赖这些域因为这些域是由系统而非客户设置的。这些域对用户接收的消息并没什么意义,因此返回消息中的这些域由 NtRequestWaitReplyPort() 函数置为 0。

SharedSectionSize 只在共享 section 的 LPC 中用到。当向服务器程序传递   LPC_CONNECTION_REQUEST 类型的消息时系统将此域设为共享 section 的大小。

最后一个域是实际的消息,一个变长的域。 客户机-服务器可以可以选择只分配足够的空间来保存结构体参数和实际的消息。当为了接收消息而传递此结构体的指针时,必须分配足够的内存空间来放置端口另一端进程送来的消息。若分配的不够,就会收到一个“Invalid Access”或类似的错误。为了安全起见,传递用于接收消息的指针时应该总是为消息分配最大空间。

NtAcceptConnectPort 
int _stdcall
NtAcceptConnectPort(
PHANDLE PortHandle,
DWORD Unknown1,
PLPCMESSAGE pLpcMessage,
DWORD acceptIt,
DWORD Unknown3,
PLPCSECTIONMAPINFO mapInfo);

只要服务器一收到连接请求,就进行一个连接建立过程。首先调用 NtAcceptConnectPort() 函数,然后是NtCompleteConnectPort() 函数。这一系列的操作在客户与服务器之间建立了一个通讯通道。通道的客户一端是用从 NtConnectPort() 函数得到的端口句柄来表示。NtAcceptConnectPort() 的第一个参数是一个端口句柄指针,返回时这个指针被设置为指向消息端口的另一句柄。这个句柄就是通讯通道服务器端的句柄,尽管服务器可以使用
由 NtAcceptConnectPort() 函数返回的句柄来接受所有的客户请求。当服务器不再想使用某一通讯通道接受请求时,服务器可以关闭这个由 NtAcceptConnectPort() 返回的句柄。 客户向已关闭的通讯通道发送的任何请求都会失败。

我们现在还没有确定出第二个参数——一般置 0。 第三个参数就是向客户返回的 LPC 消息,此消息就是服务器发送的连接信息。第四个参数是 acceptIt,若服务器不能接受连接请求就传递 0;若可以接受连接请求,则传非 0。第五个参数,尚未确定,可设为 0。 最后一个参数是一个指向 LpcSectionMapInfo 结构体的指针,在返回时会填入相应的数据。我们已经解释了这些结构体的成员。这个结构体为共享 section 提供供服务器以后与客户通讯的信息。


NtCompleteConnectPort 
int _stdcall
NtCompleteConnectPort(
HANDLE PortHandle);

服务器用 NtCompleteConnectPort() 结束连接过程。此函数的唯一参数就是由上一次调用 NtAcceptConnectPort()
函数所返回的端口句柄。客户在 NtConnectPort() 中等待直到直到服务器调用 NtCompleteConnectPort() 函数完
成连接过程。


NtRequestWaitReplyPort 
int _stdcall
NtRequestWaitReplyPort(
HANDLE PortHandle,
PLPCMESSAGE pLpcMessageIn,
PLPCMESSAGE pLpcMessageOut);

客户使用此函数来向服务器发送请求并等待其回复。 第一个参数是通过上一次调用 NtConnectPort() 函数得到的端口句柄。 参数 pLpcMessageIn 是一个指向发送给服务器的 LPC 请求消息的指针。最后一个参数是一个指向另一个 LPC 消息结构体的指针,函数返回时,该消息结构体中包含的是服务器的回复消息。

NtListenPort 
int _stdcall
NtListenPort(
HANDLE PortHandle,
PLPCMESSAGE pLpcMessage);

这个函数非常小,在内部使用了 NtReplyWaitReceivePort() 函数。这里我们给处此函数的伪码:

NtListenPort(HANDLE PortHandle, PLPCMESSAGE pLpcMessage)
{
while(1) {
rc = NtReplyWaitReceivePort( PortHandle, 
NULL,
NULL,
pLpcMessage);
if (rc == 0)
if(pLpcMessage->MessageType == LPC_CONNECTION_REQUEST)
break;
}

return rc;
}

从此伪码中可以看到,NtListenPort() 函数忽略了除连接请求之外的所有消息。 要是服务多个客户就不能使用这个函数了。服务多个客户时,服务器会将连接请求和其它的请求混合起来。服务器需要把连接请求从其它的请求中捡出来并做相应的处理。若一次只能有一个客户连接,服务器就可以用 NtListenPort() 函数来得到连接请求,然后启动一个循环来接受并处理其它客户的连接请求。


NtRequestPort 
int _stdcall
NtRequestPort(
HANDLE PortHandle,
PLPCMESSAGE pLpcMessage);

这个函数只是向端口发送一条消息然后返回。在此端口等待的服务器线程取得消息并作相应处理。服务器线程不需要向调用者返回结果。这种情况下,header 中的消息类型为 LPC_DATAGRAM。使用此函数发送的消息就像一个发送方不用收到确认的数据报。


NtReplyPort 
int _stdcall
NtReplyPort(
HANDLE PortHandle,
PLPCMESSAGE pLpcMessage);

若服务器要给客户发送回复,但又不想被阻塞以致收不到客户的下一请求,就可以使用此函数。此函数的第一个参数是一个端口句柄,第二个参数是发送给客户的回复。


NtRegisterThreadTerminatePort 
int _stdcall
NtRegisterThreadTerminatePort(
HANDLE PortHandle);

若客户在连接到端口后调用此函数,则当客户结束时,操作系统向客户发送 LPC_CLIENT_DIED 消息。 尽管客户关闭了端口句柄并保持运行,系统还是维护一个对端口的引用。 因此,操作系统在 LPC_CLIENT_DIED 消息之后发送 LPC_PORT_CLOSED 消息,而不是在关闭端口句柄之后。


NtSetDefaultHardErrorPort 
int _stdcall
NtSetDefaultHardErrorPort(
HANDLE PortHandle);

CSRSS 子系统在初始化时调用此函数。 NtRaiseHardError() 函数是在出现严重系统错误时调用的。它向已注册的 hard error port 发送一条消息。因此当应用程序出现启动错误时,CSRSS 子系统可以弹出一条消息。内核中只存着一组全局变量。这些变量保存着指向 hard error port 的指针,因此只有一个进程能捕获系统错误。在 Windows NT 上就是 Win32 子系统。调用此函数需要特殊的优先级。

这里我们给出此函数的伪码:

NtSetDefaultHardErrorPort(HANDLE PortHandle)
{
if (PrivilegeNotHeld)
return STATUS_PRIVILEGE_NOT_HELD);

if (ExReadyForErrors == 0) {
Get a pointer to the kernel port object from PortHandle;

ExpDefaultErrorPort = pointer to kernel port object;
ExpDefaultErrorPortProcess = CurrentProcess;
ExReadyForErrors = 1;
} else {
return STATUS_UNSUCCESSFUL
}
return STATUS_SUCCESS;
}

NtImpersonateClientOfPort 
int _stdcall
NtImpersonateClientOfPort(
HANDLE PortHandle,
PLPCMESSAGE pLpcMessage);

子系统可能需要在调用线程的安全上下文中完成一些处理。NtImpersonateClientOfPort() 使服务器线程能假定客户线程的上下文。函数使用参数 pLpcMessage 来区分进程 ID 和线程 ID。


LPC SAMPLE PROGRAMS

在这一节,我们给出两个样例程序。第一个程序演示了使用 LPC 的短消息通讯,第二个程序演示了使用共享内存的通讯。

On the CD: 样例程序可以在配套光盘的 PORT.C 文件中找到。端口相关函数的数据原型和结构体定义可以在光盘里的 UNDOCNT.H 里找到。


Short Message LPC Sample

PORT.C 文件里包含着用于演示短消息通讯的程序,该程序既是客户又是服务器。当不使用认何参数运行此程序时,程序用作服务器。若使用了参数,则程序作为客户运行(参数本身没用,忽略掉)。应首先用服务器模式启动程序,服务器模式的程序首先创建端口然后进入循环,进行“接收请求-处理请求-回复请求”操作序列。程序使用NtReplyWaitReceivePort() 函数来接受请求。 连接请求与其它请求区别对待。若是连接请求,服务器线程必须接受连接并完成一系列连接动作。对于其它请求,服务器打印消息,将消息中的所有字节取反,再将取反后的消息作为回复送回。

一旦服务器准备好接受连接,就可以运行程序的另一个实例了——这次是可户模式。可户模式的程序连接到由服务器模式实例创建的端口上。 程序首先演示使用 NtRequestPort() 发送一个数据报。然后,客户发送请求并进入循环来等待回复。可以启动多个客户会话,程序的服务器部分能处理多个客户请求。

这一节,我们给出并解释 PORT.C 文件

Listing 8-1: PORT.C 
/***************************************************/

/* Demonstrates the short message LPC provided by the

* port object

*/



#i nclude <windows.h>
#i nclude <stdio.h>

#i nclude "undocnt.h"
#i nclude "print.h"

#define PORTNAME L"\\Windows\\MyPort"

除了一般的头文件包含,PORT.C 文件的开头部分还有样例程序使用的消息端口名的定义。这是个完整的路径名,起始于对象目录的根目录。注意,这里使用的是宽字符集而不是一般的 ASCII 字符集,因为我们直接调用了系统服务,而系统服务又只认识 Unicode 字符集。

/* A real server function would do some meaningful
* processing here. As we are writing just a sample
* server, we have a dummy server function that just
* inverts all the bytes in the message
*/

void ProcessMessageData(PLPCMESSAGE pLpcMessage)
{
DWORD *ptr;
DWORD i;

ptr = (DWORD *)(pLpcMessage->MessageData);

for(i = 0; i < pLpcMessage->ActualMessageLength / sizeof(DWORD); i++) {
ptr[i] = ~ptr[i];
}
return;
}

这就是那个服务器端的傻瓜处理函数。 此函数接受一个 LPC 请求消息,该消息自然是由服务器接收到的。函数应在同一内存空间返回回复消息。正像注释所说的,函数仅仅是将消息中的所有字节取反。因为我们只想演示 LPC 的工作情况,所以并没有提供任何复杂的服务器功能。可以自己修改此函数来实现由服务器提供的功能。

BOOL
ProcessConnectionRequest(
PLPCMESSAGE LpcMessage,
PHANDLE pAcceptPortHandle)
{
HANDLE AcceptPortHandle;
int rc;

*pAcceptPortHandle=NULL;

printf("Got the connection request\n");

PrintMessage(LpcMessage);

ProcessMessageData(LpcMessage);

rc = NtAcceptConnectPort(
&AcceptPortHandle,
0,
LpcMessage,
1,
0,
NULL);
if (rc != 0) {
printf("NtAcceptConnectPort failed, rc=%x\n", rc);
return FALSE;
}

printf("AcceptPortHandle=%x\n", AcceptPortHandle);

rc = NtCompleteConnectPort(AcceptPortHandle);
if (rc != 0) {
CloseHandle(AcceptPortHandle);
printf("NtCompleteConnectPort failed, rc=%x\n", rc);
return FALSE;
}

*pAcceptPortHandle = AcceptPortHandle;
return TRUE;
}

当程序的服务器部分接收到客户的连接请求时,就调用此函数。这个函数接收包含连接请求的消息并将端口句柄返回给指定客户。函数首先打印消息,然后调用 ProcessMessageData() 函数。如前所述,连接请求中的消息数据里没什么内容,就是由客户传给 NtConnectPort() 函数的 ConnectInfo 。

ProcessConnectionRequest() 调用 NtAcceptConnectPort() 函数开始实质性的工作。此函数唯一重要的参数就是包含着连接请求的消息。函数在 AcceptPortHandle 参数中返回一个端口句柄。若函数失败则返回一个非零值。若成功,则 ProcessConnectionRequest() 函数调用 NtCompleteConnectPort() 函数接受由 NtAcceptConnectPort() 函数在参数中返回的端口句柄。NtCompleteConnectPort() 函数也是成功时返回 0,失败时返回非 0 的其它值。

在这一节里,我们接受所有的连接请求。有人可能想修改此函数来有选择地接受连接请求。比如说,可能只想允许某一客户的连接或是只在客户提供某种特殊连接信息时接受连接。如果服务器一次只接受一个客户的连接,就需要拒绝所有其它的连接请求。如前所述,可以将 acceptIt 设为 0 来拒绝连接请求。

BOOL
ProcessLpcRequest(
HANDLE PortHandle,
PLPCMESSAGE LpcMessage)
{
int rc;

printf("Got the LPC request\n");

PrintMessage(LpcMessage);

ProcessMessageData(LpcMessage);

rc = NtReplyPort(PortHandle, LpcMessage);

if (rc != 0) {
printf("NtReplyPort failed, rc=%x\n", rc);
return FALSE;
}
return TRUE;
}

在这段程序里,我们选择用两个函数调用来回复消息并接收下一消息,而不是使用一个 NtReplyWaitReceive() 函数。ProcessLpcRequest() 函数,一个小工具函数,打印接收到的消息,处理该消息(调用 ProcessMessageData() 函数将字节取反)然后将处理后的数据作为回复使用 NtReplyPort()函数将消息送回。


int server(OBJECT_ATTRIBUTES *ObjectAttr)
{
BOOL RetVal;
HANDLE PortHandle;
int rc;
LPCMESSAGE LpcMessage;

/* Create the named port */

rc = NtCreatePort(&PortHandle, ObjectAttr, 0x0, 0x0, 0x00000);
if (rc != 0) {
printf("Error creating port, rc=%x\n", rc);
return -1;
}

printf("Port created, PortHandle=%d\n", PortHandle);

memset(&LpcMessage, 0, sizeof(LpcMessage));

while (1) {
HANDLE AcceptPortHandle;

/* Wait for the message on the port*/
rc = NtReplyWaitReceivePort(PortHandle,
NULL,
NULL,
&LpcMessage);
if (rc != 0) {
printf("NtReplyWaitReceivePort failed");
CloseHandle(PortHandle);
return -1;
}
RetVal = TRUE;

switch (LpcMessage.MessageType) {
case LPC_CONNECTION_REQUEST:
RetVal = ProcessConnectionRequest(
&LpcMessage,
&AcceptPortHandle);
break;
case LPC_REQUEST:
RetVal = ProcessLpcRequest(
PortHandle,
&LpcMessage);
break;
default:
PrintMessage(&LpcMessage);
break;
}

if (RetVal == FALSE) {
break;
}
}
return 0;
}

如前所述,LPC 演示程序既是服务器又是客户。当程序不带参数启动时,main() 函数调用 server() 函数。server() 函数接受一个指向 OBJECT_ATTRIBUTES 结构体的指针,该结构体包含着通讯端口的对象名。 函数用此名创建一个端口,并得到此端口的句柄。如前所述,对于 NtCreatePort() 函数,参数 MaxConnectInfoLength 和 MaxDataLength 都被忽略掉了,故此处设为 0。NtCreatePort() 函数成功时返回 0,失败则返回一个非 0 值。

成功创建端口后,server() 函数进入一个接收-处理-回复的循环。 函数使用 NtReplyWaitReceivePort() 函数来接收客户的请求。 因我们只用此函数接收请求,pLpcMessageOut 就设为 NULL。NtReplyWaitReceivePort() 函数成功时返回 0,且 pLpcMessageIn 中保存着客户请求。这个请求的形式可以为 LPC_CONNECTION_REQUEST、
LPC_DATAGRAM、LPC_REQUEST 等等。服务器对每种类型的消息的处理有所不同。处理 LPC_CONNECTION_REQUEST 消息是通过连接握手进行的,这靠调用 ProcessConnectionRequest() 函数来完成。 对于 LPC_REQUEST 消息,服务器需要做所请求的处理并回复请求。因为我们不想在服务器里实现复杂的功能,所以只是打印消息、取反消息字节、再将回复返回。这些操作都是在 ProcessLpcRequest() 里进行的。LPC_DATAGRAM 消息并不需要回复。这些以及所有其它的消息,包括 LPC_PORT_CLOSED 和 LPC_CLIENT_DIED,都是在 switch 语句的 default case 里处理的。真正的服务器对这些消息可能就要完成不同的操作。 例如,对于 LPC_PORT_CLOSED 消息,真正的服务器可能会释放掉各个客户所占用的资源。

程序的服务器端连续循环,接收、处理、回复着客户的请求。我们并未给服务器部分提供出口。服务器一般是这样的,这也就是它们为什么在 Unix 术语中被称为 daemons(阴魂不散?)的原因。一般来讲,服务器随系统启动而启动,连续处理客户请求直至系统关闭。对于我们的服务器,可以在命令窗口使用 Ctrl+C 或是用任务管理器将其杀掉。

int client(UNICODE_STRING *uString)
{
static int Param3;
HANDLE PortHandle;
DWORD ConnectDataBuffer[] = {0, 1, 2, 3, 4, 5};
int Size = sizeof(ConnectDataBuffer);
DWORD i;
DWORD Value=0xFFFFFFFF;
int rc;
LPCMESSAGE LpcMessage;
DWORD *ptr;

printf("ClientProcessId=%x, ClientThreadId=%x\n",
GetCurrentProcessId(),
GetCurrentThreadId());

rc = NtConnectPort(&PortHandle,
uString,
&Param3,
0,
0,
0,
ConnectDataBuffer,
&Size);
if (rc != 0) {
printf("Connect failed, rc=%x\n", rc);
return -1;


printf("Connect success, PortHandle=%d\n", PortHandle);

for (i = 0; i < Size / sizeof(DWORD); i++) {
printf("%x ", ConnectDataBuffer[i]);
}
printf("\n\n");

rc = NtRegisterThreadTerminatePort(PortHandle);
if (rc != 0) {
printf("Unable to register thread"
" termination port\n");
CloseHandle(PortHandle);
return -1;
}

/* Demonstrates how to send a datagram using
* NtRequestPort
*/

memset(&LpcMessage, 0, sizeof(LpcMessage));
LpcMessage.ActualMessageLength = 0x08;
LpcMessage.TotalMessageLength = 0x20;
ptr=(DWORD *)LpcMessage.MessageData;
ptr[0] = 0xBABABABA;
ptr[1] = 0xCACACACA;

rc=NtRequestPort(PortHandle, &LpcMessage);

while (1) {
/* Fill in the message */
memset(&LpcMessage, 0, sizeof(LpcMessage));
LpcMessage.ActualMessageLength = 0x08;
LpcMessage.TotalMessageLength = 0x20;
ptr = (DWORD *)LpcMessage.MessageData;
ptr[0] = Value;
ptr[1] = Value-1;

printf("Stop sending message (Y/N)? ");
fflush(stdin);

if (toupper(getchar()) == ’Y’) {
CloseHandle(PortHandle);
break;
}

/* Send the message and wait for the reply
*/

printf("Sending request/waiting for reply");
rc = NtRequestWaitReplyPort(PortHandle,
&LpcMessage,
&LpcMessage);
if (rc != 0) {
printf("NtRequestWaitReplyport failed, rc=%x\n", rc);
return -1;
}
/* Print the reply received */
printf("Got the reply\n");
PrintMessage(&LpcMessage);
Value -= 2;
}
return 0;
}

client() 函数实现了 LPC 样例程序的客户端部分。函数打印进程 ID 和线程 ID,可以用进程 ID 和线程 ID 与服务器打印出的接收消息中的进线程 ID 相匹配。

client() 函数先连接到服务器创建的端口上。它传递6个双字来作为 connectInfo。可以验证服务器将这些字作为LPC_CONNECTION_REQUEST 的消息数据进行接收。在从 NtConnectPort() 函数返回时,客户取得端口的句柄。而且,connectInfo 缓冲区也填入了由服务器传递给 NtAcceptConnectPort() 函数的数据消息。

接下来,客户用新得到的端口句柄作为参数调用 NtRegisterThreadTerminatePort() 函数,这样当客户程序终结时,操作系统会向端口发送一个 LPC_CLIENT_DIED 消息。 仅当服务器需要知道客户结束的信息时,客户调用此函数。我们这里调用此函数是为了演示这种机制。

客户还演示了通过 LPC 的数据报通讯。 如前所述,NtRequestPort() 函数传递 LPC_DATAGRAM 类型的请求。注意再将消息传给服务器之前,客户仅填入消息长度域和实际的消息数据, LPCMESSAGE 结构体的其它域由操作系统填入。client() 函数发送两个双字的消息数据,服务器一收到此消息就将这个消息打印。

在演示了数据报通讯之后,客户进入“发送请求-等待回复”的循环。 每次在发送请求之前,客户程序问用户是要继续还是要退出。若用户想要继续演示,客户就使用 NtRequestWaitReply() 函数通过端口发送一个样本消息。消息的数据由两个双字构成,这两个双字将被服务器取反并作为回复送回。在这个程序里,我们使用了同一块缓冲区来传递请求消息、接收回复消息,使用不同的缓冲区也是可以的。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值