Visual C++ Directories/Inherited Properties Changes

前言

在 Visual C++ 裡頭,C++ Directories 定義了 Visual C++ 這個 IDE 要去哪些路徑下找尋 header files/library files/sources files ,在 2005/2008 裡頭,全域的設定可以在 Tools > Options > Projects and Solutions > VC++ Directories 下找到。

image

圖一、VC++ 2005 下的 VC++ Directories Dialog

這個 dialog 在 VC++ 2010 不復存在了,取而代之的是請使用者使用 property sheet 以及 per-project 的設定。

image

如果好奇為什麼做了這樣的更動,可以參考 [2] 的文章,主要是因為:

  1. 這樣全域的設定太過強大,當使用者有許多 projects 在同一台機器上 build 時,容易混淆出錯。
  2. 因為這個全域的設定檔,是存放在 %LOCALAPPDATA%\Microsoft\VisualStudio\8.0\VCComponents.dat (8.0 為 VC++ 2005,9.0 則是 2008) 這個檔案裡頭,而不是跟著每個專案檔。因此當使用者使用 source control system 時,不會把設定跟著同步到 server 上,check-out 的人也因此無法正確建置專案了。
  3. VCComponents.dat 是給 VCBuild.exe 讀的 INI 檔,而 VC++ 2010 開始改用 MSBuild.exe 來作為統一的 build utility [3] ,因此竟然要改,就連同格式一起換掉吧。

按編:這三個理由實在都很說不過去,不過既然這是 Visual C++ team 的決定,我們也只能接受了 :~

UI 變動

從 2010 之後,變動後的 VC++ Directories 不再放在 Tools > Options > Projects and Solutions > VC++ Directories ,而是以 per-project 方式存在,使用者可以:

  • 對 project 按右鍵,選擇 properties > Configuration Properties > VC++ Directories
  • 或是,從工具列:Project > <your-project-name> properties > Configuration Properties > VC++ Directories

選取 VC++ Directories 。

image

很快地,妳/你會發現,新的 dialog 跟原有的沒什麼差別,而實際上也是,只是現在的變更都是 per-project 的,不再對所有的專案有效,因此若是妳/你想將 Boost library 作為所有 VC++ project 都會使用的函式庫,就沒辦法從這裡完成;也或是,妳/你更新了 Boost 版本,每個已經使用 Boost 又想升級的 projects ,就得手動來更改。

檔案位置與格式變化

說完了使用上的變動,接著是檔案位置的更動,存放的位置從:

%LOCALAPPDATA%\Microsoft\VisualStudio\8.0\VCComponents.dat

換到了:

%LOCALAPPDATA%\Microsoft\MSBuild\v4.0\

下,並且以 Microsoft.Cpp.<Platform>.user 命名:

  • Microsoft.Cpp.ARM.user
  • Microsoft.Cpp.Itanium.user
  • Microsoft.Cpp.Win32.user
  • Microsoft.Cpp.x64.user

image

新的檔案格式稱作:Property sheet ,附檔名 props 。有了這個檔案,我們還是有機會可以做到 2005 & 2008 時的全域設定。先前提到了:為了迎接 MSBuild 的來臨,VC++ 將 INI 格式變成了 XML 格式,所以客製化上還是很簡單的,任選一個 platform 的檔案打開:

image

所以一種修改方式,就是直接用編輯器修改 XML ,看想修改哪些就可以改,值得一提的是,以往在 2005 & 2008 裡頭,路徑名不可以是 Visual C++ 裡頭的 Macro ,現在反倒是可以了,因為 IDE 是將檔案中的值讀入後才做解析。另外一種方式則是透過 Visual C++ 內建的 Property Manager 修改,我們放到下一節介紹。

Inherited Values

wdk803

當 Visual C++ 將 property sheet 讀入後,會當做 Inherited values ,對於當前 project 來說,是不可編輯的,我們必須繞到 props 檔去,或是使用 property manager 編輯。property manager 可以從工具列 > View > Property Manager 找到:

image

叫起 Property Manager 後,可以看到目前 project configuration 所涵蓋的 platform 的設定。 Double click 後,可以看到跳出一個視窗,

image

跳出來的視窗長得很像 project properties ,不過仔細看會發現 Configuration 和 Platform 已經被鎖死成對應的 property sheet 檔所代表的設定,接著,直接編輯,然後存檔,這樣就完成了修改全域 property sheet 了。

VC++ 2010 & VC++2012

值得注意的是,property sheet 放在 MSBuild 的 local app data 下,而目前 2010 和 2012 所使用的 MSBuild 都是 4.0 [4],因此一旦我們修改了 property sheet ,可是會同時影響到 2010 跟 2012 的 ... Orz ...

Reference

  1. Inherited Properties and Property Sheets: http://blogs.msdn.com/b/vsproject/archive/2009/06/23/inherited-properties-and-property-sheets.aspx
  2. VC++ Directories: http://blogs.msdn.com/b/vsproject/archive/2009/07/07/vc-directories.aspx
  3. VCBuild vs. C++ MSBuild on the Command Line: http://blogs.msdn.com/b/vcblog/archive/2010/01/11/vcbuild-vs-c-msbuild-on-the-command-line.aspx
  4. MSBuild Toolset: http://msdn.microsoft.com/en-us/library/bb383796.aspx

VC++ 2012 Can't Build Driver Project

這幾天終於趁著聽課的空檔,把電腦裝了 VC++ 2012 ,這版的一大特色:整合了 device driver 的開發。很快地試玩了一下它的 project wizard :

wdk801

結果,很不幸地失敗 ...

1>C:\Program Files (x86)\Windows Kits\8.0\Include\KM\wdm.h(10920): fatal error C1003: error count exceeds 100; stopping compilation

再看一下 Error list:

Error    1    error C2220: warning treated as error - no 'object' file generated    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\sal.h    2884
Error    4    error C2054: expected '(' to follow '_IRQL_requires_same_'    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\ntdef.h    1897
Error    5    error C2085: 'EXCEPTION_ROUTINE' : not in formal parameter list    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\ntdef.h    1903
Error    6    error C2085: 'EXCEPTION_ROUTINE' : not in formal parameter list    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\ntdef.h    1905
Error    7    error C2143: syntax error : missing ';' before '*'    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\ntdef.h    1905
Error    8    error C2143: syntax error : missing ')' before 'constant'    C:\Program Files (x86)\Windows Kits\8.0\Include\KM\wdm.h    10384
Error    9    error C2143: syntax error : missing '{' before 'constant'    C:\Program Files (x86)\Windows Kits\8.0\Include\KM\wdm.h    10384
Error    10    error C2059: syntax error : '<Unknown>'    C:\Program Files (x86)\Windows Kits\8.0\Include\KM\wdm.h    10384

真的是很 ooxx 。問題都是發生在 sal.h, ntdef.h wdm.h 等等基本的 header files 裡。

這類的問題通常是發生在 include 了錯誤的 header files ,所以回頭檢視 solution property : Project >  Properties > Configuration Properties > VC++ Directories 。

image

include directories 多了很多路徑,其他像是 Executable Directories 、Library Directories 也有一樣的問題。嗯嗯,VC++ 2012 匯入了電腦上其他版本 VC++ 的設定,可能的原因之一:安裝完 2012 、第一次啟動時,我允許了 2012 去找尋舊版本的設定並匯入所導致。再點開 Include Directories 看一下:

image

發現這些多餘的路徑都是來自 Inherited values 。這裡,我試了兩種解決方式:

  1. 解法一
    取消 Inherit from parent or project defaults ,然後重新把需要的路徑一個一個加回去,也就是手動把
    $(ProjectDir)、$(CRT_INC_PATH)、$(ddk_INC_PATH)、$(KIT_SHARED_INC_PATH) 加上去。手動作這件事除了麻煩外,還只能用在這個 project 上,也就是一旦我開了新的 driver solution 或是在當下 solution 下新增新的 driver project 都得重做一次。
  2. 解法二
    比較一勞永逸, VC++ 把這個 Inherited Values 定義在 C:\Users\<user-name>\AppData\Local\Microsoft\MSBuild\v4.0 下的檔案裡頭,檔案是根據不同 configuration 而命名:
    Microsoft.Cpp.ARM.user.props
    Microsoft.Cpp.Itanium.user.props
    Microsoft.Cpp.Win32.user.props
    Microsoft.Cpp.x64.user.props
    *.props 是 XML 格式,把不用的 path 都清除乾淨,只留下 default value 就可以了。
    <?xml version="1.0" encoding="utf-8"?>
    <Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <PropertyGroup>
        <ExecutablePath>$(ExecutablePath)</ExecutablePath>
        <IncludePath>$(IncludePath)</IncludePath>
        <ReferencePath>$(ReferencePath)</ReferencePath>
        <LibraryPath>$(LibraryPath)</LibraryPath>
        <SourcePath>$(SourcePath)</SourcePath>
        <ExcludePath>$(ExcludePath)</ExcludePath>
      </PropertyGroup>
    </Project>

    這個方法是比較一勞永逸,不過有個大缺點:不同版本的 Visual C++ 會共用這個檔案。像我的電腦修改了後, VC++ 2010 的 VC++ project 的預設值也會受到影響,但已經產生的、既有 project 則不受影響,算是不幸中的大幸。

Facebook App -- Getting Started with Apps on Facebook + Heroku

Facebook App 時下分成三類:

  1. Facebook for Websites
    這類的 apps ,可以讓妳/你使用 Facebook 提供的 social plugins 來幫自己的網站加分,像是在你的網頁中加入"讚"按鈕、讓使用者使用 Facebook 帳號登入、存取使用者在 Facebook 的資料。
  2. Mobile Apps
    Facebook 分別在 iOS 和 Android 上提供 Object-C 和 Java 版本的 SDK ,來讓開發者可以提供 native apps。其他還有 PHP 和 JavaScript 可以選擇。
  3. Apps on Facebook
    把我們的 apps 整合在 Facebook 上,使用者必須透過 Facebook 來存取我們的 apps , Facebook 提供適當的頁面,並把我們的 apps 導入其中的 iframe 裡頭。

