1. 程式人生 > >淺談Windows API程式設計 (這個經典)

淺談Windows API程式設計 (這個經典)

原文地址:http://blog.sina.com.cn/s/blog_46d85b2a01010qpt.html

WinSDK是程式設計中的傳統難點,個人寫的WinAPI程式也不少了,其實之所以難就難在每個呼叫的API都包含著Windows這個作業系統的潛規則或者是windows內部的執行機制……

WinSDK是程式設計中的傳統難點,曾經聽有一個技術不是很好的朋友亂說什麼給你API誰都會用,其實並非那麼簡單,個人寫的WinAPI程式也不少了,其實之所以難就難在每個呼叫的API都包含著Windows這個作業系統的潛規則或者是windows內部的執行機制。

首先來談談控制代碼,初學習WinSDK的朋友剛看到這個詞頭大了吧?其實我也是了,我們來看看programming windows裡面是怎麼說的,一個控制代碼僅僅是用來識別某些事情的數字。它唯一的標識這當前的一個例項。這樣說確實不容易懂。那麼我們這麼看,比如你開啟 windows自帶的計算器。你多開啟幾次是不是桌面上出現了很多個計算器呢?你使用其中一個計算器的時候當你按下等於按鈕的時候運算結果是否會出現在其他的計算機結果欄裡?不會,那windows怎麼知道讓結果出現在哪裡呢?這就是控制代碼的作用了,控制代碼唯一的標識著一個程式,你開啟的每一個視窗(計算器) 都有一個不同的控制代碼你你每一步操作都是指定了在某個控制代碼下的,所以,他不會出錯。而且你開啟的每一個計算機都共享著同樣的程式碼和記憶體。通過控制代碼系統會把所需的資源充分的呼叫到當前的某個程式自己的資料區。

不僅是視窗,各種選單,GDI物件都有自己的控制代碼,獲取控制代碼的手段也是多重多樣,不過當然是通過呼叫API函式實現了,如:

MFC中的hHandle = GetSafeHandle();

API程式設計中的hBrush = GetStorkObject(BLACK_BRUSH);

很多操作都需要將控制代碼新增到引數列表中,當你沒有直接定義控制代碼變數的時候可能要記憶很多API的返回型別來間接獲取。如:

      hPen = SelectObject(hdc,GetStockObject(&logicpen));
// SelectObject()這個函式在設定本裝置描述表下的GDI物件時會返回設定前的GDI物件控制代碼
MoveToEx(hdc, pt1.x, pt1.y, &apt);
LineTo(hdc, pt2.x,pt2.y);
SelectObject(hdc,hPen);

完成選擇自定義的GDI物件的操作。控制代碼的種類很多,掌握一種的使用方法所有的不學自通,WinAPI程式設計永遠伴隨的元素中控制代碼是其中之一。非常重要。由於是淺談,所以就說到這裡了.

接下來是windows下的訊息對映機制了,呵呵,視窗過程,剛學的朋友難理解吧?WinSDK程式設計基於C,但是和C的理念有著完全的不同,這中間的不同,在我看來最多的也就是來自於這個訊息對映,後面什麼吹的很炫的Hook技術,木馬技術,鍵盤截獲,都是來自於特殊訊息的捕捉,對映自定義的特殊訊息來實現的(當然和我接下來談的稍微有點不同)。

首先我們應該先明白訊息和事件的區別,Windows是訊息驅動的作業系統,這裡的訊息的產生來自於某個例項化的物件上使用者的操作,來自控制元件,選單,或者是系統本身產生的,而事件是靠訊息觸發的,但這也不是絕對的。可以用一個簡單的例子去解釋,我這裡越寫越覺得自己難表達清楚,就比如這麼一個例子:“某男殺人這條訊息導致被槍斃這個事件”不過最重要的區別是在訊息產生後並不會被直接處理,而是先插入windows系統的訊息佇列,然後系統判斷此訊息產生於哪個程式,送入此程式的訊息迴圈,由LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam)處理。而事件是作業系統處理訊息的過程中反饋的結果。

使用者操作-> 產生訊息->傳送系統->系統判斷來源->發給相應的視窗過程或者其他Callback函式->訊息處理->等待下一條訊息的產生

