diff options
Diffstat (limited to 'private/ntos/mm/queryvm.c')
-rw-r--r-- | private/ntos/mm/queryvm.c | 920 |
1 files changed, 920 insertions, 0 deletions
diff --git a/private/ntos/mm/queryvm.c b/private/ntos/mm/queryvm.c new file mode 100644 index 000000000..55736bf9f --- /dev/null +++ b/private/ntos/mm/queryvm.c @@ -0,0 +1,920 @@ +/*++ + +Copyright (c) 1989 Microsoft Corporation + +Module Name: + + queryvm.c + +Abstract: + + This module contains the routines which implement the + NtQueryVirtualMemory service. + +Author: + + Lou Perazzoli (loup) 21-Aug-1989 + +Revision History: + +--*/ + +#include "mi.h" + +extern POBJECT_TYPE IoFileObjectType; + +NTSTATUS +MiGetWorkingSetInfo ( + IN PMEMORY_WORKING_SET_INFORMATION WorkingSetInfo, + IN ULONG Length, + IN PEPROCESS Process + ); + +MMPTE +MiCaptureSystemPte ( + IN PMMPTE PointerProtoPte, + IN PEPROCESS Process + ); + +#if DBG +PEPROCESS MmWatchProcess; +VOID MmFooBar(VOID); +#endif // DBG + +ULONG +MiQueryAddressState ( + IN PVOID Va, + IN PMMVAD Vad, + IN PEPROCESS TargetProcess, + OUT PULONG ReturnedProtect + ); + +#ifdef ALLOC_PRAGMA +#pragma alloc_text(PAGE,NtQueryVirtualMemory) +#pragma alloc_text(PAGE,MiQueryAddressState) +#pragma alloc_text(PAGELK,MiGetWorkingSetInfo) +#endif + + +NTSTATUS +NtQueryVirtualMemory ( + IN HANDLE ProcessHandle, + IN PVOID BaseAddress, + IN MEMORY_INFORMATION_CLASS MemoryInformationClass, + OUT PVOID MemoryInformation, + IN ULONG MemoryInformationLength, + OUT PULONG ReturnLength OPTIONAL + ) + +/*++ + +Routine Description: + + This function provides the capability to determine the state, + protection, and type of a region of pages within the virtual address + space of the subject process. + + The state of the first page within the region is determined and then + subsequent entries in the process address map are scanned from the + base address upward until either the entire range of pages has been + scanned or until a page with a nonmatching set of attributes is + encountered. The region attributes, the length of the region of pages + with matching attributes, and an appropriate status value are + returned. + + If the entire region of pages does not have a matching set of + attributes, then the returned length parameter value can be used to + calculate the address and length of the region of pages that was not + scanned. + +Arguments: + + + ProcessHandle - An open handle to a process object. + + BaseAddress - The base address of the region of pages to be + queried. This value is rounded down to the next host-page- + address boundary. + + MemoryInformationClass - The memory information class about which + to retrieve information. + + MemoryInformation - A pointer to a buffer that receives the + specified information. The format and content of the buffer + depend on the specified information class. + + + MemoryBasicInformation - Data type is PMEMORY_BASIC_INFORMATION. + + MEMORY_BASIC_INFORMATION Structure + + + ULONG RegionSize - The size of the region in bytes + beginning at the base address in which all pages have + identical attributes. + + ULONG State - The state of the pages within the region. + + State Values State Values + + MEM_COMMIT - The state of the pages within the region + is committed. + + MEM_FREE - The state of the pages within the region + is free. + + MEM_RESERVE - The state of the pages within the + region is reserved. + + ULONG Protect - The protection of the pages within the + region. + + + Protect Values Protect Values + + PAGE_NOACCESS - No access to the region of pages is + allowed. An attempt to read, write, or execute + within the region results in an access violation + (i.e., a GP fault). + + PAGE_EXECUTE - Execute access to the region of pages + is allowed. An attempt to read or write within + the region results in an access violation. + + PAGE_READONLY - Read-only and execute access to the + region of pages is allowed. An attempt to write + within the region results in an access violation. + + PAGE_READWRITE - Read, write, and execute access to + the region of pages is allowed. If write access + to the underlying section is allowed, then a + single copy of the pages are shared. Otherwise, + the pages are shared read-only/copy-on-write. + + PAGE_GUARD - Read, write, and execute access to the + region of pages is allowed; however, access to + the region causes a "guard region entered" + condition to be raised in the subject process. + + PAGE_NOCACHE - Disable the placement of committed + pages into the data cache. + + ULONG Type - The type of pages within the region. + + + Type Values + + MEM_PRIVATE - The pages within the region are + private. + + MEM_MAPPED - The pages within the region are mapped + into the view of a section. + + MEM_IMAGE - The pages within the region are mapped + into the view of an image section. + + MemoryInformationLength - Specifies the length in bytes of + the memory information buffer. + + ReturnLength - An optional pointer which, if specified, + receives the number of bytes placed in the process + information buffer. + + +Return Value: + + Returns the status + + TBS + + +Environment: + + Kernel mode. + +--*/ + +{ + KPROCESSOR_MODE PreviousMode; + PEPROCESS TargetProcess; + NTSTATUS Status; + PMMVAD Vad; + BOOLEAN PteIsZero = FALSE; + PVOID Va; + BOOLEAN Found = FALSE; + ULONG TheRegionSize; + ULONG NewProtect; + ULONG NewState; + PVOID FilePointer; + + MEMORY_BASIC_INFORMATION Info; + + // + // The only supported option is MEMORY_BASIC_INFORMATION, make + // sure the user's buffer is large enough for this. + // + + // + // Check argument validity. + // + switch (MemoryInformationClass) { + case MemoryBasicInformation: + if (MemoryInformationLength < sizeof(MEMORY_BASIC_INFORMATION)) { + return STATUS_INFO_LENGTH_MISMATCH; + } + break; + + case MemoryWorkingSetInformation: + break; + + case MemoryMappedFilenameInformation: + FilePointer = NULL; + break; + default: + return STATUS_INVALID_INFO_CLASS; + } + + PreviousMode = KeGetPreviousMode(); + + if (PreviousMode != KernelMode) { + + // + // Check arguments. + // + + try { + + ProbeForWrite(MemoryInformation, + MemoryInformationLength, + sizeof(ULONG)); + + if (ARGUMENT_PRESENT(ReturnLength)) { + ProbeForWriteUlong(ReturnLength); + } + + } except (EXCEPTION_EXECUTE_HANDLER) { + + // + // If an exception occurs during the probe or capture + // of the initial values, then handle the exception and + // return the exception code as the status value. + // + + return GetExceptionCode(); + } + } + if (BaseAddress > MM_HIGHEST_USER_ADDRESS) { + return STATUS_INVALID_PARAMETER; + } + + if (BaseAddress >= MM_HIGHEST_VAD_ADDRESS) { + + // + // Indicate a reserved area from this point on. + // + + if ( MemoryInformationClass == MemoryBasicInformation ) { + + try { + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->AllocationBase = + (PVOID)((ULONG)MM_HIGHEST_VAD_ADDRESS + 1); + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->AllocationProtect = + PAGE_READONLY; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->BaseAddress = + PAGE_ALIGN(BaseAddress); + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->RegionSize = + ((ULONG)MM_HIGHEST_USER_ADDRESS + 1) - + (ULONG)PAGE_ALIGN(BaseAddress); + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->State = MEM_RESERVE; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->Protect = PAGE_NOACCESS; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->Type = MEM_PRIVATE; + + if (ARGUMENT_PRESENT(ReturnLength)) { + *ReturnLength = sizeof(MEMORY_BASIC_INFORMATION); + } + +#if defined(MM_SHARED_USER_DATA_VA) + if (PAGE_ALIGN(BaseAddress) == (PVOID)MM_SHARED_USER_DATA_VA) { + + // + // This is the page that is double mapped between + // user mode and kernel mode. + // + + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->Protect = + PAGE_READONLY; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->RegionSize = + PAGE_SIZE; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->State = + MEM_COMMIT; + } +#endif + + } except (EXCEPTION_EXECUTE_HANDLER) { + + // + // Just return success. + // + } + + return STATUS_SUCCESS; + } else { + return STATUS_INVALID_ADDRESS; + } + } + + if ( ProcessHandle == NtCurrentProcess() ) { + TargetProcess = PsGetCurrentProcess(); + } else { + Status = ObReferenceObjectByHandle ( ProcessHandle, + PROCESS_QUERY_INFORMATION, + PsProcessType, + PreviousMode, + (PVOID *)&TargetProcess, + NULL ); + + if (!NT_SUCCESS(Status)) { + return Status; + } + } + + if (MemoryInformationClass == MemoryWorkingSetInformation) { + + MmLockPagableSectionByHandle(ExPageLockHandle); + + Status = MiGetWorkingSetInfo (MemoryInformation, + MemoryInformationLength, + TargetProcess); + MmUnlockPagableImageSection(ExPageLockHandle); + + if ( ProcessHandle != NtCurrentProcess() ) { + ObDereferenceObject (TargetProcess); + } + try { + + if (ARGUMENT_PRESENT(ReturnLength)) { + *ReturnLength = ((((PMEMORY_WORKING_SET_INFORMATION) + MemoryInformation)->NumberOfEntries - 1) * + sizeof(ULONG)) + + sizeof(MEMORY_WORKING_SET_INFORMATION); + } + + } except (EXCEPTION_EXECUTE_HANDLER) { + } + + return STATUS_SUCCESS; + } + + // + // If the specified process is not the current process, attach + // to the specified process. + // + + KeAttachProcess (&TargetProcess->Pcb); + + // + // Get working set mutex and block APCs. + // + + LOCK_WS_AND_ADDRESS_SPACE (TargetProcess); + + // + // Make sure the address space was not deleted, if so, return an error. + // + + if (TargetProcess->AddressSpaceDeleted != 0) { + UNLOCK_WS (TargetProcess); + UNLOCK_ADDRESS_SPACE (TargetProcess); + KeDetachProcess(); + if ( ProcessHandle != NtCurrentProcess() ) { + ObDereferenceObject (TargetProcess); + } + return STATUS_PROCESS_IS_TERMINATING; + } + + // + // Locate the VAD that contiains the base address or the VAD + // which follows the base address. + // + + Vad = TargetProcess->VadRoot; + + for (;;) { + + if (Vad == (PMMVAD)NULL) { + break; + } + + if ((BaseAddress >= Vad->StartingVa) && + (BaseAddress <= Vad->EndingVa)) { + Found = TRUE; + break; + } + + if (BaseAddress < Vad->StartingVa) { + if (Vad->LeftChild == (PMMVAD)NULL) { + break; + } + Vad = Vad->LeftChild; + + } else { + if (BaseAddress < Vad->EndingVa) { + break; + } + if (Vad->RightChild == (PMMVAD)NULL) { + break; + } + Vad = Vad->RightChild; + } + } + + if (!Found) { + + // + // There is no virtual address allocated at the base + // address. Return the size of the hole starting at + // the base address. + // + + if (Vad == NULL) { + TheRegionSize = ((ULONG)MM_HIGHEST_VAD_ADDRESS + 1) - + (ULONG)PAGE_ALIGN(BaseAddress); + } else { + if (Vad->StartingVa < BaseAddress) { + + // + // We are looking at the Vad which occupies the range + // just before the desired range. Get the next Vad. + // + + Vad = MiGetNextVad (Vad); + if (Vad == NULL) { + TheRegionSize = ((ULONG)MM_HIGHEST_VAD_ADDRESS + 1) - + (ULONG)PAGE_ALIGN(BaseAddress); + } else { + TheRegionSize = (ULONG)Vad->StartingVa - + (ULONG)PAGE_ALIGN(BaseAddress); + } + } else { + TheRegionSize = (ULONG)Vad->StartingVa - + (ULONG)PAGE_ALIGN(BaseAddress); + } + } + + UNLOCK_WS (TargetProcess); + UNLOCK_ADDRESS_SPACE (TargetProcess); + KeDetachProcess(); + + if ( ProcessHandle != NtCurrentProcess() ) { + ObDereferenceObject (TargetProcess); + } + + // + // Establish an exception handler and write the information and + // returned length. + // + + if ( MemoryInformationClass == MemoryBasicInformation ) { + try { + + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->AllocationBase = + NULL; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->AllocationProtect = + 0; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->BaseAddress = + PAGE_ALIGN(BaseAddress); + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->RegionSize = + TheRegionSize; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->State = MEM_FREE; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->Protect = PAGE_NOACCESS; + ((PMEMORY_BASIC_INFORMATION)MemoryInformation)->Type = 0; + + if (ARGUMENT_PRESENT(ReturnLength)) { + *ReturnLength = sizeof(MEMORY_BASIC_INFORMATION); + } + + } except (EXCEPTION_EXECUTE_HANDLER) { + + // + // Just return success. + // + } + + return STATUS_SUCCESS; + } + return STATUS_INVALID_ADDRESS; + } + + // + // Found a vad. + // + + Va = PAGE_ALIGN(BaseAddress); + Info.BaseAddress = Va; + + // + // There is a page mapped at the base address. + // + + if (Vad->u.VadFlags.PrivateMemory) { + Info.Type = MEM_PRIVATE; + } else if (Vad->u.VadFlags.ImageMap == 0) { + Info.Type = MEM_MAPPED; + + if ( MemoryInformationClass == MemoryMappedFilenameInformation ) { + if (Vad->ControlArea) { + FilePointer = Vad->ControlArea->FilePointer; + } + if ( !FilePointer ) { + FilePointer = (PVOID)1; + } else { + ObReferenceObject(FilePointer); + } + } + + } else { + Info.Type = MEM_IMAGE; + } + + Info.State = MiQueryAddressState (Va, Vad, TargetProcess, &Info.Protect); + + Va = (PVOID)((PCHAR)Va + PAGE_SIZE); + + while (Va <= Vad->EndingVa) { + + NewState = MiQueryAddressState (Va, + Vad, + TargetProcess, + &NewProtect); + + if ((NewState != Info.State) || (NewProtect != Info.Protect)) { + + // + // The state for this address does not match, calculate + // size and return. + // + + break; + } + Va = (PVOID)((ULONG)Va + PAGE_SIZE); + } // end while + + Info.RegionSize = ((ULONG)Va - (ULONG)Info.BaseAddress); + Info.AllocationBase = Vad->StartingVa; + Info.AllocationProtect = MI_CONVERT_FROM_PTE_PROTECTION ( + Vad->u.VadFlags.Protection); + + // + // A range has been found, release the mutexes, deattach from the + // target process and return the information. + // + + UNLOCK_WS (TargetProcess); + UNLOCK_ADDRESS_SPACE (TargetProcess); + KeDetachProcess(); + + if ( ProcessHandle != NtCurrentProcess() ) { + ObDereferenceObject (TargetProcess); + } + +#if DBG + if (MmDebug & MM_DBG_SHOW_NT_CALLS) { + if ( !MmWatchProcess ) { + DbgPrint("queryvm base %lx allocbase %lx protect %lx size %lx\n", + Info.BaseAddress, Info.AllocationBase, Info.AllocationProtect, + Info.RegionSize); + DbgPrint(" state %lx protect %lx type %lx\n", + Info.State, Info.Protect, Info.Type); + } + } +#endif //DBG + + if ( MemoryInformationClass == MemoryBasicInformation ) { + try { + + *(PMEMORY_BASIC_INFORMATION)MemoryInformation = Info; + + if (ARGUMENT_PRESENT(ReturnLength)) { + *ReturnLength = sizeof(MEMORY_BASIC_INFORMATION); + } + + } except (EXCEPTION_EXECUTE_HANDLER) { + } + return STATUS_SUCCESS; + } + + // + // Try to return the name of the file that is mapped. + // + + if ( !FilePointer ) { + return STATUS_INVALID_ADDRESS; + } else if ( FilePointer == (PVOID)1 ) { + return STATUS_FILE_INVALID; + } + + // + // We have a referenced pointer to the file. Call ObQueryNameString + // and get the file name + // + + Status = ObQueryNameString( + FilePointer, + MemoryInformation, + MemoryInformationLength, + ReturnLength + ); + ObDereferenceObject(FilePointer); + return Status; +} + + +ULONG +MiQueryAddressState ( + IN PVOID Va, + IN PMMVAD Vad, + IN PEPROCESS TargetProcess, + OUT PULONG ReturnedProtect + ) + +/*++ + +Routine Description: + + +Arguments: + +Return Value: + + Returns the state (MEM_COMMIT, MEM_RESERVE, MEM_PRIVATE). + +Environment: + + Kernel mode. Working set lock and address creation lock held. + +--*/ + +{ + PMMPTE PointerPte; + PMMPTE PointerPde; + MMPTE CapturedProtoPte; + PMMPTE ProtoPte; + ULONG PteIsZero; + ULONG State; + ULONG Protect; + +#ifdef LARGE_PAGES + if (Vad->u.VadFlags.LargePages) { + *ReturnedProtect = MI_CONVERT_FROM_PTE_PROTECTION ( + Vad->u.VadFlags.Protection); + return MEM_COMMIT; + } +#endif //LARGE_PAGES + + PointerPde = MiGetPdeAddress (Va); + PointerPte = MiGetPteAddress (Va); + + ASSERT ((Vad->StartingVa <= Va) && (Vad->EndingVa >= Va)); + + PteIsZero = TRUE; + + if (MiDoesPdeExistAndMakeValid(PointerPde, TargetProcess, FALSE)) { + + // + // A PTE exists at this address, see if it is zero. + // + + if (PointerPte->u.Long != 0) { + + PteIsZero = FALSE; + + // + // There is a non-zero PTE at this address, use + // it to build the information block. + // + + if (MiIsPteDecommittedPage (PointerPte)) { + Protect = 0; + State = MEM_RESERVE; + } else { + + State = MEM_COMMIT; + if (Vad->u.VadFlags.PhysicalMapping == 1) { + + // + // Physical mapping, there is no corresponding + // PFN element to get the page protection from. + // + + Protect = MI_CONVERT_FROM_PTE_PROTECTION ( + Vad->u.VadFlags.Protection); + } else { + Protect = MiGetPageProtection (PointerPte, + TargetProcess); + + if ((PointerPte->u.Soft.Valid == 0) && + (PointerPte->u.Soft.Prototype == 1) && + (Vad->u.VadFlags.PrivateMemory == 0) && + (Vad->ControlArea != (PCONTROL_AREA)NULL)) { + + // + // Make sure protoPTE is committed. + // + + ProtoPte = MiGetProtoPteAddress(Vad,Va); + + CapturedProtoPte = MiCaptureSystemPte (ProtoPte, + TargetProcess); + if (CapturedProtoPte.u.Long == 0) { + State = MEM_RESERVE; + Protect = 0; + } + } + } + } + } + } + + if (PteIsZero) { + + // + // There is no PDE at this address, the template from + // the VAD supplies the information unless the VAD is + // for an image file. For image files the individual + // protection is on the prototype PTE. + // + + // + // Get the default protection information. + // + + State = MEM_RESERVE; + Protect = 0; + + if (Vad->u.VadFlags.PhysicalMapping == 1) { + + // + // Must be banked memory, just return reserved. + // + + NOTHING; + + } else if ((Vad->u.VadFlags.PrivateMemory == 0) && + (Vad->ControlArea != (PCONTROL_AREA)NULL)) { + + // + // This VAD refers to a section. Even though the PTE is + // zero, the actual page may be committed in the section. + // + + ProtoPte = MiGetProtoPteAddress(Vad,Va); + + CapturedProtoPte = MiCaptureSystemPte (ProtoPte, + TargetProcess); + + if (CapturedProtoPte.u.Long != 0) { + State = MEM_COMMIT; + + if (Vad->u.VadFlags.ImageMap == 0) { + Protect = MI_CONVERT_FROM_PTE_PROTECTION ( + Vad->u.VadFlags.Protection); + } else { + + // + // This is an image file, the protection is in the + // prototype PTE. + // + + Protect = MiGetPageProtection (&CapturedProtoPte, + TargetProcess); + } + } + + } else { + + // + // Get the protection from the corresponding VAD. + // + + if (Vad->u.VadFlags.MemCommit) { + State = MEM_COMMIT; + Protect = MI_CONVERT_FROM_PTE_PROTECTION ( + Vad->u.VadFlags.Protection); + } + } + } + + *ReturnedProtect = Protect; + return State; +} + + + +NTSTATUS +MiGetWorkingSetInfo ( + IN PMEMORY_WORKING_SET_INFORMATION WorkingSetInfo, + IN ULONG Length, + IN PEPROCESS Process + ) + +{ + PMDL Mdl; + PMEMORY_WORKING_SET_INFORMATION Info; + PMEMORY_WORKING_SET_BLOCK Entry; + PMEMORY_WORKING_SET_BLOCK LastEntry; + PMMWSLE Wsle; + PMMWSLE LastWsle; + ULONG WsSize; + PMMPTE PointerPte; + PMMPFN Pfn1; + NTSTATUS status; + + // + // Allocate an MDL to map the request. + // + + Mdl = ExAllocatePoolWithTag (NonPagedPool, + sizeof(MDL) + sizeof(ULONG) + + BYTES_TO_PAGES (Length) * sizeof(ULONG), + ' mM'); + + if (Mdl == NULL) { + return(STATUS_INSUFFICIENT_RESOURCES); + } + + // + // Initialize MDL for request. + // + + MmInitializeMdl(Mdl, WorkingSetInfo, Length); + + try { + MmProbeAndLockPages (Mdl, KeGetPreviousMode(), IoWriteAccess); + } except (EXCEPTION_EXECUTE_HANDLER) { + ExFreePool (Mdl); + return GetExceptionCode(); + } + + Info = MmGetSystemAddressForMdl (Mdl); + + if (PsGetCurrentProcess() != Process) { + KeAttachProcess (&Process->Pcb); + } + + LOCK_WS (Process); + + status = STATUS_SUCCESS; + + if (Process->AddressSpaceDeleted != 0) { + status = STATUS_PROCESS_IS_TERMINATING; + } + + WsSize = Process->Vm.WorkingSetSize; + Info->NumberOfEntries = WsSize; + + if ((WsSize * sizeof(ULONG)) >= Length) { + status = STATUS_INFO_LENGTH_MISMATCH; + } + + if (status != STATUS_SUCCESS) { + UNLOCK_WS (Process); + KeDetachProcess (); + MmUnlockPages (Mdl); + ExFreePool (Mdl); + return status; + } + + Wsle = MmWsle; + LastWsle = &MmWsle[MmWorkingSetList->LastEntry]; + Entry = &Info->WorkingSetInfo[0]; + LastEntry = (PMEMORY_WORKING_SET_BLOCK)( + (PCHAR)Info + (Length & (~(sizeof(ULONG) - 1)))); + + do { + if (Wsle->u1.e1.Valid == 1) { + Entry->VirtualPage = Wsle->u1.e1.VirtualPageNumber; + PointerPte = MiGetPteAddress (Wsle->u1.VirtualAddress); + ASSERT (PointerPte->u.Hard.Valid == 1); + Pfn1 = MI_PFN_ELEMENT (PointerPte->u.Hard.PageFrameNumber); + + Entry->Shared = Pfn1->u3.e1.PrototypePte; + if (Pfn1->u3.e1.PrototypePte == 0) { + Entry->Protection = Pfn1->OriginalPte.u.Soft.Protection; + } else { + if (Wsle->u1.e1.SameProtectAsProto == 1) { + Entry->Protection = Pfn1->OriginalPte.u.Soft.Protection; + } else { + Entry->Protection = Wsle->u1.e1.Protection; + } + } + Entry += 1; + } + Wsle += 1; + }while ((Entry < LastEntry) && (Wsle <= LastWsle)); + + UNLOCK_WS (Process); + KeDetachProcess (); + MmUnlockPages (Mdl); + ExFreePool (Mdl); + return STATUS_SUCCESS; +} |