1. 程式人生 > >VC 在呼叫main函式之前的操作

VC 在呼叫main函式之前的操作

在C/C++語言中規定,程式是從main函式開始,也就是C/C++語言中以main函式作為程式的入口,但是作業系統是如何載入這個main函式的呢,程式真正的入口是否是main函式呢?本文主要圍繞這個主題,通過逆向的方式來探討這個問題。本文的所有環境都是在xp上的,IDE主要使用IDA 與 VC++ 6.0。為何不選更高版本的編譯器,為何不在Windows 7或者更高版本的Windows上實驗呢?我覺得主要是VC6更能體現程式的原始行為,想一些更高版本的VS 它可能會做一些優化與檢查,從而造成反彙編生成的程式碼過於複雜不利於學習,當逆向的功力更深之後肯定得去分析新版本VS 生成的程式碼,至於現在,我的水平不夠只能看看VC6 生成的程式碼

首先通過VC 6編寫這麼一個簡單的程式

#include <stdio.h>
#include <windows.h>
#include <tchar.h>

int main()
{
    wchar_t str[] = L"hello world";
    size_t s = wcslen(str);
    return 0;
}

通過單步除錯,開啟VC6 的呼叫堆疊介面,發現在呼叫main函式之前還呼叫了mainCRTStartup 函式:
呼叫堆疊
在VC6 的反彙編視窗中好像不太好找到mainCRTStartup函式的程式碼,因此在這裡改用IDA pro來開啟生成的exe,在IDA的 export視窗中雙擊 mainCRTStartup 函式,程式碼就會跳轉到函式對應的位置。
找到mainCRTStartup 函式程式碼


它的程式碼比較長,剛開始也是進行函式的堆疊初始化操作,這個初始化主要是儲存原始的ebp,儲存重要暫存器的值,並且改變ESP的指標值初始化函式堆疊,這些就不詳細說明了,感興趣的可以去看看我之前寫的關於函式反彙編分析的內容:
C函式原理

在初始化完成之後,它有這樣的彙編程式碼

.text:004010EA                 push    offset __except_handler3
.text:004010EF                 mov     eax, large fs:0
.text:004010F5                 push    eax
.text:004010F6                 mov
large fs:0, esp

這段程式碼主要是用來註冊主執行緒的的異常處理函式的,為什麼它這裡的4行程式碼就可以設定執行緒的異常處理函式呢?這得從SEH的結構說起。

每個執行緒都有自己的SEH鏈,當發生異常的時候會呼叫鏈中儲存的處理函式,然後根據處理函式的返回來確定是繼續執行原先的程式碼,還是停止程式還是繼續將異常傳遞下去。這個連結串列資訊儲存在每個執行緒的NT_TIB結構中,這個結構每個執行緒都有,用來記錄當前執行緒的相關內容,以便在進行執行緒切換的時候做資料備份和恢復。當然不是所有的執行緒資料都儲存在這個結構中,它只保留部分。該結構的定義如下:

typedef struct _NT_TIB
{
     PEXCEPTION_REGISTRATION_RECORD ExceptionList;
     PVOID StackBase;
     PVOID StackLimit;
     PVOID SubSystemTib;
     union
     {
          PVOID FiberData;
          ULONG Version;
     };
     PVOID ArbitraryUserPointer;
     PNT_TIB Self;
} NT_TIB, *PNT_TIB;

這個結構的第一個引數是一個異常處理鏈的連結串列頭指標,連結串列結構的定義如下:

typedef struct _EXCEPTION_REGISTRATION_RECORD
{
     PEXCEPTION_REGISTRATION_RECORD Next;
     PEXCEPTION_DISPOSITION Handler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

這個結構很簡單的定義了一個連結串列,第一個成員是指向下一個節點的指標,第二個引數是一個異常處理函式的指標,當發生異常的時候會去呼叫這個函式。而這個連結串列的頭指標被存到fs暫存器中

知道了這點之後再來看這段程式碼,首先將異常函式入棧,然後將之前的連結串列頭指標入棧,這樣就組成了一個EXCEPTION_REGISTRATION_RECORD結構的節點而這個節點的指標現在就是ESP中儲存的值,之後再將連結串列的頭指標更新,也就是最後一句對fs的重新賦值,這是一個典型的使用頭插法新增連結串列節點的操作。通過這樣的幾句程式碼就向主執行緒中注入了一個新的異常處理函式。

之後就是進行各種初始化的操作,呼叫GetVersion 獲取版本號,呼叫 __heap_init 函式初始化C執行時的堆疊,這個函式後面有一個 esp + 4的操作,這裡可以看出這個函式是由呼叫者來做堆疊平衡的,也就是說它並不是Windows提供的api函式(API函式一般都是stdcall的方式呼叫,並且命名採用駝峰的方式命名)。呼叫GetCommandLineA函式獲取命令列引數,呼叫 GetEnvironmentStringsA 函式獲取系統環境變數,最後有這麼幾句話:

.text:004011B0                 mov     edx, __environ
.text:004011B6                 push    edx             ; envp
.text:004011B7                 mov     eax, ___argv
.text:004011BC                 push    eax             ; argv
.text:004011BD                 mov     ecx, ___argc
.text:004011C3                 push    ecx             ; argc
.text:004011C4                 call    _main_0

這段程式碼將環境變數、命令列引數和引數個數作為引數傳入main函式中。 在C語言中規定了main函式的三種形式,但是從這段程式碼上看,不管使用哪種形式,這三個引數都會被傳入,程式設計師使用哪種形式的main函式並不影響在VC環境在呼叫main函式時的傳參。只是我們程式碼中不使用這些變數罷了。

到此,這篇博文簡單的介紹了下在呼叫main函式之前執行的相關操作,這些彙編程式碼其實很容易理解,只是在註冊異常的程式碼有點難懂。最後總結一下在呼叫main函式之前的相關操作
1. 註冊異常處理函式
2. 呼叫GetVersion 獲取版本資訊
3. 呼叫函式 __heap_init初始化堆疊
4. 呼叫 __ioinit函式初始化啊IO環境,這個函式主要在初始化控制檯資訊,在未呼叫這個函式之前是不能進行printf的
5. 呼叫 GetCommandLineA函式獲取命令列引數
6. 呼叫 GetEnvironmentStringsA 函式獲取環境變數
7. 呼叫main函式