以上為訊息迴圈整個過程。

      LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam);
int WINAPI WinMain(…)
{
MSG msg;
RegisterClass(…); // 註冊視窗類
CreateWindow(…); // 建立視窗
ShowWindow(…); // 顯示視窗
UpdateWindow(…);
While(GetMessage(&msg,…)){ // 訊息迴圈
TranslateMessage(…);
DispatchMessage(…);
}
LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam);
//視窗過程函式,用於對映switch語句中各個需要被處理的訊息
{
While((UINT)message)
{
Switch(message)
Case…
Case…
………
Default……….
}
}

以上是最基本的WinAPi程式設計的程式碼結構。其實這裡面最重要的結構莫過於while(GetMessage(&msg))和 Winproc這個函式,這也是傳統的C面向過程程式設計的區別,win程式設計總等著特定事件觸發對應的訊息對映函式來完成程式碼功能,並不是一條程式碼從頭走到尾。關於特殊訊息的對映,這裡不談,這裡僅是個入門指引。

最後談一點就是重繪問題。其實在我看來這個東西更多是屬於GDI程式設計裡面的東西,說起來其實難度不大,但是處理起來確實是個難點。先拿剛才的程式碼來說吧。先新增一條關於WM_LBUTTONDOWN的訊息對映:

      Static int apt[2];
case WM_LBUTTONDOWN:
hdc = GetDC(hwnd);
apt[1].x = LOWORD (lParam);
apt[1].y = HIWORD (lParam);
hPen = CreatePen(BLACK_PEN,3,RGB(125,125,125));
SelectObject(hdc,hPen);
MoveToEx(hdc,apt[0].x,apt[0].y,NULL);
LineTo(hdc,apt[1].x,apt[1].y);
apt[0].x = apt[1].x;
apt[0].y = apt[1].y;
DeleteObject(hPen);
ReleaseDC(hwnd,hdc);
return 0;

這段程式碼實現一個簡單的畫線功能,當你在你的客戶區胡點一通滑鼠後試著拖動一下視窗大小,或者將其最小化或者被其他視窗覆蓋一下你都會發現你原來畫的線沒了,可是其他視窗為什麼被覆蓋了以後再彈出視窗還會有原來的東西呢?那就是重繪,要重新繪製整個客戶區(準確的說是失效的矩形),以上說的操作都會導致你的客戶區失效,這時會產生重繪訊息WM_PAINT,我們要想儲存這些線那麼我們就必須儲存這些你用滑鼠左鍵點過的點。當然這是重繪技術中最簡單的,當你的客戶區上是一個複雜的畫面的話,就不僅僅需要儲存點,還有各種形狀的圖形,顏色等等……這裡給大家一段我自己寫的程式碼來實現以上的 WM_LBUTTONDOWN訊息對映來產生的點。通過單鏈表來動態新增點來實現重繪。

      case WM_PAINT:
hdc = BeginPaint(hwnd,&ps);
TextOut(hdc,cxClient/6,cyClient/6,TEXT("圖形重繪"),strlen("圖形重繪"));
ReDrawLines(&MyList,hdc);
EndPaint(hwnd,&ps);
return 0;
case WM_LBUTTONDOWN:
hdc = GetDC(hwnd);
apt[1].x = LOWORD (lParam);
apt[1].y = HIWORD (lParam);
hPen = CreatePen(BLACK_PEN,2,RGB(125,0,0));
SelectObject(hdc,hPen);
MoveToEx(hdc,apt[0].x,apt[0].y,NULL);
LineTo(hdc,apt[1].x,apt[1].y);
MyList.pCurrent->x = apt[0].x;
MyList.pCurrent->y = apt[0].y;
MyList.pCurrent->pNext->x = apt[1].x;
MyList.pCurrent->pNext->y = apt[1].y;
MyList.m_iCounter = MyList.m_iCounter+2;
MyList.pCurrent = MyList.pCurrent->pNext->pNext;
apt[0].x = apt[1].x;
apt[0].y = apt[1].y;
DeleteObject(hPen);
ReleaseDC(hwnd,hdc);
return 0;

