的體會呢 ? 一個小小的 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 編程的人會更好)。
沒有留言:
張貼留言