2013年3月5日 星期二

從 FASM 呼叫 C 副常式來自創 C 語言進入點

看完了上一篇對 Ternary operator 的深入研究之後,是否對程式語言有更進一步
的體會呢 ? 一個小小的 Ternary operator 可以引出對於 指令設計硬體化 基本觀念與
利用來自不同層次程式語言思考模式放入更高階語言內實現的想法,相信不少寫
C 程式語言者很少想過之間的關連性,或同時有寫過 HDL 和 C 語言兩者可能也沒
甚麼感覺,反正就是寫出功能達成任務,但是假如你學會這種多層次語言思考模式
則對程式語言的應用將會更顯靈活,這大概就是上一篇小品文大意所在。

現在讓我們來看看本文的標題,講的是說要 「自創 C 語言進入點」,搞技術的人
常常都要寫程式,也大概知道程式會有一個進入點(a entry point),進入點通常是
一條特殊的函式名稱,你不可以拿這個名稱去用,例如令成一條函式,因為 Linker
在進行鏈結的時候,會去尋找這些特殊的符號名稱,讓它們當作程式活動起來的門口
這樣子軟體工程師就會有所根據,知道說「某特殊名稱函式會自動被執行」,這樣
工程師就會知道程式要開始從這邊寫與規劃整個程式的架構,整個程式就運作起來了...
所以這些特殊名稱的函式一般又稱為 Entry-point function,所以說上一篇的小品文不全然
無關,上一篇講 「? :」 運算子的原貌,這篇就是講 Entry-point function 的原貌,讓你
知道,這種函式還可以自己根據需求創造 。



先複習一下一支可執行程式產生的過程,如圖1.所示
圖1. 基本程式編譯與連結流程圖

圖1. 告訴我們一件事情,不管你用甚麼程式語言撰寫程式,只要編譯器或組譯器能夠
產生出正確的 Object File,最後 Linker 將會把它們全部結合在一起產生可執行檔,有
練習過前幾篇實戰呼叫慣例的讀者們應該會知道,在 Windows 系統上 Object File 使用
的是微軟的 COFF,假如你是慣用 Linux 系統下並使用 GCC 的讀者也無妨,只是格式不同
而已,Linux 系統目前主流用的是 ELF (Extensible Linking Format),這邊講的執行檔是很廣義
的形式,包含了 動態鏈結程式庫(DLL)、驅動程式(SYS)、可執行檔(EXE) 、程式庫(LIB) 與 二進制檔案,反正只要能產生某種環境下可接受的目的檔都算在其中。

各位還記得之前在實戰呼叫慣例有提到,FASM 能夠輕易支援各種不同型式的二進制格式
而且完全不需要依賴連結器即可產生可執行檔,所以我們就能夠用FASM來自創進入點
也就是 Entry-point function,利用 FASM 產生的執行檔呼叫自訂 DLL 中的某一條函式當
做進入點,這樣就可以達成自創具有特殊功能的進入點,先給出 FASM 端程式碼。

; Create a custom entry point with fasm (fasm_entry.exe)
format PE GUI 4.0
entry fasm_main

include 'include\win32a.inc'

;BOOL WINAPI WriteConsole
; _In_        HANDLE hConsoleOutput,
; _In_        const VOID *lpBuffer,
; _In_        DWORD nNumberOfCharsToWrite,
; _Out_       LPDWORD lpNumberOfCharsWritten,
; _Reserved_  LPVOID lpReserved

section '.text' code readable executable
  fasm_main:
    invoke  AllocConsole
    invoke  GetStdHandle, STD_OUTPUT_HANDLE
    invoke  WriteConsole, eax,  g_text, 26, g_written, 0

    ; main(hInstance:dd, hStdOut:dd)
    invoke GetStdHandle, STD_OUTPUT_HANDLE
    mov    [g_hStdOut], eax        ; Copy eax to g_hStdOut
    push   g_hStdOut               ; push address of g_hStdOut onto stack
    invoke GetModuleHandle, NULL   ; Get hInstance
    mov    [g_hInst], eax          ; Copy eax to g_hInst
    push   g_hInst                 ; push address of g_hInst onto stack
    call   [main]                  ; our custom entry point
    
    invoke ExitProcess, 0

section '.idata' import data readable writeable
  library kernel, 'kernel32.dll', \
          fmain, 'fasm_main.dll'
       
  import kernel, \
         GetModuleHandle, 'GetModuleHandleA', \
         ExitProcess, 'ExitProcess', \
         GetStdHandle, 'GetStdHandle', \
         AllocConsole, 'AllocConsole', \
         WriteConsole, 'WriteConsoleA'

  import fmain, \
         main, '_fasm_main@8'

