大理寺少卿 发表于 2025-6-19 00:03:40

X86C++反汇编14.结构体、返回值

## 结构体

首先要考虑对齐值问题,但是在逆向角度是不关心的,因为结构体大小在代码中会体现

ida 如果读不到pbd,可以自动从微软服务器下载pbd (系统api的pbd)

### 结构体成员2个整形

看不出是结构体,很像定义2个 变量,没啥区别

```
struct Point {
    int x;
    int y;
};


int main()
{
    Point pt1 = { 10,20 };
    printf("x:%d y:%d", pt1.x, pt1.y);
    return 0;
}
```

debug版反汇编代码

```
.text:00411865               mov   dword ptr , 0Ah       ;第一个成员
.text:0041186C               mov   dword ptr , 14h            ;第二个成员
.text:00411873               mov   eax,
.text:00411876               push    eax
.text:00411877               mov   ecx,
.text:0041187A               push    ecx             ; char
.text:0041187B               push    offset aXDYD    ; "x:%d y:%d"
.text:00411880               call    sub_4110CD
.text:00411885               add   esp, 0Ch
```

release版

```
struct Point {
    int x;
    int y;
};
int main()
{
    Point pt1 = { 10,20 };
    scanf_s("%d%d", &pt1.x, &pt1.y);
    printf("x:%d y:%d", pt1.x, pt1.y);
    return 0;
}
反汇编代码:

.text:00401080               push    ebp
.text:00401081               mov   ebp, esp
.text:00401083               sub   esp, 0Ch      ;抬了12字节,其中四字节是cookie,所以剩下8字节是变量
.text:00401086               mov   eax, ___security_cookie
.text:0040108B               xor   eax, ebp
.text:0040108D               mov   , eax
.text:00401090               lea   eax,
.text:00401093               mov   dword ptr , 0Ah
.text:0040109A               push    eax
.text:0040109B               lea   eax,
.text:0040109E               mov   dword ptr , 14h
.text:004010A5               push    eax             ; Arglist
.text:004010A6               push    offset aDD      ; "%d%d"
.text:004010AB               call    sub_401110
.text:004010B0               push    dword ptr
.text:004010B3               push    dword ptr ; ArgList
.text:004010B6               push    offset Format   ; "x:%d y:%d"
.text:004010BB               call    sub_4010E0
.text:004010C0               mov   ecx,
.text:004010C3               add   esp, 18h

一样看不出来是结构体
```

### 结构体成员数多个整形

结构体和一堆变量最大的区别是 内存空间连续,多个变量内存不一定是连续的

```
struct Point {
    int x;
    int y;
    int z;
};


void showPoint(Point pt1) {

    printf("x=%d y:%d z:%d\n", pt1.x, pt1.y, pt1.z);
}

int main()
{
    Point pt1 = { 10,20,30 };
    scanf_s("%d%d%d", &pt1.x, &pt1.y, &pt1.z);
    showPoint(pt1);
    return 0;
}
```

debug版

```
mov   dword ptr , 0Ah
mov   dword ptr , 14h
mov   dword ptr , 1Eh
lea   eax,
push    eax
lea   ecx,
push    ecx
lea   edx,
push    edx             ; pt1
push    offset aDDD   ; "%d%d%d"
call    j__scanf_s
add   esp, 10h
sub   esp, 0Ch
mov   eax, esp
mov   ecx,
mov   , ecx
mov   edx,
mov   , edx
mov   ecx,
mov   , ecx
call    j_?showPoint@@YAXUPoint@@@Z ; showPoint(Point)
add   esp, 0Ch
xor   eax, eax
```

release版