其中的重繪函式程式碼如下:

      void ReDrawLines(LinkList* pLinkList,HDC hdc)
{
pMyPoint p = pLinkList->pHead;
int iSaver =pLinkList->m_iCounter;
while(iSaver!=0)
{
MoveToEx(hdc,p->x,p->y,NULL);
LineTo(hdc,p->pNext->x,p->pNext->y);
p=p->pNext->pNext;
iSaver=iSaver-2;
}
}

添加了以上的程式碼你會發現再次拖動視窗大小等等你原來畫的線就都能重現出來了。呵呵是不是覺得一個看似簡單的東西其實裡面需要很多程式碼實現呢?也許,這就是windows.

好了,WinSDK入門的東西就談這麼多,希望能給初學者一定的幫助,這麼多的字,都是我一個一個打出來沒有任何借鑑和摘抄的。相信做為一個過來人能更多的理解大家學習中的困難。

Win32環境下動態連結庫(DLL)程式設計原理

比較大應用程式都由很多模組組成,這些模組分別完成相對獨立的功能,它們彼此協作來完成整個軟體系統的工作。其中可能存在一些模組的功能較為通用,在構造其它軟體系統時仍會被使用。在構造軟體系統時,如果將所有模組的原始碼都靜態編譯到整個應用程式EXE檔案中,會產生一些問題:一個缺點是增加了應用程式的大小,它會佔用更多的磁碟空間,程式執行時也會消耗較大的記憶體空間,造成系統資源的浪費;另一個缺點是,在編寫大的EXE程式時,在每次修改重建時都必須調整編譯所有原始碼,增加了編譯過程的複雜性,也不利於階段性的單元測試。

Windows系統平臺上提供了一種完全不同的較有效的程式設計和執行環境,你可以將獨立的程式模組建立為較小的DLL(Dynamic Linkable Library)檔案,並可對它們單獨編譯和測試。在執行時,只有當EXE程式確實要呼叫這些DLL模組的情況下,系統才會將它們裝載到記憶體空間中。這種方式不僅減少了EXE檔案的大小和對記憶體空間的需求,而且使這些DLL模組可以同時被多個應用程式使用。Microsoft Windows自己就將一些主要的系統功能以DLL模組的形式實現。例如IE中的一些基本功能就是由DLL檔案實現的,它可以被其它應用程式呼叫和整合。

一般來說,DLL是一種磁碟檔案(通常帶有DLL副檔名),它由全域性資料、服務函式和資源組成,在執行時被系統載入到程序的虛擬空間中,成為呼叫程序的一部分。如果與其它DLL之間沒有衝突,該檔案通常對映到程序虛擬空間的同一地址上。DLL模組中包含各種匯出函式,用於向外界提供服務。Windows 在載入DLL模組時將程序函式呼叫與DLL檔案的匯出函式相匹配。

在Win32環境中,每個程序都複製了自己的讀/寫全域性變數。如果想要與其它程序共享記憶體,必須使用記憶體對映檔案或者宣告一個共享資料段。DLL模組需要的堆疊記憶體都是從執行程序的堆疊中分配出來的。

DLL現在越來越容易編寫。Win32已經大大簡化了其程式設計模式,並有許多來自AppWizard和MFC類庫的支援。

一、匯出和匯入函式的匹配

DLL檔案中包含一個匯出函式表。這些匯出函式由它們的符號名和稱為標識號的整數與外界聯絡起來。函式表中還包含了DLL中函式的地址。當應用程式載入 DLL模組時時,它並不知道呼叫函式的實際地址,但它知道函式的符號名和標識號。動態連結過程在載入的DLL模組時動態建立一個函式呼叫與函式地址的對應表。如果重新編譯和重建DLL檔案,並不需要修改應用程式,除非你改變了匯出函式的符號名和引數序列。

簡單的DLL檔案只為應用程式提供匯出函式,比較複雜的DLL檔案除了提供匯出函式以外,還呼叫其它DLL檔案中的函式。這樣,一個特殊的DLL可以既有匯入函式,又有匯入函式。這並不是一個問題,因為動態連結過程可以處理交叉相關的情況。

在DLL程式碼中,必須像下面這樣明確宣告匯出函式:

__declspec(dllexport) int MyFunction(int n);

