Sunday, July 10, 2011

Windbg: oleaut32!DllMain+0x76 除錯筆記

最近看到了一個令人匪夷所思的 Crash Dump,Call Stack 是長這樣:

0c 078efe7c 770d15a8 oleaut32!DllMain+0x76 (Unloaded)
0d 078efe9c 7c94118a oleaut32!_DllMainCRTStartup+0x52 (Unloaded)
0e 078efebc 7c953a23 ntdll!LdrpCallInitRoutine+0x14
0f 078eff34 7c80c126 ntdll!LdrShutdownThread+0xd7
10 078eff6c 7813299f kernel32!ExitThread+0x3e
11 078eff74 781329c0 msvcr80!_endthreadex+0x1f
12 078effac 78132a47 msvcr80!_callthreadstartex+0x20
13 078effb4 7c80b713 msvcr80!_threadstartex+0x66

在程式要回到 oleaut32!DllMain+0x76 的時候,就發生 Access Violation,原因是因為 oleaut32.dll 已經被 Unloaded 了。

花了一些時間追 oleaut32.dll 的實作,發現它在 InitAppData() 裡面會去呼叫 CoCreateInstanceEx(),這邊有個神祕的 Error Handling:

如果 Calling Thread 沒有執行過 CoInitialize(),它會回傳 0x800401F0h,也就是 COM 沒有初始化的錯誤碼。

在此時,oleaut32.dll 會在這個 Calling Thread 的 APP_DATA 中留下一個 Flag,當在 Thread 結束的時候,oleaut32.dll 就會在 DLL_THREAD_DETACH 裡面去檢查這個 Flag。如果 Flag 有被標記,那麼 oleaut32.dll 就會呼叫 ole32!CoSetState(0) 去重置 COM Thread  的狀態。

(APP_DATA 是放在 Thread Local Storage 的一個 Structure,每個有跑過 OLE 的 Thread 都應該會有一個。)

然而,CoSetState() 有個行為,如果有 State 被重置,就會把 oleaut32.dll 給 Unload 掉,可是此時 oleaut32.dll 就是 CoSetState() 的 Caller,當 CoSetState() 一回去之後,就 Access Violation。

(這裡有個更匪夷所思的實作,ole32.dll 是用 global variable 去存放 oleaut32.dll 的 handle。)

好啦,這些都是微軟的 Binary,遇到了這個問題該怎麼辦?根據分析,至少我們知道這一切都是起因於 OLE Automation 遇到了 COM 初始化的問題,所以至少可以先朝這個方向先 Troubleshooting。

[這篇文章是針對 Windows XP SP3 的環境做分析,Windows 7 的 ole32.dll 和 oleaut32.dll 已經拿掉了 CoSetState(),也許新的平台已經不會再有這種問題。]


參考資料:

Sunday, May 29, 2011

Automate Characterization Test by Googletest

也許你我都有面對過自己不熟析的程式碼的經驗,通常我會用兩種方式來了解程式碼:一種方式就是直接面對做 Code Tracing,讀文件,檢視所有的流程;另一種方式就是從外面去測試,Live Debugging,看看這個程式碼的行為為何。

Characterization Test 指的就是類似後者的策略。Characterization Test 主要是要藉由測試的過程,去檢查(Check)現有的程式碼的行為,而不是去驗證(Verify)現有程式碼是否有符合Specification。最終的目的就是要取出可以代表目前程式碼的特徵(Characterizing)。

事實上,很多程式碼本身就沒有 Specification 來定義行為,純粹就是做到剛好可堪用(Just work)。當後來在修改的時候,如果又沒有 Unit Test,要避免任何不必要的 Side-Effect,大概只能夠依靠英雄和好運氣。

還好事情其實沒有那麼糟糕,這時候 Characterization Test 還是可以在悲觀的頹勢中成為我們的安全網。如果收集到的 Test Case 夠多,這時就可以了解到改變會 Break 多少的 Test Case,如果利用這個資訊就可以進而評估影響的範圍。

Characterization Test 可以被實作在各種 Testing Framework 上,對於程式設計師來說,最熟悉的測試工具莫過於 Unit Test Framework。如果是 C/C++ Programmer,Googletest 剛剛好就可以成為自動化 Characterization Test 的好工具。

