?? 漫談兼容內(nèi)核之十八:windows的lpc機制.txt
字號:
漫談兼容內(nèi)核之十八:Windows的LPC機制
毛德操
LPC是“本地過程調(diào)用(Local Procedure Call)”的縮寫。所謂“本地過程調(diào)用”是與“遠程過程調(diào)用”即RPC相對而言的。其實RPC是廣義的,RPC可以發(fā)生在不同的主機之間,也可以發(fā)生在同一臺主機上,發(fā)生在同一臺主機上就是LPC。所以在Unix語境下就沒有LPC這一說,即使發(fā)生在同一臺主機上也稱為RPC。在歷史上,RPC是“開放軟件基金會(OSF)”設(shè)計和提出的一種用以實現(xiàn)“Unix分布計算環(huán)境(Unix DCE)”的標(biāo)準(zhǔn)。實際上,微軟的DCOM技術(shù),就是建立在RPC基礎(chǔ)上的。Win2000的RPC可以采用TCP/IP、SPX、NetBIOS、命名管道、以及“本地”作為底層的通信手段,這“本地”就是LPC。另一方面,Windows是一個帶有許多微內(nèi)核系統(tǒng)特征的操作系統(tǒng)(盡管它的內(nèi)核不是微內(nèi)核),系統(tǒng)中有不少“系統(tǒng)級”的服務(wù)進程,例如大家已經(jīng)熟知的csrss、管理用戶登錄的“本地安全認(rèn)證服務(wù)”進程LSASS等等,用戶進程以及微軟提供的系統(tǒng)工具軟件經(jīng)常需要調(diào)用由這些服務(wù)進程提供的服務(wù),這里L(fēng)PC就起著重要的作用。
LPC的基礎(chǔ)是一種稱為“端口(Port)”的進程間通信機制,類似于本地的(Unix域的)Socket。這種Port機制提供了面向報文傳遞(message passing)的進程間通信,而LPC則是建立在這個基礎(chǔ)上的高層機制,目的是提供跨進程的過程調(diào)用。注意這里所謂“跨進程的過程調(diào)用”不同于以前所說的“跨進程操作”。前者是雙方有約定、遵循一定規(guī)程的、有控制的服務(wù)提供,被調(diào)用者在向外提供一些什么服務(wù)、即提供哪些函數(shù)調(diào)用方面是自主的,而后者則可以是在不知不覺之間的被利用、被操縱。前者是良性的,而后者可以是惡性的。
“Microsoft Windows Internals”書中說LPC是“用于快速報文傳遞的進程間通信機制”。其實這是誤導(dǎo)的,應(yīng)該說Port才是這樣的進程間通信機制,而LPC是建立在這上面的應(yīng)用。然而這種說法已經(jīng)被廣泛接受和采納,都把LPC和Port混淆起來了,所以本文也只好跟著說“Windows的LPC機制”,而實際上要說的則主要是Windows的Port機制。在下面的敘述中,凡說到LPC的地方往往實際上是在說Port,讀者要注意區(qū)分。
端口是一種面向連接的通信機制,通信的雙方需要先建立起“連接”。這種連接一般建立在用戶進程之間。在建立了連接的雙方之間有幾種交換報文的方法:
? l 不帶數(shù)據(jù)的純報文。
? l 不大于256字節(jié)的短報文。
? l 如果是大于256字節(jié)的長報文,就要在雙方之間建立兩個共享內(nèi)存區(qū)(Section)。雙方通過共享內(nèi)存區(qū)交換數(shù)據(jù),但通過報文進行協(xié)調(diào)和同步。
大塊數(shù)據(jù)之所以要通過共享內(nèi)存區(qū)交換,一來是因為這樣就為用于Port機制的緩沖區(qū)設(shè)置了一個上限,便于內(nèi)存管理。而更重要的是提高了效率,因為否則便要在發(fā)送端把大塊數(shù)據(jù)搬入內(nèi)核空間,又在接收端把大塊數(shù)據(jù)搬到用戶空間。
Windows內(nèi)核為基于端口的進程間通信機制提供了不少系統(tǒng)調(diào)用,包括(但并不限于):
[code]? l NtCreatePort()
? l NtCreateWaitablePort()
? l NtListenPort()
? l NtConnectPort()
? l NtAcceptConnectPort()
? l NtCompleteConnectPort()
? l NtRequestPort()
? l NtRequestWaiReplyPort()
? l NtReplyPort()
? l NtReplyWaitReceivePort()
? l NtReplyWaitReceivePortEx()。同上,但是帶有超時控制
? l NtReadRequestData()
? l NtWriteRequestData()
? l NtQueryInformationPort()[/code]
這么多的系統(tǒng)調(diào)用(由此也可見LPC在Windows操作系統(tǒng)中的份量),當(dāng)然不可能在這里一一加以介紹。本文只是從中揀幾個關(guān)鍵而典型的作些介紹。另一方面,由于Port與Socket的相似性,對于兼容內(nèi)核的開發(fā)而言應(yīng)該比較容易把它嫁接到Socket機制上去。
值得一提的是,Port在Win32 API界面上是不可見的(實際上甚至LPC也不是直接可見的),而Windows的系統(tǒng)調(diào)用界面又不公開。這說明Port只是供微軟“內(nèi)部使用”的。讀者后面就會看到,Port是一種既包括進程間的數(shù)據(jù)傳輸,又包括進程間的同步、數(shù)據(jù)量又可大可小的綜合性的進程間通信機制。這樣,由微軟自己開發(fā)的軟件、特別是一些系統(tǒng)工具,當(dāng)然可以使用這些系統(tǒng)調(diào)用、也即利用Port這種功能比較強的進程間通信機制,而第三方開發(fā)的軟件可就用不上了。
端口分“連接端口(connection port)”和“通信端口(communication port)”兩種,各自扮演著不同的角色。連接端口用于連接的建立,通信端口才真正用于雙方的通信。只有服務(wù)進程才需要有連接端口,但是通信雙方都需要有通信端口。
雖然LPC一般發(fā)生在進程之間,但是實際參與通信的總是具體的線程,所以在下面的敘述中都以線程作為通信的兩端。
典型的建立連接和通信的過程如下:
? l 需要建立LPC通信時,其中提供服務(wù)的一方、即服務(wù)線程首先要通過NtCreatePort()創(chuàng)建一個命名的連接端口、即Port對象。這個對象名應(yīng)為請求服務(wù)的一方、即客戶線程所知。
? l 建立了上述連接端口以后,服務(wù)線程應(yīng)通過NtListenPort()等待接收來自客戶線程的連接請求(服務(wù)線程被阻塞)。
? l 客戶線程通過NtConnectPort()創(chuàng)建一個客戶方的無名通信端口,并向上述命名的連接端口發(fā)出連接請求(客戶線程被阻塞)。
? l 服務(wù)線程收到連接請求(因而被喚醒)以后,如果同意建立連接就通過NtAcceptConnectPort()創(chuàng)建一個服務(wù)方的無名通信端口、接受連接、并返回該無名通信端口的Handle。然后再通過NtCompleteConnectPort()喚醒客戶線程。
? l 客戶線程被喚醒,并返回所創(chuàng)建的無名通信端口的Handle。
? l 服務(wù)線程另創(chuàng)建一個新的線程,負(fù)責(zé)為客戶線程提供LPC服務(wù)。該線程因企圖從上述通信端口接收報文、等待來自客戶端的服務(wù)請求而被阻塞。所以,新創(chuàng)建的線程時LPC服務(wù)線程,而原來的服務(wù)線程是端口服務(wù)進程。
? l 端口服務(wù)線程再次調(diào)用NtListenPort(),等待來自其它客戶的連接請求。
? l 客戶線程通過NtRequestWaiReplyPort()向?qū)Ψ桨l(fā)送報文,請求得到LPC服務(wù),并因等待回答而被阻塞。
? l 服務(wù)端的相應(yīng)線程、即LPC服務(wù)線程因接收到報文而被喚醒,并根據(jù)報文內(nèi)容提供相應(yīng)的LPC服務(wù)。
? l LPC服務(wù)線程通過NtReplyPort()向客戶方發(fā)送回答報文(一般是計算結(jié)果)。客戶線程解除阻塞。
如果回顧一下Wine進程與服務(wù)進程wineserver之間的通信,就可以明白Wine是用命名管道和Socket在模仿Windows的LPC通信,只不過那是在用戶空間的模仿。另一方面,熟悉Socket通信的讀者可以看到,Port與Socket是很相像的。
先看Port的創(chuàng)建。我們看系統(tǒng)調(diào)用NtCreatePort()的代碼:
[code]NTSTATUS STDCALL
NtCreatePort (OUT PHANDLE PortHandle,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN ULONG MaxConnectInfoLength,
IN ULONG MaxDataLength,
IN ULONG MaxPoolUsage)
{
PEPORT Port;
NTSTATUS Status;
DPRINT("NtCreatePort() Name %x\n", ObjectAttributes->ObjectName->Buffer);
/* Verify parameters */
Status = LpcpVerifyCreateParameters (PortHandle, ObjectAttributes,
MaxConnectInfoLength, MaxDataLength, MaxPoolUsage);
. . . . . .
/* Ask Ob to create the object */
Status = ObCreateObject (ExGetPreviousMode(), LpcPortObjectType,
ObjectAttributes, ExGetPreviousMode(),
NULL, sizeof(EPORT), 0, 0, (PVOID*)&Port);
. . . . . .
Status = ObInsertObject ((PVOID)Port, NULL, PORT_ALL_ACCESS,
0, NULL, PortHandle);
. . . . . .
Status = LpcpInitializePort (Port, EPORT_TYPE_SERVER_RQST_PORT, NULL);
Port->MaxConnectInfoLength = PORT_MAX_DATA_LENGTH;
Port->MaxDataLength = PORT_MAX_MESSAGE_LENGTH;
Port->MaxPoolUsage = MaxPoolUsage;
ObDereferenceObject (Port);
return (Status);
}[/code]
參數(shù)ObjectAttributes、即OBJECT_ATTRIBUTES結(jié)構(gòu)中有個Unicode字符串,那就是對象名,需要在調(diào)用NtCreatePort()之前加以設(shè)置,這跟創(chuàng)建/打開文件時的文件名設(shè)置是一樣的。當(dāng)然,除對象名以外,ObjectAttributes中還有別的信息,那就不是我們此刻所關(guān)心的了。其余參數(shù)的作用則不言自明。代碼中的LpcpVerifyCreateParameters()對參數(shù)進行合理性檢查,ObCreateObject()和ObInsertObject()就無需多說了,而LpcpInitializePort()主要是對代表著端口的EPORT數(shù)據(jù)結(jié)構(gòu)進行初始化。EPORT數(shù)據(jù)結(jié)構(gòu)的定義如下:
[code]typedef struct _EPORT
{
KSPIN_LOCK Lock;
KSEMAPHORE Semaphore;
USHORT Type;
USHORT State;
struct _EPORT *RequestPort;
struct _EPORT *OtherPort;
ULONG QueueLength;
LIST_ENTRY QueueListHead;
ULONG ConnectQueueLength;
LIST_ENTRY ConnectQueueListHead;
ULONG MaxDataLength;
ULONG MaxConnectInfoLength;
ULONG MaxPoolUsage; /* size of NP zone */
} EPORT, * PEPORT;[/code]
結(jié)構(gòu)中的QueueListHead就是用來接收報文的隊列。ConnectQueueListHead則是用來緩存連接請求的隊列,這是因為一個Port可能會一下子接收到好幾個連接請求。字段Type用來紀(jì)錄端口的類型,一共有三種類型:
[code]#define EPORT_TYPE_SERVER_RQST_PORT (0)
#define EPORT_TYPE_SERVER_COMM_PORT (1)
#define EPORT_TYPE_CLIENT_COMM_PORT (2)[/code]
從上面的代碼中可以看出,NtCreatePort()所創(chuàng)建的是“請求端口”,即類型為EPORT_TYPE_SERVER_RQST_PORT的端口,也就是“連接端口”。
字段state說明端口的狀態(tài)和性質(zhì),例如EPORT_WAIT_FOR_CONNECT、EPORT_CONNECTED_CLIENT等等。
注意每個EPORT結(jié)構(gòu)中都嵌有一個“信號量”結(jié)構(gòu)Semaphore,這就是通信雙方用來實現(xiàn)同步的手段。所以說,Port是集成了數(shù)據(jù)交換和行為同步的綜合性的進程間通信機制。
還有個字段OtherPort也值得一說,這是個指針,互相指向已經(jīng)建立了連接的對方端口。
LpcpInitializePort()的代碼就不看了。只是要說明一下,端口對象的初始化也包括了對其“信號量”數(shù)據(jù)結(jié)構(gòu)的初始化,并且信號量的初值是0,而最大值則為最大整數(shù)LONG_MAX,所以實際上沒有限制。
創(chuàng)建了連接端口以后,服務(wù)進程就通過NtListenPort()等待連接請求,并因此而被阻塞進入睡眠。
[code]NTSTATUS STDCALL
NtListenPort (IN HANDLE PortHandle, IN PLPC_MESSAGE ConnectMsg)
{
NTSTATUS Status;
/* Wait forever for a connection request. */
for (;;)
{
Status = NtReplyWaitReceivePort(PortHandle, NULL, NULL, ConnectMsg);
/* Accept only LPC_CONNECTION_REQUEST requests. Drop any other message. */
if (!NT_SUCCESS(Status) ||
LPC_CONNECTION_REQUEST == ConnectMsg->MessageType)
{
DPRINT("Got message (type %x)\n", LPC_CONNECTION_REQUEST);
break;
}
DPRINT("Got message (type %x)\n", ConnectMsg->MessageType);
}
return (Status);
}[/code]
所謂“收聽(Listen)”,就是在一個for循環(huán)中反復(fù)調(diào)用NtReplyWaitReceivePort(),等待接收來自客戶方的報文,直至接收到報文、并且所收到報文的類型為“連接請求”、即LPC_CONNECTION_REQUEST時為止。如果接收到的報文不是連接請求就回過去再等,所以才把它放在無限for循環(huán)中。
NtReplyWaitReceivePort()本來的作用是“發(fā)送一個應(yīng)答報文并等待接收”,但是這里的應(yīng)答報文指針為NULL,也就是無應(yīng)答報文可發(fā),這樣就成為只是等待來自客戶方的請求了。另一方面,這個函數(shù)是不帶超時(Timeout)的,只要沒有收到客戶方的請求就一直等待下去。
[code][NtListenPort() > NtReplyWaitReceivePort()]
NTSTATUS STDCALL
NtReplyWaitReceivePort (IN HANDLE PortHandle, OUT PULONG PortId,
IN PLPC_MESSAGE LpcReply, OUT PLPC_MESSAGE LpcMessage)
{
return(NtReplyWaitReceivePortEx (PortHandle, PortId, LpcReply, LpcMessage, NULL));
}[/code]
NtReplyWaitReceivePort()是不帶超時的,另一個系統(tǒng)調(diào)用NtReplyWaitReceivePortEx()則有超時功能,所以前者是通過后者實現(xiàn)的,只是把(最后那個)參數(shù)Timeout設(shè)成NULL。
[code][NtListenPort() > NtReplyWaitReceivePort() > NtReplyWaitReceivePortEx()]
NTSTATUS STDCALL
NtReplyWaitReceivePortEx(IN HANDLE PortHandle, OUT PULONG PortId,
IN PLPC_MESSAGE LpcReply, OUT PLPC_MESSAGE LpcMessage,
IN PLARGE_INTEGER Timeout)
{
. . . . . .
if( Port->State == EPORT_DISCONNECTED )
{
/* If the port is disconnected, force the timeout to be 0
so we don't wait for new messages, because there won't be
any, only try to remove any existing messages */
Disconnected = TRUE;
to.QuadPart = 0;
Timeout = &to;
}
else Disconnected = FALSE;
Status = ObReferenceObjectByHandle(PortHandle, PORT_ALL_ACCESS,
LpcPortObjectType, UserMode, (PVOID*)&Port, NULL);
. . . . . .
/* Send the reply, only if port is connected */
if (LpcReply != NULL && !Disconnected)
{
Status = EiReplyOrRequestPort(Port->OtherPort, LpcReply, LPC_REPLY, Port);
KeReleaseSemaphore(&Port->OtherPort->Semaphore,
IO_NO_INCREMENT, 1, FALSE);
. . . . . .
}
/* Want for a message to be received */
Status = KeWaitForSingleObject(&Port->Semaphore, UserRequest,
UserMode, FALSE, Timeout);
if( Status == STATUS_TIMEOUT )
{
. . . . . .
}
. . . . . .
/* Dequeue the message */
KeAcquireSpinLock(&Port->Lock, &oldIrql);
Request = EiDequeueMessagePort(Port);
KeReleaseSpinLock(&Port->Lock, oldIrql);
if (Request->Message.MessageType == LPC_CONNECTION_REQUEST)
{
LPC_MESSAGE Header;
PEPORT_CONNECT_REQUEST_MESSAGE CRequest;
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -