Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / system

Tracing and Logging Technologies on Windows. Part 4 - Saving Log

5.00/5 (2 votes)
18 Aug 2023CPOL18 min read 4.3K   233  
Discussion of how to organize saving application trace helper information.
Article shows how to save log information in the application with implementation in different programming languages. It also describes implementation logging for the kernel mode drivers.

Table of Contents

Introduction

Saving helper information of what goes in the application into a file which later helps to determine program issues called logging. I think anyone who develops an application knows about logging. There are lots of libraries and components which can be integrated into your application and used. Or you can say that the easy way is just redirecting output where it is needed and or replace debug API calls with file writing and that’s all. But that is not the correct implementation for saving data due to some reasons.

The first reason is the multithreading. As applications can have lots of threads and calls for logging data can be made from any of them. We can arrange critical sections or mutexes and perform single access for logs but the periods of accessing file writing can vary, which is not good, so we need to think about how to minimize the period of the log writing operation. And another issue which comes in the case of logging in the kernel driver is that in the kernel we have another dimension: calls for writing logs can be made at different interrupt request levels (IRQL), but the file writing API only works at the low level - passive. So the result of calling such an API at the higher level will be BSOD with IRQL NOT LESS OR EQUAL. So in the scenarios above, I suggest having one thread which will perform writing operations.

Create Your Own Logger in Application

Based on the above notes, we can create a simple log saving implementation. We just arrange simple text line output, but you can add any of your own metadata along with it.

C++

C++
// Simple log record structure
typedef struct _log_record_t
{
    // Our text field
    std::string text;
}_log_record_t,*p_log_record_t;

Our aim is just to display a basis of how that should be organized for fast and safe processing. And, as was mentioned, we need to arrange a list of those structures, which will be accessed from any threads. For those needs, we define next variables:

C++
// List of log records
std::list<_log_record_t>    g_LogData;
// Lock accessing log records list
std::recursive_mutex        g_csLock;
// Notify that there is a new log record or quit
std::condition_variable     g_cvNotify;
// Condition variable accessing mutex
std::mutex                  g_LockMutex;
// Exit flag
std::atomic<bool>           g_bExit;

The definitions above are in C++11. We have a list of the log records structures with type of _log_record_t. The recursive mutex variable for locking access to that list. Conditional variable for notification that list updated. The quit signal atomic variable which is used for exit threads. Log record structure is possible to extend for any other fields which may be needed for logging.

The logger function will just arrange a log record with data after putting that into a list and signal that it contains something to output.

C++
// Put log message into queue
void LogPrint(const char * format, ...)
{
    // Just skip message once we exiting
    if (!g_bExit.load())
    {
        va_list    args;
        __crt_va_start_a(args, format);
        int _length = _vscprintf(format, args) + 1;
        char * _string = (char *)malloc(_length);
        if (_string)
        {
            memset(_string, 0, _length);
            _vsprintf_p(_string, _length, format, args);
            // Initialize log record data
            _log_record_t record;
            record.text = _string;
            g_csLock.lock();
            // append record into list and 
            g_LogData.push_back(record);
            // notify that new record persist
            g_cvNotify.notify_all();
            g_csLock.unlock();
            free(_string);
        }
        __crt_va_end(args);
    }
}

As you can see only the code part with adding record into list and update of that list notification are locked with critical section. At the same time in the logger thread, we wait for notification to appear, then get the record from the list and write it into the log file.

C++
// Thread for writing received log messages
void LogThread()
{
    while (true) {
        _log_record_t record;
        bool have_data;
        g_csLock.lock();
        // extract data if new record in a list
        if (have_data = (g_LogData.size() > 0)) {
            auto it = g_LogData.begin();
            record = *it;
            g_LogData.erase(it);
        }
        g_csLock.unlock();
        if (have_data) {
            // If have data write it into a file
            std::ofstream ofs("d:\\test.log", 
                std::ofstream::out | std::ofstream::app);
            ofs << record.text << std::endl;
            ofs.close();
        } else {
            // otherwise wait for notification
            if (g_bExit.load()) break;
            std::unique_lock<std::mutex> lock(g_LockMutex);
            g_cvNotify.wait(lock);
        }
    }
}

In thread, we are locked with critical section only actual accessing of the record list, so writing output latency does not affect all other threads which output log messages at the same time. In code, extract all available records from the list and if nothing to write, then we check for quit or new record arrives. In the example, we just open a file each time we need to write data but in real implementation, I suggest opening the file at the beginning of the log thread and with shared reading access. In that case, you avoid issues with accessing the file and will be able to view log changes during updates in real time.

To test our implementation with multithreading access, we manage processing threads which will perform sending log messages with random delay:

C++
// Simple thread which send log messages
void ProcessThread()
{
    std::srand((uint32_t)std::time(nullptr));
    int period = std::rand() * 300 / RAND_MAX;
    auto id = std::this_thread::get_id();
    while (!g_bExit.load()) {
        // Just writing some text into log until quit signal
        LogPrint("Hello from thread: %d",id);
        // Sleep for random selected period
        std::this_thread::sleep_for(std::chrono::milliseconds(period));
    }
}

In the main function, we start a log thread. Along with it, we also started the 100 of processing threads to feed the log. Then wait for 5 seconds and shut down.

C++
int main(int argc, char* argv[])
{
    g_bExit = false;
    std::cout << "Application Started" << std::endl;
    // Starting log writer thread
    std::thread    log_thread = std::thread(LogThread);
    // Starting processint threads
    std::vector<std::thread> processes;
    int threads = 100;
    while (threads-- > 0) {
        processes.push_back(std::thread(ProcessThread));
    }
    std::this_thread::sleep_for(std::chrono::seconds(5));
    // Set quit flag and notify threads if they are waiting
    g_bExit = true;
    g_cvNotify.notify_all();
    // Wait for processing threads 
    while (processes.size()) {
        auto it = processes.begin();
        it->join();
        processes.erase(it);
    }
    // Wait for log thread
    log_thread.join();
    std::cout << "Application Exit" << std::endl;
    _getch();
    return 0;
}

C

The classic C way is more general. To organize a list of the _log_record_t structures, we should make a pointer to the next structure as one of the fields.