但也可以在模組定義(DEF)檔案中列出匯出函式,不過這樣做常常引起更多的麻煩。在應用程式方面,要求像下面這樣明確宣告相應的輸入函式:

__declspec(dllimport) int MyFuncition(int n);

僅有匯入和匯出宣告並不能使應用程式內部的函式呼叫連結到相應的DLL檔案上。應用程式的專案必須為連結程式指定所需的輸入庫(LIB檔案)。而且應用程式事實上必須至少包含一個對DLL函式的呼叫。

二、與DLL模組建立連結

應用程式匯入函式與DLL檔案中的匯出函式進行連結有兩種方式:隱式連結和顯式連結。所謂的隱式連結是指在應用程式中不需指明DLL檔案的實際儲存路徑,程式設計師不需關心DLL檔案的實際裝載。而顯式連結與此相反。

採用隱式連結方式,程式設計師在建立一個DLL檔案時,連結程式會自動生成一個與之對應的LIB匯入檔案。該檔案包含了每一個DLL匯出函式的符號名和可選的標識號,但是並不含有實際的程式碼。LIB檔案作為DLL的替代檔案被編譯到應用程式專案中。當程式設計師通過靜態連結方式編譯生成應用程式時,應用程式中的呼叫函式與LIB檔案中匯出符號相匹配,這些符號或標識號進入到生成的EXE檔案中。LIB檔案中也包含了對應的DLL檔名(但不是完全的路徑名),連結程式將其儲存在EXE檔案內部。當應用程式執行過程中需要載入DLL檔案時,Windows根據這些資訊發現並載入DLL,然後通過符號名或標識號實現對DLL函式的動態連結。

顯式連結方式對於整合化的開發語言(例如VB)比較適合。有了顯式連結,程式設計師就不必再使用匯入檔案,而是直接呼叫Win32 的LoadLibary函式,並指定DLL的路徑作為引數。LoadLibary返回HINSTANCE引數,應用程式在呼叫 GetProcAddress函式時使用這一引數。GetProcAddress函式將符號名或標識號轉換為DLL內部的地址。假設有一個匯出如下函式的 DLL檔案:

extern "C" __declspec(dllexport) double SquareRoot(double d);

下面是應用程式對該匯出函式的顯式連結的例子:

typedef double(SQRTPROC)(double);
HINSTANCE hInstance;
SQRTPROC* pFunction;
VERIFY(hInstance=::LoadLibrary("c:\\winnt\\system32\\mydll.dll"));
VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,"SquareRoot"));
double d=(*pFunction)(81.0);//呼叫該DLL函式

在隱式連結方式中,所有被應用程式呼叫的DLL檔案都會在應用程式EXE檔案載入時被載入在到記憶體中;但如果採用顯式連結方式,程式設計師可以決定DLL檔案何時載入或不載入。顯式連結在執行時決定載入哪個DLL檔案。例如,可以將一個帶有字串資源的DLL模組以英語載入,而另一個以西班牙語載入。應用程式在使用者選擇了合適的語種後再載入與之對應的DLL檔案。

三、使用符號名連結與標識號連結

在Win16環境中,符號名連結效率較低,所有那時標識號連結是主要的連結方式。在Win32環境中,符號名連結的效率得到了改善。Microsoft 現在推薦使用符號名連結。但在MFC庫中的DLL版本仍然採用的是標識號連結。一個典型的MFC程式可能會連結到數百個MFC DLL函式上。採用標識號連結的應用程式的EXE檔案體相對較小,因為它不必包含匯入函式的長字串符號名。



四、編寫DllMain函式

DllMain函式是DLL模組的預設入口點。當Windows載入DLL模組時呼叫這一函式。系統首先呼叫全域性物件的建構函式,然後呼叫全域性函式 DLLMain。DLLMain函式不僅在將DLL連結載入到程序時被呼叫,在DLL模組與程序分離時(以及其它時候)也被呼叫。下面是一個框架 DLLMain函式的例子。

HINSTANCE g_hInstance;
extern "C" int APIENTRY DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved)
{
if(dwReason==DLL_PROCESS_ATTACH)
{
TRACE0("EX22A.DLL Initializing!\n");
//在這裡進行初始化
}
else if(dwReason=DLL_PROCESS_DETACH)
{
TRACE0("EX22A.DLL Terminating!\n");
//在這裡進行清除工作
}
return 1;//成功
}