```
lea   eax,
mov   dword ptr , 0Ah
push    eax
lea   eax,
mov   dword ptr , 14h
push    eax
lea   eax,
mov   dword ptr , 1Eh
push    eax             ; pt1
push    offset aDDD   ; "%d%d%d"
call    _scanf_s
movq    xmm0, qword ptr
add   esp, 4      ; scanf 的栈需要平16字节,平了4字节还剩12字节,刚好结构体大小
mov   eax,
mov   ecx, esp
movq    qword ptr , xmm0            ;8字节大小
mov   , eax                                 ;4字节大小的
call    ?showPoint@@YAXUPoint@@@Z ; showPoint(Point)

函数:
push    ebp
mov   ebp, esp
push    dword ptr        ;结构体首地址
push    dword ptr    ;本来应该是首地址 +4   即    ebp+10h - 4被优了
push    dword ptr
push    offset _Format; "x=%d y:%d z:%d\n"
call    _printf
add   esp, 10h
pop   ebp
retn

所以上面等于
sub    esp,sizeof(Point)
mencpy (esp,pt1);
c语言语法规定当函数参数是结构体时,整个结构体拷贝到栈顶当参数

寻址方式:
   当寄存器不够时就会出现 首地址 +偏移 寻址
```

### 当结构体成员比较多

```
struct Point {
    int x;
    int y;
    int z;
};


void showPoint(Point pt1) {

    printf("x=%d y:%d z:%d\n", pt1.x, pt1.y, pt1.z);
}

int main()
{
    Point pt1 = { 10,20,30 };
    scanf_s("%d%d%d", &pt1.x, &pt1.y, &pt1.z);
    showPoint(pt1);
    return 0;
}
```

debug版

```
mov   dword ptr , 0Ah
mov   dword ptr , 14h
mov   dword ptr , 1Eh
push    18Ch            ; Size
push    0               ; Val
lea   eax,
push    eax             ; void *
call    j__memset
add   esp, 0Ch
lea   eax,
push    eax
lea   ecx,
push    ecx
lea   edx,
push    edx
push    offset aDDD   ; "%d%d%d"
call    j__scanf_s
add   esp, 10h
sub   esp, 198h                  ;抬栈抬了 408字节
mov   ecx, 66h ; 'f'
lea   esi,           ;源首地址(结构体首地址)
mov   edi, esp                      ;目的地址
rep movsd                              ;内存拷贝大小是ecx * 4= 408
call    j_?showPoint@@YAXUPoint@@@Z ; showPoint(Point)
add   esp, 198h
xor   eax, eax
```

release版

```
push    edi
push    18Ch            ; Size
lea   eax,
mov   dword ptr , 0Ah
push    0               ; Val
push    eax             ; void *
mov   dword ptr , 14h
mov   dword ptr , 1Eh
call    _memset                  ;396+ 12 (3个mov)= 408
lea   eax,
push    eax
lea   eax,
push    eax
lea   eax,
push    eax
push    offset aDDD   ; "%d%d%d"
call    _scanf_s
sub   esp, 17Ch            ;抬栈
lea   esi,
mov   ecx, 66h ; 'f'
mov   edi, esp
rep movsd
call    ?showPoint@@YAXUPoint@@@Z ; showPoint(Point)
```

### 结构体指针

```
struct Point {
    int x;
    int y;
    int z;
};


void showPoint(Point* pt1) {

    printf("x=%d y:%d z:%d\n", pt1->x, pt1->y, pt1->z);
}

int main()
{
    Point pt1 = { 10,20,30 };
    scanf_s("%d%d%d", &pt1.x, &pt1.y, &pt1.z);
    showPoint(&pt1);
    return 0;
}
```

```
push    18Ch            ; Size
lea   eax,
mov   , 0Ah
push    0               ; Val
push    eax             ; void *
mov   , 14h
mov   , 1Eh
call    _memset
lea   eax,
push    eax
lea   eax,
push    eax
lea   eax,
push    eax
push    offset aDDD   ; "%d%d%d"
call    _scanf_s
lea   eax,
push    eax             ; pt1
call    ?showPoint@@YAXPAUPoint@@@Z ; showPoint(Point *)

函数
push    ebp
mov   ebp, esp
mov   ecx,     ;获取指针(首地址)
lea   eax,          ;首地址 + 偏移寻址
push    eax
push    dword ptr     ;首地址 + 偏移寻址
push    dword ptr       ;首地址 + 偏移寻址
push    offset _Format; "x=%d y:%d z:%d\n"
call    _printf
add   esp, 10h
pop   ebp
retn
```

