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

Windows 上的跟踪和日志记录技术。第 1 部分 - 提供跟踪信息的简单方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2023 年 5 月 18 日

CPOL

17分钟阅读

viewsIcon

9985

downloadIcon

313

本系列文章涵盖了可以嵌入到您的应用程序中的日志记录和跟踪机制的绝大多数方面。文章讨论了简单的跟踪方法以及 Windows 10 中引入的新跟踪技术。

目录

引言

基本的跟踪方法有助于在不涉及特定内容或深入研究任何特殊技术的情况下检查应用程序的功能。

在本主题中,我们将回顾最流行且最简单的提供跟踪信息实现的。我们还将讨论如何扩展这些跟踪方法以及接收这些方法的跟踪通知。我们将找出调试器和 DbgView 工具的工作原理。

控制台输出

在开发应用程序时,我们应该考虑并设计一种方法来提供有关执行进度的测试信息。如果应用程序在没有任何通知或有关出错信息的说明的情况下关闭,那是不好的。如果您正在开发控制台应用程序,提供跟踪信息的简单方法就是将信息输出到 stderr。这是我们从学习编程开始就知道的基础。

#include <stdio.h>
#include <conio.h>
int main(int argc, char* argv[])
{
    // Output into stderr device
    fprintf(stderr, "Something went wrong\n");
    _getch();
    return 0;
}

对于那些偏爱使用 std 流进行输出的人,可以使用 std::cerr

#include <iostream>
#include <conio.h>
int main(int argc, char* argv[])
{
    using namespace std;
    // Output into stderr device
    cerr << "Something went wrong"  << endl;
    _getch();
    return 0;
}

.NET 提供了 Console.Error 对象用于此类目的。这是一个特殊的 IO.TextWriter 类,代表 stderr

static void Main(string[] args)
{
    Console.Error.WriteLine("Something went wrong");
    Console.ReadKey();
}

执行结果只是在控制台窗口中显示文本。

好的,现在我们已经理解了根据我们输出的消息可能出了什么问题。但是,如果我们的应用程序不是控制台应用程序怎么办?在这种情况下,我们也可以使用控制台输出,并在需要显示某些信息时创建一个控制台窗口。要创建控制台窗口,请使用 AllocConsole API。分配控制台后,我们应该使用 freopen API 重新打开 stderr 流。

#include <stdio.h>
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,int nCmdShow)
{
    FILE *out_stream = stderr;
    // Call to create console window
    if (AllocConsole())    {
        // Reopen output stderr stream 
        freopen_s(&out_stream,"CONOUT$", "w", stderr);
    }
    // Providing information message 
    fprintf(out_stream, "Something went wrong\n");
    // Just to stop execution
    MessageBoxA(NULL,"Application Quit","",MB_OK);
    return 0;
}

std::cerr 应该被清除以重置输出缓冲区。这通过调用 cerr.clear() 方法来完成。

您可能会说“这种分配控制台进行跟踪的技术 nowhere 使用”而您将是错误的,因为著名的 VLC 播放器就是这样做的。

如果您按照图片中的菜单操作,控制台窗口会弹出,您可以看到应用程序提供的跟踪信息。例如,我的 VLC 版本不支持视频编解码器,它会在弹出对话框中通知这一点,但您可以在控制台窗口中找到详细信息。

在我们的应用程序中,我们可以设计控制台窗口从菜单或作为启动参数打开,或者仅仅在构建调试配置时始终显示。
在 .NET Windows 应用程序中调用 AllocConsole API 也会打开控制台窗口,但如果应用程序在 Visual Studio 下运行,控制台窗口不会与输出关联,将文本写入控制台会导致在输出窗口中显示消息。

常规 Windows 应用程序不是在 Visual Studio 下执行的。一旦分配了控制台,这些方法就会正确输出到控制台窗口。在 .NET 应用程序中,不需要像我们在 C++ 实现中所做的那样调用重置输出流。

[DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall,
SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AllocConsole();

static void Main(string[] args)
{
    AllocConsole();
    Console.WriteLine("Something went wrong");
    MessageBox.Show("Application Quit");
}

在 Visual Studio 下未执行的程序的执行结果

控制台输入和输出流可以被重定向,这为我们将常规输出保存到文件提供了一种方式。为此不需要分配控制台窗口。在这种情况下,我们将调用我们已经提到的同一个 API:freopenstd::cerr 的输出缓冲区也应该通过调用 cerr.clear() 方法来清除。我们可以稍后使用文件中保存的信息来确定出了什么问题。

#include <stdio.h>
#include <windows.h>
#include <iostream>

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,int nCmdShow)
{
    using namespace std;
    // Redirect stderr into a file
    FILE *temp;
    freopen_s(&temp,"d:\\temp.log", "w", stderr);
    // Clear cerr buffers
    cerr.clear();
    // Providing information message 
    fprintf(stderr, "Something went wrong\n");
    // Using IO stream
    cerr << "Another message"  << endl;
    // Signal that we quit
    MessageBoxA(NULL,"Application Quit","",MB_OK);
    return 0;
}

结果是,我们根本看不到控制台窗口,但应用程序执行后创建了一个新文件 d:\temp.log

.NET 还允许您将控制台输出重定向到文件。接下来的代码显示了创建 StreamWriter 对象并将其作为控制台错误流的输出传递。执行结果是,我们也有 d:\temp.log 文件。

using System;
using System.IO;
using System.Text;
using System.Windows.Forms;

class Program
{
    static void Main(string[] args)
    {
        var sw = new StreamWriter(@"d:\temp.log", true, Encoding.ASCII);
        Console.SetError(sw);
        Console.Error.WriteLine("Something went wrong");
        MessageBox.Show("Application Quit");
        sw.Dispose();
    }
}

还可以获取控制台输出流的文件句柄并将其与文件 API 一起使用。如果应用程序没有控制台,那么这样的句柄是空的。

#include <stdio.h>
#include <windows.h>

// Print formatted string with arguments into stderr
void TracePrint(const char * format, ...)
{
    // Get stderr Handle
    HANDLE hFile = GetStdHandle(STD_ERROR_HANDLE);
    if (hFile && hFile != INVALID_HANDLE_VALUE) {
        va_list    args;
        va_start(args, format);
        // Allocate string buffer
        int _length = _vscprintf(format, args) + 1;
        char * _string = (char *)malloc(_length);
        if (_string) {
            memset(_string, 0, _length);
            // Format string
            _vsprintf_p(_string, _length, format, args);
            __try {
                DWORD dw = 0;
                // Write resulted string
                WriteFile(hFile,_string,_length,&dw,NULL);
            } __finally {
                free(_string);
            }
        }
        va_end(args);
    }
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPWSTR lpCmdLine, int nCmdShow)
{
    // Allocate console
    AllocConsole();
    // Write Message Text
    TracePrint("Something went wrong\n");
    // Just Notify that we are done
    MessageBoxA(NULL,"Application Quit","",MB_OK);
}

