65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 上的跟踪和日志记录技术。第 4 部分 - 保存日志

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2023年8月18日

CPOL

18分钟阅读

viewsIcon

5408

downloadIcon

371

讨论如何组织保存应用程序跟踪辅助信息。

目录

引言

将应用程序中发生的辅助信息保存到一个文件中,以便日后帮助确定程序问题,这被称为日志记录。我想任何开发应用程序的人都知道日志记录。有很多库和组件可以集成到您的应用程序中使用。或者您可能会说,简单的方法就是将输出重定向到需要的地方,或者用文件写入替换调试 API 调用,仅此而已。但由于某些原因,这并不是保存数据的正确实现方式。

第一个原因是多线程。因为应用程序可能有很多线程,并且可以从任何一个线程调用日志记录数据。我们可以安排临界区或互斥锁来对日志进行单次访问,但访问文件写入的周期可能会变化,这并不好,所以我们需要考虑如何最小化日志写入操作的周期。在内核驱动程序中进行日志记录时出现的另一个问题是,在内核中我们有另一个维度:写入日志的调用可以在不同的中断请求级别(IRQL)上进行,但文件写入 API 只在低级别(被动级别)工作。因此,在更高级别调用此类 API 的结果将是带有 IRQL NOT LESS OR EQUAL蓝屏死机BSOD)。因此,在上述场景中,我建议使用一个线程来执行写入操作。

在应用程序中创建您自己的日志记录器

根据上述说明,我们可以创建一个简单的日志保存实现。我们只安排了简单的文本行输出,但您可以随之添加任何自己的元数据。

C++

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

我们的目标只是展示如何组织以实现快速和安全的处理。而且,如前所述,我们需要安排一个这些结构的列表,这个列表可以从任何线程访问。为此,我们定义了以下变量:

// 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;

以上定义是在 C++11 中。我们有一个类型为 `_log_record_t` 的日志记录结构列表。一个用于锁定该列表访问的递归互斥变量。一个用于通知列表已更新的条件变量。一个用于退出线程的原子变量作为退出信号。日志记录结构可以扩展以包含日志记录可能需要的任何其他字段。

日志记录器函数将只安排一个带有数据的日志记录,将其放入列表后,发出信号表明其中有内容要输出。

// 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);
    }
}

如您所见,只有将记录添加到列表和更新该列表通知的代码部分被临界区锁定。同时,在日志记录器线程中,我们等待通知出现,然后从列表中获取记录并将其写入日志文件。

// 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);
        }
    }
}

在线程中,我们仅对实际访问记录列表的操作使用临界区锁定,因此写入输出的延迟不会影响同时输出日志消息的所有其他线程。在代码中,我们从列表中提取所有可用的记录,如果没有要写入的内容,则检查退出信号或是否有新记录到达。在本例中,我们每次需要写入数据时都打开一个文件,但在实际实现中,我建议在日志线程开始时就打开文件,并使用共享读取访问权限。这样,您可以避免访问文件的问题,并能够实时查看日志更新时的变化。

为了测试我们的多线程访问实现,我们管理了多个处理线程,这些线程将以随机延迟发送日志消息。

// 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));
    }
}

在主函数中,我们启动一个日志线程。与此同时,我们还启动了 100 个处理线程来提供日志。然后等待 5 秒钟后关闭。

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

经典的 C 语言方式更通用。要组织一个 `_log_record_t` 结构列表,我们应该将指向下一个结构的指针作为字段之一。

// 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;

对于组织队列,因为我们需要将记录放入队尾并从队头取出,所以实际的列表定义如下。

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;

我们需要组织处理的变量。

// 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;

这里,我们有两个事件:一个用于退出,另一个用于队列信号。临界区用于锁定我们列表的访问。

将记录添加到处理队列的实现略有不同,因为我们组织了自己的列表。

// 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);
    }
}

我们向列表添加新记录,`g_LogData` 变量的 `tail` 字段始终指向最后一个记录,同时,`head` 字段指向第一个记录。一旦记录被添加,我们就触发通知事件。

// 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;
}

将记录出队并写入文件的日志线程会等待两个事件之一被触发:要么是新记录到达,要么是退出。我们从列表的 `head` 指针字段取出记录,并将其赋给提取记录的下一个字段。最后一个记录的下一个记录为 `NULL`,如果提取的指针等于列表的 `tail`,我们也会将其设置为 `NULL`。只有出队记录的操作被临界区锁定。通过我们自己的列表实现,我们在这里获得了很好的性能优势。

用于输出日志消息的处理线程

// 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;
}