### 总结

因此判断是否结构体的方法

1. 访问方式      [首地址 +偏移]
2. 传参在拷贝

如果上面2种都不存在,那么可能是局部变量

结构体成员如果没有被使用,可以当做不存在,还原结构体大小可以按照访问的最大偏移的来计算

而且一般结构体会初始化,可以从初始化获取结构体大小

### 数组和结构体的区别

- **(1)数组**
- - 还原为数组:(1)类型一致,业务一致。(2)地址首地址参与比例因子寻址或有循环。
- 数组传参:数组传参必然是指针。
- **(2)结构体**
- - 还原为结构体:(1)类型一致,业务不一致。(2)类型不一致,业务不一致。(3)!(地址首地址参与比例因子寻址或有循环)。
- 结构体传参:结构体传参是浅拷贝。如果是引用参数的话,以栈顶为目标,执行memcpy,在高版本下会使用两个多媒体指令相当于传入了一个参数,即,栈顶为参数地址。

### 结构体和类的区别

- 类:有成员函数的调用。
- 结构体:没有成员函数的调用

### iad自动识别指定偏移位置的结构体成员

SDK定义的结构体会自动导入结构体类型

#### 第一种方法:添加结构体类型

当结构体成员很多时,我们使用ida 可能没法记住所有成员的偏移,因此ida 提供了该功能

!(./notesimg/1658763359571-6ba8ba17-9c6b-4097-8bc5-604bdd090cf9.png)

右键创建结构体   快捷键   **ins**

!(./notesimg/1658763468008-062a41e1-d3c7-4cbf-8d30-70962ae956aa.png)

!(./notesimg/1658763504586-52cd87bf-1e36-4c55-b8d5-509658141a92.png)

编辑对齐值

!(./notesimg/1658763545443-4a1f21a8-a041-4055-9878-bad4a2702c2f.png)

选中地址通过快捷键 d 可以添加成员

!(./notesimg/1658763604055-6c000dcb-8439-4c27-9cab-dfcb01bc8019.png)

alt + q可以吧改地址解释为结构体

!(./notesimg/1658763799621-1ffcce4c-e8e4-44fa-a44d-37b7364f8e6a.png)

或者到栈里面 按 alt + q

!(./notesimg/1658763877623-693c3a7d-7760-48db-92ad-a82a0b988bbc.png)

!(./notesimg/1658764089721-7bdeb2d7-2ccf-43ce-9ad1-a7bcbbd4a4c3.png)

或者到变量地址    **alt + q**

!(./notesimg/1658764230274-303a9963-7978-418e-933c-0441542e2cf1.png)

#### 第二种方法: 头文件导入

建一个结构体头文件

!(./notesimg/1658764553847-6dab75b1-07b6-4497-ae83-d28c784c3fa2.png)

导入

!(./notesimg/1658764665799-45bc625a-ffbf-4698-8a72-1cdf5b4e47ee.png)

导入成功

!(./notesimg/1658764705490-aaa18474-e914-4cf2-94ee-0b70fcf515ad.png)

有时候结构体会被折叠导致看不到,可以直接右键插入结构体(ins)

!(./notesimg/1658764822109-8625ab84-33b4-4411-9993-01eef340041e.png)

!(./notesimg/1658764930934-3ebd3561-7451-44df-a651-7714af574984.png)

!(./notesimg/1658765238423-9d613eac-5af4-4fe6-a522-676e56526ec7.png)

!(./notesimg/1658765181299-377b182c-91d7-4e32-9f07-dcf58fd337f5.png)

!(./notesimg/1658765280688-cc5bf325-7fb9-46c5-9882-92d35d4fa96b.png)

!(./notesimg/1658765310584-ca557bb6-79cc-4e5e-9289-14b1ae20c7ff.png)

### x32 ,OD 解析结构体