上面的示例分配控制台窗口并将格式化的文本消息写入其中。一旦我们有了控制台,这就能够正常工作。但是,如果我们将 AllocConsole API 调用行注释掉,那么在调试器下,我们可以看到输出句柄是无效的。因此,在应用程序中,可以根据设置、构建或命令行参数来设计输出。

在 .NET 中,也可以完全重定向控制台输出。要处理控制台输出重定向,需要创建自己的类并继承自 TextWriter。在该类中,需要重写两个 Write 方法和 Encoding 属性。此类实现的示例如下。

class MyConsoleWriter : TextWriter
{
    public override Encoding Encoding { get { return Encoding.Default; } }

    public MyConsoleWriter() { }

    public override void Write(string value)
    {
        IntPtr file = fopen("d:\\mylog.txt", "ab+");
        var bytes = Encoding.Default.GetBytes(value);
        fwrite(bytes, 1, bytes.Length, file);
        fclose(file);
    }

    public override void WriteLine(string value)
    {
        Write((string.IsNullOrEmpty(value) ? "" : value) + "\n");
    }
}

之后,需要通过调用 Console.SetOut 方法将该类的实例设置为控制台的输出。

static void Main(string[] args)
{
    // Redirect Console Output
    Console.SetOut(new MyConsoleWriter());
    Console.WriteLine("Simple Console Output Message");
    MessageBox.Show("Application Quit");
}

执行上述程序的结果是 d:\ 驱动器上有一个文件 mylog.txt。您可以在以下截图中看到文件的内容。

重定向控制台输出

获取应用程序工作流信息的另一个有趣的方法是附加子进程的父控制台,这样应用程序就可以向其父进程提供信息。因此,常规应用程序是 Windows 应用程序,没有控制台窗口,但它以常见方式使用写入控制台输出的函数。而在跟踪模式下,应用程序会以分配控制台窗口的方式启动自身,因此子进程会被附加到它。让我们修改之前的代码示例来实现这一点。

int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
    LPWSTR lpCmdLine,int nCmdShow)
{
    INT nArgs;
    // Get Command line arguments
    LPWSTR * pszArgv = CommandLineToArgvW(lpCmdLine, &nArgs);
    // Check if we starting child Process
    if (pszArgv && nArgs >= 1 && _wcsicmp(pszArgv[0], L"child") == 0) {
        // Attach to parent process console
        AttachConsole(ATTACH_PARENT_PROCESS);
        int idx = 0;
        // Simple loop for message printing
        while (idx++ < 100){
            TracePrint("Child Message #%d\n",idx);
            Sleep(100);
        }
    } else {
        // Get Path of executable
        WCHAR szPath[1024] = {0};
        GetModuleFileNameW(NULL,szPath + 1,_countof(szPath) - 1);
        szPath[0] = '\"';
        // Append Argument
        wcscat_s(szPath,L"\" child");
        // Allocate Console
        AllocConsole();

        PROCESS_INFORMATION pi = {0};
        STARTUPINFO si = {0};
        si.cb = sizeof(STARTUPINFO);
        
        // Starting child process
        if (CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
            // Wait Child Process To Quit
            WaitForSingleObject(pi.hThread, INFINITE);
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
            MessageBoxA(NULL, "Application Quit", "", MB_OK);
        }
    }
    // Free Arguments List
    if (pszArgv) LocalFree(pszArgv);
    return 0;
} 

所以,我们有一个 Windows 应用程序,我们分配了一个控制台并启动了一个子进程,该子进程也是一个 Windows 应用程序。在该进程中,我们附加到父控制台并在循环中提供一些消息。子进程完成后,父进程会显示一个消息框。

在 .NET 中也可以做到这一点。我们只需要有一个 AttachConsoleAllocConsole API 的包装器。

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Diagnostics;
using System.Windows.Forms;

class Program
{
    [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall,
            SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool AllocConsole();

    [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall,
            SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool AttachConsole([In,MarshalAs(UnmanagedType.U4)] int dwProcessId);

    static void Main(string[] args)
    {
        // Check if we starting child Process
        if (args.Length >= 1 && args[0].ToLower() == "child")
        {
            // Attach to parent process console
            AttachConsole(-1);
            // Do the stuff
            int idx = 0;
            // Simple loop for message printing
            while (idx++ < 100)
            {
                // Console Output Messages
                Console.WriteLine(string.Format("Child Message #{0}", idx));
                Thread.Sleep(100);
            }
        }
        else
        {
            // Allocate Console
            AllocConsole();
            // Starting child process
            var process = Process.Start(Application.ExecutablePath, "child");
            if (process != null)
            {
                process.WaitForExit();
                process.Dispose();
                MessageBox.Show("Application Quit");
            }
        }
    }
}

执行上述代码的结果与 C++ 版本相同。
如果您根本不想有控制台窗口,那么可以创建一个管道并替换子进程的标准错误输出句柄,这样父进程也可以接收信息。让我们修改之前的代码示例来看实现。

int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
                              LPWSTR lpCmdLine,int nCmdShow)
{
    INT nArgs;
    LPWSTR * pszArgv = CommandLineToArgvW(lpCmdLine, &nArgs);
    // Check if we starting child Process
    if (pszArgv && nArgs >= 1 && _wcsicmp(pszArgv[0], L"child") == 0) {
        int idx = 0;
        // Simple loop for message printing
        while (idx++ < 100) {
            TracePrint("Child Message #%d\n",idx);
            Sleep(100);
        }
    } else {
        // Get Path of executable
        WCHAR szPath[1024] = {0};
        GetModuleFileNameW(NULL,szPath + 1,_countof(szPath) - 1);
        szPath[0] = '\"';
        // Append Argument
        wcscat_s(szPath,L"\" child");
        // Allocate Console
        AllocConsole();

        SECURITY_ATTRIBUTES sa = {0};
        // Setup Attributes To Inherit Handles
        sa.nLength = sizeof(SECURITY_ATTRIBUTES); 
        sa.bInheritHandle = TRUE; 
        sa.lpSecurityDescriptor = NULL; 

        HANDLE hChildInReadPipe = NULL;
        HANDLE hChildInWritePipe = NULL;
        HANDLE hChildOutReadPipe = NULL;
        HANDLE hChildOutWritePipe = NULL;
        // Create Pipes
        if (CreatePipe(&hChildOutReadPipe, &hChildOutWritePipe, &sa, 0)) {
            SetHandleInformation(hChildOutReadPipe, HANDLE_FLAG_INHERIT, 0);
            if (CreatePipe(&hChildInReadPipe, &hChildInWritePipe, &sa, 0)) {
                SetHandleInformation(hChildInWritePipe, HANDLE_FLAG_INHERIT, 0);

                PROCESS_INFORMATION pi = {0};
                STARTUPINFO si = {0};
                si.cb = sizeof(STARTUPINFO);
                // Specify Pipe Handles for stdin and stdout
                si.hStdError = hChildOutWritePipe;
                si.hStdOutput = hChildOutWritePipe;
                si.hStdInput = hChildInReadPipe;
                si.dwFlags |= STARTF_USESTDHANDLES;
                // Starting child process
                if (CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, 
                                  NULL, NULL, &si, &pi)) {
                    // Get Current stderr handle
                    HANDLE hCurrentHandle = GetStdHandle(STD_ERROR_HANDLE);
                    while (true) {
                        DWORD dwRead = 0;
                        BYTE buf[1024] = { 0 };
                        // Check for any information available on a pipe
                        if (PeekNamedPipe(hChildOutReadPipe,buf,sizeof(buf),
                            &dwRead,NULL,NULL) && dwRead) {
                            // Pull data From pipe
                            if (!ReadFile(hChildOutReadPipe, buf, 
                                sizeof(buf), &dwRead, NULL) || dwRead == 0) {
                                break;
                            }
                        }
                        // If Something readed then output it into stderr
                        if (dwRead) {
                            WriteFile(hCurrentHandle, buf, dwRead, &dwRead, NULL);
                        }
                        // Check if child process quit
                        if (WAIT_TIMEOUT != WaitForSingleObject(pi.hThread, 10)) {
                            break;
                        }
                    }
                    CloseHandle(pi.hProcess);
                    CloseHandle(pi.hThread);
                    MessageBoxA(NULL, "Application Quit", "", MB_OK);
                }
                CloseHandle(hChildInReadPipe);
                CloseHandle(hChildInWritePipe);
            }
            CloseHandle(hChildOutReadPipe);
            CloseHandle(hChildOutWritePipe);
        }
    }
    // Free Arguments List
    if (pszArgv) LocalFree(pszArgv);
    return 0;
}