特別要注意的是, Facebook 只提供我們使用 Facebook 的 social platform ,但它並不提供 code hosting ,因此開發 Apps on Facebook 需要自己準備 web server 或是第三方的 web hosting ,運氣不錯的是,Facebook 和 Heroku 有正式的合作,因此若是找不到代管服務或是懶得自己架站的開發者,可以先使用 Heroku 來試試看。

Apps on Facebook

步驟一

登入 https://developers.facebook.com/apps 。第一次登入時, Facebook 會詢問您相關的 permission ,選 allow 就可以。之後就會看到下面的畫面,點選 Create New App

FB_App_1

步驟二

image

  • App Name: 輸入妳/你的 app name 。
  • App Namespace: 其實就是我們 app 的 URL 。它會附加在 http://apps.facebook.com 之後,所以必須是個 unique name ,另外還有一些名規則。 輸入完後 Dialog 會幫你 check ,若是失敗也會報錯。
  • Web Hosting: 目前 Facebook 只跟 Heroku 官方合作,所以要是我們沒有自己的 hosting server ,也可以考慮一下 Heroku ,它支援 PHP 、 Python 、 JavaScript 、 Ruby 。

步驟三

跟 Facebook 說一下,妳/你不是機器人吧~

步驟四

image

選擇一下妳/你想用的開發語言。

image

若是事先沒有 Heroku 帳號也不用擔心,透過這個步驟填入 email 就會幫妳/你申請。

步驟五

image

看到這個 dialog 出現,就恭喜妳/你成功囉。

步驟六

當妳/你點下 Go To App 後,瀏覽頁會被導到 Heroku 代管的 app page 去,以這邊的例子會是:http://evening-plateau-9255.herokuapp.com

FB_App_3

開發

步驟一、找到對應的 Git repository

若是之前沒有 Heroku 的帳號,當完成上面的步驟後,Heroku 會寄一封啟動信到步驟四的信箱去。或是之前已經有 Heroku 帳號,可以直接到 https://dashboard.heroku.com/apps? 去看一下,會發現多了一個 evening-plateau-9255 ,點進去後,請找 Settings > Info > Git URL ,以本文的例子, Heroku 配給我們的 git url 是 git@heroku.com:evening-plateau-9255.git

步驟二、下載 Herolu toolbelt

toolbelt 是 cmd line 工具包,主要包含了 Heroku 環境初始化工具、app 的 local 模擬和 Git 。

步驟三、初始化環境

安裝完 toolbelt 後,它應該已經被加入到環境變數裡頭。這時,請準備一個新資料夾來當做 working folder ,並使用 cmd.exe 切到該 folder 下,執行:

heroku login

image

步驟四、Clone 一份來改吧

還記得自己的 app git URL 嗎?

git clone git@heroku.com:your_app_name_in_heroku.git -o heroku

到這裡,妳/你已經可以開始做些了,若是不熟悉 Git ,可以參考下面指令把你的修改上傳到 server 上去:

git commit -am "commit message"
git push heroku master

Reference

  1. Canvas Tutorial: http://developers.facebook.com/docs/appsonfacebook/tutorial/
  2. Getting Started with Your Facebook App on Heroku: https://devcenter.heroku.com/articles/facebook

Windows Debugging 2 – Kernel Debugging with WinDbg and VMware

Windows Vista 之後已經不再將開機選項儲存於 boot.ini 中,而是改以 BCD (Boot Configuration Data) 儲存,因此以前透過編輯 boot.ini 啟動 Windows kernel debugging 的方法也得更新一下。這裡我們透過 bcdedit.exe 來編輯 BCD ,讓 Windows 啟動後可以進入 debug mode 。

VMware 設定

Debuggee 部分,和以前一樣使用 named piped 來替 guest OS 模擬 serial port (COM port)。

  1. 進入 guest OS 設定。 com port
  2. 新增硬體。
    com port 2
  3. 選擇 Serial Port 裝置。
    com port 3
  4. 選擇 named pipe 來模擬我們的 serial port。
    com port 4
  5. 替 named pipe 取一個名字,這個名字等會會被 WinDbg 使用。
    com port 5
  6. 最後別忘了把 Yield CPU on Poll 選項勾起。
    com port 6
  7. 完成新增後,請留意 VMware 替我們新增的 Serial Port 的編號,以上圖為例是:Serial Port 2 

Windows Boot 設定

  1. 使用 administrator 權限開啟一個 cmd。
    start cmd
  2. 透過 BCDEdit 編輯開機選項,使用 COM port (serial port)來做為 debugger 和 debuggee 間的溝通,debugport 的編號請根據 VMware 的 Serial Port 設定而定:
    bcdedit /dbgsettings serial debugport:2
  3. 複製現有的開機選項到 DebugEntr:
    bcdedit /copy {current} /d DebugEntry
    執行完該指令後,會出現一個 ID ,代表我們要複製出來的開機選項,該 ID 在每台電腦上都不太一樣。
  4. 調整開機選樣的順序:
    bcdedit /displayorder {current} {ID}
  5. 打開 debug 選項:
    bcdedit /debug {ID} ON
  6. 設定成預設開機選項:
    bcdedit /default {ID}
    bcdedit edit

 

Enable Debug Print

Vista之後,DbgPrintEx()、vDbgPrintEx()、vDbgPrintExWithPrefix()、KdPrintEx() 等選擇性的輸出函式預設是關閉的,要啟動的話,需要到 registry 上設定對應的 log level [3]:

到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter 下新增 DEFAULT=dword:0000ffff
實際數值,依照需求而定,0000ffff 足以應付大多數用途。

Debug Print Filter

使用

在 bcdedit 設定完成後,重開機,便會看到新的開機選項 DebugEntry:

boot 1

選擇 DebugEntry 後,我們便可以在 debugger 的機器上的 cmd 執行下面指令來進行 kernel debugging,com_1 就是我們當初在 VMware guest OS 裡頭設定的名字:

windbg -b -k com:pipe,port=\\.\pipe\com_1,resets=0

停用驅動程式簽章增強(Disable Driver Signature Enforcement)

Vista 之後的 64-bit OS ,Microsoft 都會要求 drivers 必須有 WHQL 的認證才能啟動。這對於一般使用者來說可能沒有影響,但對於有在開發、測試 drivers 的人就會是個問題。

最簡單的方式是,開機時,使用 F8 進入進階模式:
boot 1
並選擇(停用驅動程式簽章增強)Disable Driver Signature Enforcement。
boot 2

Test Signing

若是 drivers 有 test sign ,那麼我們還可以使用 BCDEdit 來允許 testing sign driver 的載入[4]:
bcdedit /set TESTSIGNING ON
設定完成後,需要重開機。

要驗證 test sign 是否成功,可以使用 bcdedit 檢查,或是觀察桌面右下角的文字說明:

test signing

Reference

  1. Boot Parameters to Enable Debugging
    http://msdn.microsoft.com/en-us/library/windows/hardware/ff542279%28v=vs.85%29.aspx
  2. Kernel Debugging in Windows Vista
    http://msdn.microsoft.com/en-us/windows/hardware/gg487520
  3. Reading and Filtering Debugging Messages
    http://msdn.microsoft.com/en-us/library/windows/hardware/ff551519%28v=vs.85%29.aspx
  4. The TESTSIGNING Boot Configuration Option
    http://msdn.microsoft.com/en-us/library/windows/hardware/ff553484%28v=vs.85%29.aspx

See Also

  1. Windows Debugging – Kernel Debugging with WinDbg and VMware
    http://keikoblog.blogspot.com/2009/03/windows-debugging-kernel-debugging-with.html

VMware 64-bit Guest OS Support

去年聖誕節買的 x201 ,下單前還特別注意 CPU 是否支援 Virtualization ,直到上個月拿來跑 64 bit guest OS 時,才發現 Windows 會進入開機、當機的無窮迴圈。後來才發現一些小眉角,尤其是在筆電上:

  1. 不是 64-bit OS 就可以直接支援 64-bit guest OS ,必須 CPU 有支援 VT 才行。
  2. 大多數筆電的 VT 預設值都是關閉,必須進入 BIOS 啟動。
  3. 在 BIOS 中,若有 Hardware data execution 之類的 H/W security 功能最好也關閉。
  4. 部分筆電,像是 Lenovo x201 ,要讓上述設定生效,還必須將電池、電源移除,按下電源開關,進行放電, BIOS 的改動才會生效。

測試方式

如果不確定 CPU 是否支援 VT ,可以到 Intel/AMS 的網站查詢,像是 Intel Processor Spec. FinderAMD Desktop Processor

Processor Check for 64-Bit Compatibility

[感謝 Allen Lai 提醒]

VMware 提供了一個快速的檢測工具——Processor Check for 64-Bit Compatibility,雖然已經很久沒更新了,不過它的結果明確易懂,像是下圖就說明:Processor Check for 64-Bit Compatibility

不過根據說明文件,它目前只支援特定的 processors ,而且還有幾項是 experimental support:

  • AMD™ Athlon™ 64, revision D or later
  • AMD Opteron™, revision E or later
  • AMD Turion™ 64, revision E or later
  • AMD Sempron™, 64-bit-capable revision D or later (experimental support)
  • Intel™ EM64T VT-capable processors (experimental support)

VMware CPU Identification Utility

或是利用 VMware 的 CPU Identification Utility ,這是個 可用來開機的 ISO 檔,使用時只需要掛載進 VMware 即可 。

未啟動 VT 時的輸出:

CPU Identification Utility (Before)

啟動 VT 後:

CPU Identification Utility

上面的圖是 x201 開啟 VT 後的結果,根據說明文件和實驗,似乎紅框裡有一個是 Yes ,就可以支援 64-bit guest OS 。根據 Allen Lai 的測試,即使 64-bit LongMode 顯示 YES ,也並非能支援 64-bit guest OS。所以目前還沒有比較好的解讀方式 … 大概只是方便你把結果丟到 support 或 community 去發問 …

最後要注意的是,當 CPU features 設定改變後,都可能讓已經建立好的 snapshots 出問題。

Reference

  1. VMware Shared Utility: http://www.vmware.com/download/shared_utilities.html
  2. Ensuring your hardware is functioning correctly: http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=1003690
  3. Ensuring Virtualization Technology is enabled on your VMware host: http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=1003944
  4. Hardware and firmware requirements for 64-bit Guest Operating Systems: http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=1003945
  5. Intel Processor Spec. Finder: http://ark.intel.com/Default.aspx
  6. AMD Desktop Processor: http://products.amd.com/en-us/desktopcpuresult.aspx