如果程式設計師沒有為DLL模組編寫一個DLLMain函式,系統會從其它執行庫中引入一個不做任何操作的預設DLLMain函式版本。在單個執行緒啟動和終止時,DLLMain函式也被呼叫。正如由dwReason引數所表明的那樣。

五、模組控制代碼



程序中的每個DLL模組被全域性唯一的32位元組的HINSTANCE控制代碼標識。程序自己還有一個HINSTANCE控制代碼。所有這些模組控制代碼都只有在特定的程序內部有效,它們代表了DLL或EXE模組在程序虛擬空間中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,這個兩種型別可以替換使用。程序模組控制代碼幾乎總是等於0x400000,而DLL模組的載入地址的預設控制代碼是0x10000000。如果程式同時使用了幾個DLL模組,每一個都會有不同的HINSTANCE值。這是因為在建立DLL檔案時指定了不同的基地址,或者是因為載入程式對DLL程式碼進行了重定位。
模組控制代碼對於載入資源特別重要。Win32 的FindResource函式中帶有一個HINSTANCE引數。EXE和DLL都有其自己的資源。如果應用程式需要來自於DLL的資源,就將此引數指定為DLL的模組控制代碼。如果需要EXE檔案中包含的資源,就指定EXE的模組控制代碼。

但是在使用這些控制代碼之前存在一個問題,你怎樣得到它們呢?如果需要得到EXE模組控制代碼,呼叫帶有Null引數的Win32函式GetModuleHandle;如果需要DLL模組控制代碼,就呼叫以DLL檔名為引數的Win32函式GetModuleHandle


六、應用程式怎樣找到DLL檔案

如果應用程式使用LoadLibrary顯式連結,那麼在這個函式的引數中可以指定DLL檔案的完整路徑。如果不指定路徑,或是進行隱式連結,Windows將遵循下面的搜尋順序來定位DLL:

1. 包含EXE檔案的目錄,
2. 程序的當前工作目錄,
3. Windows系統目錄,
4. Windows目錄,
5. 列在Path環境變數中的一系列目錄。

這裡有一個很容易發生錯誤的陷阱。如果你使用VC++進行專案開發,並且為DLL模組專門建立了一個專案,然後將生成的DLL檔案拷貝到系統目錄下,從應用程式中呼叫DLL模組。到目前為止,一切正常。接下來對DLL模組做了一些修改後重新生成了新的DLL檔案,但你忘記將新的DLL檔案拷貝到系統目錄下。下一次當你執行應用程式時,它仍載入了老版本的DLL檔案,這可要當心!

七、除錯DLL程式

Microsoft 的VC++是開發和測試DLL的有效工具,只需從DLL專案中執行除錯程式即可。當你第一次這樣操作時,除錯程式會向你詢問EXE檔案的路徑。此後每次在除錯程式中執行DLL時,除錯程式會自動載入該EXE檔案。然後該EXE檔案用上面的搜尋序列發現DLL檔案,這意味著你必須設定Path環境變數讓其包含DLL檔案的磁碟路徑,或者也可以將DLL檔案拷貝到搜尋序列中的目錄路徑下。

HOOK API是一個永恆的話題,如果沒有HOOK,許多技術將很難實現,也許根本不能實現。這裡所說的API,是廣義上的API,它包括DOS下的中斷, WINDOWS裡的API、中斷服務、IFS和NDIS過濾等。比如大家熟悉的即時翻譯軟體,就是靠HOOK TextOut()或ExtTextOut()這兩個函式實現的,在作業系統用這兩個函式輸出文字之前,就把相應的英文替換成中文而達到即時翻譯;IFS 和NDIS過濾也是如此,在讀寫磁碟和收發資料之前,系統會呼叫第三方提供的回撥函式來判斷操作是否可以放行,它與普通HOOK不同,它是作業系統允許的,由作業系統提供介面來安裝回調函式。

