Windows進程內標準輸出重新導向及其在程式調試上的應用

來源:互聯網
上載者:User

一、如何?

列印調試資訊的方法有很多,最常用的是使用標準輸出裝置(如printf、cout等),也可以用OutPutDebugString輸出、用DebugView工具查看,還可以寫入記錄檔。如果程式運行需要記錄日誌(log),往往需要開啟個檔案,或許是寫入系統事件、用系統的事件檢視器查看。

應用程式列印調試資訊、日誌的方法往往是確定的,但如果是要編寫一個模組或者說組件,那樣的輸出資訊應該寫入哪裡呢?或者說程式本身對此也沒有明確需求的話,那該怎麼辦呢?

可喜的是一個進程的標準輸出是可以重新導向的,所以我建議把調試資訊直接打到標準輸出上,這樣代碼中可以統一使用cout或者printf,然後根據需要將標準輸出重新導向。

Linux中重新導向標準輸出就容易了,因為有強大的dup2函數。而對於Windows的重新導向,貌似往往是用於子進程的,在用CreateProcess建立子進程時設定子進程的標準輸出控制代碼。而我們想要的是重新導向自己這個進程的標準輸出,那個用不上。我在MSDN中也沒找到類似dup2的Win32 API函數,只有一個DuplicateHandle函數,這相當於linux中的dup,也用不上。

這裡順便提下,SetStdHandle是不能實現重新導向的。這個函數的功能是將某控制代碼指向標準裝置,並不能將標準裝置控制代碼重新導向到另外的控制代碼。

於是我就想到,Windows不是支援一部分POSIX標準的嗎。於是我找到了一個CRT的函數,叫_dup2,看起來是不是特眼熟,對了,這就是Windows中dup2的相容版本。

值得注意的是,_dup2以及與此相關的一系列CRT中的IO函數(如_read,_write)均以底線開頭,其餘與linux大致相同,其參數中所謂的檔案描述符與Win32中的控制代碼不一樣,檔案描述符實際上是控制代碼數組的索引,也就是說檔案描述符不能與控制代碼混用。比如0,1,2分別是標準輸入、標準輸出、標準錯誤的檔案描述符,但控制代碼值不是這樣確定的。檔案描述符不是Win32的概念,是POSIX中的概念。

_dup2用法與dup2大致相同,不多解釋,不瞭解的可以查閱dup2相關資料。下面講點應用。

二、如何應用於調試

寫一個模組時,我們可以直接用cout/printf來作調試。但是如果這個模組用於圖形介面或許是系統服務呢?這時標準輸出看不到了,我們可以用OutPutDebugString函數和DebugView這樣的調試工具。這樣就帶來一種選擇,而選擇往往是增加軟體複雜度的因素。所以我的想法是代碼中只用cout/printf,如果需要將其重新導向到調試工具中去。

如何?呢,用匿名管道和線程。

用一個pipe,標準輸出重新導向到其write端,然後建立一個線程,線程要做的就是從pipe的read端讀出資料後用OutPutDebugString輸出。

下面是實現代碼。

標頭檔是這樣子的,構造時重新導向,析構時解除:

 namespace common {

    // 將標準輸出重新導向到DebugView,保持對象存在即有效

    class StdoutToDebugString {
    public:
        StdoutToDebugString();
        
        ~StdoutToDebugString();

    private:
        int fds_[2];
        int orign_stdout_;
        uintptr_t thread_handle_;
    };
}

實現檔案:

#include <io.h>
#include <fcntl.h>
#include <stdio.h>
#include <process.h>
#include <Windows.h>
#include "StdoutRedirect.h"

using namespace common;

const int kBufferSize = 4096;

unsigned __stdcall RedirectThreadProc(void* param) {
    int pipe_read = (int)param;
    char buf[kBufferSize];
    int bytes_read;
    do {
        bytes_read = ::_read(pipe_read, buf, kBufferSize);
        buf[bytes_read] = 0;
        ::OutputDebugString(buf);
    } while (bytes_read);
    return 0;
};

StdoutToDebugString::StdoutToDebugString() {
    ::_pipe(fds_, kBufferSize, _O_TEXT);
    orign_stdout_ = ::_dup(_fileno(stdout));
    ::_dup2(fds_[1], _fileno(stdout));
    thread_handle_ = ::_beginthreadex(NULL, 0, RedirectThreadProc, (void*)fds_[0], 0, NULL);
    ::CloseHandle((HANDLE)thread_handle_);
}

StdoutToDebugString::~StdoutToDebugString() {
    ::_dup2(orign_stdout_, _fileno(stdout));
}

測試代碼:

#include <iostream>
#include <Windows.h>
#include "../Common/StdoutRedirect.h"
using namespace std;
using namespace common;

void main() {
    StdoutToDebugStringredirect;
    cout<< "hello" << endl;
    ::system("pause");
}
 

運行時可以看到控制台上並沒有列印出hello,而在DebugView中可以看到。

三、未解決的問題?

因為_read是阻塞的,我沒有辦法能安全地結束掉那個線程,所以我無法在析構時等待線程結束。如果析構中用_close關閉檔案描述符的話,當對象已經析構而線程依然在跑,_read會引發assert導致崩潰。

不過在一般情況下,在main的開始構造這樣一個對象來實現重新導向,一直維持到main結束,是沒有問題的。

對於此問題,還望牛人指點。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.