更新

2011/4/26: 補上 x201 未啟動 VT 的 VMware CPU Identification 結果截圖。

2011/4/27: 補充 Allen Lai 的連結和測試工具 Processor Check for 64-Bit Compatibility 。

Re: GCC Bug

原文:GCC Bug

哈,這次老師給我的作業可是難題,有好一陣子沒有碰觸 gdb ,更何況這次還要跟 FPU 打交道,下面是我的測試程式,和原文只有小部份差異,但重點部份是一樣的:

#include <cstdio>
using namespace std;

double f(double z)
{
    return z;
}

void foo(double fraction)
{
    double z = fraction * 20.0;
    int t1 = z;
    int t2 = f(z);
    printf("t1=%d, t2=%d\n", t1, t2);
}

int main()
{
    foo(6.0/20.0);
    return 0;
}

問題

輸出:

6, 6

是我們預期的結果。但在某些情況下卻會產生

5, 6

這樣的輸出。但若是在 compile 時加上 -ffloat-store 則又能得到正確的結果。根據 gcc optimization options 的說明:

Do not store floating point variables in registers, and inhibit other options that might change whether a floating point value is taken from a register or memory.

我們懷疑是從 register 讀出時發生意想不到的進位,導致結果有所出入。

實驗

一開始在嘗試 reproduce 時就遇上問題,在和 aaa 討論後才發現,我們使用的 gcc 版本和 compile options 都不盡相同。最後確定了發生問題的環境:

  1. cygwin + gcc 3.4.4
  2. gcc –O text.c

下面是我嘗試過的平台和結果:

  -O -O3
gcc 4.4.1 No No
gcc 3.4.4 (cygwin) Yes No

有趣的開始!

O3 版本

g++ -Wall -O3 -S -masm=intel main.cpp

這個版本是最好理解的, compiler 已經最佳化到:

  • 不呼叫 foo()、f(),直接以常數取代。(沒記錯,這算是 compiler const folding 和 const propagation 的範疇)

看一下 assembly 就很清楚:

LC2:
	.ascii "t1=%d, t2=%d\12\0"
	.align 4
...
_main:
    push   ebp
    mov    eax, 16
    mov    ebp, esp
    sub    esp, 24
    and    esp, -16
    call   __alloca
    call   ___main
    mov    DWORD PTR [esp], OFFSET FLAT:LC2 ; 看一眼 LC2 ,一個取址動作 :)
    mov    edx, 6
    mov    eax, 6
    mov    DWORD PTR [esp+8], edx
    mov    DWORD PTR [esp+4], eax
    call   _printf
    leave
    xor    eax, eax
    ret

O 版本

g++ -Wall -O -S -masm=intel main.cpp

O 版本就沒那麼聰明,而是真正的去呼叫 foo() :

_main:
    push   ebp
    mov    ebp, esp
    sub    esp, 8
    and    esp, -16
    mov    eax, 16
    call   __alloca
    call   ___main
    fld    QWORD PTR LC4
    fstp   QWORD PTR [esp]
    call   __Z3food
    mov    eax, 0
    leave
    ret

而且可以發現,LC4 是 foo() 的參數:

LC4:
	.long	858993459
	.long	1070805811
	.text
	.align 2


0.3 的 IEEE 754 Double precision (64 bits) 表示式:
0 01111111101 00110011001100110011  110011001100110011001100110011
                        1070805811                       858993459

嗯嗯,O 版本先做了簡單的運算展開。不過在進入 foo() 的 floating point 計算前,得先對 x87 FPU 有簡單的概念 :

image節錄自: Intel® 64 and IA-32 Architectures Software Developer’s Manual ─ Volume 1: Basic Architecture

x87 上用來儲存資料的 registers 被組織成一塊 stack 的,並且搭配了 control, status 等 registers 可供控制、查詢之用。 Stack 以下圖的方式運做:

image節錄自: Intel® 64 and IA-32 Architectures Software Developer’s Manual ─ Volume 1: Basic Architecture

Stack 的 top 又稱為 ST(0),是許多 FPU 指令的隱藏 operand 之一,像是待會在 foo() 看見的 fld、fstp、fmul 指令都會 implicitly 操作 ST(0) 。

__Z3food:
    push    ebp
    mov     ebp, esp
    push    ebx
    sub     esp, 20
    fld     DWORD PTR LC1
    fmul    QWORD PTR [ebp+8]
    fnstcw  WORD PTR [ebp-6]
    movzx   eax, WORD PTR [ebp-6]
    or      ax, 3072
    mov     WORD PTR [ebp-8], ax
    fldcw   WORD PTR [ebp-8]
    fist    DWORD PTR [ebp-12]      ; store integer
    fldcw   WORD PTR [ebp-6]
    mov     ebx, DWORD PTR [ebp-12]
    fstp    QWORD PTR [esp]
    call    __Z1fd
    fnstcw  WORD PTR [ebp-6]
    movzx   eax, WORD PTR [ebp-6]
    or      ax, 3072
    mov     WORD PTR [ebp-8], ax
    fldcw   WORD PTR [ebp-8]
    fistp   DWORD PTR [esp+8]
    fldcw   WORD PTR [ebp-6]
    mov     DWORD PTR [esp+4], ebx
    mov     DWORD PTR [esp], OFFSET FLAT:LC2
    call    _printf
    add     esp, 20
    pop     ebx
    pop     ebp
    ret

這邊使用倒推法,來找到 t1 及其在 assembly 的表示,尋著上頭的 highlight 部份,可以發現:

  1. printf 從 ebx 拿到 t1 的值,也就是 ebx 、
  2. ebx 由 ebp-12 得到、
  3. ebp-12 則是 fist 的結果,也就是 FPU 的 ST(0)、
  4. 往上回溯,只有 L6 處有個 fld ,因此,ST(0) 指的是 fmul 的結果、

反推的過程也符合程式的邏輯,所以下一步就是用 debugger 去追蹤:

Breakpoint 1, 0x0040104f in foo ()
1: x/i $pc
0x40104f <_Z3food+7>:   flds   0x402030
(gdb) info float
  R7: Empty   0x3ffd9999999999999800
  R6: Empty   0x00e07c93022200240000
  R5: Empty   0x51c27c92fe956123a74c
  R4: Empty   0x79d37c9a06007c930460
  R3: Empty   0x0a887c9479870022bf80
  R2: Empty   0xbf9800010a886123debc
  R1: Empty   0x00006123a74c7c93043e
=>R0: Empty   0xdee46123de0c6123d8cc

Status Word:         0xffff0000
                       TOP: 0
Control Word:        0xffff037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest
Tag Word:            0xffffffff
Instruction Pointer: 0x1b:0x004010c8
Operand Pointer:     0xffff0023:0x0022cd10
Opcode:              0xdd1c

我們將中斷點設在 foo() 函式,接著使用 info float 去 dump FPU 相關的 register (FPU 相關 register 似乎無法加到 gdb 的 Automatic Display!?所以每次都需手打 ),比較特別的幾個要點都被 highlight 了:

  1. R0 ~ R7 即剛剛提到的 FPU data register stack : stack 目前是空的,雖然指向 R0 ,但顯示為 Empty
  2. Control Word 裡頭的 RC (rounding-control)決定了 floating-point 的進位(round)方式。
    image 
    節錄自: Intel® 64 and IA-32 Architectures Software Developer’s Manual ─ Volume 1: Basic Architecture

認識了 gdb 對於 FPU 的輸出後,可以開始一步步逼近問題核心 ── ebp-12 了。

foo() 的一開始,是讀入 0x402030 上的值到 FPU stack 上:

Breakpoint 1, 0x0040104f in foo ()
1: x/i $pc
0x40104f <_Z3food+7>:   flds   0x402030

好奇這 magic number 是何來的,可以透過之前的 disassembly 結果:

LC1:
	.long	1101004800    ; = 0x41a00000
	.text
	.align 2

或是動態的用 debugger 觀看:

(gdb) x 0x402030
0x402030 <_data_start__+48>:    0x41a00000 ; = 1101004800

而 0x41a00000 便是 20.0 的 IEEE 754 0.3 的 IEEE 754 Double precision (64 bits) 表示式。來用 debugger 確認一下:

Breakpoint 1, 0x0040104f in foo ()
1: x/i $pc
0x40104f <_Z3food+7>:   flds   0x402030
(gdb) info float
  R7: Empty   0x3ffd9999999999999800
  R6: Empty   0x00e07c93022200240000
  R5: Empty   0x51c27c92fe956123a74c
  R4: Empty   0x79d37c9a06007c930460
  R3: Empty   0x0a887c9479870022bf80
  R2: Empty   0xbf9800010a886123debc
  R1: Empty   0x00006123a74c7c93043e
=>R0: Empty   0xdee46123de0c6123d8cc

(gdb) si
0x00401055 in foo ()
1: x/i $pc
0x401055 <_Z3food+13>:  fmull  0x8(%ebp)

(gdb) info float
=>R7: Valid   0x4003a000000000000000 +20
  R6: Empty   0x00e07c93022200240000

執行完上面的指令後, FPU 的 data stack 現在是 20 ,接著進行 x 0.3 的乘法(ebp+0x8 是 foo() 的參數,即 0.3):

0x401055 <_Z3food+13>:  fmull  0x8(%ebp)

(gdb) si
0x00401058 in foo ()
1: x/i $pc
0x401058 <_Z3food+16>:  fnstcw -0x6(%ebp)
(gdb) info float
=>R7: Valid   0x4001bffffffffffffe00 +6
  R6: Empty   0x00e07c93022200240000

得到了結果:+6 ,無誤。不過接下來的 FPU control word 卻令人意外:

(gdb) info float
...
Control Word:        0xffff037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest

0x40105f <_Z3food+23>:  or     $0xc00,%ax
0x401063 <_Z3food+27>:  mov    %ax,-0x8(%ebp)
0x401067 <_Z3food+31>:  fldcw  -0x8(%ebp)

(gdb) info float
...
Control Word:        0xffff0f7f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round toward zero