C++
// Simple log record structure
typedef struct _log_record_t
{
    // Need that for list implementation
    struct _log_record_t * next;
    // Our text field
    char * text;
}_log_record_t,*p_log_record_t;

For the organizing queue, as we need to put records to the tail and pick them from the head, the actual list definition looks next.

C++
typedef struct
{
    // list head for pop for writing
    struct _log_record_t * head;
    // list tail for append records
    struct _log_record_t * tail;
}_log_list_t,*p_log_list_t;

Variables which we need to organize processing.

C++
// list of log records
_log_list_t                 g_LogData;
// lock accessing log records list
CRITICAL_SECTION            g_csLock;
// notify that there is a new log record or quit
HANDLE                      g_evNotify;
// Exit Event
HANDLE                      g_evQuit;

Here, we have two events: one for quit and another one for queue signal. Critical section for locking our list accessing.

Implementation of adding records to the processing queue differs a bit, as we organize our own list.

C++
// Put log message into queue
void LogPrint(const char * format, ...)
{
    // Just skip message once we exiting
    if (WAIT_TIMEOUT == WaitForSingleObject(g_evQuit,0)) {
        va_list    args;
        __crt_va_start_a(args, format);
        int _length = _vscprintf(format, args) + 2;
        char * _string = (char *)malloc(_length);
        if (_string) {
            __try {
                memset(_string, 0, _length);
                _vsprintf_p(_string, _length, format, args);
                if (_string[_length - 1] != '\n') strcat_s(_string,_length,"\n");
                // Initialize log record data
                _log_record_t * record = (_log_record_t*)malloc(sizeof(_log_record_t));
                if (record) {
                    memset(record, 0x00, sizeof(_log_record_t));
                    record->text = _string;
                    record->next = NULL;
                    _string = NULL;
                    EnterCriticalSection(&g_csLock);
                    __try {
                        // append record into list end 
                        _log_record_t * tail = g_LogData.tail;
                        if (tail) {
                            tail->next = record;
                        }
                        else {
                            g_LogData.head = record;
                        }
                        g_LogData.tail = record;
                        // notify that new record persist
                        SetEvent(g_evNotify);
                    }
                    __finally {
                        LeaveCriticalSection(&g_csLock);
                    }
                }
            } __finally {
                if (_string) free(_string);
            }
        }
        __crt_va_end(args);
    }
}

We add new records to the list and the tail field of the g_LogData variable always points to the last record, at the same time, the head field points to the first one. Once the record is added, we signal the notification event.