x32 也可以载入头文件来解析结构体

!(./notesimg/1658765756399-672d9fb8-7783-4364-a797-8289dc1f73e3.png)

!(./notesimg/1658765852958-9a030daa-5211-4872-9a4e-b7bf492464de.png)

!(./notesimg/1658766029328-ef76d114-da33-4a91-9ac9-9cd7dfe1d7e9.png)

!(./notesimg/1658766049014-9f537038-b90e-4c50-b413-30366a016985.png)

OD 没有改功能

## 函数的传参和返回值

### 参数是结构体

#### 结构体成员只有1个

push 1个成员 ,并没有传地址

```
struct Point {
    int x;

};

void showPoint(Point pt1) {

printf("x=%d\n", pt1.x);
}

int main()
{
    Point pt1 = { 10};
    scanf_s("%d%d%d", &pt1.x);
    showPoint(pt1);
    return 0;
}
```

按照我们传参的约定,是吧结构体压入栈顶,然后把栈顶地址当做参数传进去,当做一个参数

```
lea   eax,
mov   dword ptr , 0Ah
push    eax             ; Arglist
push    offset aD       ; "%d"
call    sub_401110
push    dword ptr ; ArgList
call    sub_401000
mov   ecx,
add   esp, 0Ch

编译器视为:    void showPoint(intx )
```

#### 结构体有2个成员

push 2个成员 ,并没有传地址

```
struct Point {
    int x;
    int y;
};

void showPoint(Point pt1) {

printf("x=%d\n", pt1.x);
}

int main()
{
    Point pt1 = { 10};
    scanf_s("%d%d", &pt1.x, &pt1.y);
    showPoint(pt1);
    return 0;
}
```

lea   eax,

mov   , 0Ah

push    eax

lea   eax,

mov   , 0

push    eax

push    offset aDD      ; "%d%d"

call    _scanf_s

**push    **

**push       ; pt1**

call    ?showPoint@@YAXUPoint@@@Z ; showPoint(Point)

编译器视为:    void showPoint(intx ,int y)

#### 结构体有多个成员

```
struct Point {
    int x;
    int y;
    int z;
};

void showPoint(Point pt1) {
    printf("x=%d y:%d z:%d\n", pt1.x, pt1.y, pt1.z);
}

int main()
{
    Point pt1 = { 10};
    scanf_s("%d%d%d", &pt1.x, &pt1.y, &pt1.z);
    showPoint(pt1);
    return 0;
}
```

```
lea   eax,
mov   , 0Ah
push    eax
lea   eax,
xorps   xmm0, xmm0
push    eax
lea   eax,
movq    qword ptr , xmm0
push    eax             ; pt1
push    offset aDDD   ; "%d%d%d"
call    _scanf_s
movq    xmm0, qword ptr
add   esp, 4
mov   eax,
mov   ecx, esp
movq    qword ptr , xmm0
mov   , eax
call    ?showPoint@@YAXUPoint@@@Z ; showPoint(Point)

产生了拷贝就是结构体 ,当参数超过 12字节,才会产生拷贝
```

### 返回值是结构体

#### 结构体只有一个成员    (返回值 长度<= 4 字节)

通过 eax 返回

```

struct Point {
    int x;
};

Point GetPoint() {
    Point p1 = { 1};
    return p1;
}

int main()
{
    Point ret = GetPoint();
    printf("x:%d", ret.x);
    return 0;
}
```

```
?GetPoint@@YA?AUPoint@@XZ proc near
mov   eax, 1
retn
?GetPoint@@YA?AUPoint@@XZ endp
```

#### 返回值只有2个成员   (返回值 长度 4~ 8 字节)

通过 eax (结构体第一个成员),edx(结构体第二个成员) 返回

调用函数前会保存edx的值 (push),调完后会恢复edx的值 (pop)

```
struct Point {
    int x;
    int y;
};

Point GetPoint() {
    Point p1 = { 1,2};
    return p1;
}

int main()
{
    Point ret = GetPoint();
    printf("x:%d,y:%d", ret.x, ret.y);
    return 0;
}
```

