Building Clang with openSUSE Tumbleweed + Virtual Box 6.0

圖片
很久沒有編譯怪物級的專案來折磨 CPU 和硬碟了。 基本上照著 Clang 文件 https://clang.llvm.org/get_started.html 就不會在 compilation 上運到奇怪問題,只有幾個細節需要調校一下: Virtual Box 的 storage controller 要開啟 Host I/O Cache ,不然很容易在大量 I/O 時遇到 Virtual Box 沒有反應,然後出現 Disk I/O 讀取錯誤: 00:39:53.953457 Console: VM runtime error: fatal=false, errorID=BLKCACHE_IOERR message="The I/O cache encountered an error while updating data in medium "ahci-0-0" (rc=VERR_IO_NOT_READY). Make sure there is enough free space on the disk and that the disk is working properly. Operation can be resumed afterwards" 不要編譯 libcxx ,Clang 教學文件雖然提到這是 optional ,但實際跑時會在 linker 階段炸出很多 undefined reference  (乍看下感覺是 linker 只使用了 C runtime 而漏了 C++ runtime) 使用 gold 作為 linker 而不是 gnu ld ,因為 ld 在工作階段會耗用過多 memory ,以我的實驗為例:即使已經設定了 8GB Ram 和 8GB swap 空間,仍然會遇到 ld 耗盡 memory 的問題。 [ 63%] Linking CXX shared library ../../lib/libLTO.so /usr/bin/ld: failed to set dynamic section sizes: Memory exhausted Let's Build 先 checkout llvm svn co http://llvm.org/svn...

Missing Process Argument

哈利波特有消失的密室,CreateProcess() 有消失的參數?!
有一次升級了產品的 common module 後,同事發現某個功能失效了,失效的原因是 process 收不到 creator 給它的參數,WTF ... 不過有 bug 就有文章,讓我們看下去。

命案現場

發生問題的程式碼如下:
// Creator
CreateProcess( app, arg );
// Createe
int CALLBACK wWinMain( ..., wchar_t* pCmdLine, ... )
{
// ...
if ( nullptr == pCmdLine || L'\0' == *pCmdLine ) {
return -1;
}
// ...
}
而升級也只是把 wmain() 改成 wWinMain(),為什麼這樣的改動後, arg1 就消失了呢?

Short Answer

這問題最簡單的答案就是:這是 Windows 的行為。
當年 WinMain() 作為 GUI 程序的進入點,勢必要進行一些轉型正義處理,所以丟掉 program name 就是第一步。
main() 和 WinMain() 都是 Windows 上兩個標準的 entry points 。但 WinMain() 的 lpCmdLine 會去掉 program name [1]。
int CALLBACK WinMain(
    _In_ HINSTANCE hInstance,
    _In_ HINSTANCE hPrevInstance,
    _In_ LPSTR     lpCmdLine,
    _In_ int       nCmdShow
  );
lpCmdLine [in] Type: LPSTR
The command line for the application, excluding the program name. To retrieve the entire command line, use the GetCommandLine function.
那要怎麼從 command line 參數中去掉 program name 呢?Windows 的作法也相當簡單(暴力美學),去掉 command line 中的第一個 token 。下面我們參考 VC++2008 附上的 crt 程式碼,__tmainCRTStartup() 是呼叫 WinMain() 前,會把 _wcmdln 進行跳過第一個 token 的動作(需要考慮double quote)。
// moderated from Microsoft Visual Studio 9.0\VC\crt\src\crtexe.c
__declspec(noinline)
int __tmainCRTStartup( void )
{
    // ...
    BOOL inDoubleQuote=FALSE;
    /*
     * Skip past program name (first token in command line).
     * Check for and handle quoted program name.
     */
    if (_wcmdln == NULL)
        return 255;
    lpszCommandLine = (wchar_t *)_wcmdln;
    while (*lpszCommandLine > SPACECHAR || (*lpszCommandLine && inDoubleQuote)) {
        if (*lpszCommandLine==DQUOTECHAR)
            inDoubleQuote=!inDoubleQuote;
        ++lpszCommandLine;
   }
    /*
     * Skip past any white space preceeding the second token.
     */
    while (*lpszCommandLine && (*lpszCommandLine <= SPACECHAR)) {
        lpszCommandLine++;
   }
    mainret = wWinMain(
               (HINSTANCE)&__ImageBase,
                NULL,
                lpszCommandLine,
                StartupInfo.dwFlags & STARTF_USESHOWWINDOW
                 ? StartupInfo.wShowWindow
                 : SW_SHOWDEFAULT
               );
    // ...
}
那如果 creator 根本沒有傳遞參數給 createe ,參數是怎麼處理的呢?
Microsoft 在 INFO: Understanding CreateProcess and Command-line Arguments 有解釋到:
If the ApplicationName parameter is passed and the CommandLine parameter is NULL, then the ApplicationName parameter is also used as the CommandLine.
也就是 CreateProcess( app, NULL ) 這種情況下,相當於 CreateProcess( app, app ) 。因此上面的程式碼還是可以運作。

Fix?