假設你即將要修正/理解一個函式:(但這個例子好像太簡單了XD)
bool CheckSupportedLocale(std::string locale)
{
    return (0 == locale.compare("en-us"));
}
除了讀程式碼之外,還可以幫它寫一點測試。我現在還不確定結果為何,先隨便給一個 Expected Result:
TEST_F(suite_Locale, CheckSupportedLocale_ENUS)
{
    ASSERT_EQ(false, CheckSupportedLocale("en-us"));
}
執行完之後,Googletest 應該會回報失敗,並且記錄 Actual Result 應該是 true。那麼我們就根據這個結果修正 Test Case。
TEST_F(suite_Locale, CheckSupportedLocale_ENUS_ReturnTrue)
{
    ASSERT_EQ(true, CheckSupportedLocale("en-us"));
}
那麼要怎麼知道自己已經收集足夠多的 Test Case 能夠代表目前程式碼的行為呢?一種簡單的方式就是用 Coverage Tool 來看看 Test Case 目前的 Function/Branch Coverage Rate,比例要多少其實就是要是情況而定了。(大概就是看良心吧XD)

最後,Characterization Test 所找出來的 Test Case,其實就可以在其中篩選出合理的且具有代表性的當做是 Specification,那些 Test Case 最終就可以被轉變成為 Regression Test 中的一環,這樣就可以減輕日後再回頭過來修改程式碼的維護成本了。

延伸閱讀:

Saturday, April 30, 2011

WinDbg: CreateProcessAsUser 回傳 ACCESS_DENIED 之案例分析

最近手邊處理了一個案例。因為一些特殊需求,想要試試看在 Administrator 帳號下,呼叫 CreateProcessAsUser 建立在別的 Logon Session 的進程。這並不是一個典型的應用。你可以照著 MSDN 上的說明處理,幫 Administrator 加上需要的權限,看起來可以在 Console Session 上正常運作。

事情沒那麼簡單,在 Windows Server 2003 下面,CreateProcessAsUser 沒辦法在 Terminal Service 的 Logon Session 下面建立進程,永遠會得到一個 ACCESS_DENIED (5) 的錯誤碼。不過,MSDN 並沒有描述這個錯誤碼的原因。

看來,要有把手弄髒的心理準備。

首先,嘗試用 WinDbg 逐步執行看看是哪裡丟出了這個錯誤碼。結果發現,CreateProcessAsUser 裡面最終會用 CreateFile 去開一個 Terminal Server 的 Named Pipe,這個地方是讓 CreateProcessAsUser 呼叫失敗的點。

0:000> kvn
 # ChildEBP RetAddr  Args to Child              
00 002cce10 00525227 002cee90 c0000000 00000000 kernel32!CreateFileW (FPO: [Non-Fpo])
01 002ceef4 00515fd4 00000001 00000000 000003bc ADVAPI32!CreateRemoteSessionProcessW+0xbd (FPO: [Non-Fpo])
02 002cef44 0040433c 000003bc 00000000 002cf1a8 ADVAPI32!CreateProcessAsUserW+0xb0 (FPO: [Non-Fpo])

0:000> du 002cee90 
002cee90  "\\.\Pipe\TerminalServer\ozUBFyty"
002ceed0  "9LQoprqROq\1"

看來問題似乎是 Administrator 並沒有權限去讀寫這個 Named Pipe,於是乎,接下來的方向轉為確認這個 Name Piped 的 Security Context。

C:\>PsExec.exe -s c:\accesschk.exe \Pipe\TerminalServer\ozUBFyty9LQoprqROq\1

\\.\Pipe\TerminalServer\ozUBFyty9LQoprqROq\1
  RW NT AUTHORITY\SYSTEM
  RW NT AUTHORITY\LOCAL SERVICE
  RW NT AUTHORITY\NETWORK SERVICE
c:\accesschk.exe exited on 6BD2PFN1Z with error code 0.

結果發現 Administrator 果然不行開 Named Pipe。只有System、Local Service、和 Network Service 這幾個帳戶才有能力讀寫這個 Named Pipe。

在 Windows Server 2003 有 Terminal Service 的 System 帳戶下,呼叫 CreateProcessAsUser 在別的 Remote Logon Session 建立新進程,會發現新進程的 Parent Process 並不是我們自己的 Caller Process,而是 Winlogon.exe 這個系統進程。

這裡可以做個猜測,這個 Named Pipe 的作用就是讓 Caller Process 可以送出建立新進程的指令給 Winlogon.exe。打開 Process Explorer,確認一下果然發現 Named Pipe 是由 Winlogon.exe 所建立。


追到這邊,大概心裡有數了。在 Windows 2003 Server 上面,CreateProcessAsUser 是沒辦法跑在非系統內建的幾個重要帳戶裡面。Winlogon.exe 已經限制了 Named Pipe 的讀寫權限,若是沒有符合的帳戶,就是沒有辦法執行 CreateProcessAsUSer。