O 版將 RC 從 Round to nearest 改變成 Round toward zero 。這會影響到 fist 的運作。在執行 fist 前,我們把預期的結果: 6.0 的 IEEE 754 Double precision 求出來:0x4018000000000000 ;而目前的 R7 值是 0x4001bffffffffffffe00 ,小於該值。表示 R7 現在是不足 6.0 ,而此時又改以 Round toward zero 來進行取整數,那結果會是 5 則是預期了。

(gdb) si
0x0040106a in foo ()
1: x/i $pc
0x40106a <_Z3food+34>:  fistl  -0xc(%ebp)
(gdb) info float
=>R7: Valid   0x4001bffffffffffffe00 +6
  R6: Empty   0x4bc000010101e259acd0
...
Control Word:        0xffff0f7f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round toward zero

(gdb) x 0x22ccfc              ; ebp - 0xc = 0x22ccfc
0x22ccfc:       0x61100049

(gdb) si
0x0040106d in foo ()
1: x/i $pc
0x40106d <_Z3food+37>:  fldcw  -0x6(%ebp)
(gdb) x 0x22ccfc
0x22ccfc:       0x00000005

執行結果印證了我們的預期。

-ffloat-store 版

模仿取得 O 版 disassembly 的方式來取得 -ffloat-store 的結果:

g++ -Wall -O -ffloat-store -S -masm=intel main.cpp

-ffloat-store 版和 O 版相同,會去呼叫 foo() ,不過比較兩者 foo() 可以發現: -ffloat-store 版只多了兩行,在進行 x 0.3 後,會馬上將值寫回 call stack 上,並再讀入。

image也就是,當 6.0 被寫入記憶體時,是以 Round to nearest 的方式寫進:

(gdb) si
0x00401058 in foo ()
1: x/i $pc
0x401058 <_Z3food+16>:  fstpl  -0x10(%ebp)
(gdb) info float
=>R7: Valid   0x4001bffffffffffffe00 +6
  R6: Empty   0xdbc000010101e4ada9d8
...
Control Word:        0xffff037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest

而讀回 FPU stack 時,已經是個精確的 6.0 :

(gdb) si
0x0040105e in foo ()
1: x/i $pc
0x40105e <_Z3food+22>:  fnstcw -0x12(%ebp)
(gdb) info float
=>R7: Valid   0x4001c000000000000000 +6
  R6: Empty   0x10000000000400010000
...
Control Word:        0xffff037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest

WinDbg 實驗: Command Line Arguments 的傳遞

一個 C/C++ 程式可以透過 main() 的 argument list 取得 client 端輸入的 command line arguments:

int main( int argc, char* argv[] ) { ... }

如果好奇這是如何地從無到有,可以寫一段程式碼來 trace 。

先準備好 sample code:

#include <iostream>
using namespace std;

int main( int argc, char* argv[] )
{   // 程式的內容不是重點
    cout << "hello world";
    cin.get();
    return 0;
}

透過 VC++ 或是 WinDbg 在 main() 設定 breakpoint 來追蹤:

  1. Compiler: VC++ 2005
  2. OS: Windows XP
  3. 在 cmd 中執行 CrtDemo05 test

image

0:000> bp CrtDemo05!main
*** WARNING: Unable to verify checksum for CrtDemo05.exe

0:000> bl
 0 e 004377d0     0001 (0001)  0:**** CrtDemo05!main

0:000> g
Breakpoint 0 hit
eax=00383088 ebx=7ffda000 ecx=00383028 edx=00000001 esi=010df742 edi=010df6f2
eip=004377d0 esp=0012ff58 ebp=0012ffb8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
CrtDemo05!main:
004377d0 55              push    ebp

0:000> k
ChildEBP RetAddr  
0012ff54 0044a203 CrtDemo05!main [f:\src\_experiment\crtdemo05\crtdemo05\main.cpp @ 6]
0012ffb8 00449fbd CrtDemo05!__tmainCRTStartup+0x233 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 327]
0012ffc0 7c816d4f CrtDemo05!mainCRTStartup+0xd [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 196]
0012fff0 00000000 kernel32!BaseProcessStart+0x23

進入 main() 之前的兩個函式都是 CRT 的一部分,負責完成基本但必要的初始化,舉凡 Global variables、Heap、I/O 等等都屬於這個範疇。這樣一來,身處 main() 後頭的我們才能順利工作。所以照這情勢看來,想知道 argument list 怎麼來,就得去 trace 這兩個 functions ,幸運的, VC++ CRT 的 source code 是隨著安裝程式散發的,通常就在:

C:\Program Files\Microsoft Visual Studio 8\VC\crt

視安裝路徑而定。也可以 double click VC++ 的 Call Stack 進入程式碼裡頭。

如果想利用 debugger 追蹤或試驗 CRT ,不妨把 CRT link 改成 debug mode 並且是 static link 。這可以省下一些時間、增加 tracing 時的可讀性和便利性,因為像是 debug 下的 dynamic link 會使用 ILT ,它所引入的間接性,會讓定位 functions 或 symbols 徒增額外的時間,造成實驗的不便。

VC++ 有所謂的 Microsoft C++ ,其程式進入點有自己的一套方式來定義,至少在名稱上,就可以找到四種:

  1. main
  2. wmain
  3. WinMain
  4. wWinMain

不過它們使用的 CRT 是同一份程式碼,並且使用了跟 TCHAR.h 相同的手法來區別,但確實由 WPRFLAG 這個 macro 所控制凡;凡是見到 t、_t、__t 開頭的名稱,都有機會透過它來替換掉,不過 __tmainCRTStartup() 可說是例外。下表節錄與 command line 相關的 t、_t、_t 開頭 symbols:

 

ansi/console

ansi/GUI

wide/console

wide/GUI

_tmainCRTStartup

mainCRTStartup

WinMainCRTStartup

wmainCRTStartup

wWinMainCRTStartup

_tcmdln

_acmdln

_acmdln

_wcmdln

_wcmdln

_targv

__argv

__argv

__wargv

__wargv

GetCommandLineT()

GetCommandLineA

GetCommandLineA

__crtGetCommandLineW

__crtGetCommandLineW

_tsetargv()

_setargv

_setargv

_wsetargv

_wsetargv

我們先從 ansi 版的 main() ,也是 C/C++ Standard 所描述的 main() 開始:

mainCRTStartup()

mainCRTStartup() 基本上是一個 forward function :

int _tmainCRTStartup( void )
{
    __security_init_cookie();
    return __tmainCRTStartup();
}

__tmainCRTStartup()

就像前面提到的:__tmainCRTStartup() 就是正港的 __tmainCRTStartup() 沒有 ansi, wide 的替換,很快地就可以梳理出跟 command line 相關的程式:

int __tmainCRTStartup( void )
{
    // ...

    __try {
        // ...
    
        /* get wide cmd line info */
        _tcmdln = (_TSCHAR *)GetCommandLineT();

        /* get wide environ info */
        _tenvptr = (_TSCHAR *)GetEnvironmentStringsT();

        if ( _tsetargv() < 0 )
            _amsg_exit(_RT_SPACEARG);
        if ( _tsetenvp() < 0 )
            _amsg_exit(_RT_SPACEENV);

        // ...

    #ifdef _WINMAIN_
        lpszCommandLine = _twincmdln();
        mainret = _tWinMain( (HINSTANCE)&__ImageBase,
                             NULL,
                             lpszCommandLine,
                             StartupInfo.dwFlags & STARTF_USESHOWWINDOW
                                  ? StartupInfo.wShowWindow
                                  : SW_SHOWDEFAULT
                            );
    #else  /* _WINMAIN_ */
        _tinitenv = _tenviron;
        mainret = _tmain(__argc, _targv, _tenviron);
    #endif  /* _WINMAIN_ */

        // ...
    }
    __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) {
        // ...
    }

    return mainret;
}

從上面的程式碼可以發現,重點就在於兩個 functions 上:

  1. GetCommandLineT()
  2. _tsetargv()

但是當我們將 breakpoint 設在 GetCommandLineT() 時,卻無法 step in 。這時可以切換到 assembly 模式,透過 WinDbg diassembly window 或是 WinDbg 的 uf 幫助來持續追蹤:

0:000> uf __tmainCRTStartup
...
CrtDemo05!__tmainCRTStartup+0x1b4 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 300]:
  300 0044a184 ff1564924b00    call    dword ptr [CrtDemo05!_imp__GetCommandLineA (004b9264)]
  300 0044a18a a3a4814b00      mov     dword ptr [CrtDemo05!_acmdln (004b81a4)],eax
  303 0044a18f e8a89efeff      call    CrtDemo05!ILT+55(___crtGetEnvironmentStringsA) (0043403c)
  303 0044a194 a3d45e4b00      mov     dword ptr [CrtDemo05!_aenvptr (004b5ed4)],eax
  305 0044a199 e87a9ffeff      call    CrtDemo05!ILT+275(__setargv) (00434118)
  305 0044a19e 85c0            test    eax,eax
  305 0044a1a0 7d0a            jge     CrtDemo05!__tmainCRTStartup+0x1dc (0044a1ac)
...

在 Windows 裡頭, _imp、__imp 開頭的 name mangling 代表: symbol 代表是從 DLL export 出的 ,可見 CrtDemo05!_imp__GetCommandLineA 存在與另外一個 DLL 之中。 dereference 該位址便可以找到真正的 symbol 。

0:000> dd 004b9264
004b9264  7c812c8d 7c93043d 7c812851 7c9305d4
004b9274  7c80aa49 7c80a0c7 7c809cad 7c832e2b

由於 CrtDemo05 裡頭使用 call 來呼叫這個位址,想必這個位址會是一段可以執行的指令,使用 uf 來查找:

0:000> uf 7c812c8d 
kernel32!GetCommandLineA:
7c812c8d a1f435887c      mov     eax,dword ptr [kernel32!BaseAnsiCommandLine+0x4 (7c8835f4)]
7c812c92 c3              ret

原來是落在 kernel32.dll 這個基礎函式庫裡頭,而且發現它的實做相當簡單:僅僅是從 kernel32!BaseAnsiCommandLine+0x4 位址讀取而已。為了驗證,我們以 dereference 該位址看是否真的儲存了要傳遞給 CrtDemo05 的 arguments 。

