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@9
DWORD 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