結論:好奇心除了可以殺死一隻貓,也可以殺掉你寶貴的時間。:P

[Updated]

  • CodeProject 裡有個小工具 RunAsEx,你可以用它來調整各種組合,實驗看看各種不同情境下的 CreateProcessAsUser,可以很方便的達到 PoC 的效果。

Saturday, April 23, 2011

WinDbg:手把手教你看C++ Exception

不管是用WinDbg打開一個Dump File或是Live Debug,如果遇到的是C++ Exception的話,WinDbg就會丟出C++ EH exception的訊息。

(abc.2dc): C++ EH exception - code e06d7363 (first/second chance not available)
eax=073bee80 ebx=03bb12f0 ecx=00000000 edx=00000000 esi=073bef08 edi=0000005c
eip=7c812afb esp=073bee7c ebp=073beed0 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
kernel32!RaiseException+0x53:
7c812afb 5e              pop     esi
0:055> kn
 # ChildEBP RetAddr  
00 073beed0 78158e89 kernel32!RaiseException+0x53
01 073bef08 647050a6 msvcr80!_CxxThrowException+0x46 [f:\dd\vctools\crt_bld\self_x86\crt\prebuild\eh\throw.cpp @ 161]

如果丟出來的exception不是標準函式庫裡面的exception object的話,用!analyze -v也沒辦法幫你對到exception的資訊。這時候,你可以試著按照以下的步驟操作,找出exception class。

0:055> .exr -1
ExceptionAddress: 7c812afb (kernel32!RaiseException+0x00000053)
   ExceptionCode: e06d7363 (C++ EH exception)
  ExceptionFlags: 00000001
NumberParameters: 3
   Parameter[0]: 19930520
   Parameter[1]: 073bef28
   Parameter[2]: 647261ac

首先,先用".exr -1"印出目前EXCEPTION_RECORD的資訊。根據_CxxThrowException丟出的例外結構,Parameter[0]會是一個0x19930520的Magic Number,表示這是一個C++ exception。而Parameter[1]會是exception object address。至於Parameter[2],存放了_s__ThrowInfo結構的address,裡面會有C++ exception class的資訊,我們需要先看看exception object到底是什麼class type。

0:055> dt msvcr80!_s__ThrowInfo 647261ac
   +0x000 attributes       : 0
   +0x004 pmfnUnwind       : 0x64705cf0     void  testDLL!TestDbException::~TestDbException+0
   +0x008 pForwardCompat   : (null) 
   +0x00c pCatchableTypeArray : 0x647261a0 _s__CatchableTypeArray

知道class type後,就可以傾印出exception object的資訊,看看到底是什麼原因丟出C++ exception。

0:055> dt testDLL!TestDbException 073bef28  
   +0x000 __VFN_table : 0x64722524 
   +0x004 m_nType         : 1
   +0x008 m_iErrorCode     : 0n-536570191

參考資料:

Thursday, April 14, 2011

Googlemock: Mock Object 應用於 C++ RAII 實例

在 Unit Test 中,Mocking 的技巧是用假的元件去取代受測元件所依賴的外部元件,稱為 Mock Object。測試者藉由控制 Mock Object 去可以改變受測元件的內部流程、或是驗證受測元件與外部元件的互動行為。

在 C++ 當中,常常慣用 RAII 的技巧,讓 Object 在進入 Function/Block 的範圍內獲取資源,離開範圍內釋放資源。舉例來說:
void example()
{
  File file("output.log");
  file.write("test");
}
在這種情況下,通常都會在測試程式中另外實作一組假的 File。不過,因為 Object 生成的時機是在進入受測的函式中,沒有機會用 ON_CALL 去控制 Mock Object 的行為。

所幸,你可以用下面的方式將 File 的實作,交給另外一個擁有相同介面的類別。這麼一來,我們還是可以控制這個新的類別。
// MockFile 是真正使用 Googlemock 的類別
class MockFile
{
public:
  MOCK_METHOD0(write, void(
    char *szLine));
};

MockFile *g_pMockFile = NULL;

// 將 File::write 導到 MockFile 上
File::write(char *szLine) 
{ 
  g_pMockFile->write(szLine); 
};

TEST_F(TestSuite, test_file_write)
{
  MockFile mockFile;
  EXPECT_CALL(mockFile, write(_));

  // 設定給 File::write() 使用這個 Mock Object
  g_pMockFile = &mockFile;

  example();
}
如此一來,就可以使用 Googlemock 幫我們取代掉 RAII 技巧所使用的類別。