0:000> dd 7c8835f4
7c8835f4  00151ee0 000a0008 7c80e300 ffff02ff
7c883604  00000001 000a0008 7c831874 000a0008

接著

0:000> da 00151ee0 
00151ee0  "C:\test\CrtDemo05.exe test"

若是覺得 dd 兩層的 dereference 有點麻煩,針對字串可以使用 dp* ,

kd> dpa 7c8835f4 L1
7c8835f4  001522f8 "CrtDemo05.exe"

無論是哪種方式,都驗證了我們的想法── kernel32!GetCommandLineA() 是取得 command line 的函式,並且有著極簡單的實做──讀取一個預先填好值的 variable。

wmain() 版本也是相同的:

0:000> uf CrtDemo05!__tmainCRTStartup
...
CrtDemo05!__tmainCRTStartup+0x1b4 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 300]:
  300 0044a194 e8ad9efeff      call    CrtDemo05!ILT+65(___crtGetCommandLineW) (00434046)
  300 0044a199 a3e4824b00      mov     dword ptr [CrtDemo05!_wcmdln (004b82e4)],eax
  303 0044a19e e8439ffeff      call    CrtDemo05!ILT+225(___crtGetEnvironmentStringsW) (004340e6)
  303 0044a1a3 a3d85e4b00      mov     dword ptr [CrtDemo05!_wenvptr (004b5ed8)],eax
  305 0044a1a8 e845b1feff      call    CrtDemo05!ILT+4845(__wsetargv) (004352f2)
  305 0044a1ad 85c0            test    eax,eax
  305 0044a1af 7d0a            jge     CrtDemo05!__tmainCRTStartup+0x1db (0044a1bb)
...

函式,你可能會好奇為什麼 wide 版本並不是呼叫 _imp__GetCommandLineW() ,其實它被包裝在 ___crtGetCommandLineW() 裡頭。

0:000> uf 00434046
...
CrtDemo05!__crtGetCommandLineW+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\aw_com.c @ 52]:
   52 0046336f ff1530934b00    call    dword ptr [CrtDemo05!_imp__GetCommandLineW (004b9330)]
   52 00463375 85c0            test    eax,eax
   52 00463377 740c            je      CrtDemo05!__crtGetCommandLineW+0x25 (00463385)
...

並且同樣地,可以使用如同 ansi 版本的方式,去 trace :

0:000> dd 004b9330
004b9330  7c816cfb 7c80c6cf 7c801eee 7c873d83
004b9340  7c81abe4 7c80180e 7c810da6 7c80cd58

0:000> uf 7c816cfb 
kernel32!GetCommandLineW:
7c816cfb a10430887c      mov     eax,dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)]
7c816d00 c3              ret

0:000> dd 7c883004
7c883004  0002062c 00000000 7c809784 7c80979d
7c883014  7c8097d2 7c8097e7 7c8097b7 00000000

0:000> du 0002062c 
0002062c  "C:\test\CrtDemo05.exe test"

使用 Kernel Debugging

或許你會好奇為什麼 trace command 如何填入一個 process 需要動要到 kernel debug ?其實原因很簡單,因為當 user-mode debug 無法滿足時,以這次的參數傳遞來看,當我們利用 windbg 載入一個 program 時,program 需要執行到某個狀態後, user-mode debugger 才能介入,此時 arguments 已經設置完成。

Microsoft (R) Windows Debugger Version 6.11.0001.404 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: C:\test\CrtDemo05.exe
Symbol search path is: srv*C:\sym_cache*http://msdl.microsoft.com/download/symbols;C:\test
Executable search path is: 
ModLoad: 00400000 004bb000   CrtDemo05.exe
ModLoad: 7c920000 7c9b5000   ntdll.dll
ModLoad: 7c800000 7c91d000   C:\WINDOWS\system32\kernel32.dll
(7d8.568): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffd8000 ecx=00000001 edx=00000002 esi=00251f48 edi=00251eb4
eip=7c921230 esp=0012fb20 ebp=0012fc94 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!DbgBreakPoint:
7c921230 cc              int     3

0:000> kvn
 # ChildEBP RetAddr  Args to Child              
00 0012fb1c 7c95edc0 7ffdf000 7ffd8000 00000000 ntdll!DbgBreakPoint (FPO: [0,0,0])
01 0012fc94 7c941639 0012fd30 7c920000 0012fce0 ntdll!LdrpInitializeProcess+0xffa (FPO: [5,89,4])
02 0012fd1c 7c92eac7 0012fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x183 (FPO: [Non-Fpo])
03 00000000 00000000 00000000 00000000 00000000 ntdll!KiUserApcDispatcher+0x7

0:000> da 00151ee0 
00151ee0  "C:\test\CrtDemo05.exe test"

注意到沒?當 CrtDemo05 整支程式還在被 OS 的 loader 所初始化時,在 __tmainCRTStartup 還未執行到之前,arguments 已經填入了。因此這個例子需要透過 kernel debugging 來進行更早期的追蹤。 kernel debugging 的方式有許多種,使用 VMWare 算是便利的方法之一,可以參考 Windows Debugging – Kernel Debugging with WinDbg and VMWare 設定環境。

Catch Me If You Can

為了捕捉 kernel32!BaseUnicodeCommanLine 以及 kernel32!BaseAnsiCommandLine 是如何被填寫的?我們可以透過 WinDbg 的 break on access ,不過很快就會遇到第一個問題:

kd> x kernel32!BaseUnicodeCommandLine
7c885730 kernel32!BaseUnicodeCommandLine = <no type information>
7c883000 kernel32!BaseUnicodeCommandLine = <no type information>

遇到兩個同名的 symbols ,理論上這是件會造成 symbol resolve 上 ambiguous 的事。雖然暫時無解,但幸運的是,根據之前追蹤結果:

7c816cfb a10430887c mov eax,dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)]

我們可以直接選擇 7c885730

kd> ba r4 7c883004
kd> g
Breakpoint 0 hit
nt!MiCopyOnWrite+0x148:
0008:805222bc f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
kd> k
ChildEBP RetAddr  
b22bad00 8051d019 nt!MiCopyOnWrite+0x148
b22bad4c 805406ec nt!MmAccessFault+0x9f9
b22bad4c 7c93a100 nt!KiTrap0E+0xcc
0012f938 7c93d8a0 ntdll!__security_init_cookie_ex+0x31
0012f944 7c93d83a ntdll!LdrpInitSecurityCookie+0x2f
0012fa40 7c939b78 ntdll!LdrpRunInitializeRoutines+0x124
0012faf0 7c939ba0 ntdll!LdrpGetProcedureAddress+0x1c6
0012fb0c 7c942334 ntdll!LdrGetProcedureAddress+0x18
0012fc94 7c941639 ntdll!LdrpInitializeProcess+0xd7a
0012fd1c 7c92eac7 ntdll!_LdrpInitialize+0x183
00000000 00000000 ntdll!KiUserApcDispatcher+0x7

這一次還不是我們想要的 stack:

kd> g
Breakpoint 0 hit
kernel32!_BaseDllInitialize+0x20f:
7c817ea8 ff157c10807c    call    dword ptr [kernel32!_imp__RtlUnicodeStringToAnsiString (7c80107c)]
kd> bl
 0 e 7c883004 r 4 0001 (0001) kernel32!BaseUnicodeCommandLine+0x4
kd> k
ChildEBP RetAddr  
0012f918 7c9211a7 kernel32!_BaseDllInitialize+0x20f
0012f938 7c93cbab ntdll!LdrpCallInitRoutine+0x14
0012fa40 7c939b78 ntdll!LdrpRunInitializeRoutines+0x344
0012faf0 7c939ba0 ntdll!LdrpGetProcedureAddress+0x1c6
0012fb0c 7c942334 ntdll!LdrGetProcedureAddress+0x18
0012fc94 7c941639 ntdll!LdrpInitializeProcess+0xd7a
0012fd1c 7c92eac7 ntdll!_LdrpInitialize+0x183
00000000 00000000 ntdll!KiUserApcDispatcher+0x7

這次 stack 看起來成功率比較高。 disassembly 一段 breakpoint 前的程式碼,使用 WinDbg 的 disassembly 視窗或是指令都可以做到:

kd> ub 7c817ea8 L10
kernel32!_BaseDllInitialize+0x1cc:
...
7c817e7f 64a118000000    mov     eax,dword ptr fs:[00000018h]
7c817e85 8b4030          mov     eax,dword ptr [eax+30h]
7c817e88 8b4010          mov     eax,dword ptr [eax+10h]
7c817e8b 8b4840          mov     ecx,dword ptr [eax+40h]
7c817e8e 6a01            push    1
7c817e90 890d0030887c    mov     dword ptr [kernel32!BaseUnicodeCommandLine (7c883000)],ecx
7c817e96 8b4044          mov     eax,dword ptr [eax+44h]
7c817e99 680030887c      push    offset kernel32!BaseUnicodeCommandLine (7c883000)
7c817e9e 68f035887c      push    offset kernel32!BaseAnsiCommandLine (7c8835f0)
7c817ea3 a30430887c      mov     dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)],eax

解讀 fs:[00000018h]

可以發現,kernel32!BaseUnicodeCommandLine 是由 ecx 決定,而 ecx 則是由 fs:[00000018h] 所決定,這部份的解讀,需要一些的隱藏知識:在 x86 上Windows user mode 將每個 thread 的 TEB (Thread Environment Block),存放在 fs:[0];而在 kernel mode 中, fs:[0] 則是存放 KPCR (Process Control Region) 。

kd> dt _TEB
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
   ...

整個 ntdll!_TEB 有點龐大,不過根據 ub 的結果,fs:[00000018h] 落在第一個欄位 ── _NT_TIB (Thread Information Block)裡頭,再進行一次 dt :

kd> dt _NT_TIB
ntdll!_NT_TIB
   +0x000 ExceptionList    : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 StackBase        : Ptr32 Void
   +0x008 StackLimit       : Ptr32 Void
   +0x00c SubSystemTib     : Ptr32 Void
   +0x010 FiberData        : Ptr32 Void
   +0x010 Version          : Uint4B
   +0x014 ArbitraryUserPointer : Ptr32 Void
   +0x018 Self             : Ptr32 _NT_TIB