在代码中,我们创建了两个管道对。一对用作 stdin,另一对用作 stdout。我们将管道对中的一个传递给子进程,作为 stdout 输出,然后我们在父进程中使用另一对进行读取。在示例中,我们定期检查是否有数据可用。如果存在新数据,则进行读取并输出到控制台。此代码的执行结果与上一个示例相同。

与 C++ 代码相比,.NET 中相同场景的实现要简单得多。

// Allocate Console
AllocConsole();
// Starting child process
var info = new ProcessStartInfo(Application.ExecutablePath, "child");
info.UseShellExecute = false;
// Redirect stdout on child process
info.RedirectStandardOutput = true;
var process = Process.Start(info);
if (process != null)
{
    // Setup data receiving callback
    process.OutputDataReceived += Child_OutputDataReceived;
    process.BeginOutputReadLine();
    // Wait for child process to quit
    process.WaitForExit();
    process.Dispose();
    MessageBox.Show("Application Quit");
}

在代码中,我们使用 ProcessStartInfo 结构来启动子进程,并将 RedirectStandardOutput 属性的值设置为 true。创建进程对象后,需要设置 OutputDataReceived 事件处理程序并调用 BeginOutputReadLine 方法来开始接收数据。

static void Child_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
    Console.WriteLine(e.Data);
}

在回调中,我们只需将接收到的内容写入先前分配的控制台窗口。最后,我们得到了与前面示例相同的输出,但如果此代码在 Visual Studio 下启动,则会在输出窗口中显示消息。

输出调试信息

在某些情况下,不可能有控制台视图,或者不可能启动进程作为子进程 - 例如,如果我们有 Windows 服务甚至内核驱动程序。在这些情况下,有一种简单的方法可以提供应用程序工作流的实时数据。这就是使用 Debug API。此方法适用于控制台和窗口应用程序,也适用于 Windows 服务。在应用程序中,通过使用 OutputDebugString API 来实现。

#include <stdio.h>
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow)
{
    // Output Debug Text 
    OutputDebugStringW(L"This is application workflow message\n");
    // Just Display Quit Message
    MessageBoxA(NULL,"Application Quit","",MB_OK);
} 

通过此 API 输出的文本会显示在附加的调试器的输出窗口中。

OutputDebugString API 可以轻松地从 .NET 调用。

using System.Runtime.InteropServices;
using System.Windows.Forms;

class Program
{
    [DllImport("kernel32.dll", EntryPoint = "OutputDebugStringW", 
                CharSet = CharSet.Unicode)]
    static extern void OutputDebugString(string _text);

    static void Main(string[] args)
    {
        OutputDebugString("This is application workflow message\n");
        MessageBox.Show("Application Quit");
    }
} 

.NET 还具有将跟踪信息输出到附加调试器的能力。这包含在 System.Diagnostics 命名空间中。有两个类允许为调试器提供输出文本:Trace 类 - 在调试版本中输出文本或定义 TRACE 时输出;Debug 类 - 仅在调试版本中或定义了 DEBUG 时输出数据。

static void Main(string[] args)
{
    System.Diagnostics.Trace.WriteLine(
            "This is a trace message");
    System.Diagnostics.Debug.WriteLine(
        "This message displayed under debug configuration only");
    MessageBox.Show("Application Quit");
} 

您可以在输出中看到调试版本下的执行结果。

您可能会问:如果平台已经有了专为此目的设计的类,那么直接使用 OutputDebugString API 有什么好处?让我们用下面的代码来比较这两种方式的执行输出性能。

[DllImport("kernel32.dll", 
            EntryPoint = "OutputDebugStringW", CharSet = CharSet.Unicode)]
static extern void OutputDebugString(string _text);

static void Main(string[] args)
{
    int iterations = 100;
    long elapsed_trace = 0;
    long elapsed_output = 0;
    {
        int idx = 0;
        long start = System.Diagnostics.Stopwatch.GetTimestamp();
        while (idx < iterations)
        {
            System.Diagnostics.Trace.WriteLine(
                string.Format("This is a trace message {0}",++idx));
        }
        elapsed_trace = System.Diagnostics.Stopwatch.GetTimestamp();
        elapsed_trace -= start;
    }
    {
        int idx = 0;
        long start = System.Diagnostics.Stopwatch.GetTimestamp();
        while (idx < iterations)
        {
            OutputDebugString(
                string.Format("This is a output debug message {0}\n",++idx));
        }
        elapsed_output = System.Diagnostics.Stopwatch.GetTimestamp();
        elapsed_output -= start;
    }
    Console.WriteLine("Time Measurments in Seconds Trace: {0}  Output: {1}", 
        elapsed_trace * 1.0 / System.Diagnostics.Stopwatch.Frequency, 
        elapsed_output * 1.0 / System.Diagnostics.Stopwatch.Frequency);

    Console.ReadKey();
} 

好吧,从上面的代码中,您可以看到我们测量了 100 次迭代的跟踪打印和调试输出 API 使用情况。我们保存开始时间并在循环结束后计算经过的时间。执行结果如下:

正如您所看到的,.NET 跟踪调用非常慢。如果您的应用程序执行量很大,您可能需要去喝杯咖啡,因为它还在处理。当然,在非性能关键部分使用 trace 类输出一些消息是可以的,或者如果您喜欢咖啡。
另一个好问题是:我们只能在调试器下的输出窗口中看到我们的文本消息,那么我们如何确定用户 PC 上发生了什么?我们需要安装 Visual Studio 或任何其他调试器来捕获输出吗?答案是 - 不。一个可以用于此的有用工具是 sysinternalsDbgView 工具。