Monday, April 11, 2011

NacoOS期末作業

[本來想要把另一個已經廢棄已久的 Blog 關掉,發現有兩篇文章被大量閱讀。看起來應該是對某些人還是有些幫助,我決定將他們轉錄到這邊來。感謝讀者的支持。]

期末專題(NachOS)有兩個作業-
  1. Implement system call "Sleep"
  2. Implement SJF scheduling
紀錄一些過程,免得做報告時忘記了...
  1. Implement system call "Sleep"
    Implement Alarm.waitUtil(...) by InterruptHandler, getTime(). Then implement syscall sleep(...).
    Sleep(...)一定會不準,因為waitUtil(...)是以Real-Time為準,可是卻是以InterruptHandler實做(InterruptHandler天生就不準)。
  2. Implement SJF scheduling
    只 要動到nachos.threads這個package中的class就可以。以proj1的ThreadKernel為主,要產生Thread就去 KThread.selfTest()中修改。然後在nachos.threads底下增加一個SJFScheduler,模仿RoundRobin,只 需要修改RR中的FifoQueue.waitForAccess(...)。因為SJF需要Thread的單一執行時間,所以可以學習PingTest 用Loop模擬。最好是新增一個類似PingTest的類別,然後在裡面加入worktime之類的項目。
根據Berkeley的作業說明,只需要動到nachos.threads和nachos.userprog就可以,其他的package就假設都是正確的就好了。

NachOS安裝心得

[本來想要把另一個已經廢棄已久的 Blog 關掉,發現有兩篇文章被大量閱讀。看起來應該是對某些人還是有些幫助,我決定將他們轉錄到這邊來。感謝讀者的支持。]

Date: 2005/06/15

作業系統期末作業是以NachOS這個教學用的平台,體驗作業系統的運作。不過,安裝的過程卻一點也不順遂,所以我把一些安裝心得整理在這邊;方邊我自己參考,也幫助其他同樣遭遇到安裝問題的人。

目前我有找到四個NachOS版本,一個是最原始的,用C++實做的版本;另一個也是用C++實做的版本;最後兩個是跟隨其後,以Java來實做的版本。四種版本的架構都很類似,而我就是在安裝C++版本遇到挫折之後,轉而決定要來累積一些心得的。
NachOS 4.0 (C++ version) @UC Berkeley
NachOS 4.1 @Universität Karlsruhe
NachOS 5.0j @UC Berkeley
NachOS (Java version) @Rice University

安裝前的環境需求:
  1. cygwin-在MS Windows的環境下也可以跑Linux-Like Shell的模擬器。
  2. MIPS cross-compiler-用來把UserProgram編譯成在MIPS下跑的執行檔。NachOS是吃MIPS的coff格式的檔案。
  3. 把jdk/bin的目錄加入環境變數PATH。
NachOS 5.0j 安裝步驟:
  1. 把nachos-java-2005spring.tar.gz放在/opt底下。
    用tar zxvf nachos-java-2005spring.tar.gz把他解開。
  2. 把/nachos/Makefile用notepad打開來編輯。
    javac -source 1.3 -nowarn -classpath . -d . -sourcepath ../.. -g $< (加上這粗體字的參數) test: cd ../test ; make(從gmake改成make)
  3. 編輯/nachos/test/Makefile...
    GCCDIR = /xxx/
    在xxx處放上cross-compiler的所在目錄。
    CC=$(GCCDIR)mips-gcc
    其他的類推...
  4. cd /proj1
    make
    make test
    ../bin/nachos
  5. 接下來應該會看到一連串正常執行結果。