section '.data' data readable writeable
    g_hInst     dd    ?
    g_hStdOut   dd    ?
    g_text      db    'The entry point: fasm_main'
    g_written   rd    1

這裡可以看到 format PE GUI 4.0 這一句就是講說要產生的執行檔格式為 PE 格式
Subsystem 為 GUI 也就是視窗型的程式,Subsystem Version 為 4.0,其他 Windows
系統只要用 Dependency Walker 看一下 Kernel32.dll 就知道對應的號碼是多少。
指令 entry 可以指定用哪個 Label 當作程式開始位置,上面的程式我設計FASM起始
進入點為 fasm_main,接著頭三行的 API 也許就開始出現讀者陌生的函式了,因為
大部分的人只知道用 VC 提供的 C 標準程式庫,可惜在 FASM 下這種原始的環境
你知道的 printf 這類的函式一點用處也沒有,你窮的只剩下 Windows API 可以用
沒錯,主題出現了,我要自創的是一種能同時帶有 Console 又能讓你擁有視窗
程式進入點的特殊函式,在這麼低階的環境,想要 Console 視窗,你只好乖乖的跟
系統原始的 Console WinAPI 打交道,首先我們先配置一個 Console 視窗,這點利用
 AllocConsole 就可以辦到,第二句是講說要操控這個Console區域,要先取得 stdout
因此呼叫 GetStdHandle API,參數使用 STD_OUTPUT_HANDLE,這樣就可以在 eax
得到 stdout 使用的 Handle 了 (沒有忘記吧 WinAPI 回傳值放在 eax),不過你可能
出現疑問,怎麼突然出現一個 invoke 指令,x86組語沒這種指令吧,的確,這是我用了
FASM的巨集,因為每次要 push 參數很麻煩,希望有類似 C 函式操作的手法使用函式

invoke GetStdHandle, STD_OUTPUT_HANDLE

就等同於

push STD_OUTPUT_HANDLE
call   GetStdHandle

至於 WriteConsole 其實用途就很明顯,我們已經獲得了操作該 Console 的 Handle,那就
把剛剛得到的 Handle 填入第一個參數,在把你想寫入的字串位址與長度塞給這條 API,你
就可以在這個黑漆漆的Console視窗 寫一些字,有沒有覺得功能很類似 printf 阿,因為 printf
其實最底層就會呼叫這條 API 阿,當然會長的些許神似(前面兩條也會用到)。 

接下來看看我們自創的進入點函式長甚麼模樣,它帶有兩個參數一個是 Stdout Handle 的位址,另外一個是寫視窗必備的參數 Instance Handle 的位址,在呼叫的時候為了方便,因此
使用了 FASM 強大的函式化名功能,把 _fasm_main@8 化名為 main 在 FASM 裡面呼叫,
這個就是自創的進入點函式,它在 DLL 內實際樣子如下 :

int WINAPI fasm_main(PDWORD pdwInst,PDWORD pdwStdOut)


你可以看到 既不是寫視窗常用的 WinMain 也不是 Console 程式常見的 main
而是我自創的進入點,傳入的 pdwInst 可以用來寫視窗程式,而傳入的 pdwStdOut 可以用來
控制在 FASM 內配置好給 fasm_main 使用的 Console 視窗,這樣就可以做到視窗程式還能夠
帶有一個 Console 視窗,這樣有甚麼好處呢? 寫視窗程式常常要除錯,寫過的人都知道,對於
視窗來說甚麼都是圖形,但是你可能只是要顯示某些程式內的變數出來除錯,這個時候
把這些值顯示在 Console 視窗除錯就很方便了,而不用再大費周章取得 HDC 這麻煩的傢伙。

section '.idata' 這個就是你平常不會看到隱藏在所有 PE 執行檔格式的特殊 section
這個 .idata 就存放一個執行檔會引入哪些 DLL 與 DLL 內的函式,這部分就不詳細解說
因為這個解說下去會變成 解釋 PE 格式文,這絕不是一個小篇幅可以解釋,且並不是本文重點
讀者只需要了解在 FASM 引入一個 DLL 函式的語法即可 跟著我寫的程式模仿即可,這邊也順便也可以體會到 FASM 真的非常強大,你完全可以自行控制 .idata 區段直接把 DLL 內某某函式引入直接使用,比在 C 語言內動態或靜態載入在 DLL 內的函式還簡單,FASM 完全不管函式的原型,因為對組語來說原型沒有意義,C 要原型是因為編譯器翻譯的時候才知道到底
幾個參數要 push,可是組語是撰寫者要自己 push,所以不需要原型。