DbgView 工具可以正确打印上面 .NET 应用程序性能比较示例的输出,只需记住直接运行程序可执行文件,而不是在 Visual Studio 下运行。

正如您所看到的,.NET Trace 类比在 Visual Studio 下运行,在将输出从应用程序传输到 DbgView 工具方面效果更好。

.NET Trace 类将信息输出到指定的侦听器对象,其中之一是调试器输出。访问侦听器集合会导致这种延迟。但是我们可以添加自己的侦听器实例,它可以执行任何额外的功能,例如文件保存。我们自己的基本侦听器实现如下。

class MyListener : TraceListener
{
    public override void Write(string message)
    {
        OutputDebugString(message);
    }
    public override void WriteLine(string message)
    {
        Write((string.IsNullOrEmpty(message) ? "" : message) + "\n");
    }
} 

在初始侦听器集合中,我们删除所有现有对象并添加我们自己实现的新实例。

System.Diagnostics.Trace.Listeners.Clear();
System.Diagnostics.Trace.Listeners.Add(new MyListener());
System.Diagnostics.Trace.WriteLine("This is a trace message");

因此,上面的示例中的文本输出将传递给我们的对象。如果您像下面的截图中那样在 Write 方法中设置一个断点,就可以看到这一点。

.NET 有一个特殊的跟踪侦听器类 ConsoleTraceListener,它将数据传递到 stderrstdout

另一个 .NET 中用于将跟踪信息输出到调试器的类是 static 类:System.Diagnostics.Debugger

Debugger.Log(0, "Info",string.Format("This is a Debugger.Log message {0}\n", ++idx)); 

在时间测量执行示例中,我们可以添加 Debugger 类调用,看看是否能获得更好的性能。

调试输出是线程安全的。我们可以通过在应用程序中启动多个线程来检查这一点。所有这些线程都将使用 OutputDebugString API 输出文本。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow)
{
    g_evQuit = CreateEvent(NULL,TRUE,FALSE,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);
    CloseHandle(g_evQuit);
    MessageBoxA(NULL,"Application Quit","",MB_OK);
} 

以及 ProcessThread 函数实现。

DWORD WINAPI ProcessThread(PVOID p)
{
    UNREFERENCED_PARAMETER(p);
    srand(GetTickCount());
    int period = rand() * 300 / RAND_MAX;
    DWORD id = GetCurrentThreadId();
    WCHAR szTemp[200] = {0};
    while (TRUE) {
        // Just writing some text into debugger until quit signal
        swprintf_s(szTemp,L"Hello from thread: %d\n",id);
        // Call Output Debug String
        OutputDebugStringW(szTemp);
        // Sleep for random selected period
        if (WAIT_OBJECT_0 == WaitForSingleObject(g_evQuit,period)) break;
    }
    return 0;
} 

您可以直接在调试器下或单独启动应用程序。在收到的结果中,输出没有出现损坏的消息。

使用 Trace.WriteLine 调用进行相同功能的 .NET 实现也运行良好。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;

class Program
{
    // Thread for output Trace Messages
    static void ProcessThread()
    {
        var rand = new Random();
        int id = Thread.CurrentThread.ManagedThreadId;
        int period = rand.Next(1, 300);
        do
        {
            Trace.WriteLine(string.Format("Hello From thread {0}", id));
        }
        while (!g_evQuit.WaitOne(period));
    }

    // Event For quit
    static EventWaitHandle g_evQuit = 
           new EventWaitHandle(false, EventResetMode.ManualReset);

    static void Main(string[] args)
    {
        // Create 100 threads
        List<Thread> threads = new List<Thread>();
        while (threads.Count < 100)
        {
            threads.Add(new Thread(ProcessThread));
        }
        // Start threads
        foreach (var t in threads) { t.Start(); }
        // Wait for 5 seconds
        Thread.Sleep(5000);
        // Signal to exit
        g_evQuit.Set();
        while (threads.Count > 0)
        {
            threads[0].Join();
            threads.RemoveAt(0);
        }
        MessageBox.Show("Application Quit");
    }
} 

但是,如果您改用 Debugger.Log 调用,您将收到损坏的消息。这意味着 Debugger.Log 调用不是线程安全的。这是因为该方法将每个输出分割成单独的 string

接收调试信息输出

让我们看看如何接收通过之前描述的方法发送的数据。输出调试信息内部会引发一个异常,该异常可以被附加的调试器接收。每次 API 调用时都会抛出异常,如果附加了调试器,它就会被通知出现调试文本。在系统中,我们有 IsDebuggerPresent API,它允许我们确定我们的应用程序是否在调试器下运行。为了避免任何延迟,可以只在应用程序在调试器下运行时调用 OutputDebugString API。

// Check whatever we running under debugging
if (IsDebuggerPresent()) {
    // Output this text under debugger only
    OutputDebugStringW(L"This message sent if affplication under debugger\n");
} 

此 API 可以轻松地导入到 .NET 中。

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool IsDebuggerPresent();

static void Main(string[] args)
{
    if (IsDebuggerPresent())
    {
        Console.WriteLine("We Under Debugger");
    }
    Console.ReadKey();
} 

上面的代码在从 Visual Studio 启动应用程序时显示文本。在 .NET 中,我们还有一个 staticDebugger,其中包含 IsAttached 属性,该属性也允许我们确定我们是否在调试下。

if (System.Diagnostics.Debugger.IsAttached)
{
    Console.WriteLine("We Under Debugger");
} 

如前所述,当前调试器 string 输出 API 的实现是通过引发特殊系统异常来完成的。有两种不同的异常用于输出调试字符串。旧的异常是 DBG_PRINTEXCEPTION_C (0x40010006),它仅支持 ANSI 字符串输入。支持 Unicode 字符串的异常是 DBG_PRINTEXCEPTION_WIDE_C (0x4001000A)。让我们尝试以相同的方式实现我们自己的调试器 string 输出,看看它是如何工作的。

void MyOutputDebugStringW(LPCWSTR pszText) {
    if (pszText && wcslen(pszText)) {
        size_t cch = wcslen(pszText) + 1;
        // Alloc text buffer for multibyte text
        char * text = (char *)malloc(cch << 1);
        if (text) {
            __try {
                memset(text, 0x00, cch);
                size_t c = 0;
                // convert wide character into multibyte
                wcstombs_s(&c, text, cch << 1, pszText, cch - 1);

                // We need to provide both UNICODE and MBCS text
                // As old API which receive debug event work with MBCS
                ULONG_PTR args[] = {
                    cch,(ULONG_PTR)pszText,cch,(ULONG_PTR)text
                };
                __try {
                    // Raise exception which system transform 
                    // Into specified debugger event
                    RaiseException(DBG_PRINTEXCEPTION_WIDE_C,
                        0, _countof(args), args);
                }
                __except (EXCEPTION_EXECUTE_HANDLER) {
                }
            }
            __finally {
                free(text);
            }
        }
    }
} 

