Komunikace pomocí LPC

LPC (Local Procedure Call) je část jádra Windows NT, určená k rychlé komunikaci mezi thready nebo mezi procesy ve Windows NT. Je možné ji použít i pro komunikaci mezi komponentami v uživatelském režimu (aplikace a systémové služby) a režimu jádra (ovladače). Tento článek obsahuje popis použití LPC, včetně příkladu komunikace mezi dvěma vlákny.

Poznámka: Tento článek je určen pokročilým programátorům. U čtenáře předpokládá znalost procesů, vláken, meziprocesové komunikace. Výhodou (ne však podmínkou) při studování článku je aspoň částečná znalost nativních API funkcí Windows řady NT a zkušenosti v programování s použitím Windows DDK.

Pouze pomocí nativních API

LPC je součástí pouze Windows řady NT (Windows NT 3.51, NT 4.0, Windows 2000, XP, 2003 Server a Longhorn). Navíc jde o (zatím) nedokumentovanou část Windows, která je ale kompatibilní na všech verzích Windows řady NT. Je obsažena pouze ve skupině tzv. nativních NT API, které jsou exportovány knihovnou ntdll.dll (Resp. ntoskrnl.exe v režimu jádra). K sestavení aplikace používající ntdll.dll je nutné přilinkovat knihovnu ntdll.lib/ntoskrnl.lib, která je součástí Windows DDK.

Jak to funguje

Komunikace pomocí LPC spočívá v posílání bloků dat, tzv. zpráv LPC (LPC messages) mezi klientem a serverem. Klient a server mohou být různá vlákna, různé procesy a mohou se dokonce nacházet v odlišném režimu jádra (klient může být ovladač, běžící v režimu jádra, server může být uživatelská aplikace).

Komunikace probíhá pomocí tzv. portu LPC, vytvořeného serverem. Při vytvoření portu server specifikuje jméno portu a přístupová práva pomocí přístupového deskriptoru (security descriptor). Po úspěšném vytvoření portu server čeká na připojení klienta. Jakmile klient požádá o spojení na daný port, server může zkontrolovat kdo se chce připojit a případně spojení odmítnout. Pokud je spojení přijato, může klient zaslat požadavek a případně čekat na odpověď serveru.

Při každé komunikaci mezi klientem a serverem je poslána tzv. hlavička zprávy LPC (LPC message header), reprezentovaná strukturou PORT_MESSAGE.

Data předaná mezi klientem a serverem mohou být přenesena dvěma způsoby

Komunikaci LPC znázorňuje následující schéma:

Klient

 

Server

   

Server vytvoří LPC port pomocí funkce NtCreatePort. Při vytvoření portu je zadáno jméno, pomocí kterého se klient na port připojí. Volitelně je možné zadat i SECURITY_DESCRIPTOR, a tím specifikovat práva k vytvořenému portu.

     
   

Server čeká na požadavek na spojení (funkce NtListenPort).

Klient se pokusí připojit k portu pomocí funkce NtConnectPort. Při volání funkce klient specifikuje jméno portu, na který se chce připojit.

   
   

Server přijme hlavičku zprávy LPC (struktura MESSAGE_HEADER). Tato hlavička obsahuje tzv. CLIENT_ID, což je struktura obsahující ID procesu a vlákna, které se chce připojit k portu.

     
   

Server se rozhodne zda požadavek na spojení přijme nebo odmítne (obojí pomocí parametru funkce NtAcceptConnectPort).

     
   

Server dokončí operaci spojování pomocí funkce NtCompleteConnectPort. Po zavolání této funkce bude klient pokračovat zasláním požadavku (pokud server souhlasil se spojením v předchozím kroku).

Funkce NtConnectPort vrátí výsledek pokusu o spojení. Návratová hodnota může být STATUS_SUCCESS (spojení bylo navázáno), STATUS_ACCESS_DENIED (klient nemá přístup k portu), STATUS_OBJECT_NAME_NOT_FOUND (neexistuje port požadovaného jména), STATUS_PORT_CONNECTION_REFUSED (server odmítl spojení), nebo jiná chyba. Pokud bylo spojení navázáno, klient obdrží handle portu.

   
   

Server čeká na data od klienta (funkce NtReplyWaitReceivePort).

Klient pošle data do serveru pomocí funkce NtRequestPort (klient nečeká na odpověď) nebo funkce NtRequestWaitReplyPort (klient čeká na odpověď).

   
   

Server zpracuje data, která přijal od klienta. Pokud klient čeká na odpověď, server zavolá funkci NtReplyPort. Tato funkce zajistí zaslání dat klientovi.

Klient ukončí proces komunikace uzavřením handlu, které ziskal při navázání spojení (NtClose). Komunikace je dokončena

 

Serverový proces komunikace pomocí LPC je ukončen, server se vrací do stavu čekání na další připojení (NtListenPort).

Struktury a funkce

Následující kapitola popisuje některé funkce a struktury důležité pro systém LPC. Seznam funkcí a struktur není úplný, kompletní popis hledejte v doprovodném materiálu ke stažení.