BOOL CreateProcessW(
LPCWSTR               lpApplicationName,
LPWSTR                lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL                  bInheritHandles,
DWORD                 dwCreationFlags,
LPVOID                lpEnvironment,
LPCWSTR               lpCurrentDirectory,
LPSTARTUPINFOW        lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
那我們可以怎麼解決這個問題呢?有三類解決方式:
  1. 改用 ShellExecute() or ShellExecuteEx(),很直白,就不細訴。
  2. 依然使用 CreateProcess() ,但呼叫時 lpCommandLine 改成由 app + arg 組成,即 CreateProcess( ..., app + arg ),透過讓 __tmainCRTStartup() 消除掉 lpCommandLine 中重複的 app ,以保留接在後頭的參數(放個西瓜呢?)。而 ... 則視需求看是否保留 app (為什麼?不要忘了 lpApplicationName 需要指定 file extension 和不支持 search path,但 lpCommandLine 則支持,不小心就會被 hijacking)
  3. 依然使用 CreateProcess( app, arg ) ,但程式不使用 wWinMain() 的 pCmdLine 參數,而是透過 GetCommandLineW()
    // Createe
    int CALLBACK wWinMain( ..., wchar_t* pCmdLine, ... )
    {
        // ...
        wchar_t* pNewCmdLine = ::GetCommandLineW();
        if ( nullptr == pNewCmdLine || L'\0' == *pNewCmdLine ) {
            return -1;
       }
        // ...
    }

More

問題解決了,也回頭重看 CreateProcess() 的文件了,知道了透過 lpApplicationName 或 lpCommandLine (以下用 app, arg 表示)都能建立 process ,是不是想寫些程式,驗證一下兩者的交互和對產生的 process (main, WinMain)的影響了呢?

實驗設計

有效的 CreateProcess() 呼叫可以根據 app、arg 分為三種:
  1. CreateProcess( app, NULL )
    無參數、相當於 CreateProcess( app, app )
  2. CreateProcess( NULL, app )
  3. CreateProcess( NULL, app arg )
  4. CreateProcess( app, arg )
entry points 則有:
  1. wmain( int argc ,wchar_t* argv[] )/main( int argc, char* argv[] )
  2. wWinMain( ..., wchar_t* pCmdLine, ... )/WinMain( ..., char* pCmdLine, ... )

main()

跟預期的一樣,只需要注意 lpApplicationName 和 lpCommandLine 都是有效參數的時候,若 lpCommandLine 没有带上 app ,则 argv[ 0 ] 不会是 program name/path。
CreateProcess( SlaveCli.exe, arg1 ) -> PID 9324
argc: 1
argv[ 0 ]: arg1
GetCommandLine(): arg1
lpCommandLine 需要重複 app 才能讓 main()/wmain() 的 argv[ 0 ] 如預期地拿到 program name/path。
CreateProcess( (null), SlaveCli.exe arg1 ) -> PID 16788
argc: 2
argv[ 0 ]: SlaveCli.exe
argv[ 1 ]: arg1
GetCommandLine(): SlaveCli.exe arg1
---------------------------------------------------
CreateProcess( SlaveCli.exe, SlaveCli.exe arg1 ) -> PID 18956
argc: 2
argv[ 0 ]: SlaveCli.exe
argv[ 1 ]: arg1
GetCommandLine(): SlaveCli.exe arg1
---------------------------------------------------

WinMain()

跟一開始的問題一樣,lpCommandLine 沒帶上 placeholder ,導致 arg1 都消失了。
CreateProcess( SlaveGui.exe, arg1 ) -> PID 7696
[WinMain] pCmdLine: '\0'
[WinMain] GetCommandLine(): arg1
argc: 1
argv[ 0 ]: arg1
GetCommandLine(): arg1
在 lpCommandLine 重複 app 後,pCmdLine 成功地被刪除到只剩參數。
CreateProcess( (null), SlaveGui.exe arg1 ) -> PID 14400
[WinMain] pCmdLine: arg1
[WinMain] GetCommandLine(): SlaveGui.exe arg1
argc: 2
argv[ 0 ]: SlaveGui.exe
argv[ 1 ]: arg1
GetCommandLine(): SlaveGui.exe arg1
---------------------------------------------------
CreateProcess( SlaveGui.exe, SlaveGui.exe arg1 ) -> PID 5152
[WinMain] pCmdLine: arg1
[WinMain] GetCommandLine(): SlaveGui.exe arg1
argc: 2
argv[ 0 ]: SlaveGui.exe
argv[ 1 ]: arg1
GetCommandLine(): SlaveGui.exe arg1
---------------------------------------------------

Rule

  1. GetCommandLine() 可以取得最原始的參數。
  2. 不要假設 main(), wmain() 的 argv[0] 一定是 program name/path。
  3. 即使使用者沒有在參數帶上 program name/path ,也不要假設 WinMain(), wWinMain() 的參數一定不會帶有 program name/path。
  4. CreateProcess( app, app + arg ) 和 CreateProcess( NULL, app + arg ) 要考慮一下 search path。

後話

不知道大家有沒有好奇過:在工作管理員這類工具中去檢視 process 的 command line 時,常常會有好幾種情況出現:
  1. 參數中有 exe 名稱
  2. 參數中只有 cmdline 參數
  3. 參數中有 exe 名稱和 cmdline 參數
  4. 沒有出現過空的 cmdline
為什麼會有 1 ~ 3 的差異?又為什麼沒看過 4 呢?是不是跟這次提到的 CreateProcess() 使用方式有關係呢?
1543462603097

Reference

  1. WinMain: The Application Entry Point
  2. INFO: Understanding CreateProcess and Command-line Arguments
  3. https://github.com/WCChou/CrashTonightOh/tree/master/MissingProcessArgument

留言

這個網誌中的熱門文章

Building Clang with openSUSE Tumbleweed + Virtual Box 6.0

Syntax Hightlight