我们引发 DBG_PRINTEXCEPTION_WIDE_C,这需要传递两个不同的 string:Ansi 和 Unicode。DBG_PRINTEXCEPTION_C 只需要 Ansi string 参数。哪个 string 将提供给调试器取决于它使用的 API。
调用我们函数的 main 函数实现。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow)
{
    MyOutputDebugStringW(
        L"This text provided with own implementation of debug string output\n");
    MessageBoxA(NULL,"Application Quit","",MB_OK);
} 

可以使用 PInvoke 将实现导入到 .NET 中。

[DllImport("kernel32.dll")]
static extern void RaiseException(
           [MarshalAs(UnmanagedType.U4)] int dwExceptionCode,
           [MarshalAs(UnmanagedType.U4)] int dwExceptionFlags,
           [MarshalAs(UnmanagedType.U4)] int nNumberOfArguments,
           [MarshalAs(UnmanagedType.LPArray,
            ArraySubType = UnmanagedType.SysInt, SizeParamIndex =2)]
                IntPtr[] lpArguments
           );

const int DBG_PRINTEXCEPTION_WIDE_C = 0x4001000A;

// Custom Output Debug String Implementation
static void MyOutputDebugString(string text)
{
    if (string.IsNullOrEmpty(text)) return;
    var unicode = Encoding.Unicode.GetBytes(text);
    // convert wide character into multibyte
    var ansi = Encoding.Convert(Encoding.Unicode, Encoding.Default, unicode);

    // Prepare Arguments
    IntPtr[] args = new IntPtr[4];
    args[0] = new IntPtr(text.Length + 1);
    args[1] = Marshal.AllocCoTaskMem(unicode.Length + 2);
    args[2] = new IntPtr(ansi.Length + 1);
    args[3] = Marshal.AllocCoTaskMem(ansi.Length + 1);
    // We need to provide both UNICODE and MBCS text
    // As old API which receive debug event work with MBCS
    Marshal.Copy(unicode, 0, args[1], unicode.Length);
    Marshal.Copy(ansi, 0, args[3], ansi.Length);
    try
    {
        // Raise exception which system transform 
        // Into specified debugger event
        RaiseException(DBG_PRINTEXCEPTION_WIDE_C,0, args.Length, args);
    }
    catch
    {
    }
    Marshal.FreeCoTaskMem(args[1]);
    Marshal.FreeCoTaskMem(args[3]);
} 

如果我们从 Visual Studio 启动上面的代码,我们可以在输出窗口中看到文本。

调试器应用程序可以使用 WaitForDebugEvent API 接收 OutputDebugString 抛出的异常。该函数处理接收 Ansi string。要允许接收 Unicode 调试字符串,必须使用 WaitForDebugEventEx API。这两个函数具有相同的参数。异常调试器输出也有一些延迟,因为调试器应用程序一旦收到消息,就会停止执行,直到它处理完调试事件并调用 ContinueDebugEvent API。这就是为什么调试输出字符串是线程安全的,并且主调试进程会接收到所有这些消息。

作为处理输出调试 string 的示例,我们创建了一个应用程序,它以子进程的形式启动自身,而主进程作为调试器附加到它。为了确定进程是作为子进程启动的,我们将使用命令行参数。子进程将通过 OutputDebugString API 执行输出文本。

INT nArgs;
LPWSTR * pszArgv = CommandLineToArgvW(lpCmdLine, &nArgs);
// Check if we starting child Process
if (pszArgv && nArgs >= 1 && _wcsicmp(pszArgv[0], L"child") == 0) {
    int idx = 0;
    // Simple loop for message printing
    while (idx++ < 100) {
        TracePrint("Child Message #%d\n", idx);
        Sleep(100);
    }
} 

TracePrint 函数定义如下:

// Print formatted string with arguments into debug output
void TracePrint(const char * format, ...) {
    va_list    args;
    va_start(args, format);
    // Allocate string buffer
    int _length = _vscprintf(format, args) + 1;
    char * _string = (char *)malloc(_length);
    if (_string) {
        // Format string
        memset(_string, 0, _length);
        _vsprintf_p(_string, _length, format, args);
        __try {
            // Write resulted string
            OutputDebugStringA(_string);
        } __finally {
            free(_string);
        }
    }
    va_end(args);
} 

为避免任何安全配置问题,我们在创建进程时设置了 DEBUG_ONLY_THIS_PROCESS 标志。

SECURITY_ATTRIBUTES sa = { 0 };
PROCESS_INFORMATION pi = { 0 };
STARTUPINFO si = { 0 };
// Get Path of executable
WCHAR szPath[1024] = { 0 };
GetModuleFileNameW(NULL, szPath + 1, _countof(szPath) - 1);
szPath[0] = '\"';
// Append Argument
wcscat_s(szPath, L"\" child");
// Setup Attributes To Inherit Handles
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;        
si.cb = sizeof(STARTUPINFO);
// Starting child process
CreateProcess(NULL, szPath, NULL, NULL, TRUE,
        DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi); 

在调试创建的进程并处理 WaitForDebugEvent API 生成的消息后,我们只对 OUTPUT_DEBUG_STRING_EVENT 事件感兴趣。一旦收到,我们就使用 ReadProcessMemory API 将数据从调试事件的 OUTPUT_DEBUG_STRING_INFO 结构指向的内存中复制出来。

char * p = nullptr;
size_t size = 0;
// Attach For Debug Events
if (!DebugActiveProcess(pi.dwProcessId)) {
    while (true) {
        DEBUG_EVENT _event = { 0 };
        // Waiting For Debug Event
        if (!WaitForDebugEvent(&_event, INFINITE)) {
            wprintf(L"WaitForDebugEvent Failed 0x%08x\n",GetLastError());
            break;
        }
        // We Interested in only for OutputDebugString 
        if (_event.dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENT) {
            auto data = &_event.u.DebugString;
            size_t cch = data->nDebugStringLength + 2;
            // Allocate buffer
            if (cch > size) {
                p = (char *)realloc(p,cch);
                size = cch;
            }
            memset(p,0x00,size);
            cch = data->nDebugStringLength;
            if (data->fUnicode) cch *= 2;
            // Reading Output String Data
            if (ReadProcessMemory(pi.hProcess,
                data->lpDebugStringData, p,
                cch, &cch)
                ) {
                wchar_t Format[200] = {0};
                // Prepare Format String
                swprintf_s(Format,L"Process Output String: \"%%%s\"\n",
                    data->fUnicode ? L"s" : L"S");
                if (data->fUnicode) {
                    wchar_t * pwsz = (wchar_t *)p;
                    while (wcslen(pwsz) && pwsz[wcslen(pwsz) - 1] == '\n') {
                        pwsz[wcslen(pwsz) - 1] = '\0';
                    }
                }
                else {
                    while (strlen(p) && p[strlen(p) - 1] == '\n') {
                        p[strlen(p) - 1] = '\0';
                    }
                }
                // Output To Console Window
                wprintf(Format,p);
            } else {
                wprintf(L"ReadProcessMemory Failed 0x%08x\n",GetLastError());
            }
        }
        // Continue Receiving Events
        ContinueDebugEvent(_event.dwProcessId,_event.dwThreadId,DBG_CONTINUE);
        // Check if child process quit
        if (WAIT_TIMEOUT != WaitForSingleObject(pi.hThread, 10)) {
            break;
        }
    }
    // Detach Debugger
    DebugActiveProcessStop(pi.dwProcessId);
} else {
    wprintf(L"DebugActiveProcess Failed 0x%08x\n",GetLastError());
}
if (p) free(p);