typedef struct _PORT_MESSAGE
{
    union
    {
        struct
        {
            USHORT DataLength;          // Length of data following the header (bytes)
            USHORT TotalLength;         // Length of data + sizeof(PORT_MESSAGE)
        } s1;
        ULONG Length;
    } u1;

    union
    {
        struct
        {
            USHORT Type;
            USHORT DataInfoOffset;
        } s2;
        ULONG ZeroInit;
    } u2;

    union
    {
        CLIENT_ID ClientId;
        double   DoNotUseThisField;     // Force quadword alignment
    };

    ULONG  MessageId;                   // Identifier of the particular message instance

    union
    {
        ULONG_PTR ClientViewSize;       // Size of section created by the sender (in bytes)
        ULONG  CallbackId;              // 
    };

} PORT_MESSAGE, *PPORT_MESSAGE;

Struktura PORT_MESSAGE se používá k popisu dat zasílaných pomocí LPC. Každé poslání dat obsahuje vždy informaci o datech, která jsou zaslána. Spolu s touto strukturou je možné poslat i data, pokud jejich velikost nepřesáhne 0x130 bytů.

typedef struct _PORT_VIEW {

    ULONG  Length;                      // Velikost této struktury
    HANDLE SectionHandle;               // Handle na sekci. Musí mít přístup
                                        // SECTION_MAP_WRITE a SECTION_MAP_READ
    ULONG  SectionOffset;               // Offset v sekci, který bude namapován
                                        // do paměťového prostoru příjemce. Musí být násobkem 
                                        // alokační granularity systému.
    ULONG  ViewSize;                    // Velikost mapovaných dat v bytech
    PVOID  ViewBase;                    // Bázová adresa namapovaných dat v paměťovém
                                        // prostoru odesílatele 
    PVOID  ViewRemoteBase;              // Bázová adresa namapovaných dat v adresovém prostoru
                                        // cílového procesu (příjemce zprávy LPC).
} PORT_VIEW, *PPORT_VIEW;

Struktura PORT_VIEW se používá k popisu paměťové sekce (vytvořené funkcí NtCreateSection nebo CreateFileMapping), která bude zaslána příjemci. Sekce musí být vytvořena ve stránkovacím souboru (handle souboru při vytvoření sekce musí být INVALID_HANDLE_VALUE). Struktura musí být naplněna procesem, který sekci vytvořil. Velikost sekce musí být násobkem alokační granularity systému.

typedef struct _REMOTE_PORT_VIEW {

    ULONG Length;                       // Velikost struktury
    ULONG ViewSize;                     // Velikost bloku dat namapovaného
                                        // do paměťového prostoru příjemce zprávy LPC
    PVOID ViewBase;                     // Bázová adresa bloku dat

} REMOTE_PORT_VIEW, *PREMOTE_PORT_VIEW;

Pokud odesílatel zprávy LPC pošle data ve formě paměťově mapované sekce, systém LPC provede přemapování sekce do paměťového prostoru příjemce a předá data ve formě struktury REMOTE_PORT_VIEW.

NtCreatePort

Funkce NtCreatePort je použita serverem LPC k vytvoření portu.

NTSTATUS
NTAPI
NtCreatePort(
    OUT PHANDLE PortHandle,                     
    IN  POBJECT_ATTRIBUTES ObjectAttributes,
    IN  ULONG MaxConnectionInfoLength,
    IN  ULONG MaxMessageLength,
    IN  ULONG MaxPoolUsage
    );
Parametry
PortHandle
[out] Ukazatel na proměnnou typu HANDLE, která bude naplněna handlem portu.
ObjectAttributes
[in] Jméno a bezpečnostní deskriptor vytvořeného portu. Jméno portu musí začínat znakem backslash (např. "\\MyTestPort"). Pokud bude LPC komunikace probíhat mezi různými procesy, měl by bezpečnostní deskriptor obsahovat prázdný DACL.
MaxConnectionInfoLength
[in] Maximální velikost dat která mohou být zaslána ve formě krátké zprávy.
MaxMessageLength
[in] Maximální velikost zprávy která může být poslána na port LPC.
MaxPoolUsage
[in] Maximální velikost nestránkovatelné paměti která může být použita pro uložení zprávy LPC. Nula znamená výchozí hodnotu.
Návratová hodnota

Výsledek operace jako hodnota typu NTSTATUS.

Poznámka

NtCreatePort kontroluje zda (MaxConnectionInfoLength <= 0x104) a (MaxMessageLength <= 0x148).

Popis ostatních funkcí a funkční příklad

Považuji za zbytečné přepisovat popis všech funkcí k LPC. Podrobná dokumentace je k dispozici v doprovodném souboru ke stažení. Tento balíček obsahuje

Poznámka pro 64-bitové systémy

Pokud spustíte 32-bitovou verze příkladu pod 64-bitovými Windows, zjistíte že na několika místech příklad nefunguje správně. Jak se ukázalo, vrstva mezi 32-bitovou Ntdll.dll a 64-bitovou Ntdll.dll nepřekládá strukturu PORT_MESSAGE na její 64-bitový tvar. Funkce LPC v kernelu (které jsou pouze 64-bitové) pak nemohou rozpoznat formát strutury PORT_MESSAGE a obvykle vrací STATUS_INVALID_PARAMETER (0xC000000D). Pro 64-bitová Windows vždy použijte 64-bitovou verzi příkladu.

Literatura