C++
// Thread for writing received log messages
DWORD WINAPI LogThread(PVOID p)
{
    UNREFERENCED_PARAMETER(p);
    while (TRUE) {
        _log_record_t * record = NULL;
        EnterCriticalSection(&g_csLock);
        // extract data if new record in a list
        record = g_LogData.head;
        if (record) {
            g_LogData.head = record->next;
            if (g_LogData.tail == record) {
                g_LogData.tail = NULL;
            }
        }
        LeaveCriticalSection(&g_csLock);
        if (record) {
            if (record->text) {
                // If have data write it into a file
                HANDLE hFile = CreateFileA("d:\\test.log",FILE_GENERIC_WRITE,
                    FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
                if (hFile != INVALID_HANDLE_VALUE) {
                    DWORD written = 0;
                    SetFilePointer(hFile,0,NULL,FILE_END);
                    WriteFile(hFile,record->text,(DWORD)strlen(record->text),&written,NULL);
                    CloseHandle(hFile);
                }
            }
            // Free Allocated Text 
            if (record->text) {
                free(record->text);
            }
            free(record);
        } else {
            // otherwise wait for notification
            HANDLE hHandles[2];
            hHandles[0] = g_evNotify;
            hHandles[1] = g_evQuit;
            if (WAIT_OBJECT_0 != WaitForMultipleObjects(
                _countof(hHandles), hHandles, FALSE, INFINITE)) {
                break;
            }
        }
    }
    return 0;
}

The log thread which dequeues the records and writes them into the file waits for one of the two events to be signaled. Either a new record arrived or exits. We pick up records from the list head pointer field and assign it to the next field of the extracted record. The last record has NULL on the last record and if the extracted pointer equals the list tail, we set it to NULL also. Only dequeue records operation is locked with the critical section. With our own list implementation, we have a good performance advantage here.

The processing thread for output log messages:

C++
// Simple thread which send log messages
DWORD WINAPI ProcessThread(PVOID p)
{
    UNREFERENCED_PARAMETER(p);
    srand(GetTickCount());
    int period = rand() * 300 / RAND_MAX;
    DWORD id = GetCurrentThreadId();
    while (TRUE) {
        // Just writing some text into log until quit signal
        LogPrint("Hello from thread: %d",id);
        // Sleep for random selected period
        if (WAIT_OBJECT_0 == WaitForSingleObject(g_evQuit,period)) break;
    }
    return 0;
}

Main function with similar functionality: starting 100 processing threads which output log messages at the same time, and exiting after 5 seconds.

C++
int main(int argc, char* argv[])
{
    g_LogData.head = NULL;
    g_LogData.tail = NULL;
    g_evQuit = CreateEvent(NULL,TRUE,FALSE,NULL);
    g_evNotify = CreateEvent(NULL,FALSE,FALSE,NULL);
    InitializeCriticalSection(&g_csLock);
    printf("Application Started\n");
    // Starting log writer thread
    HANDLE log_thread = CreateThread(NULL,0,LogThread,NULL,0,NULL);
    // Starting processint threads
    int threads = 100;
    HANDLE * processes = (HANDLE *)malloc(threads * sizeof(HANDLE));
    for (int i = 0; i < threads; i++) {
        processes[i] = CreateThread(NULL, 0, ProcessThread, NULL, 0, NULL);
    }
    Sleep(5000);
    // Set quit flag and notify threads if they are waiting
    SetEvent(g_evQuit);
    // Wait for processing threads 
    for (int i = 0; i < threads; i++) {
        WaitForSingleObject(processes[i], INFINITE);
        CloseHandle(processes[i]);
    }
    free(processes);
    // Wait for log thread
    WaitForSingleObject(log_thread,INFINITE);
    CloseHandle(log_thread);
    CloseHandle(g_evQuit);
    CloseHandle(g_evNotify);
    DeleteCriticalSection(&g_csLock);
    // In case if something left which is not possible but...
    assert(g_LogData.head == NULL);
    _log_record_t * record = g_LogData.head;
    while (record) {
        _log_record_t * temp = record->next;
        if (record->callback) record->callback(record);
        if (record->text) free(record->text);
        free(record);
        record = temp;
    }
    printf("Application Exit\n");
    _getch();
    return 0;
}

C#

In C# declaration of the log record structure is not much different from C++.

C#
// Simple log record structure
class log_record
{
    // Text Message
    public string text;
}

As for the variables: we also have two events, one for exit and another one for notification, log records we will be storing in the generic list, and for locking list access, there will be a simple object.

C#
// list of log records
static List<log_record> g_LogData = new List<log_record>();
// lock accessing log records list
static object g_csLock = new object();
// notify that there is a new log record
static EventWaitHandle g_evNotify = new AutoResetEvent(false);
// Exit Event
static EventWaitHandle g_evQuit = new ManualResetEvent(false);

Adding record to the list in C# is done most simply, as it is not necessary to have string formatting.

C#
// Put log message into queue
static void LogPrint(string text)
{
    // Just skip message once we exiting
    if (!g_evQuit.WaitOne(0))
    {
        // Initialize log record data
        log_record record = new log_record();
        record.text = text;
        lock (g_csLock)
        {
            // append record into list and 
            g_LogData.Add(record);
            // notify that new record persist
            g_evNotify.Set();
        }
    }
}

We are using lock C# keyword on the threading critical code part. This can be replaced with the Monitor.Enter() and Monitor.Exit() methods in other .NET languages. The log output thread has the same logic as implementations in C++ or C.

C#
// Thread for writing received log messages
static void LogThread()
{
    while (true)
    {
        log_record record = null;
        lock (g_csLock)
        {
            // extract data if new record in a list
            if (g_LogData.Count > 0)
            {
                record = g_LogData[0];
                g_LogData.RemoveAt(0);
            }
        }
        if (record != null)
        {
            // If have data write it into a file
            TextWriter tw = new StreamWriter("d:\\test.log", true, Encoding.ASCII);
            try
            {
                tw.WriteLine(record.text);
            }
            finally
            {
                tw.Dispose();
            }
        }
        else
        {
            // otherwise wait for notification
            if (0 != WaitHandle.WaitAny(new WaitHandle[]  {
                    g_evNotify, g_evQuit
                }
            )) break;
        }
    }
}

Thread implementation for output log messages.

C#
// Simple thread which send log messages
static void ProcessThread()
{
    int id = Thread.CurrentThread.ManagedThreadId;
    int period = (new Random(id)).Next(20,1000);
    while (true)
    {
        // Just writing some text into log until quit signal
        LogPrint(string.Format("Hello from thread: {0}",id));
        // Sleep for random selected period
        if (g_evQuit.WaitOne(period)) break;
    }
}

And the main function.

C#
static void Main(string[] args)
{
    Console.WriteLine("Application Started");
    // Starting log writer thread
    Thread log_thread = new Thread(LogThread);
    log_thread.Start();
    // Starting processint threads
    List<Thread> processes = new List<Thread>();
    int threads = 100;
    while (threads-- > 0) {
        var t = new Thread(ProcessThread);
        t.Start();
        processes.Add(t);
    }
    Thread.Sleep(5000);
    // Set quit flag and notify threads if they are waiting
    g_evQuit.Set();
    // Wait for processing threads 
    while (processes.Count > 0) {
        processes[0].Join();
        processes.RemoveAt(0);
    }
    // Wait for log thread
    log_thread.Join();
    Console.WriteLine("Application Exit");
    Console.ReadKey();
}

As you can see, the implementations on different programming languages of this article look mostly similar. The result of the test.log file after execution of the code:

Image 1

Designing Callback to Free Any Memory of the Record

It is possible that we supply some additional memory, object or COM object for providing some additional information for logging. And that memory or object should be freed properly. Of course in .NET, it may be handled automatically but the question with the COM objects remains opened.

C++

We design a logger function to accept the callback function which will be called once the record is written to the log, so resources may be freed. Also, we pass the user context as an argument which will be stored in the record structure. So it will look as follows:

C++
// Simple log record structure
typedef struct _log_record_t
{
    // Our text field
    std::string text;
    // Context
    void * ctx;
    // Callback Function
    std::function<void(_log_record_t *)> callback;
}_log_record_t,*p_log_record_t; 

For the callback type, we are using the function template. The extended logger function will look next.

C++
// Function for otuput log with the callback
void LogPrintEx(void * ctx, std::function<void(_log_record_t *)> callback, 
                    const char * format, ...)
{
    // Just skip message once we exiting
    if (!g_bExit.load())
    {
        va_list    args;
        __crt_va_start_a(args, format);
        int _length = _vscprintf(format, args) + 1;
        char * _string = (char *)malloc(_length);
        if (_string)
        {
            memset(_string, 0, _length);
            _vsprintf_p(_string, _length, format, args);
            // Initialize log record data
            _log_record_t record;
            record.text = _string;
            record.ctx = ctx;
            record.callback = callback;
            g_csLock.lock();
            // append record into list and 
            g_LogData.push_back(record);
            // notify that new record persist
            g_cvNotify.notify_all();
            g_csLock.unlock();
            free(_string);
        }
        __crt_va_end(args);
    }
    else {
        if (callback) {
            _log_record_t record;
            record.ctx = ctx;
            callback(&record);
        }
    }
}

In the function above, we design that the record may not be placed into the log queue due to the signal of exit. In that case, we should manually free resources and call the callback manually to free extra memory.

In the log output thread, we just call the callback once the record is written.

C++
if (record.callback) {
    record.callback(&record);
}

In the callback function, the user context data can be accessed as the ctx field of the record structure which passed as an argument.

C

In this implementation, we have only differences with the callback type declaration. Instead of a usage template, we have our own function type.

C++
typedef void (* LogPrintCallback)(struct _log_record_t * record);

// Simple log record structure
typedef struct _log_record_t
{
    // Need that for list implementation
    struct _log_record_t * next;
    // Our text field
    char * text;
    // Context
    PVOID ctx;
    // Callback Function
    LogPrintCallback callback;
}_log_record_t,*p_log_record_t;

The extended logger function also accepts the callback and context arguments like we did in the previous implementation.

C#

In .NET, we can use any existing function delegate as the callback. At least we need only one argument of the object which can be casted into our record structure. For example, we just choose an existing delegate for the EventHandler. In that case, our record structure will be next.

C#
// Simple log record structure
class log_record
{
    // Text Message
    public string text;
    // Context
    public object ctx;
    // Callback
    public EventHandler callback;
}

In the logger function, we pass that delegate and the context object.

C#
// Put log message into queue with callback
static void LogPrintEx(string text, EventHandler callback, object ctx)
{
    // Just skip message once we exiting
    if (!g_evQuit.WaitOne(0))
    {
        // Initialize log record data
        log_record record = new log_record();
        record.text = text;
        record.callback = callback;
        record.ctx = ctx;
        lock (g_csLock)
        {
            // append record into list and 
            g_LogData.Add(record);
            // notify that new record persist
            g_evNotify.Set();
        }
    }
    else
    {
        if (callback != null)
        {
            log_record record = new log_record();
            record.text = text;
            record.callback = callback;
            record.ctx = ctx;
            callback(record,EventArgs.Empty);
        }
    }
}

The callback will be invoked in the logger thread.

C#
if (record.callback != null)
{
    record.callback(record,EventArgs.Empty);
}

In the callback function, we should cast an object into our record structure type.

Wait for Record to Be Written

In some cases, we may need to know what exactly is written before something happens in our application. For that, we can use a previously designed callback. We design it so that the context parameter for that callback is the notification handle. That handle sets into signaled state once the callback is executed. In the thread which is output log message, we just need to wait for this notification to become the signaled state.

C++

In C++, we will use the conditional variable for this. We are also modifying the record structure by adding the pointer to the notification variable.

C++
// Simple log record structure
typedef struct _log_record_t
{
    // Our text field
    std::string text;
    union {
        // Notification Variable
        std::condition_variable * notify;
        // Context
        void * ctx;
    };
    // Callback Function
    std::function<void(_log_record_t *)> callback;
}_log_record_t,*p_log_record_t; 

This is done as the union as we can use either context or the notification. In the callback function, we signal the conditional variable.

C++
// Event Notification Callback
void LogPrintNotifyCallback(_log_record_t * record) {
    // Check Once we have notification
    if (record->notify) {
        // Signal that we are done
        record->notify->notify_all();
    }
}

The call of the log output we design with the macro in which we just call the function created previously and pass our callback implementation. As the notification in record is designed within the union, along with the context, then it will be placed into the same address space.

C++
// We setting up the definition for notification
#define LogPrint2(notify,format,...) LogPrintEx(  \
                (void*)notify,LogPrintNotifyCallback,format,__VA_ARGS__)

