x86 反彙編/變數
我們已經看到了一些在堆疊上建立本地儲存的機制。本章將討論其他一些變數,包括全域性變數、靜態變數、標記為 "const"、"register" 和 "volatile" 的變數。它還將考慮一些關於變數的一般技術,包括訪問器和設定器方法(借鑑面向物件的術語)。本節還可能討論在偵錯程式中設定記憶體斷點以跟蹤變數的記憶體 I/O。
變數有兩種不同的型別:在堆疊上建立的變數(區域性變數)和透過硬編碼記憶體地址訪問的變數(全域性變數)。任何透過硬編碼地址訪問的記憶體通常都是全域性變數。透過 esp 或 ebp 的偏移量訪問的變數通常是區域性變數。
- 硬編碼地址
- 任何硬編碼的值都是直接儲存在二進位制檔案中的值,並且在執行時不會改變。例如,值 0x2054 是硬編碼的,而變數 X 的當前值不是硬編碼的,它可能在執行時改變。
硬編碼地址的示例
mov eax, [0x77651010]
或
mov ecx, 0x77651010
mov eax, [ecx]
非硬編碼(軟編碼?)地址的示例
mov ecx, [esp + 4]
add ecx, ebx
mov eax, [ecx]
在最後一個例子中,ecx 的值是在執行時計算的,而在前兩個例子中,該值在每次都是相同的。RVA 被認為是硬編碼地址,即使載入器需要“修復它們”以指向正確的地址。
.bss 和 .data 段都包含可以在執行時改變的值(例如變數)。通常,在原始碼中初始化為非零值的變數被分配到 .data 段(例如 "int a = 10;")。未初始化或初始化為零值的變數可以分配到 .bss 段(例如 "int arr[100];")。因為保證所有 .bss 變數的值在程式開始時都是零,所以連結器不需要在二進位制檔案中分配空間。因此,無論其大小如何,.bss 段在二進位制檔案中都不佔用空間。
標記為static 的區域性變數在函式呼叫之間保持其值,因此不能像其他區域性變數一樣在堆疊上建立。靜態變數是如何建立的呢?讓我們來看一個簡單的 C 函式示例
void MyFunction(int a)
{
static int x = 0;
printf("my number: ");
printf("%d, %d\n", a, x);
}
使用cl.exe 編譯到列表檔案,我們得到以下程式碼
_BSS SEGMENT
?x@?1??MyFunction@@9@9 DD 01H DUP (?) ; `MyFunction'::`2'::x
_BSS ENDS
_DATA SEGMENT
$SG796 DB 'my number: ', 00H
$SG797 DB '%d, %d', 0aH, 00H
_DATA ENDS
PUBLIC _MyFunction
EXTRN _printf:NEAR
; Function compile flags: /Odt
_TEXT SEGMENT
_a$ = 8 ; size = 4
_MyFunction PROC NEAR
; Line 4
push ebp
mov ebp, esp
; Line 6
push OFFSET FLAT:$SG796
call _printf
add esp, 4
; Line 7
mov eax, DWORD PTR ?x@?1??MyFunction@@9@9
push eax
mov ecx, DWORD PTR _a$[ebp]
push ecx
push OFFSET FLAT:$SG797
call _printf
add esp, 12 ; 0000000cH
; Line 8
pop ebp
ret 0
_MyFunction ENDP
_TEXT ENDS
通常,當彙編列表釋出到這個華夏公益教科書時,大多數程式碼亂碼會被丟棄以提高可讀性,但在這個例子中,"亂碼"包含了我們正在尋找的答案。可以清楚地看到,此函式建立了一個標準的堆疊幀,並且它沒有在堆疊上建立任何區域性變數。為了完整起見,我們將在這裡逐步進行,並以邏輯的方式得出結論。
在第 7 行的程式碼中,有一個對 _printf 的呼叫,它帶有 3 個引數。Printf 是一個標準的libc 函式,因此可以假設它是 cdecl 呼叫約定。因此,引數是從右到左壓入的。在呼叫 _printf 之前,三個引數被壓入堆疊
DWORD PTR ?x@?1??MyFunction@@9@9DWORD PTR _a$[ebp]OFFSET FLAT:$SG797
第二個,_a$[ebp] 在此彙編指令中被部分定義
_a$ = 8
因此,_a$[ebp] 是位於 ebp 偏移量 +8 的變數,或函式的第一個引數。OFFSET FLAT:$SG797 同樣在彙編列表中這樣宣告
SG797 DB '%d, %d', 0aH, 00H
如果你有你的 ASCII 表,你會注意到 0aH = 0x0A = '\n'。OFFSET FLAT:$SG797 然後是我們 printf 語句的格式字串。那麼我們最後一個選擇是神秘的 "?x@?1??MyFunction@@9@9",它在以下彙編程式碼段中定義
_BSS SEGMENT
?x@?1??MyFunction@@9@9 DD 01H DUP (?)
_BSS ENDS
這表明 Microsoft C 編譯器在 .bss 段中建立靜態變數。這可能不適用於所有編譯器,但教訓是一樣的:區域性靜態變數的建立和使用方式與全域性值非常相似,如果不是完全相同的話。事實上,就反彙編者而言,這兩者通常可以互換。請記住,靜態變數和全域性變數之間真正的區別在於 "範圍" 的概念,該概念僅由編譯器使用。
整數格式的變數,例如int、char、short 和long,可以在 C 原始碼中宣告為有符號或無符號變數。有兩種不同的處理方式
- 有符號變數使用帶符號指令,例如add 和sub。無符號變數使用無符號算術指令,例如addi 和subi。
- 有符號變數使用帶符號分支指令,例如jge 和jl。無符號變數使用無符號分支指令,例如jae 和jb。
有符號和無符號指令之間的區別在於設定大於或小於(溢位標誌)的各種標誌的條件。對於有符號和無符號資料,整數結果值完全相同。
浮點值通常是 32 位資料值(對於float)或 64 位資料值(對於double)。這些值與普通整數變數的區別在於它們與浮點指令一起使用。浮點指令通常以字母f開頭。例如,fadd、fcmp 等指令與浮點值一起使用。特別要注意的是fload指令及其變體。這些指令獲取一個整數值變數並將其轉換為浮點變數。
我們將在後面的章節中更詳細地討論浮點變數。
全域性變數沒有像函式體內的詞法變數那樣的有限範圍。由於詞法範圍的概念意味著使用系統堆疊,而全域性變數不是詞法性的,因此它們通常不會在堆疊中找到。全域性變數傾向於在程式中以硬編碼的記憶體地址存在,這個地址在整個程式執行期間都不會改變。這些地址可能存在於可執行檔案的 DATA 段中,或任何其他可以使用硬編碼記憶體地址儲存資料的位置。
在 C 中,全域性變數是在任何函式體之外定義的。沒有“全域性”關鍵字。任何不在函式內部定義的變數都是全域性的。但是,在 C 中,不在函式內部定義的變數只對定義它的特定原始碼檔案是全域性的。例如,我們有兩個檔案Foo.c 和Bar.c,以及一個全域性變數MyGlobalVar
| Foo.c | Bar.c |
|---|---|
int MyGlobalVar;
int GetVarFoo(void)
{
//right!
return MyGlobalVar;
}
|
int GetVarBar(void)
{
//wrong!
return MyGlobalVar;
}
|
在上面的例子中,變數MyGlobalVar 在檔案Foo.c 中可見,但在檔案Bar.c 中不可見。為了使MyGlobalVar 在所有專案檔案中可見,我們需要使用extern 關鍵字,我們將在下面討論。
C 程式語言指定了一個特殊的關鍵字 "static" 來定義對函式是詞法的變數(它們不能從函式外部引用),但它們在函式呼叫之間保持其值。與在函式進入時在堆疊上建立,並在函式返回時從堆疊中銷燬的普通詞法變數不同,靜態變數只建立一次,並且永遠不會銷燬。
int MyFunction(void)
{
static int x;
...
}
C 中的靜態變數是全域性變數,除了編譯器採取措施防止從父函式的範圍之外訪問變數。像全域性變數一樣,靜態變數是使用硬編碼的記憶體地址引用的,而不是像普通變數那樣在堆疊上的位置。然而,與全域性變數不同,靜態變數只在單個函式內部使用。在單個函式中使用的全域性變數與同一個函式內部的靜態變數之間沒有區別。但是,良好的程式設計實踐是限制全域性變數的數量,因此,在反彙編時,你應該更願意將這些變數解釋為靜態變數而不是全域性變數。
extern 關鍵字由 C 編譯器用來指示某個特定變數是全域性的,對整個專案有效,而不只是單個原始碼檔案。除了這個區別,以及 extern 變數略大的詞法範圍,它們應該被視為普通的全域性變數。
在靜態庫中,標記為 extern 的變數可能可供連結到該庫的程式使用。
以下表格總結了一些關於全域性變數的要點
| 引用方式 | 詞法範圍 | 註釋 | |
|---|---|---|---|
static 變數 |
硬編碼的記憶體地址,只在一個函式中 | 僅一個函式 | 在反彙編中,除了它只在一個函式中使用之外,與全域性變數無法區分。全域性變數只有在從未在另一個函式中使用時才為靜態的。 |
| 全域性變數 | 硬編碼的記憶體地址,只在一個檔案中 | 僅一個原始碼檔案 | 全域性變數只在一個檔案中使用。這可以幫助您在反彙編時大致瞭解原始原始碼的組織方式。 |
extern 變數 |
硬編碼的記憶體地址,在整個專案中 | 整個專案 | Extern 變數可供專案中的所有函式使用,也可供連結到專案的程式(例如外部庫)使用。 |
在反彙編時,硬編碼的記憶體地址應被視為普通全域性變數,除非您可以從變數的範圍確定它是靜態的還是 extern 的。
使用 const 關鍵字(在 C 中)限定的變數通常儲存在可執行檔案的 .data 部分。常量值可以區分,因為它們在程式開始時初始化,並且不會被程式本身修改。由於這個原因,一些編譯器可能會選擇將常量變數(尤其是字串)儲存在可執行檔案的 .text 部分,從而允許在同一程序的多個例項之間共享這些變數。這給逆向工程師帶來了一個大問題,他們現在必須決定他們正在檢視的程式碼是常量變數的一部分還是子程式的一部分。
在 C 和 C++ 中,變數可以宣告為 "易失性的",這告訴編譯器記憶體位置可以被外部或併發程序訪問,並且編譯器不應對變數執行任何最佳化。例如,如果多個執行緒都訪問和修改單個全域性值,編譯器有時將該變數儲存在暫存器中,然後不經常地將其重新整理到記憶體,這將是不好的。通常,易失性記憶體必須在每次計算後重新整理到記憶體,以確保當其他程序查詢時記憶體中包含最新的資料版本。
從反彙編列表中無法始終確定給定變數是否為易失性變數。但是,如果頻繁地從記憶體中訪問變數,並且它的值不斷更新到記憶體中(尤其是在有可用空閒暫存器的情況下),這是一個很好的提示,表明該變數可能是易失性的。
訪問器方法是源於面向物件理論和實踐的工具。在最簡單的形式中,訪問器方法是一個函式,它不接收任何引數(或者可能只接收一個偏移量),並返回變數的值。訪問器和設定器方法是限制對某些變數訪問的方式。獲取變數值的唯一標準方法是使用訪問器。
訪問器可以防止一些簡單的問題,例如陣列索引越界和使用未初始化資料。通常,訪問器包含很少或沒有錯誤檢查。
以下是一個示例
push ebp
mov ebp, esp
mov eax, [ecx + 8] ;THISCALL function, passes "this" pointer in ecx
mov esp, ebp
pop ebp
ret
由於訪問器方法非常簡單,因此它們經常被高度最佳化(通常不需要堆疊幀),甚至偶爾會被編譯器內聯。
設定器方法是訪問器方法的對立面,它們提供了一種統一的方式來更改給定變數的值。設定器方法通常將要設定的值作為引數傳遞給變數,儘管某些方法(初始化器)只是將變數設定為預定義值。設定器方法通常會在設定變數之前對變數進行邊界檢查和錯誤檢查,並且經常會 a) 不返回值,或 b) 返回一個簡單的布林值來確定成功與否。
以下是一個示例
push ebp
mov ebp, esp
cmp [ebp + 8], 0
je error
mov eax, [ebp + 8]
mov [ecx + 0], eax
mov eax, 1
jmp end
:error
mov eax, 0
:end
mov esp, ebp
pop ebp
ret