主函数具有类似的功能:启动 100 个同时输出日志消息的处理线程,并在 5 秒后退出。

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#

在 C# 中,日志记录结构的声明与 C++ 没有太大区别。

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

至于变量:我们也有两个事件,一个用于退出,另一个用于通知,日志记录我们将存储在通用列表中,并且为了锁定列表访问,会有一个简单的对象。

// 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);

在 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();
        }
    }
}

我们在线程临界代码部分使用 C# 的 `lock` 关键字。在其他 .NET 语言中,这可以替换为 `Monitor.Enter()` 和 `Monitor.Exit()` 方法。日志输出线程的逻辑与 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;
        }
    }
}

用于输出日志消息的线程实现。

// 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;
    }
}

以及主函数。

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();
}

如您所见,本文中不同编程语言的实现看起来大体相似。代码执行后 `test.log` 文件的结果如下:

设计回调以释放记录的任何内存

我们可能会提供一些额外的内存、对象或 COM 对象,以提供一些用于日志记录的附加信息。而这些内存或对象应该被正确释放。当然,在 .NET 中,这可能会被自动处理,但 COM 对象的问题仍然存在。

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; 

对于回调类型,我们使用函数模板。扩展后的日志函数将如下所示。

// 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);
        }
    }
}

在上面的函数中,我们设计了记录可能由于退出信号而无法放入日志队列的情况。在这种情况下,我们应该手动释放资源并手动调用回调来释放额外的内存。

在日志输出线程中,我们只需在记录被写入后调用回调即可。

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

在回调函数中,可以通过作为参数传递的记录结构的 `ctx` 字段来访问用户上下文数据。

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;

扩展的日志记录器函数也接受回调和上下文参数,就像我们在之前的实现中所做的那样。

C#

在 .NET 中,我们可以使用任何现有的函数委托作为回调。至少我们需要一个可以转换成我们的记录结构的对象参数。例如,我们只选择一个现有的 `EventHandler` 委托。在这种情况下,我们的记录结构将如下所示。

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

在日志函数中,我们传递该委托和上下文对象。

// 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);
        }
    }
}

回调将在日志记录器线程中被调用。

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

在回调函数中,我们应该将一个对象强制转换为我们的记录结构类型。

等待记录被写入

在某些情况下,我们可能需要确切地知道在应用程序中发生某事之前写入了什么。为此,我们可以使用先前设计的回调。我们将其设计为该回调的上下文参数是通知句柄。一旦回调被执行,该句柄就会被设置为信号状态。在输出日志消息的线程中,我们只需要等待这个通知变为信号状态即可。

C++

在 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; 

这是通过联合体(union)实现的,因为我们可以使用上下文或通知。在回调函数中,我们发出条件变量的信号。

// 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();
    }
}

我们通过宏来设计日志输出的调用,在这个宏中,我们只调用之前创建的函数并传递我们的回调实现。由于记录中的通知是与上下文一起在联合体中设计的,因此它将被放置在相同的地址空间中。

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

测试代码如下所示。

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

在 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;

回调的实现。

// 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);
    }
}

用于使用的宏定义与 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#

在 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();
    }
}

在 C# 中,我们不使用宏实现,而是创建了一个具有不同参数的函数。

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

测试代码。

// 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();

你可以在下一个屏幕截图中看到结果。如果你在回调函数上设置断点,你会看到主线程等待通知,并且回调被执行。在通知发出信号后,主函数继续执行。

内核模式下该如何处理?

好的,我们已经找出了如何为特定应用程序组织日志写入,但现在你可能会问:“*有没有在驱动程序中写入日志的方法*?”答案是有的。在驱动程序中的实现逻辑与我们之前讨论的相同,但很多东西看起来有些不同——因为这是内核。正如我已经提到的,在内核中,我们有另一个维度:**IRQL**。用户模式下的所有应用程序都在一个级别上执行,即“0”,称为被动(passive)级别。但驱动程序代码可以在更高的级别上执行,更重要的是,驱动程序的代码和分配的数据可以位于分页内存池或非分页内存池中。非分页的代码和数据总是常驻内存,而分页代码可能由于内存压力而被存储在磁盘上,以执行此类代码或访问内存。为此,系统中添加了页面文件。你可以在系统驱动器的根目录下找到名为 *pagefile.sys* 的文件。请确保你启用了显示隐藏文件的选项。系统中的文件访问是在被动级别完成的,这在更高的 **IRQL** 上是不可能的,结果……没错——**蓝屏死机**(**BSOD**)。更高的 **IRQL** 级别具有更高的执行权限,而处于“被动”级别的代码会停止,直到更高级别的代码完成。这是一个简单的描述,用于理解需要处理什么,因为在高于“被动”的级别上,代码也在执行,它也可能需要提供一些日志消息。

