Geoff Chappell, Software Analyst
The Hardware Abstraction Layer (HAL) calls this function when a profile interrupt occurs. It is the kernel’s opportunity to do such things as update execution counts for profiling.
VOID KeProfileInterruptWithSource ( KTRAP_FRAME *TrapFrame, KPROFILE_SOURCE ProfileSource);
The TrapFrame argument describes the interrupted execution.
The ProfileSource argument tells what caused the interrupt.
The KeProfileInterruptWithSource function is exported from the kernel in version 3.51 and higher.
The KeProfileInterruptWithSource function is not documented, but has a C-language declaration in the NTOSP.H file from an Enterprise edition of the Windows Driver Kit (WDK) for Windows 10.
The following implementation notes are from inspection of the kernel from the original release of Windows 10 only. They may some day get revised to account for earlier versions. Meanwhile, where anything is added about earlier versions, take it not as an attempt at comprehensiveness but as a bonus from my being unable to resist a trip down memory lane or at least a quick look into the history.
The 32-bit implementation increments the InterruptCount in the current processor’s KPRCB. This count is also incremented for other interrupts, of course. That the 32-bit kernel increments it specially for the profile interrupt has to do with the very different ways that the kernel and HAL cooperate over interrupt handling in 32-bit and 64-bit Windows (which is its own, huge, topic far beyond the scope of this note).
However the count gets incremented for any interrupt, it is the same InterruptCount that can be retrieved for each processor via the ZwQuerySystemInformation and ZwQuerySystemInformationEx functions when given the information class SystemProcessorPerformanceInformation (0x08) or SystemProcessorPerformanceInformationEx (0x8D).
The function’s main work is to process two lists of profile objects. Each profile object carries the conditions that trigger non-trivial processing, plus parameters for what’s to be done when triggered. Among the possible conditions is that profiling can be specific to a process or may apply globally. The kernel keeps separate lists for each process (the head being the ProcessListHead very near the start of the KPROCESS) and one more list for global profiling. Each time the function gets called, it processes only the current process’s list and the global list.
Profile objects come in two types. Both types can appear in any list. In practice, however, one type is only ever inserted into the global list. Both types of profile object have the same structure. Microsoft’s name for that structure is not known, but KPROFILE is here thought to be an almost certain guess. As with other kernel objects, the structure for profile objects begins with a type from the KOBJECTS enumeration and a size.
In the basic age-old profile object, the type is simply ProfileObject (0x17 nowadays, but 0x0F before version 4.0). This type of profile object is created by the NtStartProfile function using parameters that are first presented to NtCreateProfile or NtCreateProfileEx. If a process is specified among those parameters, then the profile object goes into the list for that process, and thus gets examined by this function only when interrupts occur while executing that process. Otherwise, the profile object goes into the global list and gets examined by this function on every interrupt. Whichever list it’s in, a profile object of this type triggers if all the following are true:
The profiled region’s address and size will have been among the NtCreateProfile or NtCreateProfileEx parameters. The profiled region is treated as an array of fixed-size buckets, this size being also among the parameters. One more of these parameters is the address of a buffer that has space for one 32-bit counter per bucket. The function computes which bucket holds the instruction pointer from the TrapFrame and increments the corresponding counter. The intention is that after numerous calls to this function, the buffer is left with a useful profile of execution that fits the specified conditions.
The function’s notion of the profiled region is problematic in version 6.2 and higher. In all versions, the profile object describes the profiled region not by its address and size but by its start and end addresses. The end is simply and naturally ProfileSize bytes added to the ProfileBase address (in terms of arguments to NtCreateProfile and NtCreateProfileEx). Put another way, the end is a non-inclusive end. The instruction pointer from the TrapFrame lies within the profiled region if it is greater than or equal to the start address and less than the end address. A recoding for version 6.2 incorrectly treats the end as inclusive. It increments the corresponding execution count unless the instruction pointer is less than the profiled region’s start address or greater than its end address. If the instruction pointer is exactly at the non-inclusive end address, then the function attempts to increment an execution count immediately after the buffer that was provided for execution counts. With contrivance, but also as a rare accident in valid use, this buffer overflow allows that an unprivileged user-mode program can crash Windows.
The second type of profile object dates from version 6.2, i.e., from Windows 8. It seems to have been introduced to tidy what had been a growing assortment of odd jobs. Each now is represented by a profile callback object. This has a different type at the start, specifically ProfileCallbackObject (0x11). Where the basic profile object has start and end addresses for a profiled range of address space, the profile callback object has the address of a kernel-supplied callback function and arbitrary context. The action to be taken for this type of profile object is, of course, to call the callback function. It gets the TrapFrame and the context. The only trigger is that the ProfileSource argument must be the same as specified for the profile object.
Especially notable among these profile callback objects is one for which the callback function traces each profile interrupt to whichever NT Kernel Logger sessions have enabled it. The trace shows as an event whose hook ID is PERFINFO_LOG_TYPE_SAMPLED_PROFILE (0x0F2E). The event data, as a PERFINFO_SAMPLED_PROFILE_INFORMATION structure, includes not only the address where execution was interrupted but also a summary of whether the interrupted execution was itself an interruption (for an interrupt service routine or deferred procedure call).