Test code looks next.

C++
std::condition_variable notify;
LogPrint2(&notify,"This text we are waiting to be written into the log");
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
notify.wait(lock);

C

In C for the notification, we use the auto reset event. And put it also to the record structure as the union.

C++
// Simple log record structure
typedef struct _log_record_t
{
    // Need that for list implementation
    struct _log_record_t * next;
    // Our text field
    char * text;
    union {
        // Context
        PVOID ctx;
        // Notification Event
        HANDLE notify;
    } ;
    // Callback Function
    LogPrintCallback callback;
}_log_record_t,*p_log_record_t;

The callback implementation.

C++
// Event Notification Callback
void LogPrintNotifyCallback(_log_record_t * record) {
    // Check Once we have notification event
    if (record->notify) {
        // Signal that we are done
        SetEvent(record->notify);
    }
}

The macro definition for the usage is the same as for C++ implementation. The test code also looks similar.

C++
// Notification Event
HANDLE hNotify = CreateEvent(NULL, FALSE, FALSE, NULL);
LogPrint2(hNotify, "This text we are waiting to be written into the log");
if (WAIT_OBJECT_0 == WaitForSingleObject(hNotify, INFINITE)) {
    printf("Notification Signaled\n");
}
CloseHandle(hNotify);

C#

In C#, we don’t need to change the context variable in the log record. As we can just do boxing and unboxing the object. So the structure remains the same. The callback will be next.

C#
// Event Notification Callback
static void LogPrintNotifyCallback(object sender, EventArgs e)
{
    if (sender != null 
        && (sender is log_record) 
        && (((log_record)sender).ctx is EventWaitHandle))
    {
        ((EventWaitHandle)((log_record)sender).ctx).Set();
    }
}

Instead of the macro implementation in C#, we just create the function with different arguments.

C#
// Put log message into queue with notification
static void LogPrint2(string text,EventWaitHandle notify)
{
    LogPrintEx(text,LogPrintNotifyCallback, notify);
}

The code for test.

C#
// Write Log with notify
EventWaitHandle evNotify = new AutoResetEvent(false);
LogPrint2("This text we are waiting to be written into the log", evNotify);
if (evNotify.WaitOne())
{
    Console.WriteLine("Notification Signaled");
}
evNotify.Dispose();

You can see the results in the next screen shot. If you set breakpoint to the callback function, you see that the main thread waits for the notification and the callback is executed. After notification signals, the main function is continued.

Image 2

What about Kernel Mode?

Okay, we found out how to organize log writing for particular applications, but now you can ask: “there is a way of writing log in a driver”? And here it is. Implementation in a driver has the same logic as we discussed earlier, but lots of things look a bit different - as it is a kernel. As I already mentioned, in the kernel, we have another dimension IRQL. All applications under user mode are executed at one level which is “0” and called passive. But driver code can be executed from a higher level, more of that the code of the driver as long as the allocated data can be in paged memory pool or non paged. Non paged code and data are always resident and the paged code may be stored on disk due memory pressure and to execute such code or accessing the memory. For in the system were added the page files. You can find such a file named pagefile.sys in the root on your system drive. Make sure that you enable the option to display hidden files. And the file accessing in the system is done on a passive level which is not possible on higher IRQL and the result…,right - BSOD. Higher IRQL levels have higher privileges to execution, and the code at the “passive” stops until code at higher level finishes. This is a simple description for understanding what needs to be handled, as on levels higher than “passive”, the code is also executed and it also may need to provide some log messages.