甚至如果沒有HOOK,就沒有病毒,因為不管是DOS下的病毒或WINDOWS裡的病毒,都是靠HOOK系統服務來實現自己的功能的:DOS下的病毒靠HOOK INT 21來感染檔案(檔案型病毒),靠HOOK INT 13來感染引導扇區(引導型病毒);WINDOWS下的病毒靠HOOK系統API(包括RING0層的和RING3層的),或者安裝IFS(CIH病毒所用的方法)來感染檔案。因此可以說“沒有HOOK,就沒有今天多姿多彩的軟體世界”。

由於涉及到專利和智慧財產權,或者是商業機密,微軟一直不提倡大家HOOK它的系統API,提供IFS和NDIS等其他過濾介面,也是為了適應防毒軟體和防火牆的需要才開放的。所以在大多數時候,HOOK API要靠自己的力量來完成。

HOOK API有一個原則,這個原則就是:被HOOK的API的原有功能不能受到任何影響。就象醫生救人,如果把病人身體裡的病毒殺死了,病人也死了,那麼這個 “救人”就沒有任何意義了。如果你HOOK API之後,你的目的達到了,但API的原有功能失效了,這樣不是HOOK,而是REPLACE,作業系統的正常功能就會受到影響,甚至會崩潰。

HOOK API的技術,說起來也不復雜,就是改變程式流程的技術。在CPU的指令裡,有幾條指令可以改變程式的流程:JMP,CALL,INT,RET, RETF,IRET等指令。理論上只要改變API入口和出口的任何機器碼,都可以HOOK,但是實際實現起來要複雜很多,因為要處理好以下問題:

1,CPU指令長度問題,在32位系統裡,一條JMP/CALL指令的長度是5個位元組,因此你只有替換API裡超過5個位元組長度的機器碼(或者替換幾條指令長度加起來是5位元組的指令),否則會影響被更改的小於5個位元組的機器碼後面的數條指令,甚至程式流程會被打亂,產生不可預料的後果;
2,引數問題,為了訪問原API的引數,你要通過EBP或ESP來引用引數,因此你要非常清楚你的HOOK程式碼裡此時的EBP/ESP的值是多少;
3,時機的問題,有些HOOK必須在API的開頭,有些必須在API的尾部,比如HOOK CreateFilaA(),如果你在API尾部HOOK API,那麼此時你就不能寫檔案,甚至不能訪問檔案;HOOK RECV(),如果你在API頭HOOK,此時還沒有收到資料,你就去檢視RECV()的接收緩衝區,裡面當然沒有你想要的資料,必須等RECV()正常執行後,在RECV()的尾部HOOK,此時去檢視RECV()的緩衝區,裡面才有想要的資料;
4,上下文的問題,有些HOOK程式碼不能執行某些操作,否則會破壞原API的上下文,原API就失效了;
5,同步問題,在HOOK程式碼裡儘量不使用全域性變數,而使用區域性變數,這樣也是模組化程式的需要;
6,最後要注意的是,被替換的CPU指令的原有功能一定要在HOOK程式碼的某個地方模擬實現。

下面以ws2_32.dll裡的send()為例子來說明如何HOOK這個函式:

Exported fn(): send - Ord:0013h
地址         機器碼                 彙編程式碼
:71A21AF4 55                   push ebp //將被HOOK的機器碼(第1種方法)
:71A21AF5 8BEC                 mov ebp, esp //將被HOOK的機器碼(第2種方法)
:71A21AF7 83EC10                 sub esp, 00000010
:71A21AFA 56                   push esi
:71A21AFB 57                   push edi
:71A21AFC 33FF                 xor edi, edi
:71A21AFE 813D1C20A371931CA271       cmp dword ptr [71A3201C], 71A21C93 //將被HOOK的機器碼(第4種方法)
:71A21B08 0F84853D0000             je 71A25893
:71A21B0E 8D45F8                 lea eax, dword ptr [ebp-08]
:71A21B11 50                   push eax
:71A21B12 E869F7FFFF             call 71A21280
:71A21B17 3BC7                 cmp eax, edi
:71A21B19 8945FC                 mov dword ptr [ebp-04], eax
:71A21B1C 0F85C4940000             jne 71A2AFE6
:71A21B22 FF7508                 push [ebp+08]
:71A21B25 E826F7FFFF