那為何不直接使用 fs:[0] 而要選擇 fs:[00000018h] ?因為 fs:[0] 並非 process 的 virtual address :

kd> dd fs:0
003b:00000000  0012fa30 00130000 0012f000 00000000
003b:00000010  00001e00 00000000 7ffdd000 00000000
003b:00000020  00000778 00000128 00000000 00000000

因此使用上,往往透過 fs:[00000018h] 來指向正確 TEB 或說 TIB 的位址。

重新解讀 ub

7c817e7f 64a118000000    mov     eax,dword ptr fs:[00000018h]
7c817e85 8b4030          mov     eax,dword ptr [eax+30h]
7c817e88 8b4010          mov     eax,dword ptr [eax+10h]
7c817e8b 8b4840          mov     ecx,dword ptr [eax+40h]

上述組語在知道 fs:[0] 的意義後,便可尋著指令進行 dt 的解碼,並寫出對應的虛擬碼:

_TEB            teb = fs:[00000018h];
UNICODE_STRING  kernel32!BaseUnicodeCommandLine = 
    teb.ProcessEnvironmentBlock->ProcessParameters.CommandLine;

到了這個步驟,我們已經知道即使是 kernel32!_BaseDllInitialize() 這般低階的初始化,也僅僅只是將 PEB 中的某個變數值讀出,那麼 PEB 中的值又是給負責填寫呢?還記得 Win32 API 中的 CreateProcess() 嗎?它或許是有嫌疑的一份子。

CreateProcess()

kd> dt _RTL_USER_PROCESS_PARAMETERS
ntdll!_RTL_USER_PROCESS_PARAMETERS
   +0x000 MaximumLength    : Uint4B
   +0x004 Length           : Uint4B
   +0x008 Flags            : Uint4B
   +0x00c DebugFlags       : Uint4B
   +0x010 ConsoleHandle    : Ptr32 Void
   +0x014 ConsoleFlags     : Uint4B
   +0x018 StandardInput    : Ptr32 Void
   +0x01c StandardOutput   : Ptr32 Void
   +0x020 StandardError    : Ptr32 Void
   +0x024 CurrentDirectory : _CURDIR
   +0x030 DllPath          : _UNICODE_STRING
   +0x038 ImagePathName    : _UNICODE_STRING
   +0x040 CommandLine      : _UNICODE_STRING
   +0x048 Environment      : Ptr32 Void
   +0x04c StartingX        : Uint4B
   +0x050 StartingY        : Uint4B
   +0x054 CountX           : Uint4B
   +0x058 CountY           : Uint4B
   +0x05c CountCharsX      : Uint4B
   +0x060 CountCharsY      : Uint4B
   +0x064 FillAttribute    : Uint4B
   +0x068 WindowFlags      : Uint4B
   +0x06c ShowWindowFlags  : Uint4B
   +0x070 WindowTitle      : _UNICODE_STRING
   +0x078 DesktopInfo      : _UNICODE_STRING
   +0x080 ShellInfo        : _UNICODE_STRING
   +0x088 RuntimeData      : _UNICODE_STRING
   +0x090 CurrentDirectores : [32] _RTL_DRIVE_LETTER_CURDIR

目前我們已經知道,一個程式的 command line 儲存在 _RTL_USER_PROCESS_PARAMETERS 這個 strcut 裡頭,所以要做的是,找到何時會去填寫,方法有兩種:

  1. Trace CreateProcess
  2. 尋找有關 ProcessParameter 的 function 。

我們使用方法 2. 來加速:

可以使用 WinDbg 的 x 指令:

kd> x *!*processparameter*
7c819f9e kernel32!BasePushProcessParameters = <no type information>
7c801488 kernel32!_imp__RtlCreateProcessParameters = <no type information>
7c801484 kernel32!_imp__RtlDestroyProcessParameters = <no type information>
7c9432ec ntdll!RtlDestroyProcessParameters = <no type information>
7c950ea3 ntdll!RtlCheckProcessParameters = <no type information>
7c9433c1 ntdll!RtlCreateProcessParameters = <no type information>

這幾個 functions 看起來都有機會,其中以 BasePushProcessParameters() 的名稱看來最有可能是 creater 建立 createe process parameter 的函式,為了驗證這想法,將 WinDbg 目前的 context 切換到 cmd.exe 下,並設定 breakpoint 來驗證:

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
...
PROCESS 8200d7e8  SessionId: 0  Cid: 0194    Peb: 7ffdf000  ParentCid: 0608
    DirBase: 08940240  ObjectTable: e1533828  HandleCount:  33.
    Image: cmd.exe

kd> .process /r /P /p 8200d7e8

kd> bp 7c819f9e

kd> g
Breakpoint 1 hit
kernel32!BasePushProcessParameters:
7c819f9e 68c0020000      push    2C0h

kd> k
ChildEBP RetAddr  
0013f028 7c8199bc kernel32!BasePushProcessParameters
0013fa88 7c80235e kernel32!CreateProcessInternalW+0x184e
0013fac0 4ad031dd kernel32!CreateProcessW+0x2c
0013fc20 4ad02db0 cmd!ExecPgm+0x22b
0013fc54 4ad02e0e cmd!ECWork+0x84
0013fc6c 4ad05f9f cmd!ExtCom+0x40
0013fe9c 4ad013eb cmd!FindFixAndRun+0xcf
0013fee0 4ad0bbba cmd!Dispatch+0x137
0013ff44 4ad05164 cmd!main+0x216
0013ffc0 7c816d4f cmd!mainCRTStartup+0x125
0013fff0 00000000 kernel32!BaseProcessStart+0x23

stack 目前看來是支持我們的猜測, cmd.exe 透過 CreateProcess()  來建立 CrtDemo05.exe ,並且呼叫 BasePushProcessParameters() 來初始化 process parameter。在這邊,我們也可以切換到 CrtDemo05.exe 下,確定一下它目前的建立狀態,以瞭解它仍是否仍在建立中而非建立完成:

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
...

PROCESS 8200d7e8  SessionId: 0  Cid: 0194    Peb: 7ffdf000  ParentCid: 0608
    DirBase: 08940240  ObjectTable: e1533828  HandleCount:  35.
    Image: cmd.exe

PROCESS 82476d00  SessionId: 0  Cid: 00c4    Peb: 7ffd4000  ParentCid: 0194
    DirBase: 089402c0  ObjectTable: e1d3bf68  HandleCount:  36.
    Image: conime.exe

PROCESS 8244e980  SessionId: 0  Cid: 05b4    Peb: 7ffdc000  ParentCid: 0400
    DirBase: 08940300  ObjectTable: e16e0700  HandleCount: 123.
    Image: wuauclt.exe

PROCESS 8244a460  SessionId: 0  Cid: 0224    Peb: 7ffd5000  ParentCid: 0194
    DirBase: 08940380  ObjectTable: e17871a0  HandleCount:   1.
    Image: CrtDemo0

看到 Image name 被截斷,有點訝異,不過可能是因為 kernel 在填寫時被我們中斷了。現在來看看 PEB 裡頭的 command line 是否已經準備好了:

kd> !peb 7ffd5000
PEB at 7ffd5000
error 1 InitTypeRead( nt!_PEB at 7ffd5000)...

kd> .process /r /P /p 8244a460 
Implicit process is now 8244a460
.cache forcedecodeuser done
Loading User Symbols
PEB is paged out (Peb.Ldr = 7ffd500c).  Type ".hh dbgerr001" for details
kd> dt _PEB 7ffd5000
nt!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0 ''
   +0x003 SpareBool        : 0 ''
   +0x004 Mutant           : 0xffffffff Void
   +0x008 ImageBaseAddress : 0x00400000 Void
   +0x00c Ldr              : (null) 
   +0x010 ProcessParameters : (null) 
   +0x014 SubSystemData    : (null) 
   +0x018 ProcessHeap      : (null) 
   +0x01c FastPebLock      : (null) 
   ...

試了兩種方法,都無法正確讀取 PEB ,這正和我們的預期:表示 CrtDemo05.exe 正在被建立中,其 PEB 似乎也在混沌之中。緊接著回到 cmd.exe 的 stack 上,接下來的事就需要點耐心,由於目前只知道 BasePushProcessParameters() 跟 _RTL_USER_PROCESS_PARAMETERS 有關,但是找不到可以設立 breakpoint 的地方,根據 assembly 一步步追蹤是個方法,不過直接閱讀 assembly 來找尋跟 ProcessParameters 相關的指令或許會更快,一個很快速的想法是使用關鍵字來縮小範圍,這裡若是使用 uf 來 diassembly ,可能會遇上一些麻煩,因為 CreateProcessInternalW() 被 OMAP 最佳化過,無法依照 mapping 到記憶體上的 layout 顯示:

kd> uf CreateProcessInternalW
Flow analysis was incomplete, some code may be missing
kernel32!CreateProcessInternalW+0x308:
...
...

當遇到這種狀況,我們得放棄 diassembly 整個 function ,而是直接指明記憶體位址:

kd> x kernel32!CreateProcessInternalW
7c8191eb kernel32!CreateProcessInternalW = 

kd> u 7c8191eb L600
kernel32!CreateProcessInternalW:
...
; ProcessParameter
; ebp-234h: _RTL_USER_PROCESS_PARAMETERS*
7c81a11d 8d85ccfdffff    lea     eax,[ebp-234h]
7c81a123 50              push    eax                        ; pProcessParameters
7c81a124 ff158814807c    call    dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)]
7c81a12a 33d2            xor     edx,edx                    ; edx = 0
7c81a12c 3bc2            cmp     eax,edx

...
...
7c81a538 ff158414807c    call    dword ptr [kernel32!_imp__RtlDestroyProcessParameters (7c801484)]

幸運的找到:

7c9432ec ntdll!RtlDestroyProcessParameters
7c9433c1 ntdll!RtlCreateProcessParameters

利用兩個 functions ,我們可以縮小可疑範圍,7c81a11d ~ 7c81a538 給了我們一個暗示:ebp-234h 是個 local variable ,並且傳遞至 RtlCreateProcessParameters() ,當結束後進行是否為 NULL 的比較,那麼 ebp-234h 基本上就很有機會就是一個 pointer point to _RTL_USER_PROCESS_PARAMETERS。另外在向上回溯,可以找找看是否還有其他參數被傳遞至 RtlCreateProcessParameters() :