一旦我们获得调试输出文本,我们就将其打印到控制台。然后,我们调用 ContinueDebugEvent API 继续进程执行并等待下一个事件。程序执行结果显示在下面的截图中。

.NET 不允许以简单的方式执行相同的操作,但也可以通过 PInvoke 实现此功能。子进程就像之前的类似实现一样,只是在循环中调用 OutputDebugString API。

// Check if we starting child Process
if (args.Length >= 1 && args[0].ToLower() == "child")
{
    int idx = 0;
    // Simple loop for message printing
    while (idx++ < 100) {
        // Console Output Messages
        OutputDebugString(string.Format("Child Message #{0}", idx));
        Thread.Sleep(100);
    }
}

主要功能在主进程中,由于包含本机 API 调用,它与 C++ 实现类似。

// Allocate Console
AllocConsole();
// Starting child process
var info = new ProcessStartInfo(Application.ExecutablePath, "child");
info.UseShellExecute = false;
var process = Process.Start(info);
if (process != null) {
    // Start Debugging Child Process
    if (DebugActiveProcess(process.Id)) {
        while (true) {
            DEBUG_EVENT _event;
            // Wait for debug event
            if (!WaitForDebugEvent(out _event, -1)) {
                break;
            }
            // We are interested in only debug string output
            if (_event.dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENT) {
                int cb = (_event.nDebugStringLength + 1) << 1;
                // Allocate memory buffer
                IntPtr p = Marshal.AllocCoTaskMem(cb);
                if (p != IntPtr.Zero) {
                    try {
                        cb = _event.nDebugStringLength;
                        if (_event.fUnicode != 0) cb *= 2;
                        // Reading text from process memory
                        if (ReadProcessMemory(process.Handle, 
                            _event.lpDebugStringData, p, cb, out cb)) {
                            // Output Text Into allocated console window
                            Console.WriteLine(
                                "Process Output String: \"{0}\"", 
                                _event.fUnicode != 0 ? 
                                Marshal.PtrToStringUni(p) : Marshal.PtrToStringAnsi(p));
                        } else {
                            Console.WriteLine("ReadProcessMemory Failed 0x{0:x8}\n", 
                                               GetLastError());
                        }
                    }
                    finally {
                        Marshal.FreeCoTaskMem(p);
                    }
                }
            }
            // Continue debug process execution
            ContinueDebugEvent(_event.dwProcessId, _event.dwThreadId, DBG_CONTINUE);
            if (process.WaitForExit(10)) {
                break;
            }
        }
        // Stop Debugging
        DebugActiveProcessStop(process.Id);
    }
    else {
        Console.WriteLine("DebugActiveProcess Failed {0:x8}\n", GetLastError());
    }
    if (!process.HasExited)
    {
        process.Kill();
    }
    process.Dispose();
    MessageBox.Show("Application Quit");
}

这里唯一有趣的是 WaitForDebugEvent 函数和 DEBUG_EVENT 结构。该结构具有一个联合体,其中包含取决于事件代码的不同结构参数。由于此结构由调用者分配,我们需要在函数调用包装器中为其提供足够的大小。这可以通过分配内存并将其作为 IntPtr 参数传递给 WaitForDebugEvent 函数来实现,然后在稍后从指针到结构 DEBUG_EVENT 进行封送。或者在结构本身中设置足够的大小,并让它自动执行封送。最初,我采用了第一种方法,后来我改变了它,因为我认为它看起来更好。

[StructLayout(LayoutKind.Sequential)]
struct DEBUG_EVENT
{
    [MarshalAs(UnmanagedType.U4)]
    public int dwDebugEventCode;
    [MarshalAs(UnmanagedType.U4)]
    public int dwProcessId;
    [MarshalAs(UnmanagedType.U4)]
    public int dwThreadId;
    public IntPtr lpDebugStringData;
    [MarshalAs(UnmanagedType.U2)]
    public ushort fUnicode;
    [MarshalAs(UnmanagedType.U2)]
    public ushort nDebugStringLength;
    [MarshalAs(UnmanagedType.ByValArray,SizeConst = 144)]
    public byte[] Reserved;
}

正如您在 DEBUG_EVENT 中看到的,我们只处理来自 OUTPUT_DEBUG_STRING_INFO 结构的调试文本信息。对于其他联合结构,我们添加了填充字节数组,以便我们可以直接将结构传递给 WaitForDebugEvent API。

[DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall,
SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool WaitForDebugEvent(
    [Out] out DEBUG_EVENT lpDebugEvent, 
    [In, MarshalAs(UnmanagedType.U4)] int dwMilliseconds
    );

应用程序的输出与 C++ 版本相同。

系统范围的字符串输出

如果我们启动了通过异常方法输出 string 的可执行文件的已编译版本,并且没有附加调试器,那么通过仅引发特定异常,我们就无法看到上述方法提供的输出消息。但是,如果您使用特定的 OutputDebugString API,您可以在 DbgView 工具中看到输出文本。该工具未作为调试器附加到应用程序。这意味着如果没有附加调试器,OutputDebugString API 的工作方式会有所不同。
这种方式已经描述了很多次了,但这里我也会简要提及。OutputDebugString API 内部打开一个名为 DBWinMutex 的特殊互斥体。通过锁定该互斥体,应用程序会打开其他共享对象:两个事件和共享内存。应用程序将输出 string 写入名为 DBWIN_BUFFER 的共享内存后,在执行此操作的同时,它还会等待缓冲区共享事件 DBWIN_BUFFER_READY,一旦文本写入完成,它就会发出 DBWIN_DATA_READY 共享事件信号。您可以在 sysinternalsProcess Explorer 工具 (ProcExp) 中看到这些命名对象。我们内部有两个等待操作,这可能会导致轻微的延迟。

基于上述算法描述,提供系统范围文本消息的实现示例如下。

// Opens the mutex for accessing shared objects
HANDLE hMutex = OpenMutexW(READ_CONTROL | SYNCHRONIZE | 
                           MUTEX_MODIFY_STATE, TRUE,L"DBWinMutex");
if (!hMutex) {
    CreateSecurityDescriptor();
    SECURITY_ATTRIBUTES sa = { 0 };
    sa.nLength = sizeof(sa);
    sa.bInheritHandle = FALSE;
    sa.lpSecurityDescriptor = (LPVOID)g_pSecurityDescriptor;
    hMutex = CreateMutexExW(&sa, L"DBWinMutex", 0,
        MAXIMUM_ALLOWED | SYNCHRONIZE | READ_CONTROL | MUTEX_MODIFY_STATE);
}
if (hMutex) {
    // Provide system wide debug text
    HANDLE hMap = NULL;
    HANDLE hBufferReady = NULL;
    HANDLE hDataReady = NULL;
    // Wait for shared mutex
    DWORD result = WaitForSingleObject(hMutex, 10000);
    if (result == WAIT_OBJECT_0) {
        __try {
            // Open shared objects
            hMap = OpenFileMappingW(FILE_MAP_WRITE, FALSE, L"DBWIN_BUFFER");
            hBufferReady = OpenEventW(SYNCHRONIZE, FALSE, L"DBWIN_BUFFER_READY");
            hDataReady = OpenEventW(EVENT_MODIFY_STATE, FALSE, L"DBWIN_DATA_READY");
        }
        __finally {
            if (!hDataReady) {
                ReleaseMutex(hMutex);
            }
        }
        __try {
            LPVOID pBuffer = NULL;
            if (hMap && hBufferReady && hDataReady) {
                // Map section buffer 
                pBuffer = MapViewOfFile(hMap, SECTION_MAP_WRITE | 
                                        SECTION_MAP_READ, 0, 0, 0);
            }
            if (pBuffer) {
                cch = strlen(text) + 1;
                char * p = text;
                while (cch > 0) {
                    // Split message as shared buffer have 4096 bytes length
                    size_t length = 4091;
                    if (cch < length) {
                        length = cch;
                    }
                    // Wait for buffer to be free
                    if (WAIT_OBJECT_0 == WaitForSingleObject(hBufferReady, 10000)) {
                        // First 4 bytes is the process ID
                        *((DWORD*)pBuffer) = (DWORD)GetCurrentProcessId();
                        memcpy((PUCHAR)pBuffer + sizeof(DWORD), p, length);
                        // Append string end character for large text
                        if (length == 4091) ((PUCHAR)pBuffer)[4095] = '\0';
                        // Notify that message is ready
                        SetEvent(hDataReady);
                    }
                    else {
                        break;
                    }
                    cch -= length;
                    p += length;
                }
                // Unmap shared buffer
                UnmapViewOfFile(pBuffer);
            }
        }
        __finally {
            if (hBufferReady) CloseHandle(hBufferReady);
            if (hDataReady) {
                CloseHandle(hDataReady);
                ReleaseMutex(hMutex);
            }
            if (hMap) CloseHandle(hMap);
        }
    }
    CloseHandle(hMutex);
    FreeSecurityDescriptor();
} 

将此实现添加到我们之前描述的自己的调试输出 string 中,并在 Visual Studio 下方不运行应用程序。在 DbgView 工具中,您可以看到我们文本的输出。

基于这些描述,我们可以创建完全功能化的我们自己的 OutputDebugString API。
实现中的主要问题是打开共享互斥体的安全性,因为 OutputDebugString API 可以从非管理员用户调用。
此实现也可以在 .NET 中轻松完成。第一步是打开 mutex 对象。

Mutex mutex = null;
// Open Shared Mutex
if (!Mutex.TryOpenExisting("DBWinMutex", 
    MutexRights.Synchronize | MutexRights.ReadPermissions | 
                              MutexRights.Modify, out mutex))
{
    bool bCreateNew;
    MutexSecurity security = new MutexSecurity();
    string CurrentUser = Environment.UserDomainName + "\\" + Environment.UserName;
    // Allow current user
    security.AddAccessRule(new MutexAccessRule(
        CurrentUser, MutexRights.Synchronize | MutexRights.Modify,
        AccessControlType.Allow));

    // Allow Any users
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-1-0"), MutexRights.Synchronize | MutexRights.Modify,
        AccessControlType.Allow));

    // Local System Read and modify
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-5-18"), 
            MutexRights.ReadPermissions | MutexRights.Modify,
        AccessControlType.Allow));

    // Admins Read and modify
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-5-32-544"), 
            MutexRights.ReadPermissions | MutexRights.Modify,
        AccessControlType.Allow));

    mutex = new Mutex(false, "DBWinMutex", out bCreateNew, security);
} 

接下来是打开共享对象。

EventWaitHandle DataReady = null;
EventWaitHandle BufferReady = null;
MemoryMappedFile MappedFile = null;
MemoryMappedViewAccessor Accessor = null;

// Open Events 
if (EventWaitHandle.TryOpenExisting("DBWIN_BUFFER_READY", 
                    EventWaitHandleRights.Synchronize, out BufferReady))
{
    if (EventWaitHandle.TryOpenExisting("DBWIN_DATA_READY", 
                        EventWaitHandleRights.Modify, out DataReady))
    {
        // Open Shared Section
        try
        {
            MappedFile = MemoryMappedFile.OpenExisting
                         ("DBWIN_BUFFER", MemoryMappedFileRights.Write);
        }
        catch
        {
        }
    }
}
if (MappedFile != null) {
    // Map View 
    Accessor = MappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite);
}
 

等待缓冲区就绪事件信号并写入数据。

int offset = 0;
int count = 4091;
// While we have data 
while (ansi.Length - offset > 0)
{
    // Split it 
    if (count > ansi.Length - offset) count = ansi.Length - offset;
    // Wait Buffer Access
    if (BufferReady.WaitOne(10000))
    {
        // PID
        Accessor.Write(0, PID);
        // Ansi Text
        Accessor.WriteArray<byte>(4, ansi, offset, count);
        // Zero ending string if any
        Accessor.Write(count + 4, 0);
        // Signal that we are ready
        DataReady.Set();
    }
    else
    {
        break;
    }
    offset += count;
} 

清理对象。

if (Accessor != null) Accessor.Dispose();
if (BufferReady != null) BufferReady.Dispose();
if (DataReady != null) DataReady.Dispose();
if (MappedFile != null) MappedFile.Dispose();
if (mutex != null)
{
    mutex.ReleaseMutex();
    mutex.Dispose();
}

您也可以在 DbgView 应用程序中看到函数调用的结果。

接收系统范围的字符串输出

现在是实现接收部分的时候了。接收共享 string 输出可以通过反向方式完成。最初,我们打开共享对象,但具有不同的访问请求,因为现在我们需要等待 DBWIN_DATA_READY 事件被信号化,并在完成后重置 DBWIN_BUFFER_READY 事件。如您在之前的实现中所见,我们打开现有的共享对象。这样做是因为如果没有应用程序等待数据,我们将跳过发送。因此,接收器部分应该创建这些对象的实例,以防它们未能打开。