我遇到的問題:
  1. 為什麼跑proj3會出現unknown system call?
    因為NachOS只有實做一個的system call,其餘的system call要自己實做出來。nachos.userprog.UserProcess裡有handleSyscall(...)這個函式,要自己處理各式各樣的system call。
  2. 每次執行都要打../bin/nachos嗎?
    可以去編輯/etc/profile。去下找export PATH=...這一行。
    export PATH="/usr/local/bin:/usr/bin:/bin:/opt/nachos/bin:$PATH"

    增加/nachos/bin的目錄進去,以後在任何目錄都可以執行nachos這個執行檔了。
  3. MIPS指令看不懂怎麼辦?
    MIPS Instruction Set Reference
  4. 如何增加system call?
    首先找出/nachos/test/syscall.h,增加需要的syscall的宣告。
    /**
    * System call codes, passed in $r0 to tell the kernel which system call to do.
    */
    #define syscallHalt 0
    #define syscallExit 1
    #define syscallExec 2
    ...

    這邊加入syscall的id到時候會傳給NachOS的UserProcess處理。
    當然底下也要增加函式原型的宣告。
    /* Fork a thread to run a procedure ("func") in the *same* address space
    * as the current thread.
    */
    void threadFork(void (*func)());

    然後去編輯/nachos/test/start.s
    SYSCALLSTUB(threadFork, syscallThreadFork)

    在下面會找到類似上行的程式碼,就學著他的形式把剛才新增的syscall做一個對應。
    然後你就可以在Test Program中呼叫新的syscall了。
    (不過還是要在UserProcess中實做...)
  5. C++版本的真的不能用嗎?
    資工的工作站和cygwin都試過了,都會有Compile Error,還有清大OS的課程網頁也有說明。所以應該是說,可以用,但是要debug。
  6. 要怎麼看到MIPS的Assembly Code?
    gcc加上-S參數就可以。
  7. 用哪個Timer比較準?
    InterruptHandler大約每Stats.TimerTicks會invoke一次,不過不是很準,最好還是用getTime()來獲得系統時間。

Sunday, April 10, 2011

軟體測試:Unit Test 隨便談,從動機到工作流程改善

在軟體測試的領域裡,最能夠吸引開發者的測試系統大概就是 Unit Test。這難以假手於他人的 Best Practice,莫不提起開發者對於 Unit Test 既予高度的期待。大部分的討論主題都是在教你如何寫出容易維護的 Unit Test,或是提高 Testability 的程式設計技巧。但我認為更重要的是要討論 Unit Test 的動機,進而了解它對於專案開發效益,到底可以帶給開發團隊什麼樣的好處。

上圖說明了我們對於軟體開發過程的假設。在任何階段產生的臭蟲,如果不能及時被抓出來,維修的成本提昇的速度會隨著它潛伏的時間快速成長。舉個例子,Toyota 的煞車延遲事件,如果提前在研發或工廠中發現問題,純粹就只是一個工程的問題,而不會是對簿公堂,變成競爭者攻擊的話題,它所損失的絕對超過產品本身召回的金額。

那麼,如果可以越早的找到臭蟲,我們就可以降低已知問題的維修成本,進而降低產品推出後工程問題的風險。因此,對於自己的產品,我們佈下了天羅地網的測試系統,就是為了能夠及時的將臭蟲給抓出來,也確保問題不會重複發生(Safety Net)。

Coding 的階段來看,最早能夠貢獻的測試系統之一,就是 Unit Test。身為一個測試系統,其實目的和其他層次的測試系統是一樣的。不過因為它和 Source Code 能夠較為緊密結合,底下的效益是我認為可以彌補其他層次所不足:
  • Fast Feedback Loop:如果你在修改一段安裝檔的程式碼,好死不死每次安裝都要一個小時的時間。你會選擇每次修改之後就直接來一次整合測試,還是會先把修改部分的邏輯先獨立出來先測試完畢,然後再來整合測試?通過 Unit Test 先把無關的外部模組排除,模擬直接相關的外部模組,加速目前專注範圍的開發和除錯。
  • Early Defect Detection:小範圍獨立運作的程式碼,會比獨立運作的整個系統來的容易控制。如此一來,小範圍的待測物會有更多的機會可以模擬出整合測試不容易做出的測試條件。舉例來說,不同的 File System 列舉目錄下的檔案的規則可能都不相同,要測試如此的條件必須要有不同的真實環境,測試準備工作也需要比較多的時間去建置。通過 Unit Test,模擬難以由整合測試做出的測試案例,提前評估待測物的行為。
  • Safety Net for Developer:人是容易犯錯的動物,一個模組隨著時間的演進,可能會有許多的極端的測試案例被發現。若是案例可以盡可能的加入現有的 Unit Test,最終 Unit Test 可以變成一個檢核表,提前評估是否程式碼的修改影響任何測試案例。
對於 Defect Detection 和 Safety Net,注意我這裡講的是提前評估。最終來說它還是無法成為終極的測試銀彈,它能夠提供的還是有其限制。Unit Test 互動的都是我們設計好的測試鷹架(Test Scaffolding),無法代表真實系統整合後的運作情況;使用者介面也是一個 Unit Test 無法收服的領域。

我觀察到一些初入行的程式設計師,可能同時在多個模組上修改程式碼後,一口氣用手動的方式整合測試來觀察運作結果。通常的狀況就會變成:如果有問題,要花時間確認是哪個步驟出錯,修正程式碼,然後再重來一遍整合測試。或是看似沒有問題,是其實某個模組已經出錯,只是沒有明顯到出現在整合測試上面,這種臭蟲未來會更難以處理。