基于以上说明,让我们看看如何进行实现。首先,像前面的描述一样,我们定义变量、结构和变量。我们的日志记录类似于之前的,只包含文本信息,“`Entry`”字段是组织列表所必需的。

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

我们将前面示例中声明的变量放入一个结构体中。由于前面提到的原因,这个结构体需要分配在非分页内存池中。

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;

以及其他变量

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

第一个是指向日志的已分配指针,以及用于访问该日志指针的临界区。需要说明的是:被自旋锁临界区锁定的代码会将执行级别提升到 DISPATCH IRQL。

日志打印函数的实现看起来有些不同,但做的事情和用户模式应用程序中一样。我们需要记住,这个函数会从任何 **IRQL** 级别被调用。

// 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);
    }
}

在日志线程中,我们的功能也与用户模式应用程序中的相同,实际的日志记录写入被拆分到单独的函数中。

// 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);
}

`WritePendingRecords` 函数执行将列表中所有记录写入文件的操作。从列表中获取记录的代码如下:

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);
}

文件写入操作如下:

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);
}

我们将把日志记录存储在 D: 盘根目录下名为 `DriverLog.log` 的文件中。如您所见,在内核模式下,一个简单的操作相比用户模式应用程序需要更多的代码行。现在我们需要定义启动和停止我们日志记录的函数。启动日志记录,我们在驱动程序入口处执行;停止则在驱动程序卸载消息时执行。

// 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;
}

我不会描述任何其他部分,因为那不是目标,只想说我把一些日志写入的调用放在了驱动程序例程中,以观察其效果。

现在我们需要一个测试应用程序,它将执行驱动程序的安装、加载、然后卸载。安装和卸载操作的执行方式与常规 Windows 服务应用程序类似,使用服务控制管理器 API。而加载驱动程序则是通过一个特定的 `CreateFile` API 来完成的。执行该应用程序后,我们可以看到以下日志消息。

哇,我们的日志工作了!!!驱动程序代码还包含一些用于跟踪的 `DbgPrint` API 调用,这些在之前的主题中提到过。例如,在代码中,你可以找到

DbgPrint("DriverLog: DriverEntry\n");

因此,您将能够在 DbgView 中,或在先前文章的示例应用程序中看到来自驱动程序的调试输出字符串。这些跟踪消息以 `DriverLog:` 前缀开头。

更高的 IRQL

在上面的日志文件中,消息仅来自在被动模式 **IRQL** 下运行的基本驱动程序功能,而我曾承诺它将在更高级别下工作。因此,让我们在驱动程序中安排一个**DPC** 定时器,并从其回调中调用日志记录,它将在调度级别执行。我们将添加从用户模式应用程序启动和停止该定时器的功能。首先,我们为我们的 **DPC** 定义结构。字段我在代码中描述。

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

以及该结构类型的变量

// DPC Context
LOG_DPC * s_pDPC = NULL;

我们将该变量的初始化添加到驱动程序入口例程中。

// 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);
}

这里您可以看到 **DPC** 是用 `TimerRoutine` 初始化的。这个回调函数将在定时器到期时被调用。在该回调中,我们输出日志文本,并且如果驱动程序未请求退出,则重新安排定时器。

// 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);
    }
}

如前所述,我们将从我们的测试应用程序中启动定时器。为了从用户模式与驱动程序通信,我们将使用由 `DeviceIoControl` 函数提供的设备输入和输出控制(**IOCTL**)。首先准备 **IOCTL** 消息。

// 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 )

设计好处理来自我们应用程序的 **IOCTL** 请求的回调函数之后

DriverObject->MajorFunction[ IRP_MJ_DEVICE_CONTROL ] = DriverDispatchDeviceControl;

在分发处理程序中,我们为我们的控制代码添加功能。

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;
}

我们将能够传入一个类型为 `BOOL` 的布尔变量,该变量将根据其值命令驱动程序启动或停止 **DPC** 定时器。在输出时,我们提供 **DPC** 定时器的状态:是否正在运行,同样也是一个类型为 `BOOL` 的布尔变量。

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");
        }
    }
}

因此,我们通过查询事件的状态来检查我们的定时器是否处于活动状态。如果它已停止并且用户请求启动,那么我们就启动定时器;否则,如果请求停止并且定时器处于活动状态,那么我们就停止 **DPC**。在传递了输出缓冲区的情况下,会处理输出数据。