// Open or create shared objects
hMap = OpenFileMappingW(FILE_MAP_READ, FALSE, L"DBWIN_BUFFER");
if (!hMap) {
    hMap = CreateFileMappingW(INVALID_HANDLE_VALUE,&sa,
           PAGE_READWRITE,0,0x1000,L"DBWIN_BUFFER");
    if (hMap == INVALID_HANDLE_VALUE) {
        hMap = NULL;
    }
}
hBufferReady = OpenEventW(EVENT_MODIFY_STATE, FALSE, L"DBWIN_BUFFER_READY");
if (!hBufferReady) {
    hBufferReady = CreateEventW(&sa,FALSE,FALSE,L"DBWIN_BUFFER_READY");
    if (hBufferReady == INVALID_HANDLE_VALUE) {
        hBufferReady = NULL;
    }
}
hDataReady = OpenEventW(SYNCHRONIZE, FALSE, L"DBWIN_DATA_READY");
if (!hDataReady) {
    bCreated = TRUE;
    hDataReady = CreateEventW(&sa,FALSE,FALSE,L"DBWIN_DATA_READY");
    if (hDataReady == INVALID_HANDLE_VALUE) {
        hDataReady = NULL;
    }
} 

接下来在循环中,我们等待事件并将数据从映射的共享节读取。节可以只映射为读取访问,因为我们不写入任何数据。

LPVOID pBuffer = MapViewOfFile(hMap, SECTION_MAP_READ, 0, 0, 0);
HANDLE hHandles[] = { hDataReady,g_hQuit };
while (true) {
    // Wait for event appear or quit
    if (WAIT_OBJECT_0 == WaitForMultipleObjects(
        _countof(hHandles), hHandles, FALSE, INFINITE)) {
        // First 4 bytes is the process ID
        DWORD ProcessId = *((PDWORD)pBuffer);
        // Copy data from the shared memory
        strncpy_s(text, cch, (char*)pBuffer + 4, cch - 4);
        // Notify that we are done
        SetEvent(hBufferReady);
        if (strlen(text)) {
            while (text[strlen(text) - 1] == '\n') {
                text[strlen(text) - 1] = '\0';
            }
            // Output Text
            printf("[%d] %s\n", ProcessId, text);
        }
    }
    else {
        break;
    }
}
UnmapViewOfFile(pBuffer); 

为了测试,我们可以运行之前的代码示例,该示例使用多个线程以及我们当前的接收器实现输出调试 strings,并检查结果。

我们可以在 .NET 中以同样的方式进行。首先,打开或创建 mutex 对象。

Mutex mutex = null;
// Open or create mutex
if (!Mutex.TryOpenExisting("DBWinMutex", MutexRights.Synchronize 
            | MutexRights.ReadPermissions | MutexRights.Modify, out mutex))
{
    bool bCreateNew;
    MutexSecurity security = new MutexSecurity();
    string CurrentUser = Environment.UserDomainName + "\\" + Environment.UserName;
    // Allow current user
    security.AddAccessRule(new MutexAccessRule(
        CurrentUser, MutexRights.Synchronize | MutexRights.Modify,
        AccessControlType.Allow));

    // Allow Any users
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-1-0"), 
            MutexRights.Synchronize | MutexRights.Modify,
        AccessControlType.Allow));

    // Local System Read and modify
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-5-18"), 
            MutexRights.ReadPermissions | MutexRights.Modify,
        AccessControlType.Allow));

    // Admins Read and modify
    security.AddAccessRule(new MutexAccessRule(
        new SecurityIdentifier("S-1-5-32-544"), 
            MutexRights.ReadPermissions | MutexRights.Modify,
        AccessControlType.Allow));

    mutex = new Mutex(false, "DBWinMutex", out bCreateNew, security);
} 

打开或创建共享对象后。

EventWaitHandle DataReady = null;
EventWaitHandle BufferReady = null;
MemoryMappedFile MappedFile = null;
MemoryMappedViewAccessor Accessor = null;

MemoryMappedFileSecurity memory_security = new MemoryMappedFileSecurity();
EventWaitHandleSecurity event_security = new EventWaitHandleSecurity();

memory_security.AddAccessRule(new AccessRule<MemoryMappedFileRights>(
    new SecurityIdentifier("S-1-1-0"), MemoryMappedFileRights.ReadWrite,
    AccessControlType.Allow));

event_security.AddAccessRule(new EventWaitHandleAccessRule(
    new SecurityIdentifier("S-1-1-0"), 
        EventWaitHandleRights.Synchronize | 
        EventWaitHandleRights.Modify | EventWaitHandleRights.ReadPermissions,
    AccessControlType.Allow));

// Open Buffer Event
if (!EventWaitHandle.TryOpenExisting("DBWIN_BUFFER_READY", 
     EventWaitHandleRights.Modify, out BufferReady))
{
    BufferReady = new EventWaitHandle(false, EventResetMode.AutoReset, 
                  "DBWIN_BUFFER_READY", out bCreateNew, event_security);
}
// Open Data Event
if (!EventWaitHandle.TryOpenExisting("DBWIN_DATA_READY", 
     EventWaitHandleRights.Synchronize, out DataReady))
{
    DataReady = new EventWaitHandle(false, EventResetMode.AutoReset, 
                "DBWIN_DATA_READY", out bCreateNew, event_security);
}
// Open Shared Section
try
{
    MappedFile = MemoryMappedFile.OpenExisting
                 ("DBWIN_BUFFER", MemoryMappedFileRights.Read);
}
catch
{
}
if (MappedFile == null)
{
    MappedFile = MemoryMappedFile.CreateOrOpen("DBWIN_BUFFER", cch,
        MemoryMappedFileAccess.ReadWrite, 
        MemoryMappedFileOptions.None, memory_security, 
        System.IO.HandleInheritability.None);
}
if (MappedFile != null)
{
    // Map View 
    Accessor = MappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
} 

在循环中处理通知并将消息输出到控制台窗口。

while (true)
{
    // Wait for event appear or quit
    if (0 == WaitHandle.WaitAny(new WaitHandle[] { DataReady, g_evQuit }))
    {
        // First 4 bytes is the process ID
        int ProcessId = Accessor.ReadInt32(0);
        // Copy data from the shared memory
        Accessor.ReadArray<byte>(4, text, 0, text.Length - 4);
        // Notify that we are done
        BufferReady.Set();
        int length = 0;
        while (length < text.Length && text[length] != '\0') length++;
        string ansi_text = Encoding.Default.GetString(text, 0, length);
        if (!string.IsNullOrEmpty(ansi_text))
        {
            if (ansi_text[ansi_text.Length - 1] == '\n') 
                ansi_text = ansi_text.Remove(ansi_text.Length - 1,1);
            Console.WriteLine("[{0}] {1}", ProcessId, ansi_text);
        }
    }
    else
    {
        break;
    }
} 

执行结果与 C++ 应用程序版本相同。
如果运行两个接收器应用程序实例,则会发生并发。因此,消息可能只被一个应用程序接收,因为在实现中使用了自动重置事件进行同步。因此,DbgView 应用程序阻止了其第二个实例接收数据。

历史

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