我認為,Unit Test 的策略非常適用於改善開發流程。先對所修改的個別地方做獨立測試,然後才是一輪整合測試。這樣的工作流程改善的是「尋找有問題的模組的時間」,並減少不必要的「整合測試運行的次數」。數據顯示,除錯常超過程式設計師工作時數 50% 以上,這樣的改善相信能夠影響工程師的生產力。

實務上,Unit Test 要怎麼做,要做到什麼樣的程度,其實不必太死忠於流程或是規範,矯枉過正,最後還是團隊受傷害。時常回頭看看動機,讓團隊決定怎麼做、怎麼評估,交由團隊做出承諾。

Friday, April 01, 2011

Win32程式設計: _beginthreadex or CreateThread?

先說結論,永遠先用_beginthreadex()來建立新的執行續,把它當作是你的 Code Convention。

為什麼?先從動機說起。

在 C Run-Time (CRT) 函式庫裡有許多函式在多執行續的環境下,會利用 Thread Local Storage (TLS) 去存取每個獨立執行續的資源,藉此達到 Thread-Safe。

舉例來說,errno 是檢查錯誤碼的變數,在多執行續的環境下,如果A()和B()兩個函式從不同的執行續改寫了 errno,那麼接下來的程式碼就無法確認 errno 來決定上面一個函式究竟是成功還是失敗。

為了解決這種問題,在多執行續的 CRT 下,errno 會被替換成 #define errno (*_errno())。_errno() 裡面做的事情就是去取出 TLS 裡面存放的 per-thread errno。這樣每一個執行續都有自己的 errno,不用擔心內容不正確的問題。

像是 errno 這樣的應用還有很多,它們都是存放在 _tiddata 這個結構中。_tiddata 又是放在 TLS 裡面。

再回來看_beginthreadex(),它比 CreateThread() 是多做了一些事情。最主要就是多初始一份 _tiddata 並且放在新的執行續的 TLS 中,讓 CRT 的函式可以使用這些 _tiddata 來達成 Thread-safe。

你可能會問,那...我的產品都已經釋出了,而且都是用CreateThread(),怎麼辦?先別擔心,先讓我們來看沒有正確使用 _beginthreadex() 會造成什麼影響。

  • _tiddata 沒有初始化?
    如果沒有初始化,在 CRT 的函式中只要是第一個使用到 _tiddata 的地方,還是會負責建立一份的。舉例來說,_errno() 裡面檢查到沒有 _tiddata,它會自己建立一份,再放回 TLS 中。所以你的 CRT 函式還是會正確運作的。
  • 記憶體洩漏?
    正常來說,_tiddata 只會在 _endthreadex() 呼叫之後才會被刪除掉。_beginthreadex() 產生出來的執行續在結束後會自動呼叫到 _endthreadex()。所以理論上 CreateThread() 產生的執行續就會有記憶體洩漏的問題。但是這點,Microsoft的工程師也幫你想到了,不管你是 Static-link 或是 Dynamic-link 多執行續的 CRT,CRT 會在收到 DLL_THREAD_DETACH 的時候再幫你檢查 _tiddata 然後把它刪除掉。

    不過這邊 Static-link 可能會有個問題。舉例來說,A.exe 裡面用了 errno 然後當下 malloc 了一份 _tiddata,然後 call-stack 進入到 B.dll,之後 B.dll 返回離開 call-stack 就會收到 DLL_THREAD_DETAH,B.dll 就會刪除這個 _tiddata,可是 A.exe 和 B.dll 是分別使用獨立的 Heap,這樣一刪除下去,恐怕會有 Heap Corruption 的機會。(純粹推理,還沒驗證過)
  • 還有其他的問題嗎?
    _beginthreadex() 還會呼叫 _fpclear() 和支援.NET AppDomain,前者和浮點數相關函數初始化有關,後者會將新的執行續在 Caller 相同的 AppDomain 上執行。
    (反組譯了一下我電腦上的 msvcr80.dll,_fpclear()其實什麼都沒做就返回了。)

如果是用 Multithread Dynamic-Link C Run-Time 看起來就沒什麼問題囉。(謎之聲:那個在 Production Code 用了 CreateThread 的人是誰呢? XD)


參考資料:

Monday, March 28, 2011

軟體設計:給Developer的四個好工作習慣提案

在我的工作環境中,Developer和QA是兩個不同的腳色。在開發的過程中兩者就不斷的在Coding-Testing-Debugging的反覆循環中讓事情繼續前進。就因為這個生產線般的過程,Developer所做的事情都會直接影響QA的工作效率。

