滴水逆向 02-04 结构体
结构体
正向基础我就不整理了,多说一句,其实我之前最喜欢写的就是结构体,不知道为什么觉得用起来特别爽
但是这一部分有个问题,关于课上讲的结构体中变量的存储地址的计算,就是基址+偏移嘛,但是有一个问题就是,课堂中举的那个例子(见下图),Gamer 这个的第一个变量生命写成了 +4,应该是写错了,Gamer 的生命是 +0 才对,和坐标的那个规则一样是从 +0 开始,所以坐标的基址也应当是 +C 而不是 +10。

结构体的反汇编
-
作为全局变量时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24struct AA {
int x;
int y;
};
struct st {
int a;
char b;
short c;
int arr[10];
AA aa;
};
//全局变量
st x;
void Function() {
x.a = 10;
x.b = 20;
a.c = 30;
x.arr[0] = 1;
x.arr[1] = 2;
x.aa.x = 3;
x.aa.y = 4;
}
因为是全局变量,所以地址都是分配好写死了的。可以发现结构体存储的地址是连续的,且是不等宽的(等宽的是数组)。 -
作为局部变量时
1
2
3
4
5
6
7
8
9
10
11
12
13struct st {
int a;
char b;
short c;
};
void Function() {
//局部变量
st x;
x.a = 10;
x.b = 20;
x.c = 30;
}
地址不固定,和局部变量的表示形式差不多,但是结构体分配空间的时候是和数组一样从低地址向高地址存的,和普通局部变量分配的顺序相反1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct st {
int a;
int b;
int c;
};
void Function1() {
int a = 10;
int b = 20;
int c = 30;
}
void Function2() {
st s;
s.a = 10;
s.b = 20;
s.c = 30;
}普通局部变量是从高地址向低地址存的,10–[ebp-4]

结构体是和数组一样从低地址向高地址,30–[ebp-4]

以上只演示了数据类型都是一样的情况,宽度不一样的情况会涉及到结构体对齐, 在下次课中再讲
结构体作为参数
看一个很长的结构体:
1 | struct st { |

当东西很多的时候,就不会一个一个 push 了,而是呼啦一坨整进去,重点就是黑色这一块,也不难理解,编译器在此过程做的就是:
1、sub esp, 18h,提升堆栈,在栈顶挖出一块 18h 字节的坑
2、接下来 mov、lea、mov 三条指令分别准备好了循环次数、源地址、目的地址
3、rep movsd 就是循环复制嘛,把之前存在局部变量里的结构体全都复制到刚才挖的坑里
做完后以上三步后去调用函数,被调用函数再去堆栈里拿参数,拿到的就是刚才复制过来的那一坨了
结构体作为返回值
1 | struct st { |
main 函数与 Function 反汇编如下:


首先先看 main 调用函数前做了什么:
1 | 00401058 lea eax, [ebp-30h] ; 获取main栈中一块临时内存的首地址 |
因为这个 struct 太大了,所以编译器会在 main 的局部变量栈区里划出一块临时空间存函数返回值用。[ebp-30h] 就是这块空间的起始地址。至于为什么偏移是 -30h,这是编译器自己规划的,具体偏移量由编译器决定,但这块临时空间的实际大小会严格等于结构体的大小(在本例子中就是 16 字节,也就是 10h)
然后就进入 function 了,前面一堆就是日常的提升堆栈填充缓冲区,然后就是粉色框内的给 s 赋值,重点是红色框那一部分
1 | 0040D4D7 mov eax, dword ptr [ebp+8] ; 从[ebp+8]拿出刚才传进去的那个隐藏参数 |
接下来就是把 s 复制到 eax 指向的地址里了,每次 4 字节
1 | 0040D4DA mov ecx, dword ptr [ebp-10h] ; 读出a和b(4字节) |
再下一句那个 mov eax, dword ptr [ebp+8] 其实是多余的,因为 eax 之前已经存过这个临时内存区域的首地址了,在这之后的搬运环节也没有再次改变 eax 的值,所以其实这句话写不写都一样,不管它了就
回到 main 里面,紧接着一句平衡堆栈,然后就是又一坨,这一坨其实只是干了一个很简单的事情,就是进行了两次复制。第一次复制是把 [ebp-30h]~[ebp-20h] 这一堆东西复制到了 [ebp-20h]~[ebp-10h] 这个范围里,然后第二次复制就是把 [ebp-20h]~[ebp-10h] 这一堆又复制到了 [ebp-10h]~[ebp] 这一块里,也就是 main 正常来说真正的局部变量应该在的地方。
1 | ;第一次复制:从ebp-30h复制到ebp-20h |
Debug 版就是这么麻烦,它严格保留了C语言每一句原本的逻辑,如果开了编译器优化就不会这么麻烦了
这个示例不用去记它,只是熟悉一下流程。但是有一点总结很重要:大结构体作为参数或返回值,本质上就是大量内存的复制,这会造成极大的内存压力与性能开销。可以通过使用指针来避免这种开销。
课后作业我看是正向的内容,就不做了