```
?GetPoint@@YA?AUPoint@@XZ proc near
mov   eax, 1
lea   edx,
retn
?GetPoint@@YA?AUPoint@@XZ endp
```

结构体地址当作参数传还进去,然后通过 地址+ 偏移 访问成员 ,然后返回 结构体地址

如果他前面还有一个其他参数,那么就会往后延,放到第二个

```
struct Point {
    int x;
    int y;
    int z;
};

Point GetPoint() {
    Point p1 = { 1,2,3};
    return p1;
}

int main()
{
    Point ret = GetPoint();
    printf("x:%d,y:%d,z:%d", ret.x, ret.y, ret.z);
    return 0;
}
```

```
push    ebp
mov   ebp, esp
sub   esp, 24
lea   eax,
push    eax             ; 把 ret 的地址当参数产进去了
call    ?GetPoint@@YA?AUPoint@@XZ ; GetPoint(void)
push    dword ptr
movq    xmm0, qword ptr
movq    qword ptr , xmm0
push   
push   
push    offset _Format; "x:%d,y:%d,z:%d"
call    _printf

函数实现
push    ebp
mov   ebp, esp
mov   eax,
mov   dword ptr , 1
mov   dword ptr , 2
mov   dword ptr , 3
pop   ebp
retn
```

### 其他类型的返回值

#### 返回者是 int或 char

```
返回值是 eax
```


#### 返回值是 longlong

```
返回者 通过 eax (低4位),edx (高四位) 返回
```


```
long long GetInt() {
    return 10;
}
int main()
{
    long long ret2 = GetInt();
    printf("ret:%lld", ret2);
    return 0;
}
```

?GetInt@@YA_JXZ proc near

mov   eax, 0Ah

xor   edx, edx

retn

?GetInt@@YA_JXZ endp

#### 返回值是float

通过   st(0)返回

就算禁止使用x87指令集 ,结果还是放在   st(0)里面,因为需要固定返回值

```
float GetFloat() {
    return 10.5f;
}
int main()
{
    float ret = GetFloat();
    printf("ret:%f", ret);
    return 0;
}
```

?GetFloat@@YAMXZ proc near

fld   ds:__real@41280000

retn

?GetFloat@@YAMXZ endp

#### 返回值是double

st(0)有80位,可以放下double

因此也是   通过   st(0)返回

```
double GetDouble() {
    return 10.5;
}

int main()
{
    double ret = GetDouble();
    printf("ret:%f", ret);
    return 0;
}
```

?GetDouble@@YANXZ proc near

fld   ds:__real@4025000000000000

retn

?GetDouble@@YANXZ endp

### 其他编译器

在其他编译器中,不一定是这么传的,即我们可能碰到位置的编译器,此时逆向时,如果操作了寄存,进函数去看有没有直接用寄存器,直接用了说明是参数寄存器,如果是入栈是通过 ebp 访问

asmprofan 发表于 2025-6-19 05:53:20

啥也不说了,楼主就是给力!

ldljlzw 发表于 2025-6-19 11:28:40

不管你信不信,反正我是信了。

ldljlzw 发表于 2025-6-20 09:27:51

不管你信不信,反正我是信了。

YINXN 发表于 2025-6-20 21:45:06

谢谢分享

ldljlzw 发表于 2025-6-21 08:59:51

楼主,不论什么情况你一定要hold住!hold住就是胜利!

ldljlzw 发表于 2025-6-21 09:00:09

看了LZ的帖子,我只想说一句很好很强大!

ponyjs 发表于 2025-6-21 22:16:21

楼主,不论什么情况你一定要hold住!hold住就是胜利!

ldljlzw 发表于 2025-6-25 08:56:35

不管你信不信,反正我是信了。

CatReverse 发表于 2025-6-26 17:07:38

啥也不说了,楼主就是给力!
页: [1] 2
查看完整版本: X86C++反汇编14.结构体、返回值