所以,如果你是Developer,在你提交你的程式碼以前,請保持以下的好習慣:
  • 至少測過一次:
    不管修了多小的Defect,請在自己的機器上測過一次,然後再交給QA。如果你聲稱解決了某個Defect,QA卻驗證失敗,除了浪費了QA的驗證時間(人家也是要在很多Test Case之間Context-Switch的...),也會讓QA對於Developer的程式碼失去信心。
  • 確認已同步最新的程式碼:
    如果你的團隊有用SCM(沒有嗎!God bless you...),在提交程式碼前,請再一次同步程式碼。如果有變動,就請再重新編譯一次。如果你的專案編譯一次就是好幾個小時,編譯失敗的成本就是浪費N小時乘上QA人數的時間。
  • 和QA討論你的變更:
    Developer專注於單獨的設計或是獨立的Bug Fixing,可是QA卻是要保證系統整體的品質。所以請和QA討論你的變更在哪的元件,在架構上和這個元件相關的其他部分又有哪些。這些資訊都可以幫助QA在有限的時間內安排最合適的測試案例。
  • 重大變更請提交到一個獨立一個Build:
    如果你的團隊有在使用Daily Build,對於任何重大的變更,例如:共用元件的變更,升級某個第三方的元件。這些可能對於軟體系統的風險影響較大的改變,請留到下一個Build(或是這個Build就只進這個重大改變)。如果QA在Regression Testing中真的發現了影響,Developer可以直接比較Build-to-Build的改變,快速地隔離出可能有問題的變更。
要讓QA對團隊做最大的貢獻,就是不要浪費他們的時間。留給QA更多時間去設計複雜的測試案例,就更有機會在早期發現問題,早期治療。身為Developer也就少一點機會成為下一個在版本釋出後才出包的苦主。XD


Thursday, March 24, 2011

User-Buffered I/O for C Library

昨天討論到了 Win32 Kernel Mode 下的 Buffered I/O 和 Unbuffered I/O 的差異。其實在 User Mode 的環境下,C標準函式庫的File I/O(fread, fwrite...)也存在自己的 Buffered 方式,稱之為 User-Buffered I/O 或稱為 Stream Buffering。重點在於決定何時把 Buffer 內的東西透過 System Call 倒入 Kernel Mode。

  • Fully Buffered: 
    • 系統或 Caller 決定一個固定大小的緩衝區。
    • 當透過 File I/O 寫滿緩衝區,I/O 才會發生。
  • Line Buffered: 
    • 當 File I/O 寫到換行符號,I/O才會發生。
    • 在 Win32,沒有這個選項...
  • No Buffered:
    • 就是沒有Buffer...
參考資料:

Wednesday, March 23, 2011

Buffered I/O & Unbuffered I/O

簡單來說,

  • Buffered I/O 讀寫的資料都會經過 Cache Manager,會暫存一份在 Memory 當中,然後會根據作業系統決定寫回硬碟的時機(如果用 Write Through,還是可以自行控制)。此模式適合常讀寫檔案,因為可以快速的從 Memory 讀寫。
    (想想 L1, L2 cache 之於 RAM 的關係。)
  • Unbuffered I/O 讀寫的資料不會經過 Cache Manager,而是直接對硬碟讀寫。因此也不會因為大量讀寫資料造成 Cache Trashing。
以下是我的推測,不代表真實測試結果。XD
  • Installer 的檔案複製可能就適合 Unbuffered I/O,因為 Buffered I/O 可能會造成大量的 Cache Trashing。
  • 當有大量的 Cache Trashing 發生時,可能也會影響到 Unbuffered I/O 的效能。因為 Cache Manager 會對硬碟做大量讀寫,這個行為可能會和 Unbuffered I/O 產生 Resource Contention。

參考資料:

Tuesday, March 22, 2011

C++:auto_ptr, scoped_ptr, shared_ptr and weak_ptr

簡單整理各種Smart Pointer的特色:
  • auto_ptr
    • Supported in C++ Standard Library
    • Support Ownership Transfer
  • scoped_ptr
    • Supported in C++ TR1
    • Not Support Ownership Transfer
  • shared_ptr
    • Supported in C++ TR1
    • Using Reference Count
    • Object copy or assignment will increase the reference count.
  • weak_ptr
    • Supported in C++ TR1
    • Created from shared_ptr. Cannot be created alone.
    • Support Ownership Transfer
    • Easy and safe to check a pointer valid or not, without increasing the reference count.
延伸閱讀:

Saturday, March 12, 2011

