结构体

正向基础我就不整理了,多说一句,其实我之前最喜欢写的就是结构体,不知道为什么觉得用起来特别爽

但是这一部分有个问题,关于课上讲的结构体中变量的存储地址的计算,就是基址+偏移嘛,但是有一个问题就是,课堂中举的那个例子(见下图),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
    24
    struct 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
    13
    struct 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
    16
    struct 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct st {
int a;
int b;
int c;
int d;
int e;
int f;
};
void Function(st x) {

}
int main(int argc, char* argv[]) {
st s;
s.a = 1;
s.b = 2;
s.c = 3;
s.d = 4;
s.e = 5;
s.f = 6;
Function(s);
return 0;
}


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

结构体作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct st {
char a;
short b;
int c;
int d;
int e;
};
st Fuction() {
st s;
s.a = 1;
s.b = 2;
s.c = 3;
s.d = 4;
s.e = 5;
return s;
};
int main(int argc, char* argv[]) {
st s = Fuction();
return 0;
}

main 函数与 Function 反汇编如下:


首先先看 main 调用函数前做了什么:

1
2
3
00401058  lea eax, [ebp-30h]  ; 获取main栈中一块临时内存的首地址
0040105B push eax ; 把此地址作为隐藏的第一个参数压入栈中
0040105C call Function ; 调用函数

因为这个 struct 太大了,所以编译器会在 main 的局部变量栈区里划出一块临时空间存函数返回值用。[ebp-30h] 就是这块空间的起始地址。至于为什么偏移是 -30h,这是编译器自己规划的,具体偏移量由编译器决定,但这块临时空间的实际大小会严格等于结构体的大小(在本例子中就是 16 字节,也就是 10h)
然后就进入 function 了,前面一堆就是日常的提升堆栈填充缓冲区,然后就是粉色框内的给 s 赋值,重点是红色框那一部分

1
2
0040D4D7  mov eax, dword ptr [ebp+8]  ; 从[ebp+8]拿出刚才传进去的那个隐藏参数
; eax现在变成了main传进来的[ebp-30h]这个地址

接下来就是把 s 复制到 eax 指向的地址里了,每次 4 字节

1
2
3
4
5
6
7
8
0040D4DA  mov ecx, dword ptr [ebp-10h] ; 读出a和b(4字节)
0040D4DD mov dword ptr [eax], ecx ; 写进[eax]
0040D4DF mov edx, dword ptr [ebp-0Ch] ; 读出c
0040D4E2 mov dword ptr [eax+4], edx ; 写进[eax+4]
0040D4E5 mov ecx, dword ptr [ebp-8] ; 读出d
0040D4E8 mov dword ptr [eax+8], ecx ; 写进[eax+8]
0040D4EB mov edx, dword ptr [ebp-4] ; 读出e
0040D4EE mov dword ptr [eax+0Ch], edx ; 写进[eax+0Ch]

再下一句那个 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
2
3
4
5
6
7
8
9
10
11
12
13
;第一次复制:从ebp-30h复制到ebp-20h
00401064 mov ecx, dword ptr [eax] ; 读第一块
00401066 mov dword ptr [ebp-20h], ecx ; 写第一块
...
00401075 mov edx, dword ptr [eax+0Ch] ; 读最后一块
00401078 mov dword ptr [ebp-14h], edx ; 写最后一块

;第二次复制:从ebp-20h复制到main真正的局部变量s(ebp-10h)
0040107B mov eax, dword ptr [ebp-20h]
0040107E mov dword ptr [ebp-10h], eax
...
0040108D mov eax, dword ptr [ebp-14h]
00401090 mov dword ptr [ebp-4], eax

Debug 版就是这么麻烦,它严格保留了C语言每一句原本的逻辑,如果开了编译器优化就不会这么麻烦了

这个示例不用去记它,只是熟悉一下流程。但是有一点总结很重要:大结构体作为参数或返回值,本质上就是大量内存的复制,这会造成极大的内存压力与性能开销。可以通过使用指针来避免这种开销。

课后作业我看是正向的内容,就不做了