Maybe I’m a little late with a question about your recent post, but just in case, I’ll ask: do you have any methods (strategies) of working with an external library that you can’t get rid of and that violates some (or all ) principles of writing API design (most likely, refer to the principles, recommendations, which are described in the previous article of the author. Approx. transl. )? Maybe there are some stories? This is a vague question, but I’m just asking about any experience (as a user) of using an API that I really remember.
// etw_event_trace Trace = ETWBeginTrace(); ETWAddEventType(Trace, ETWType_ContextSwitch); // event EventBuffer[4096]; int EventCount; while(EventCount = ETWGetEvents(Trace, sizeof(EventBuffer), EventBuffer)) { {for(int EventIndex = 0; EventIndex < EventCount; ++EventIndex) { assert(EventBuffer[EventIndex].Type == ETWType_ContextSwitch); // EventBuffer[EventIndex].ContextSwitch }} } // ETWEndTrace(Trace);
enum etw_event_type { ETWType_None, ETWType_ContextSwitch, ... ETWType_Count, }; struct etw_event_context_switch { int64_t TimeStamp; uint32_t ProcessID; uint32_t FromThreadID; uint32_t ToThreadID; }; struct etw_event { uint32_t Type; // event_type union { etw_event_context_switch ContextSwitch; ... }; }; struct etw_event_trace { void *Internal; }; event_trace ETWBeginTrace(void); void ETWAddEventType(event_trace Trace, event_type); int ETWGetEvents(event_trace Trace, size_t BufferSize, void *Buffer); void ETWEndTrace(event_trace Trace);
ETWGetEvents
should copy events from some internal OS buffer, since they need to be taken from somewhere) . The version will be a little more difficult to take some displayed memory using the API, which you use as a buffer for reading: // etw_event_trace Trace = ETWBeginTrace(4096*sizeof(etw_event)); ETWAddEventType(Trace, ETWType_ContextSwitch); // etw_event_range Range; while(ETWBeginEventRead(Trace, &Range)) { {for(etw_event *Event = Range.First; Event != Range.OnePastLast; ++Event) { assert(Event->Type == ETWType_ContextSwitch); // Event->ContextSwitch }} ETWEndEventRead(Trace, &Range); } // ETWEndTrace(Trace);
ETWBeginTrace
, the user transmits the maximum number of events, which affects the buffer size and the kernel allocates a memory area (in the user address space) that is sufficient for the specified number of events. Then the system, if it can, writes directly to the allocated buffer and, thus, avoids unnecessary copying. When the user calls ETWBeginEventRead()
, pointers are returned to the beginning and end of some part of the memory for the events. Since the buffer will be processed as a ring buffer, the user expects to be able to loop through all the received events in the event that there are more than one event. I added the “end of reading” call, as some implementations may require the kernel to know what part of the buffer the user is viewing, thus avoiding writing to a memory that is actively being read. I don’t really know if such things are needed at all, but if you want to get basic information and give the kernel maximum flexibility to implement, this version definitely supports more possible implementations than the version with ETWGetEvents()
. struct etw_event_range { etw_event *First; etw_event *OnePastLast; }; event_trace ETWBeginTrace(size_t BufferSize); int ETWBeginEventRead(event_trace Trace, etw_event_range *Range); void ETWEndEventRead(event_trace Trace, etw_event_range *Range);
ETWGetEvents()
call. Also, to supplement the API with error messages, it would be nice to have something like this: bool ETWLastGetEventsOverflowed(event_trace Trace);
ETWGetEvents()
be able to check, and not too many events have occurred since the last check?memcpy()
.ETWBeginTrace()
is the call proposed by Microsoft, StartTrace()
. At first glance, it seems rather harmless: ULONG StartTrace(TRACEHANDLE *SessionHandle, char const *SessionName, EVENT_TRACE_PROPERTIES *Properties);
Properties
parameter, things get a little more complicated. The structure EVENT_TRACE_PROPERTIES
, defined by Microsoft, looks like this: struct EVENT_TRACE_PROPERTIES { WNODE_HEADER Wnode; ULONG BufferSize; ULONG MinimumBuffers; ULONG MaximumBuffers; ULONG MaximumFileSize; ULONG LogFileMode; ULONG FlushTimer; ULONG EnableFlags; LONG AgeLimit; ULONG NumberOfBuffers; ULONG FreeBuffers; ULONG EventsLost; ULONG BuffersWritten; ULONG LogBuffersLost; ULONG RealTimeBuffersLost; HANDLE LoggerThreadId; ULONG LogFileNameOffset; ULONG LoggerNameOffset; };
struct WNODE_HEADER { ULONG BufferSize; ULONG ProviderId; union { ULONG64 HistoricalContext; struct { ULONG Version; ULONG Linkage; }; }; union { HANDLE KernelHandle; LARGE_INTEGER TimeStamp; }; GUID Guid; ULONG ClientContext; ULONG Flags; };
"EventsLost"
and "BuffersWritten"
(“number of "BuffersWritten"
events” and “number of recorded buffers”, respectively, from the documentation. Approx. Transl. ) ? The reason is as follows: instead of making different data structures for different operations that can be used to track events, Microsoft has grouped API functions into several groups, and all functions in each group share a combined structure for parameters. Therefore, instead of the user getting a clear idea of ​​what is being fed into the input and returning from a function, simply looking at the parameters of the function - it should depend entirely on the MSDN documentation for each API, and hope that the documentation correctly lists which members of the giant Parameter structures are used for each call and when they are intended to transfer input and output parameters. EVENT_TRACE_PROPERTIES SessionProperties = {0};
StartTrace()
function, if you just want to receive the data directly and will not log them to a file, you need to fill in some members. These two have some meaning: SessionProperties.EnableFlags = EVENT_TRACE_FLAG_CSWITCH; SessionProperties.LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
EnableFlags
says we want to get it. We want a context switch, so we set this flag. And now let's see what happens when there are more than 32 events from one provider? I do not know, but I assume that Microsoft was not particularly concerned about this opportunity. But I was, that's why I used enum
in my sentence, since it supports billions of types of events. But wait, "32 types of events should be enough for everyone" - so Microsoft offered a 32-bit flag field. There are no problems, but this is definitely a sign of a close-minded thinking, which leads to things like duplication of functions with an Ex
prefix at the end of the name (“Ex” from “Extended” is an extended version of the function. Approx. Transl. ).LogFileMode
determines whether we want to receive events directly or we want the kernel to write them to disk. Since these are completely different operations, I would like to break them into calls for different functions, but, wait a minute, we have one big structure for everything - we drop everything there. SessionProperties.Wnode.ClientContext = 1;
"ClientContext"
("Context of the "ClientContext"
. Approx. Translation ), actually refers to the way in which you want to get the time of events. "TimestampType"
("TileTimeType". Approx. Transl. ) Would be a little clearer, but not important. Real fun is a simple "1"
on the right.ClientContext
can accept, but Microsoft does not always give them names. So you just have to read the documentation and remember that 1 means that time comes from QueryPerformanceCounter
, 2 means “system time”, and 3 means the number of CPU cycles.ClientContext
with a value of "1"
, but the value of USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPS
will be crystal clear. Secondly, it makes the code searchable. No one can normally search for “1” in the code base, but USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPS
is easily searched. You might think: “well, no problem, I’ll look for ClientContext = 1
”, but remember that more complex use of this API may include the use of variables, for example: ". . .ClientContext = TimestampType;"
. Third, the code will not compile for later versions of the SDK, where some things have changed. For example, if you decide to disable the use of USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPS
, they can remove the definition ( #define
) of this constant and make USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPS_DEPRECATED
. After such changes, the old code will not compile with the new version of the SDK and the developer will look at the new documentation and, thus, will see what he should use in return. SessionProperties.Wnode.Guid = SystemTraceControlGuid;
GUID
tells who is trying to track events. In our case, we are trying to take data from the kernel log and SystemTraceControlGuid
- a globally defined GUID
that points specifically to this log. I’m sure that a better name could be given for this GUID
, but this is an insignificant problem compared to the fact that if you try to compile this line of code, you will see that the linker cannot find SystemTraceControlGuid
.GUID
so large that Microsoft could not have found a way to embed them in the header files (I can count a few possible ways, but I suppose they didn’t like any of them) So Microsoft forces you to select one file in your project, in which, the Windows header files will embed GUID
definitions. In order to do this, you must write something like this: #define INITGUID // SystemTraceControlGuid evntrace.h. #include <windows.h> #include <strsafe.h> #include <wmistr.h> #include <evntrace.h> #include <evntcons.h>
GUID
will be located - everyone can refer to them (or some kind of nonsense there). In general, you can not identify them twice.SessionName
parameter, which we have to pass as a string, right? Since this is just a session name, I guess I can just do the following: ULONG Status = StartTrace(&SessionHandle, "CaseyTown!!!", &SessionProperties);
GUID
for SessionProperties
, which indicates that the kernel is the source of events - you also need to specify the predefined constant KERNEL_LOGGER_NAME
as the name of the session. Why? Well, this is because there is a small surprise that I will save for you so that you can enjoy the intrigue of everything that happens. ULONG Status = StartTrace(&SessionHandle, KERNEL_LOGGER_NAME, &SessionProperties);
SessionName
string SessionName
passed as the second parameter, this is just a “convenient” feature. In fact, SessionName
should be implemented directly in the SessionProperties
, but since Microsoft does not want to limit the maximum length of the string name, it was decided to just go ahead and package this string after the EVENT_TRACE_PROPERTIES
structure. Which means that, in fact, you can NOT do this: EVENT_TRACE_PROPERTIES SessionProperties = {0};
ULONG BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(KERNEL_LOGGER_NAME); EVENT_TRACE_PROPERTIES *SessionProperties =(EVENT_TRACE_PROPERTIES*) malloc(BufferSize); ZeroMemory(SessionProperties, BufferSize); SessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
StartTrace()
function of this API would copy the name into the structure for you, since in the end, the name is passed as the second parameter.KERNEL_LOGGER_NAME
as a SessionName
— along with the GUID
— is not, after all, superfluous, and this is the kind of surprise I mentioned. The real reason why the SessionName
parameter should be set to KERNEL_LOGGER_NAME
is that Windows allows you to have only one session in the system — in general, a session that reads events from the SystemTraceControlGuid
. Other GUID
can have multiple sessions, but not SystemTraceControlGuid
. So, in fact, when you transfer KERNEL_LOGGER_NAME
- you say that you want to have one, unique session that can exist in the system at any time with SystemTraceControlGuid
GUID
. If someone has already started this session - your attempt to start it - will fail.StartTrace()
, but somewhere a bug went through - no matter how - and your program crashed - KERNEL_LOGGER_NAME
- the session will still work! And, when you try to start your program again, possibly after fixing the bug, your attempt to call StartTrace()
will fail with ERROR_ALREADY_EXISTS
.StartTrace()
call, which will helpfully copy SessionName
into a structure for you - in rare cases is the first call you want to do, in any case. You will most likely make the following call: ControlTrace(0, KERNEL_LOGGER_NAME, SessionProperties, EVENT_TRACE_CONTROL_STOP);
StartTrace()
call will succeed. But, of course, ControlTrace()
does not copy the session name as StartTrace()
does, which means that, in practice, you have to do it yourself, since the StartTrace()
call comes after the ControlTrace()
call! StringCbCopy((LPSTR)((char*)pSessionProperties + pSessionProperties->LoggerNameOffset), sizeof(KERNEL_LOGGER_NAME), KERNEL_LOGGER_NAME);
StartTrace()
to track the kernel log - how does the system know that our process has the ability to take and stop tracking the log and start another with our own settings?StartTrace()
, well, he gets the opportunity to configure the log tracking for himself.StartTrace
will end with an error, because the process does not have enough privileges. (In theory, you have the opportunity to add a user to the “Performance Log Users” group and thus avoid running the process as an administrator, but I just say it now - in fact, I can't remember whether it will work for connections to kernel log or just for all other types of tracking ...)ETWBeginTrace()
/ ETWAddEventType()
), , , , , , , , GUID
-, #define
#include
- , , .OpenTrace()
: TRACEHANDLE OpenTrace(EVENT_TRACE_LOGFILE *Logfile);
OpenTrace()
.OpenTrace()
, EVENT_TRACE_LOGFILE
. , , -, , - «» — . , StartTrace()
, OpenTrace()
— , -, , , , — .EVENT_TRACE_LOGFILE
: struct EVENT_TRACE_LOGFILE { LPTSTR LogFileName; LPTSTR LoggerName; LONGLONG CurrentTime; ULONG BuffersRead; union { ULONG LogFileMode; ULONG ProcessTraceMode; }; EVENT_TRACE CurrentEvent; TRACE_LOGFILE_HEADER LogfileHeader; PEVENT_TRACE_BUFFER_CALLBACK BufferCallback; ULONG BufferSize; ULONG Filled; ULONG EventsLost; union { EVENT_CALLBACK *EventCallback; EVENT_RECORD_CALLBACK *EventRecordCallback; }; ULONG IsKernelTrace; PVOID Context; };
Callback
" ( . . . ) — . — . .EVENT_TRACE_LOGFILE
- Microsoft : EVENT_TRACE_LOGFILE LogFile = {0};
OpenTrace()
, - , «» . , , : LogFile.LoggerName = KERNEL_LOGGER_NAME;
LogFile.LoggerName = KERNEL_LOGGER_NAME; LogFile.ProcessTraceMode = (PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_RAW_TIMESTAMP);
PROCESS_TRACE_MODE_REAL_TIME
, , — , -, , . PROCESS_TRACE_MODE_EVENT_RECORD
— , Windows, EventRecordCallback
, EventCallback
(, , API !). PROCESS_TRACE_MODE_RAW_TIMESTAMP
— Windows ClientContext
, StartTrace()
. , : , , , " 2
" " 2
" — " 2
", . " 1
" " 3
" — … . LogFile.EventRecordCallback = CaseyEventRecordCallback;
TRACEHANDLE ConsumerHandle = OpenTrace(&LogFile);
StartTrace()
OpenTrace()
. 2 API, . TRACEHANDLE
. . . . !StartTrace()
ULONG
, . OpenTrace()
, INVALID_HANDLE_VALUE
, - . StartTrace()
( -) . OpenTrace()
, - .OpenTrace()
: static void WINAPI CaseyEventRecordCallback(EVENT_RECORD *EventRecord) { EVENT_HEADER &Header = EventRecord->EventHeader; UCHAR ProcessorNumber = EventRecord->BufferContext.ProcessorNumber; ULONG ThreadID = Header.ThreadId; int64 CycleTime = Header.TimeStamp.QuadPart; // Process event here. }
ULONG ProcessTrace(TRACEHANDLE *HandleArray, ULONG HandleCount, LPFILETIME StartTime, LPFILETIME EndTime);
ProcessTrace
.ProcessTrace()
, , , , , . CloseTrace()
. , — , , ProcessTrace()
!ProcessTrace()
: static DWORD WINAPI Win32TracingThread(LPVOID Parameter) { ProcessTrace(&ConsumerHandle, 1, 0, 0); return(0); }
OpenTrace()
— , : DWORD ThreadID; HANDLE ThreadHandle = CreateThread(0, 0, Win32TracingThread, 0, 0, &ThreadID); CloseHandle(ThreadHandle);
SessionName
KERNEL_LOGGER_NAME
, GUID
— SystemTraceControlGuid
.Source: https://habr.com/ru/post/254795/
All Articles