Based on the notes above, let's see how to make the implementation. First, like in the previous description, we define the variables and structures and variables. Our log record is similar and contains only text information, the “Entry” field required to organize the list.

C++
typedef struct
{
    LIST_ENTRY    Entry;
    // Simple Text For Logging
    CHAR      *    Text;
}LOG_RECORD, *PLOG_RECORD;

The variables which are declared in previous samples we put into structure. That needs to be allocated in the non paged memory pool, due the already mentioned reason.

C++
typedef struct
{
    // Quit Event
    KEVENT          EvQuit;
    // Have Data Event
    KEVENT          EvHaveData;
    // Log Thread Object
    PFILE_OBJECT    ThreadObject;
    // List Lock
    KMUTEX          ListLock;
    // List
    LIST_ENTRY      List;
} DRIVER_LOG;

And the other variables:

C++
// Log Context
DRIVER_LOG *    s_pLog = NULL;
// Log Spin Lock
KSPIN_LOCK      s_LogSpinLock;

First one is the allocated pointer to the log and the critical section for accessing the log pointer. For information: the code which is locked with a spin lock critical section raises execution to dispatch IRQL.

The log printing function implementation looks a bit differ but doing the same things like in user mode application. We need to keep in mind that this function will be called from any IRQL levels.

C++
// Output Format String Into Log file
void LogPrint(const char * format, ...)
{
    if (format) {
        KIRQL irqlsp;
        va_list    args;
        va_start(args, format);
        // Just limit string with 1024 bytes size
        int _length = 1024 + 3;
        char * _string = (char *)ExAllocatePool(NonPagedPool, _length);
        if (_string) {
            __try {
                memset(_string, 0, _length);
                // RtlStringCchVPrintfA - Should run on passive level
                vsprintf_s(_string, _length - 3, format, args);
                // RtlStringCchCatA - Should run on passive level
                strcat_s(_string,_length,"\n");
                // Lock Log Accessing
                KeAcquireSpinLock(&s_LogSpinLock, &irqlsp);
                if (s_pLog) {
                    KIRQL irql = KeGetCurrentIrql();
                    LARGE_INTEGER time_out = {0};
                    // Check If Quit Requested
                    if (STATUS_TIMEOUT == KeWaitForSingleObject(&s_pLog->EvQuit, Executive, 
                        KernelMode, FALSE, &time_out)) {
                        // Alocate Record Structure
                        PLOG_RECORD rec = (PLOG_RECORD)ExAllocatePool(NonPagedPool,
                                                                      sizeof(LOG_RECORD));
                        if (rec) {
                            memset(rec, 0x00, sizeof(LOG_RECORD));
                            rec->Text = _string;
                            // Wait For List Mutex 
                            // In current implementation we may no need check irql 
                            // as we are under spin lock that just added for example
                            if (STATUS_SUCCESS == KeWaitForSingleObject(
                                &s_pLog->ListLock, Executive, KernelMode, FALSE, 
                                irql <= APC_LEVEL ? NULL : &time_out)) {
                                _string = NULL;
                                // Insert Log record into list
                                InsertTailList(&s_pLog->List, &(rec->Entry));
                                // Notify that we have some data
                                KeSetEvent(&s_pLog->EvHaveData, IO_NO_INCREMENT, FALSE);
                                KeReleaseMutex(&s_pLog->ListLock, FALSE);
                            }
                            else {
                                ExFreePool(rec);
                            }
                        }
                    }
                }
                KeReleaseSpinLock(&s_LogSpinLock, irqlsp);
            }
            __finally {
                if (_string) ExFreePool(_string);
            }
        }
        va_end(args);
    }
}

In logger thread, we also have the same functionality as in user mode application, the actual log records writing just split into separate functions:

C++
// Log Thread Function
void LogThread(PVOID Context)
{
    PAGED_CODE();
    DRIVER_LOG * log = (DRIVER_LOG *)Context;
    PVOID hEvents[2] = { 0 };
    hEvents[0] = &log->EvHaveData;
    hEvents[1] = &log->EvQuit;
    while (TRUE) {
        // Wait For Event Of Quit Or Data Arriving
        NTSTATUS Status = KeWaitForMultipleObjects(2, hEvents, 
            WaitAny, Executive, KernelMode, FALSE, NULL, NULL);
        if (Status == STATUS_WAIT_0) {
            WritePendingRecords(log);
        } else {
            break;
        }
    }
    // Just mark that we are done
    KeSetEvent(&log->EvQuit, IO_NO_INCREMENT, FALSE);
    PsTerminateSystemThread(STATUS_SUCCESS);
}

The WritePendingRecords performs writing all records from a list into a file. Getting records from list looks next:

C++
PLOG_RECORD rec = NULL;
// Lock List Mutex
if (STATUS_SUCCESS == KeWaitForSingleObject(&log->ListLock, 
                                            Executive, KernelMode, FALSE, 0)) {
    if (!IsListEmpty(&log->List)) {
        // Extract record from start of the list 
        PLIST_ENTRY entry = log->List.Flink;
        if (entry) {
            rec = CONTAINING_RECORD(entry, LOG_RECORD, Entry);
            RemoveEntryList(entry);
        }
    }
    if (!rec) {
        // Reset data fag if no records 
        // as we have manual reset event
        KeResetEvent(&log->EvHaveData);
    }
    KeReleaseMutex(&log->ListLock, FALSE);
}

And the file writing is done:

C++
if (rec->Text) {
    HANDLE hFile = NULL;
    UNICODE_STRING FileName;
    IO_STATUS_BLOCK iosb;
    OBJECT_ATTRIBUTES Attributes;

    RtlInitUnicodeString(&FileName, L"\\DosDevices\\d:\\DriverLog.log");
    InitializeObjectAttributes(&Attributes, &FileName, 
                                (OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE), NULL, NULL);
    // Open File For Writing
    if (STATUS_SUCCESS == ZwCreateFile(
                &hFile,
                GENERIC_WRITE | SYNCHRONIZE,
                &Attributes,
                &iosb,
                0,
                FILE_ATTRIBUTE_NORMAL,
                FILE_SHARE_READ,
                FILE_OPEN_IF,
                FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
                NULL,
                0
            )) {
        LARGE_INTEGER pos = {0};
        FILE_STANDARD_INFORMATION info;
        // Append Information
        if (STATUS_SUCCESS == ZwQueryInformationFile(hFile, &iosb, &info,
                                                    sizeof(info), FileStandardInformation))
        {
            pos = info.EndOfFile;
        }
        // Write Data
        ZwWriteFile(hFile, NULL, NULL, NULL, &iosb, 
                                        rec->Text, (ULONG)strlen(rec->Text), &pos, NULL);
        // Close File
        ZwClose(hFile);
    }
    ExFreePool(rec->Text);
}