7c81a0d7 ffd6            call    esi
7c81a0d9 8d85b4fdffff    lea     eax,[ebp-24Ch]
7c81a0df 50              push    eax                    ; parameter10
7c81a0e0 8d859cfdffff    lea     eax,[ebp-264h]
7c81a0e6 50              push    eax                    ; parameter9
7c81a0e7 8d85a4fdffff    lea     eax,[ebp-25Ch]
7c81a0ed 50              push    eax                    ; parameter8
7c81a0ee 8d8594fdffff    lea     eax,[ebp-26Ch]
7c81a0f4 50              push    eaxe                   ; parameter7
7c81a0f5 ffb570fdffff    push    dword ptr [ebp-290h]   ; parameter6
7c81a0fb 8d8584fdffff    lea     eax,[ebp-27Ch]
7c81a101 50              push    eax                    ; parameter5
7c81a102 f7db            neg     ebx
7c81a104 1bdb            sbb     ebx,ebx
7c81a106 8d8578fdffff    lea     eax,[ebp-288h]
7c81a10c 23d8            and     ebx,eax
7c81a10e 53              push    ebx                    ; parameter4
7c81a10f 8d85acfdffff    lea     eax,[ebp-254h]
7c81a115 50              push    eax                    ; parameter3
7c81a116 8d858cfdffff    lea     eax,[ebp-274h]
7c81a11c 50              push    eax                    ; parameter2
7c81a11d 8d85ccfdffff    lea     eax,[ebp-234h]
7c81a123 50              push    eax                    ; parameter1
7c81a124 ff158814807c    call    dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)]

看到了 10 個參數被傳遞至 RtlCreateProcessParameters() ,我們可以選擇 diassembly 它看是否有與 command line 相關的操作,或是繼續搜尋其他 7c81a11d ~ 7c81a538 內 ebp-234h 的操作,這裡有一個或許可行的快速篩選法,因為 CommandLine 位於 _RTL_USER_PROCESS_PARAMETERS offset 40 bytes 的地方:

kd> dt _RTL_USER_PROCESS_PARAMETERS
ntdll!_RTL_USER_PROCESS_PARAMETERS
   ...
   +0x040 CommandLine      : _UNICODE_STRING
   ...

我們便找尋 40h 的關鍵字,而搜尋結果的確有幾筆與 40h 有關的操作,但幸運地,都不是和 CommandLine 有關的。於是乎,把焦點放回 RtlCreateProcessParameters():

kd> x ntdll!RtlCreateProcessParameters
7c9433c1 ntdll!RtlCreateProcessParameters

kd> u 7c9433c1 L200
ntdll!RtlCreateProcessParameters:
...
7c9435e1 6a04            push    4                          ; Protect
7c9435e3 6800100000      push    1000h                      ; AllocationType
7c9435e8 8d45cc          lea     eax,[ebp-34h]
7c9435eb 50              push    eax                        ; RegionSize
7c9435ec 53              push    ebx                        ; ZeroBits
7c9435ed 8d45e4          lea     eax,[ebp-1Ch]
7c9435f0 50              push    eax                        ; BaseAddress
7c9435f1 6aff            push    0FFFFFFFFh                 ; ProcessHandle
7c9435f3 e8e69efeff      call    ntdll!ZwAllocateVirtualMemory (7c92d4de)
...

由於 ZwAllocateVirtualMemory() 是公開的 API ,所以很快地可以對參數進行 mapping ,馬上就發現 local vaiable ebp-1ch 存放的就是 allocate 的結果,並且在稍後就有一個 +40h 的操作:

kd> u 7c9433c1 L200
ntdll!RtlCreateProcessParameters:
...
7c94369a 40              inc     eax
7c94369b 40              inc     eax
7c94369c 50              push    eax
7c94369d 8b45e4          mov     eax,dword ptr [ebp-1Ch]    ; pBase
7c9436a0 83c040          add     eax,40h                    ; pBase + 40 = CommandLine
7c9436a3 57              push    edi
7c9436a4 50              push    eax
7c9436a5 8d45d8          lea     eax,[ebp-28h]
7c9436a8 50              push    eax
7c9436a9 e8ab000000      call    ntdll!RtlpCopyProcString (7c943759)
...

這和 CommandLine 的操作看似吻合,但我們仍需要找到更直接的證據,首先可以使用動態的證據,也就是設定 breakpoint 去證明,我們將 breakpoint 先設定在 add eax,40h:

kd> bp 7c9436a0

kd> g
Breakpoint 2 hit
ntdll!RtlCreateProcessParameters+0x2f5:
7c9436a0 83c040          add     eax,40h

kd> dd ebp-1c
0013ed00  00380000 0013ecd4 0013edf8 0013f018
0013ed10  7c92ee18 7c943748 00000000 0013f028

kd> dd 00380040 
00380040  00000000 00000000 00010000 00000000
00380050  00000000 00000000 00000000 00000000

接著,執行數個指令後,等到 RtlpCopyProcString() 執行完成後:

kd> dd 00380040 
00380040  00260024 003805c0 00010000 00000000
00380050  00000000 00000000 00000000 00000000

kd> du 003805c0
003805c0  "CrtDemo05.exe test"

再者,我們可以來個靜態分析:

kd> u 7c9433c1 L200
ntdll!RtlCreateProcessParameters:
...
7c94370b 8b45e4          mov     eax,dword ptr [ebp-1Ch]
7c94370e 6689988a000000  mov     word ptr [eax+8Ah],bx
7c943715 ff75e4          push    dword ptr [ebp-1Ch]
7c943718 e8f6fbffff      call    ntdll!RtlDeNormalizeProcessParams (7c943313)
7c94371d 8b4d08          mov     ecx,dword ptr [ebp+8]
7c943720 8901            mov     dword ptr [ecx],eax
...

很快地就有一個可疑的證物: ebp+8 也就是 RtlCreateProcessParameters() 的第一個參數,它被 eax 給賦值。雖然 eax 在第一行被指定為 ebp-1Ch ,看似符合我們的假設,但因為中間有個 RtlDeNormalizeProcessParams() 的呼叫,不能掉以輕心,必須進去瞧瞧:

kd> uf RtlDeNormalizeProcessParams
Flow analysis was incomplete, some code may be missing
ntdll!RtlDeNormalizeProcessParams:
7c943313 8bff            mov     edi,edi
7c943315 55              push    ebp
7c943316 8bec            mov     ebp,esp
7c943318 8b4508          mov     eax,dword ptr [ebp+8]
7c94331b 85c0            test    eax,eax
7c94331d 7478            je      ntdll!RtlDeNormalizeProcessParams+0x84 (7c943397)
...

幸運的, RtlDeNormalizeProcessParams() 沒有可觀的實作,也僅在 function 開頭將 eax 給予 ebp+8 所代表的位址;而 ebp+8 也正就是 ebp-1Ch,其餘的對應操作都是 read 。確定了 RtlDeNormalizeProcessParams() 不會更改 ebp-1Ch 後,就可以放心回到 RtlCreateProcessParameters():

kd> u 7c9433c1 L200
ntdll!RtlCreateProcessParameters:
...
7c94345b 8b7d18          mov     edi,dword ptr [ebp+18h]
...
7c943685 e8cf000000      call    ntdll!RtlpCopyProcString (7c943759)
7c94368a 668b07          mov     ax,word ptr [edi]
7c94368d 663b4702        cmp     ax,word ptr [edi+2]
7c943691 0f84f79a0200    je      ntdll!RtlCreateProcessParameters+0x2e9 (7c96d18e)
7c943697 0fb7c0          movzx   eax,ax
7c94369a 40              inc     eax
7c94369b 40              inc     eax
7c94369c 50              push    eax
7c94369d 8b45e4          mov     eax,dword ptr [ebp-1Ch]    ; pBase
7c9436a0 83c040          add     eax,40h                    ; pBase + 40 = CommandLine
7c9436a3 57              push    edi
7c9436a4 50              push    eax
7c9436a5 8d45d8          lea     eax,[ebp-28h]
7c9436a8 50              push    eax
7c9436a9 e8ab000000      call    ntdll!RtlpCopyProcString (7c943759)

可以發現,可疑的參數有:

  1. 7c94369a                 inc eax
    7c94369b                 inc eax
    7c94369c                  push eax
  2. 7c94345b                 mov edi,dword ptr [ebp+18h]
    7c9436a3                 push edi
  3. 7c9436a5                 lea eax,[ebp-28h]
    7c9436a8                 push eax

為了找出實際的賦值給 ebp-1Ch+40h 的對象,這邊可以使用 breakpoint 去檢驗,分別是:

kd> bp 7c94369c
kd> g
Breakpoint 5 hit
ntdll!RtlCreateProcessParameters+0x2f1:
7c94369c 50              push    eax
kd> r
eax=00000026 ebx=00000000 ecx=00380034 edx=00000000 esi=00000208 edi=0013edac
eip=7c94369c esp=0013ecd4 ebp=0013ed1c iopl=0         nv up ei pl nz na po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000203

kd> dpu 0013edac+4 L1
0013edb0  0015adc8 "CrtDemo05.exe test"

kd> bp 7c9436a8
kd> g
Breakpoint 6 hit
kd> dd ebp-28h
0013ecf4  003805c0 00000000 00000634 00380000
0013ed04  0013ecd4 0013edf8 0013f018 7c92ee18

kd> dpu 003805c0
003805c0  00000000

透過 breakpoint 的實驗,就可以知道 edi 才是賦值給 ebp-1Ch+40h 的來源,而且還是 RtlCreateProcessParameters() 的第五個參數。所以可以反推 edi 是怎麼來的,並一路反推,便可得到 command line 最初的來源。