軟體設計:設計是什麼?從自身經驗說起

在1992年,Jack W. Reeves發表了一篇"What is Software Design"。主要的論述就是表達"Source code is the design."的看法。

這點也和我自己的經驗不謀而合。在專案的規劃中,我們都把需求(Requirement), 設計(Design)和建構(Construction)分開成為一個循序的過程。設計過程是確保你可以建構出符合需求的working software/component。但是在真實的情況下,我所經歷的設計過程其實是一些不同的Activities不停的來回,而且沒有一定的順序。譬如說:


  • 首先要了解需求,開始設計。
  • 如果設計發現有定義模糊的需求,要進一步和stakeholder釐清。
  • 為了要確保設計是可行的,也須需要做POC(Proof of Concept)。
  • 魔鬼都在細節中。所以POC也不能馬虎,幾乎就是要做到Production品質的Code。
  • 如果POC發現問題,要回頭修正設計。
  • 如果POC發現其他Dependency的元件的問題,也要回頭在設計中考慮workaround。
  • 如果設計怎麼調整都沒有辦法解決,又要回頭修正需求/規格。
  • 甚至設計也有可能會發現需求/規格從未考慮過的事情,必須回頭和stakeholder反應。
  • 需求會中途改變,要重新修正設計,同時又要做POC。
  • ...


這一切的活動幾乎不會有停下來的一天,不過這個過程會逐漸收斂(當然也有可能不會收斂,那可能是某種大災難的指標。)。最終確保我可以建構出符合需求的程式碼,這個過程中也有個副產品,就是程式碼本身。

就譬如設計一台太空梭,為了要驗證太空梭的設計藍圖,團隊就必須打造出一台太空梭來通過各式各樣的測試。團隊可能先做出第一版的太空梭,發現一些問題,在重頭修正,打造第二版的太空梭,不停的重複這個過程,直到打造出大家都有信心把太空人送上天的太空梭。

這真的很難區分工程師到底是在建造還是在設計。很有可能在動手做的過程中,卻啟發了他可以改善設計元素。這就是真實世界的情況。

回到Jack的文章,對我來說Jack就只是希望軟體業界能夠承認這個事實,"Source code is the design."。基於這個事實,我們可能需要一些討論來看"需求, 設計, 建構"這樣的模型,到底適不適合用來詮釋軟體開發的過程。

P.S. 其實軟體專案開發過程中還有品質控管(Quality Control)的階段,為了方便說明,在此簡化了這個部分。

Friday, March 11, 2011

Security Enhancement in CRT: Good or Bad Side-Effect?

從Visual Studio 2005開始,編譯器都會建議使用者把某些CRT function換成更安全的版本。舉例來說,編譯器會建議你把strcpy改成strcpy_s。

這樣改的好處是什麼呢?在程式執行的過程中,有一種類型的bug叫做buffer overflow。以strcpy來說,就是你的來源字串長度可能比準備的buffer來的長,又因為strcpy沒有buffer size的資訊,所以在資料複製的過程中,就有可能覆蓋到buffer後面的資料。

如果buffer在stack上,那就有可能覆蓋到其他變數,甚至是call stack的return address。總之,小則程式執行不正常或崩潰,大則會被attacker利用來取得控制權。

換成安全的版本,編譯器就會嘗試把buffer size偷偷帶進strcpy_s當中,幫你在執行的過程中檢查有沒有buffer overflow。如果有,就會呼叫invalid parameter handler。

看到這裡,你可能就直接把全部的strcpy換成strcpy_s。

等等,預設的invalid parameter handler會丟出一個Exception,如果沒有exception handler,預設會執行UnhandledExceptionHandler()。講白話一點,就是會造成程式的崩潰。那使用者會看到什麼呢?答案是一個程式執行無效之類的對話框。

嘿,如果這是個文件編輯程式,使用者編輯到一半的文件就這樣不見了,不氣的跳腳才奇怪。假設樂觀的工程師如你,也是會考慮使用者經驗,又專注完美近乎科科吧!XD

要避免字串複製的buffer overflow,你還可以選擇用strncpy。如果buffer比較小,頂多就是來源字串塞不進去,之後你還可以做一些比較友善的處理。(例如跳出一個切腹道歉的視窗,幫使用者自動儲存文件,之後再crash。噗~)

另外一個方式也沒忘記,你可以覆蓋預設的invalid parameter handler,指到自製的例外處理函式。如果你想要集中處理的話,這樣可以讓所有的安全版的CRT function全都使用同一個handler。

結論:多想兩分鐘,你其實也可以有另一個選擇。XD