We will store log records into a file with name DriverLog.log in the root of drive D:. As you can see in kernel mode, simple operation requires more lines of code compared to user mode application. Now we need to define functions starting and stopping our log. Starting log, we perform on driver entry, and stopping is done on driver unloading message.

C++
// Start Logging
NTSTATUS StartLog()
{
    NTSTATUS Status = STATUS_SUCCESS;
    DRIVER_LOG * log = NULL;
    KIRQL irql;    
    KeAcquireSpinLock(&s_LogSpinLock, &irql);
    if (!s_pLog) {
        s_pLog = (DRIVER_LOG *)ExAllocatePool(NonPagedPool, sizeof(DRIVER_LOG));
        if (s_pLog) {
            memset(s_pLog, 0x00, sizeof(DRIVER_LOG));
            KeInitializeMutex(&s_pLog->ListLock, 0);
            KeInitializeEvent(&s_pLog->EvQuit, NotificationEvent, TRUE);
            KeInitializeEvent(&s_pLog->EvHaveData, NotificationEvent, FALSE);
            InitializeListHead(&s_pLog->List);
            log = s_pLog;
        }
    }
    KeReleaseSpinLock(&s_LogSpinLock, irql);
    // Creating thread should be done on irql passive level
    // So put that outside spin lock
    if (log) {
        HANDLE hThread;
        KeResetEvent(&log->EvQuit);
        // Start Log Thread
        Status = PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, LogThread, (PVOID)log);
        if (NT_SUCCESS(Status)) {
            Status = ObReferenceObjectByHandle(hThread, GENERIC_READ | GENERIC_WRITE, 
                NULL, KernelMode, (PVOID *)&log->ThreadObject, NULL);
        }
        if (!NT_SUCCESS(Status)) {
            // Set drop event once we have error
            KeSetEvent(&log->EvQuit, IO_NO_INCREMENT, FALSE);
        }
    }
    LogPrint("Driver Log Started '%S'",DRIVER_NAME);
    return Status;
}

// Stop Logging
NTSTATUS StopLog()
{
    NTSTATUS Status = STATUS_SUCCESS;
    DRIVER_LOG * log = NULL;
    KIRQL irql;
    LogPrint("Driver Log Stopped '%S'",DRIVER_NAME);
    KeAcquireSpinLock(&s_LogSpinLock, &irql);
    log = s_pLog;
    s_pLog = NULL;
    KeReleaseSpinLock(&s_LogSpinLock, irql);
    if (log) {
        // Set event that we are done
        KeSetEvent(&log->EvQuit, IO_NO_INCREMENT, FALSE);
        // Stop Log Thread
        if (log->ThreadObject) {
            KeWaitForSingleObject(log->ThreadObject, Executive, KernelMode, FALSE, NULL);
            ObDereferenceObject(log->ThreadObject);
        }
        // Flush records if any
        WritePendingRecords(log);
        ExFreePool(log);
    }
    return Status;
}

I will not describe any other parts as that is not the target, just say that I put some call of the log writing in driver routines to see how it will be.

Now we need a test application which will perform driver install and loading then unloading and uninstalling. Installing and uninstalling operations performed like for regular windows service applications with usage of service control manager APIs. And loading the driver made with a particular CreateFile API. After execution of that application, we can see the following log messages:

Image 3

Wow, our log is working!!! The driver code also contains some DbgPrint API calls for tracing, which were mentioned in previous topics. For example in the code, you can find:

C++
DbgPrint("DriverLog: DriverEntry\n");

So you will be able to see debug output strings from the driver in DbgView, or in sample applications from previous articles. Those trace messages started with DriverLog: prefix:

Image 4

Higher IRQL

In the log file above, messages come just from basic driver functionality which runs out at passive mode IRQL and I were promising that it will work under higher level. So let’s arrange a DPC timer in a driver and call logging from it callback, it will be executed at dispatch level. We will add the ability of starting and stopping that timer from our user mode application. First, we define structure for our DPC. Fields I describe in the code.

C++
typedef struct LOG_DPC
{
    // Stopping Flag
    BOOLEAN           Stop;
    // DPC Stopped Event
    KEVENT            EvStopped;
    // DPC
    KDPC              DPC;
    // Timer
    KTIMER            Timer;
}LOG_DPC;

And the variable of that structure type:

C++
// DPC Context
LOG_DPC * s_pDPC = NULL;

Initialization of that variable we add into the driver entry routine.

C++
// Setup DPC Context
s_pDPC = (LOG_DPC*)ExAllocatePool(NonPagedPool,sizeof(LOG_DPC));
if (s_pDPC) {
    s_pDPC->Stop = TRUE;
    KeInitializeEvent(&s_pDPC->EvStopped, NotificationEvent, TRUE);
    KeInitializeTimer(&s_pDPC->Timer);
    KeInitializeDpc(&s_pDPC->DPC,(PKDEFERRED_ROUTINE)(TimerRoutine),s_pDPC);
}

Here you can see that DPC initialized with the TimerRoutine. This callback will be called on timer elapsed. In that callback, we output log text and reschedule the timer if exit wasn’t requested from the driver:

C++
// DPC Timer Routine
void TimerRoutine(IN PKDPC Dpc,IN LOG_DPC *pThis,
                                IN PVOID SystemArg1,IN PVOID SystemArg2)
{
    UNREFERENCED_PARAMETER(Dpc);
    UNREFERENCED_PARAMETER(SystemArg1);
    UNREFERENCED_PARAMETER(SystemArg2);

    if (!pThis->Stop) {
        LogPrint("Hello From DPC!!! IRQL: %d", KeGetCurrentIrql());
        LARGE_INTEGER time;
        KeQuerySystemTime(&time);
        // Reschedule 1 second forward 
        time.QuadPart += 10000000;
        KeSetTimer(&pThis->Timer, time, &pThis->DPC);
    } else {
        // Signal that we are done
        KeSetEvent (&pThis->EvStopped, IO_NO_INCREMENT, FALSE);
    }
}