kd> uf kernel32!CreateProcessW
kernel32!CreateProcessW:
7c802332 8bff            mov     edi,edi
7c802334 55              push    ebp
7c802335 8bec            mov     ebp,esp
7c802337 6a00            push    0                      ; parameter12
7c802339 ff752c          push    dword ptr [ebp+2Ch]    ; lpProcessInformation
7c80233c ff7528          push    dword ptr [ebp+28h]    ; lpStartupInfo
7c80233f ff7524          push    dword ptr [ebp+24h]    ; lpCurrentDirectory
7c802342 ff7520          push    dword ptr [ebp+20h]    ; lpEnvironment
7c802345 ff751c          push    dword ptr [ebp+1Ch]    ; dwCreationFlags
7c802348 ff7518          push    dword ptr [ebp+18h]    ; bInheritHandles
7c80234b ff7514          push    dword ptr [ebp+14h]    ; lpThreadAttributes
7c80234e ff7510          push    dword ptr [ebp+10h]    ; lpProcessAttributes
7c802351 ff750c          push    dword ptr [ebp+0Ch]    ; lpCommandLine
7c802354 ff7508          push    dword ptr [ebp+8]      ; lpApplicationName
7c802357 6a00            push    0                      ; parameter1
7c802359 e88d6e0100      call    kernel32!CreateProcessInternalW (7c8191eb)
7c80235e 5d              pop     ebp
7c80235f c22800          ret     28h

kd> x kernel32!CreateProcessInternalW
7c8191eb kernel32!CreateProcessInternalW = <no type information>

kd> u 7c8191eb L600
kernel32!CreateProcessInternalW:
...
7c819214 8b4510          mov     eax,dword ptr [ebp+10h]    ; lpCommandLine
7c819217 8985e0f8ffff    mov     dword ptr [ebp-720h],eax
...
7c819958 8b8de0f8ffff    mov     ecx,dword ptr [ebp-720h]   ; lpCommandLine
...
7c819964 ffb56cf9ffff    push    dword ptr [ebp-694h]    ; parameter13
7c81996a ffb500f7ffff    push    dword ptr [ebp-900h]    ; parameter12
7c819970 8a85b7f8ffff    mov     al,byte ptr [ebp-749h]
7c819976 f6d8            neg     al
7c819978 1bc0            sbb     eax,eax
7c81997a 83e002          and     eax,2
7c81997d 50              push    eax                    ; parameter11
7c81997e ff751c          push    dword ptr [ebp+1Ch]    ; parameter10
7c819981 8b85f4f6ffff    mov     eax,dword ptr [ebp-90Ch]
7c819987 0b4520          or      eax,dword ptr [ebp+20h]
7c81998a 50              push    eax                     ; parameter9
7c81998b 8d8560f7ffff    lea     eax,[ebp-8A0h]
7c819991 50              push    eax                     ; parameter8
7c819992 ffb5b0f8ffff    push    dword ptr [ebp-750h]    ; parameter7
7c819998 51              push    ecx                     ; parameter6
7c819999 ffb550f7ffff    push    dword ptr [ebp-8B0h]    ; parameter5
7c81999f ffb5e4f8ffff    push    dword ptr [ebp-71Ch]    ; parameter4
7c8199a5 ffb5b8f7ffff    push    dword ptr [ebp-848h]    ; parameter3
7c8199ab ffb594f9ffff    push    dword ptr [ebp-66Ch]    ; parameter2
7c8199b1 ffb534f7ffff    push    dword ptr [ebp-8CCh]    ; parameter1
7c8199b7 e8e2050000      call    kernel32!BasePushProcessParameters (7c819f9e)

kernel32!BasePushProcessParameters:
...
7c819fd3 8b4d1c          mov     ecx,dword ptr [ebp+1Ch]    ;
7c819fd6 898d5cfdffff    mov     dword ptr [ebp-2A4h],ecx   ; srcCmdLine
...
7c81a045 8d85d0fdffff    lea     eax,[ebp-230h]
7c81a04b 50              push    eax
7c81a04c 8d858cfdffff    lea     eax,[ebp-274h]
7c81a052 50              push    eax
7c81a053 ffd6            call    esi
7c81a055 ffb55cfdffff    push    dword ptr [ebp-2A4h]   ; srcCmdLine
7c81a05b 8d8584fdffff    lea     eax,[ebp-27Ch]
7c81a061 50              push    eax                    ; commandLine
7c81a062 ffd6            call    esi                    ; RtlInitUnicodeString
...
7c81a0cf 50              push    eax
7c81a0d0 8d8594fdffff    lea     eax,[ebp-26Ch]
7c81a0d6 50              push    eax
7c81a0d7 ffd6            call    esi
7c81a0d9 8d85b4fdffff    lea     eax,[ebp-24Ch]
7c81a0df 50              push    eax                        ; runtimeData
7c81a0e0 8d859cfdffff    lea     eax,[ebp-264h]
7c81a0e6 50              push    eax                        ; shellInfo
7c81a0e7 8d85a4fdffff    lea     eax,[ebp-25Ch]
7c81a0ed 50              push    eax                        ; desktopInfo
7c81a0ee 8d8594fdffff    lea     eax,[ebp-26Ch]
7c81a0f4 50              push    eax                        ; windowTitle
7c81a0f5 ffb570fdffff    push    dword ptr [ebp-290h]       ; environment
7c81a0fb 8d8584fdffff    lea     eax,[ebp-27Ch]
7c81a101 50              push    eax                        ; commandLine, parameter5
7c81a102 f7db            neg     ebx
7c81a104 1bdb            sbb     ebx,ebx
7c81a106 8d8578fdffff    lea     eax,[ebp-288h]
7c81a10c 23d8            and     ebx,eax
7c81a10e 53              push    ebx                        ; currentDir
7c81a10f 8d85acfdffff    lea     eax,[ebp-254h]
7c81a115 50              push    eax                        ; dllPath
7c81a116 8d858cfdffff    lea     eax,[ebp-274h]
7c81a11c 50              push    eax                        ; imagePath
; ProcessParameter
; ebp-234h: _RTL_USER_PROCESS_PARAMETERS*
7c81a11d 8d85ccfdffff    lea     eax,[ebp-234h]
7c81a123 50              push    eax                        ; pProcessParameters
7c81a124 ff158814807c    call    dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)]

最後,我們還沒來得及關心 creater 是如何的把 createe 的 PEB 建立完成,其實就在 RtlCreateProcessParameters() 稍後,透過 NtWriteVirtualMemory() 完成。

7c81a1eb 8b35f413807c    mov     esi,dword ptr [kernel32!_imp__NtWriteVirtualMemory (7c8013f4)]
...
7c81a38b 6a04            push    4                          ; Protect
7c81a38d bb00100000      mov     ebx,1000h
7c81a392 53              push    ebx                        ; AllocationtType
7c81a393 8d85c8fdffff    lea     eax,[ebp-238h]
7c81a399 50              push    eax                        ; RegionSize
7c81a39a 52              push    edx                        ; ZeroBits
7c81a39b 8d85c4fdffff    lea     eax,[ebp-23Ch]
7c81a3a1 50              push    eax                        ; BaseAddress
7c81a3a2 ffb580fdffff    push    dword ptr [ebp-280h]       ; hNewProcessHandle
7c81a3a8 8b3d9011807c    mov     edi,dword ptr [kernel32!_imp__NtAllocateVirtualMemory (7c801190)]
7c81a3ae ffd7            call    edi
7c81a3b0 898574fdffff    mov     dword ptr [ebp-28Ch],eax
7c81a3b6 8b85c8fdffff    mov     eax,dword ptr [ebp-238h]
7c81a3bc 898558fdffff    mov     dword ptr [ebp-2A8h],eax
7c81a3c2 83bd74fdffff00  cmp     dword ptr [ebp-28Ch],0
7c81a3c9 0f8c04950200    jl      kernel32!BasePushProcessParameters+0x504 (7c8438d3)
7c81a3cf 8b8dccfdffff    mov     ecx,dword ptr [ebp-234h]
7c81a3d5 8901            mov     dword ptr [ecx],eax
7c81a3d7 f6452b10        test    byte ptr [ebp+2Bh],10h
7c81a3db 0f85fa940200    jne     kernel32!BasePushProcessParameters+0x51d (7c8438db)
7c81a3e1 f6452b20        test    byte ptr [ebp+2Bh],20h
7c81a3e5 0f85ff940200    jne     kernel32!BasePushProcessParameters+0x52d (7c8438ea)
7c81a3eb f6452b40        test    byte ptr [ebp+2Bh],40h
7c81a3ef 0f8504950200    jne     kernel32!BasePushProcessParameters+0x53d (7c8438f9)
7c81a3f5 6a00            push    0                          ; nBytesWritten
7c81a3f7 8b85ccfdffff    mov     eax,dword ptr [ebp-234h]
7c81a3fd ff7004          push    dword ptr [eax+4]          ; nBytesToWrite    : pProcParameters->Length
7c81a400 50              push    eax                        ; Buffer           : pProcParameters
7c81a401 ffb5c4fdffff    push    dword ptr [ebp-23Ch]       ; BaseAddress      :
7c81a407 ffb580fdffff    push    dword ptr [ebp-280h]       ; hNewProcessHandle:
                                 ; NtWriteVirtualMemory( *(ebp-280h), *(ebp-23Ch), eax, ???, 0 )
7c81a40d ffd6            call    esi                    ; _imp__NtWriteVirtualMemory

網路上可以找到人家 reverse 過的 RtlCreateProcessParameters() prototype 可以加速 trace 的速度:

NTSTATUS RtlCreateProcessParameters( 
    PRTL_USER_PROCESS_PARAMETERS *ProcessParameters,
    PUNICODE_STRING     ImagePathName,
    PUNICODE_STRING     DllPath,
    PUNICODE_STRING     CurrentDirectory,
    PUNICODE_STRING     CommandLine,
    PWSTR               Environment, // Not sured
    PUNICODE_STRING     WindowTitle,
    PUNICODE_STRING     DesktopInfo,
    PUNICODE_STRING     ShellInfo,
    PUNICODE_STRING     RuntimeData );

不過如果想要硬派的自己來也是可以的,可以試試看這篇文章的作法 : )

Summary

image

MiniFilter InstanceSetupCallback is not called?

一般來說,MiniFilter 的 InstanceSetupCallback 會在 filter manager 把 minifilter attache 到 volume 後呼叫。如果沒有的話,可以檢查一下 minifilter 的 INF 是否把 instance fla...