x86 反汇编/对象和类
X86 反汇编维基教科书的“对象和类”页面是一个存根。您可以通过扩展此部分来提供帮助。
面向对象(OO)编程为我们提供了一个新的程序结构单元:对象。本章将研究从 C++ 中反汇编的类。本章不会直接处理 COM,但它将为将来讨论反向 COM 组件(仅限 Windows 用户)奠定许多基础。
一个没有继承任何东西的基本类可以分为两个部分,变量和方法。非静态变量被塞进一个简单的数据结构中,而方法则像其他任何函数一样进行编译和调用。
当你开始添加继承和多态时,事情变得更加复杂。为了简单起见,对象的结构将在没有继承的情况下进行描述。然而,在最后,将涵盖继承和多态。
在类中定义的所有静态变量都驻留在应用程序整个生命周期内的静态内存区域。在类中定义的每个其他变量都放置在一个称为对象的数据结构中。通常,当构造函数被调用时,变量会按照顺序放置到对象中,请参见图 1。
A
class ABC123 {
public:
int a, b, c;
ABC123():a(1), b(2), c(3) {};
};
B
0x00200000 dd 1 ;int a
0x00200004 dd 2 ;int b
0x00200008 dd 3 ;int c
图 1:内存中对象的示例 |
但是,编译器通常需要将变量分离成大小为字(2 字节)的倍数,以便定位它们。并非所有变量都满足此要求,特别是 char 数组;一些未使用的位可能会被用来填充变量,以满足此大小要求。这在图 2中有所说明。
A
class ABC123{
public:
int a;
char b[3];
double c;
ABC123():a(1),c(3) { strcpy(b,"02"); };
};
B
0x00200000 dd 1 ;int a ; offset = abc123 + 0*word_size
0x00200004 db '0' ;b[0] = '0' ; offset = abc123 + 2*word_size
0x00200005 db '2' ;b[1] = '2'
0x00200006 db 0 ;b[2] = null
0x00200007 db 0 ;<= UNUSED BYTE
0x00200008 dd 0x00000000 ;double c, lower 32 bits ; offset = abc123 + 4*word_size
0x0020000C dd 0x40080000 ;double c, upper 32 bits
图 2:具有填充变量的对象的示例 |
为了使应用程序访问这些对象变量之一,需要对对象指针进行偏移以找到所需的变量。每个变量的偏移量由编译器知道,并在需要时写入对象代码中。图 3显示了如何偏移指针以检索变量。
;abc123 = pointer to object
mov eax, [abc123] ;eax = &a ;offset = abc123+0*word_size = abc123
mov ebx, [abc123+4] ;ebx = &b ;offset = abc123+2*word_size = abc123+4
mov ecx, [abc123+8] ;ecx = &c ;offset = abc123+4*word_size = abc123+8
图 3:这显示了如何偏移指针以检索变量。第一行将变量 'a' 的地址放入 eax。第二行将变量 'b' 的地址放入 ebx。最后一行将变量 'c' 放入 ecx。
在低级,函数和方法几乎没有区别。反编译时,有时很难区分两者。它们都驻留在文本内存空间中,并且都以相同的方式被调用。方法调用示例可以在图 4中看到。
A
//method call
abc123->foo(1, 2, 3);
B
push 3 ; int c
push 2 ; int b
push 1 ; int a
push [ebp-4] ; the address of the object
call 0x00434125 ; call to method
图 4:方法调用。 |
方法调用中值得注意的特点是作为参数传入的对象的地址。但是,这不是一个始终可靠的指标。图 5显示了第一个参数是按引用传入的对象的函数。结果是看起来与方法调用相同的函数。
A
//function call
foo(abc123, 1, 2, 3);
B
push 3 ; int c
push 2 ; int b
push 1 ; int a
push [ebp+4] ; the address of the object
call 0x00498372 ; call to function
图 5:函数调用。 |
继承和多态完全改变了类的结构,对象不再只包含变量,它们还包含指向继承方法的指针。这是因为多态要求在运行时确定方法或内部对象的地址。
考虑图 6。应用程序如何知道要调用 D::one 还是 C::one?答案是编译器弄清楚了一个约定,用于在对象内部对变量和方法指针进行排序,这样,当引用它们时,对于任何继承了其方法和变量的对象,偏移量都相同。
A *obj[2];
obj[0] = new C();
obj[1] = new D();
for(int i=0; i<2; i++)
obj[i]->one();
|
图 6:一个小的 C++ 多态循环,它调用一个函数 one。类 C 和 D 都继承了一个抽象类 A。为了使此代码工作,类 A 必须具有一个名为“one”的虚方法。 |
抽象类 A 充当编译器的蓝图,定义了任何继承它的类的预期结构。在类 A 中定义的每个变量以及在 A 中定义的每个虚方法都将在其任何子类中具有完全相同的偏移量。图 7声明了一个可能的继承方案及其在内存中的结构。注意 C::one 的偏移量与 D::one 相同,而 C 中 A::a 的副本的偏移量与 D 中的副本相同。在此,我们的多态循环只需遍历指针数组并确切地知道在何处找到每个方法。
A
class A{
public:
int a;
virtual void one() = 0;
};
class B{
public:
int b;
int c;
virtual void two() = 0;
};
class C: public A{
public:
int d;
void one();
};
class D: public A, public B{
public:
int e;
void one();
void two();
};
B
;Object C
0x00200000 dd 0x00423848 ; address of C::one ;offset = 0*word_size
0x00200004 dd 1 ; C's copy of A::a ;offset = 2*word_size
0x00200008 dd 4 ; C::d ;offset = 4*word_size
;Object D
0x00200100 dd 0x00412348 ; address of D::one ;offset = 0*word_size
0x00200104 dd 1 ; D's copy of A::a ;offset = 2*word_size
0x00200108 dd 0x00431255 ; address of D::two ;offset = 4*word_size
0x0020010C dd 2 ; D's copy of B::b ;offset = 6*word_size
0x00200110 dd 3 ; D's copy of B::c ;offset = 8*word_size
0x00200114 dd 5 ; D::e ;offset = 10*word_size
图 7:一个多态继承方案。
图 7.A 定义了继承方案。它显示类 C 继承了类 A,而类 D 继承了类 A 和类 B。 |