// 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);
}

在应用程序中,我们应该在打开驱动程序句柄后,添加启动和停止 **DPC** 的调用。

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"));
}

我们还在这些调用之间添加了 5 秒的延迟,看看日志文件中会发生什么。

将文本从应用程序传递到驱动程序日志中

在系统中,日志记录功能是作为一项服务通过某种进程间通信(**IPC**)方式实现的。因此,任何应用程序都可以通过为此设计的 API 访问系统日志机制。我下面描述的系统日志记录技术,具有作为 Windows 服务或驱动程序,甚至两者相互通信的系统背景。实际上,驱动程序也是一种在内核模式下运行的服务,我们可以用它来保存日志记录。为此,我们定义了另一个 **IOCTL** 消息,该消息能够将文本消息传递给驱动程序,然后由驱动程序将其写入日志文件。

// 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 )

在驱动程序中,我们在准备好的 `IRP_MJ_DEVICE_CONTROL` 分发例程中实现对该消息的处理。

// 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;
}

所以我们只是从输入缓冲区获取数据并将其传递给日志打印函数,在输出时我们只传递处理过的数据长度。添加用于传递文本的代码如下:

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;
}

执行后,我们在日志文件中得到:

我们可以尝试添加多个线程来向驱动程序输出文本消息。我们添加了先前示例中 `ProcessThread` 的多线程代码部分并对其进行修改。

// 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;
}

从代码中可以看出,我们在每个线程中打开驱动程序的句柄,并持续传递消息直到线程退出。最终的日志文件包含以下内容:

如果我们注释掉驱动程序测试应用程序中卸载驱动程序的代码路径,那么我们就可以启动我们测试应用程序的多个实例,它们将使用同一个驱动程序实例来写入从那些正在运行的应用程序传递过来的日志消息。

使用 WriteFile API

我们可以管理我们的驱动程序来处理文件写入操作。在应用程序中,我们只需使用驱动程序句柄调用 `WriteFile` API。更改只需在驱动程序端进行。在上一篇文章中,您看到一些驱动程序在写入操作时失败了,我们设计使用 **IOCTL**。在驱动程序中规划 IO 的开始阶段,我们应该决定我们将使用哪种数据缓冲区的处理方式。这与 **IOCTL** 类似,但缓冲区访问操作是在初始化期间通过驱动程序标志设置的。

DeviceObject->Flags |= DO_DIRECT_IO;

由于我们在 **IOCTL** 实现中使用了缓冲 IO,因此对于写入操作,我们将其设置为直接 IO。接下来,我们应该添加 `IRP_MJ_WRITE` 分发处理程序例程。

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;
}

直接 IO 主要用于大数据传输,这可以提高驱动程序的性能,因为在这种情况下,数据不会被复制到中间缓冲区,我们通过 `MDL` 接收它。要访问数据指针,我们调用 `MmGetSystemAddressForMdlSafe` 并像往常一样处理返回的指针。在我们的实现中,我们将输入数据复制到一个以零结尾的单独缓冲区中,并调用我们的日志函数,就像我们对 **IOCTL** 实现所做的那样。

在测试应用程序中,如果我们要对设备使用 `WriteFile` API,则需要在调用 `CreateFile` API 时指定写访问权限 `GENERIC_WRITE`。

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

使用 `WriteFile` API 的测试代码看起来比使用 **IOCTL** 的更简单。

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);
}

执行后,您可以在输出文件中看到结果。

这不仅限于传递文本字符串,您还可以设计任何其他附加的数据字段和结构。甚至可以添加新日志记录到达时的通知,并允许应用程序订阅它。这些功能在系统中也有设计。那些只提供日志消息的应用程序被称为“`提供者(Providers)`”。我们的驱动程序控制日志记录:在我们的例子中,是将这些消息写入文件——所以它被称为“`控制器(Controller)`”。读取日志消息和/或订阅日志消息通知的应用程序被称为“`消费者(Consumers)`”。

代码示例

应用程序的输出路径是 D: 盘的根目录,这是硬编码在文件名中的,但你可以在源代码中更改它。如果你想尝试本文这部分的驱动程序。示例驱动程序可以从源代码编译。它被配置为使用 WDK 工具集进行编译。在构建过程中,它会创建测试证书 `DriverLog.cer` 并对驱动程序进行签名。为了能够使用该驱动程序,你需要在你的系统上安装该证书并启用测试模式,或者在系统上禁用驱动程序签名检查。

历史

  • 2023年8月18日:初始版本
© . All rights reserved.