剩下 ExitProcess 就沒有甚麼好討論,這個 API 會讓程式結束,讀者用 main 或 WinMain 寫
程式用 ollydbg 去追蹤,也會發現最終程式結束還是會呼叫這條 API,只是這部分是放在
呼叫 main 或是 呼叫 WinMain 的 C 執行時期程式庫,就類似我們在 FASM 自己打造的
這種原始環境,讀者可以參考微軟的 C Run-time Library 原始碼看看 main 在被呼叫
前做了甚麼,跳出 main 後又做了甚麼。

至於在 DLL 內 fasm_main 我寫了一個簡單的視窗示範這個自創進入點的使用
其實沒甚麼神奇,就是一個簡單的標準視窗程序而已,給出我寫好的原始碼
至於裡面跑些甚麼我就不講解,因為再次強調這篇的目的是讓讀者可以看到我
透過 FASM 做了一個簡單的引導設計了自己的程式進入點函式而已,一旦程式進入
進入點函式,其實就跟熟悉的 main 或 WinMain 用起來沒兩樣,讀者也可以寫其他
的程式,只是我創的這種進入點函式 還能額外提供 Console 視窗 長的樣子比較
奇怪罷了,至於不會用VC 創 DLL 專案跑我下面給的程式,還是請多多參考 MSDN。


DLL 內的原始碼 (fasm_main.dll)


#include <windows.h>

TCHAR g_szBuf[256];

static HINSTANCE g_hInst;
static HANDLE    g_hStdOut;
static HWND      g_hWnd;
static BOOL      g_bActive;
static TCHAR     g_szClassName[]  = _T("FASMWIN");
static TCHAR     g_szWindowName[] = _T("Simple Window");

LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);

extern "C" __declspec(dllexport) int g_fasm_var=0;

extern "C" __declspec(dllexport) int WINAPI fasm_main 
(
   PDWORD   pdwInst,
   PDWORD   pdwStdOut
)
{
    DWORD dwWritten;
    g_hInst   = reinterpret_cast<HINSTANCE>(*pdwInst);
    g_hStdOut = reinterpret_cast<HINSTANCE>(*pdwStdOut);

    wsprintf(g_szBuf, "\naddress of pdwInst = 0x%x, *pdwInst = 0x%x",  
             (void*)pdwInst, *pdwInst);
    WriteConsole(g_hStdOut, g_szBuf, _tcsclen(g_szBuf), &dwWritten, NULL);

    wsprintf(g_szBuf, "\naddress of pdwStdOut = 0x%x, *pdwStdOut = 0x%x",  
             (void*)pdwStdOut, *pdwStdOut);
    WriteConsole(g_hStdOut, g_szBuf, _tcsclen(g_szBuf), &dwWritten, NULL);

    MSG     msg;
    HICON   hIcon;
    HCURSOR hCursor;
    HBRUSH  hBrush;

    hIcon   = LoadIcon(NULL, IDI_APPLICATION);
    hCursor = LoadCursor(NULL, IDC_ARROW);
    hBrush  = reinterpret_cast<HBRUSH>(GetStockObject(WHITE_BRUSH));

    WNDCLASS wc =
    { 
        CS_VREDRAW | CS_HREDRAW,  // style
        MainWndProc,              // lpfnWndProc
        0,                        // cbClsExtra
        0,                        // cbWndExtra
        g_hInst,                  // hInstance
        hIcon,                    // handle to the icon
        hCursor,                  // handle to the cursor
        hBrush,                   // handle to the brush
        NULL,                     // lpszMenuName
        g_szClassName             // lpszClassName
    };

    if(!RegisterClass(&wc)) return 0;

    if( (g_hWnd = CreateWindow
                  (
                    g_szClassName, 
                    g_szWindowName,
                    WS_OVERLAPPEDWINDOW,
                    100, 100,
                    800, 600,
                    NULL,
                    NULL,
                    g_hInst,
                    NULL
                 )) == NULL )
        return 0;

    ShowWindow(g_hWnd, SW_NORMAL);
    UpdateWindow(g_hWnd);

    while(1)
    {
        if( PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE) )
        {
            if( !GetMessage(&msg, NULL, 0, 0) )
                return static_cast<int>(msg.wParam);
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else if(g_bActive)
        {}
        else
        {}
    }
}

LRESULT CALLBACK MainWndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
    switch(uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return S_OK;
    default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
} 


程式執行結果 :


讀者是否也能夠做出一樣的結果呢 ? 記得 fasm_entry.exe 與 fasm_main.dll
要放在同一個資料匣下,fasm_entry 執行的時候才找的到 fasm_main.dll。

(完)

後記 :
話說這篇拖了好久,主要是在思考到底要內容的仔細程度如何,因為不太可能把所有
的背景知識都一步一步解說,最後我定調於讀者程度是位於知道怎麼用 WinMain 寫一個
簡單的視窗與知道怎麼用 VC 寫一個簡單的 DLL (當然本來就用 nmake 編程的人會更好)。 

沒有留言:

張貼留言