As mentioned, starting our timer we will make from our test application. To communicate with the driver from user mode, we will be using Device Input and Output Control (IOCTL) which is provided by DeviceIoControl function. First prepare the IOCTL message:

C++
// IOCTL For Enabling Or Disabling DPC Log Execution
// Argument BOOL Enable/Disable - on input change value on output request value
#define IOCTL_DRIVERLOG_ENABLE_DPC_TIMER        \
        CTL_CODE( FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS )

After design callback for handling IOCTL requests from our application:

C++
DriverObject->MajorFunction[ IRP_MJ_DEVICE_CONTROL ] = DriverDispatchDeviceControl;

In the dispatch handler, we add functionality for our control code.

C++
PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation( Irp );
ULONG ControlCode = Stack->Parameters.DeviceIoControl.IoControlCode;

switch (ControlCode) {
case IOCTL_DRIVERLOG_ENABLE_DPC_TIMER:
{
    DbgPrint("DriverLog: IOCTL_DRIVERLOG_ENABLE_DPC_TIMER \n");
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    if (Irp->AssociatedIrp.SystemBuffer) {
        // ...
    }
}
break;
}

We will be able to pass a boolean variable on input with a type of BOOL which will command the driver to start or stop the DPC timer depending on its value. On output, we provide the state of the DPC timer: is it running or not, also as a boolean variable with type of BOOL.

C++
LARGE_INTEGER time_out = {0};
ULONG32 bStopped = (STATUS_SUCCESS == KeWaitForSingleObject(&s_pDPC->EvStopped,
    Suspended,KernelMode,FALSE,&time_out));
if (Stack->Parameters.DeviceIoControl.InputBufferLength >= sizeof(ULONG32)) {
    PULONG32 p = (PULONG32)Irp->AssociatedIrp.SystemBuffer;
    if (bStopped) {
        if (*p) {
            // Start DPC
            LogPrint("Start DPC");
            s_pDPC->Stop = FALSE;
            KeResetEvent(&s_pDPC->EvStopped);
            LARGE_INTEGER time;
            KeQuerySystemTime(&time);
            time.QuadPart += 1000000;
            KeSetTimer(&s_pDPC->Timer, time, &s_pDPC->DPC);
        }
    } else {
        if (!*p) {
            // Stop DPC
            s_pDPC->Stop = TRUE;
            KeWaitForSingleObject(&s_pDPC->EvStopped, Suspended,KernelMode,FALSE,NULL);
            LogPrint("Stop DPC");
        }
    }
}

So we are checking if our timer is active or not by querying the state of the event. And if it stopped and the user requested to start, then we started the timer, otherwise if stop requested and the timer was active, then we would stop DPC. The output data handled in case of output buffer passed:

C++
// We request DPC Status
if (Stack->Parameters.DeviceIoControl.OutputBufferLength >= sizeof(ULONG32)) {
    PULONG32 p = (PULONG32)Irp->AssociatedIrp.SystemBuffer;
    *p = bStopped ? 1 : s_pDPC->Stop;
    Irp->IoStatus.Information = sizeof(ULONG32);
}

In application, we should add calling of starting and stopping DPC once open the driver handle:

C++
hDevice = CreateFile(FileName, GENERIC_WRITE, 0, 
    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

DWORD dwBytesReturned = 0;

// Starting DPC
BOOL bEnabledDpc = TRUE;
if (DeviceIoControl(hDevice, IOCTL_DRIVERLOG_ENABLE_DPC_TIMER,
    &bEnabledDpc, sizeof(bEnabledDpc), NULL, 0, &dwBytesReturned, NULL))
{
    _tprintf(_T("DeviceIOControl DPC Started\n"));
}
// Stop DPC
bEnabledDpc = FALSE;
if (DeviceIoControl(hDevice, IOCTL_DRIVERLOG_ENABLE_DPC_TIMER,
    &bEnabledDpc, sizeof(bEnabledDpc), NULL, 0, &dwBytesReturned, NULL))
{
    _tprintf(_T("DeviceIOControl DPC Stopped\n"));
}

We also add 5 seconds delay between those calls and see what will be in a log file:

Image 5

Passing Text from Application into Driver Log

In the system, the logger functionality is done as a service with some way of inter-process communication (IPC). So any applications can access the system log mechanism with the API designed for that. The system logger technologies, which I describe here below, have system background as windows service or a driver, or even both which communicate with each other. Actually, the driver is also a service which is running in kernel mode and we can use it to save the log records. For that purpose, we define another IOCTL message with the ability to pass the text message to the driver, and the driver writes it into the log file.

C++
// IOCTL Just for sending text to log
// Argument char * string
#define IOCTL_DRIVERLOG_SEND_TEXT_TO_LOG        \
        CTL_CODE( FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS )

In the driver, we implement handling that message in the prepared IRP_MJ_DEVICE_CONTROL dispatch routine.

C++
// Check If Any Input Data 
if (Irp->AssociatedIrp.SystemBuffer) {
    size_t cch = Stack->Parameters.DeviceIoControl.InputBufferLength;
    if (cch) {
        // Allocate Buffer To avoid BSOD (In Case If input string not zero-terminated)
        cch += 1;
        char * text = (char *)ExAllocatePool(NonPagedPool, cch);
        if (text) {
            memset(text, 0x00, cch);
            // Copy data 
            memcpy(text, Irp->AssociatedIrp.SystemBuffer, cch - 1);
            // Call Log
            LogPrint(text);
            ExFreePool(text);
            // If we reqest any data - just write back the processed length
            if (Stack->Parameters.DeviceIoControl.OutputBufferLength >= sizeof(ULONG32)) {
                PULONG32 p = Irp->AssociatedIrp.SystemBuffer;
                *p = (ULONG32)(cch - 1);
                Irp->IoStatus.Information = sizeof(ULONG32);
            }
        }
    }else {
        LogPrint("No text to log");
        Irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL;
    }
}else {
    LogPrint("NULL buffer as argument");
    Irp->IoStatus.Status = STATUS_FWP_NULL_POINTER;
}

So we simply get data from the input buffer and pass it to the log print function, on output we just pass the processed data length. Add code for passing text code looks:

C++
char text[] = "This log text is sent from an application";
ULONG nProcessedLength = 0;
// Send Text From Application Into Driver Log
if (DeviceIoControl(
    hDevice,                            // handle to a device, file, or directory 
    IOCTL_DRIVERLOG_SEND_TEXT_TO_LOG,   // control code of operation to perform
    text,                               // pointer to buffer to supply input data
    (DWORD)strlen(text),                // size, in bytes, of input buffer
    &nProcessedLength,                  // pointer to buffer to receive output data
    sizeof(nProcessedLength),           // size, in bytes, of output buffer
    &dwBytesReturned,                   // pointer to variable to receive byte count
    NULL                                // pointer to structure for asynchronous operation
) == 0) {
    _tprintf(_T("DeviceIOControl Failed %d\n"),GetLastError());
    result = 5;
}

After execution, we got in a log file:

Image 6

We can try to add multiple threads which will perform output text messages to a driver. We add a multithreaded code part of the ProcessThread from previous examples and modify it:

C++
// Simple thread which send messages to driver
DWORD WINAPI ProcessThread(PVOID p)
{
    TCHAR * FileName = (TCHAR *)p;
    HANDLE hDevice = CreateFile(FileName, 0, 0, NULL, OPEN_EXISTING, 
        FILE_ATTRIBUTE_NORMAL, NULL);
    if (hDevice != INVALID_HANDLE_VALUE)
    {
        srand(GetTickCount());
        int period = rand() * 300 / RAND_MAX;
        DWORD id = GetCurrentThreadId();
        char text[200] ;
        while (TRUE) {
            DWORD dwBytesReturned;
            // Just writing some text into log until quit signal
            sprintf_s(text,"Driver Hello from thread: %d", id);
            if (!DeviceIoControl(hDevice,IOCTL_DRIVERLOG_SEND_TEXT_TO_LOG,
                text,(DWORD)strlen(text),NULL,0,&dwBytesReturned,NULL)) {
                _tprintf(_T("DeviceIOControl Failed %d\n"), GetLastError());
                break;
            }
            // Sleep for random selected period
            if (WAIT_OBJECT_0 == WaitForSingleObject(g_hQuit, period)) break;
        }
        CloseHandle(hDevice);
    }
    return 0;
}

From code, you can see that we are opening the handle of the driver in each thread and passing messages until the thread quits. The resulted log file contains:

Image 7

If we comment in the driver test application code path where we uninstall the driver, then we can start many instances of our test application and they will use the same driver instance for writing log messages which are passed from those running applications.

Using WriteFile API

We can manage our driver to handle file writing operations. In the application, we just call the WriteFile API with the driver handle. The changes should be made on the driver side only. In the previous article, you saw that some drivers failed with the writing operation and we design to use IOCTL. At the start of the planning IO in the driver, we should decide which processing way of data buffers we are going to use. It is similar to IOCTL but the buffer access operation is set up with the driver flags during the initialization.

C++
DeviceObject->Flags |= DO_DIRECT_IO;

As we use buffered IO in our IOCTL implementation, so for the writing operation we are setting it to direct IO. Next, we should add the IRP_MJ_WRITE dispatch handler routine.

C++
NTSTATUS DriverDispatchWrite(IN PDEVICE_OBJECT pDO, IN PIRP Irp)
{
    PAGED_CODE();

    UNREFERENCED_PARAMETER (pDO);
    NTSTATUS Status = STATUS_SUCCESS;
    PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation( Irp );
    PVOID p = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
    if (p) {
        Irp->IoStatus.Status = STATUS_SUCCESS;
        size_t cch = Stack->Parameters.Write.Length;
        if (cch) {
            // Allocate Buffer To avoid BSOD (In Case If input string not zero-terminated)
            cch += 1;
            char * text = (char *)ExAllocatePool(NonPagedPool, cch);
            if (text) {
                memset(text, 0x00, cch);
                // Copy data 
                memcpy(text, p, cch - 1);
                // Call Log
                LogPrint(text);
                ExFreePool(text);
                Irp->IoStatus.Information = cch;
            }
            else {
                Irp->IoStatus.Status = STATUS_NO_MEMORY;
            }
        }
    }
    else {
        Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
    }
    IoCompleteRequest( Irp, IO_NO_INCREMENT );
    return Status;
}

The direct IO is mostly used for large data transfer which improves the driver performance, as in such cases the data is not copied into the intermediate buffer and we receive it with the MDL. To access the data pointer, we call MmGetSystemAddressForMdlSafe and process the returned pointer as usual. In our implementation, we copy input data into a separate buffer with zero-ending and call our log function, like we did with IOCTL implementation.

In the test application, if we are going to use the WriteFile API for the device then it is required to specify write access GENERIC_WRITE during the call of the CreateFile API.

C++
hDevice = CreateFile(FileName, GENERIC_WRITE, 0, 
    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

Test code for usage of the WriteFile API looks simpler than for IOCTL.

C++
char text[] = "This log text is sent from an application by the WriteFile API";
ULONG nProcessedLength = 0;
if (!WriteFile(hDevice,text,(DWORD)strlen(text),&nProcessedLength,NULL)) {
    _tprintf(_T("WriteFile Failed %d\n"),GetLastError());
    result = 6;
}
else
{
    _tprintf(_T("WriteFile Processed Text Length: %d\n"),nProcessedLength);
}

After execution, you can see the result in the output file.

Image 8

It is not limited to just passing the text string, you also can design for any other additional data fields and structures. It is also possible to add notifications even if a new log record arrives and allows applications to subscribe to it. Such things are also designed in the system. Those applications which just provide the log messages are called Providers. Our driver controls the logging: in our case, writing those messages into a file - so it is called a Controller. The applications which read log messages and/or subscribe to log messages notifications are called Consumers.

Code Samples

The application's output path is the root of drive D: it is hardcoded with filenames, but you can change that in source code. If you want to try out the driver for this part of an article. The sample driver can be compiled from the sources. It is configured to compile with the WDK toolset. During build, it creates the test certificate DriverLog.cer and signs the driver. To be able to use the driver, you need to install that certificate on your system and enable test mode, or disable driver signing checking on the system.

History

  • 18th August, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)