天行健 发表于 2024-12-22 11:55:56

C++反汇编入门17.结构体

# 结构体

C/C++的结构体可以这么定义:它是一组存储在内存中的变量的集合,成员变量类型不要求相同。

## 18.1 SYSTEMTIME 的例子

让我们看看Win32结构体SYSTEMTIME的定义:

清单18.1: WinBase.h

```
typedef struct _SYSTEMTIME {
    WORD wYear;
    WORD wMonth;
    WORD wDayOfWeek;
    WORD wDay;
    WORD wHour;
    WORD wMinute;
    WORD wSecond;
    WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;
```

让我们写一个获取当前时间的C程序:

```
#include <windows.h>
#include <stdio.h>
void main()
{
    SYSTEMTIME t;
    GetSystemTime(&t);
    printf ("%04d-%02d-%02d %02d:%02d:%02d",
      t.wYear, t.wMonth, t.wDay,
      t.wHour, t.wMinute, t.wSecond);
    return;
};
```

反汇编结果如下(MSVC 2010):

清单18.2: MSVC 2010

```
_t$ = -16 ; size = 16
_main PROC
    push ebp
    mov ebp, esp
    sub esp, 16 ; 00000010H
    lea eax, DWORD PTR _t$
    push eax
    call DWORD PTR __imp__GetSystemTime@4
    movzx ecx, WORD PTR _t$ ; wSecond
    push ecx
    movzx edx, WORD PTR _t$ ; wMinute
    push edx
    movzx eax, WORD PTR _t$ ; wHour
    push eax
    movzx ecx, WORD PTR _t$ ; wDay
    push ecx
    movzx edx, WORD PTR _t$ ; wMonth
    push edx
    movzx eax, WORD PTR _t$ ; wYear
    push eax
    push OFFSET $SG78811 ; ’%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H
    call _printf
    add esp, 28 ; 0000001cH
    xor eax, eax
    mov esp, ebp
    pop ebp
    ret 0
_main ENDP
```

在本地栈上程序为这个结构体分配了16个字节:这正是sizeof(WORD)*8的大小(因为结构体里有8个WORD)。 请注意结构体是由wYear开始的,因此,我们既可以说这是“传给GetSystemTime()函数的,一个指向SYSTEMTIME结构体的指针”,也可以说是它“传递了wYear的指针”。这两种说法是一样的!GetSystemTime()函数会把当前的年份写入指向的WORD指针中,然后把指针向后移动2个字节(译注:WORD大小为2字节),再写入月份,以此类推。 事实上,结构体的成员其实就是一个个紧贴在一起的变量。我可以用下面的方法来访问SYSTEMTIME结构体,代码如下:

```
#include <windows.h>
#include <stdio.h>
void main()
{
    WORD array;
    GetSystemTime (array);
    printf ("%04d-%02d-%02d %02d:%02d:%02d",
      array /* wYear */, array /* wMonth */, array /* wDay */,
      array /* wHour */, array /* wMinute */, array /* wSecond */);
    return;
};
```

编译器会稍稍给出一点警告:

```
systemtime2.c(7) : warning C4133: ’function’ : incompatible types - from ’WORD ’ to ’LPSYSTEMTIME’
```

不过至少,它会产生如下代码:

清单18.3: MSVC 2010

```
$SG78573 DB ’%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H
_array$ = -16 ; size = 16
_main PROC
    push ebp
    mov ebp, esp
    sub esp, 16 ; 00000010H
    lea eax, DWORD PTR _array$
    push eax
    call DWORD PTR __imp__GetSystemTime@4
    movzx ecx, WORD PTR _array$ ; wSecond
    push ecx
    movzx edx, WORD PTR _array$ ; wMinute
    push edx
    movzx eax, WORD PTR _array$ ; wHoure
    push eax
    movzx ecx, WORD PTR _array$ ; wDay
    push ecx
    movzx edx, WORD PTR _array$ ; wMonth
    push edx
    movzx eax, WORD PTR _array$ ; wYear
    push eax
    push OFFSET $SG78573
    call _printf
    add esp, 28 ; 0000001cH
    xor eax, eax
    mov esp, ebp
    pop ebp
    ret 0
_main ENDP
```

当然,它也能一样正常工作! 一个很有趣的情况是这两次编译结果居然一样,所以光看编译结果,我们还看不出来到底别人用的结构体还是单单用的变量数组。 不过,没几个人会这么无聊的用这种方法写代码,因为这太麻烦了。还有,结构体也有可能会被开发者更改,交换,等等,所以还是用结构体方便。

## 18.2 让我们用malloc()为结构体分配空间

但是,有时候把结构体放在堆中而不是栈上却更简单:

```
#include <windows.h>
#include <stdio.h>
void main()
{
    SYSTEMTIME *t;
    t=(SYSTEMTIME *)malloc (sizeof (SYSTEMTIME));
    GetSystemTime (t);
    printf ("%04d-%02d-%02d %02d:%02d:%02d",
      t->wYear, t->wMonth, t->wDay,
      t->wHour, t->wMinute, t->wSecond);
    free (t);
    return;
};
```

让我们用优化/Ox编译一下它,看看我们得到什么东西

清单18.4: 优化的MSVC

```
_main PROC
    push esi
    push 16 ; 00000010H
    call _malloc
    add esp, 4
    mov esi, eax
    push esi
    call DWORD PTR __imp__GetSystemTime@4
    movzx eax, WORD PTR ; wSecond
    movzx ecx, WORD PTR ; wMinute
    movzx edx, WORD PTR ; wHour
    push eax
    movzx eax, WORD PTR ; wDay
    push ecx
    movzx ecx, WORD PTR ; wMonth
    push edx
    movzx edx, WORD PTR ; wYear
    push eax
    push ecx
    push edx
    push OFFSET $SG78833
    call _printf
    push esi
    call _free
    add esp, 32 ; 00000020H
    xor eax, eax
    pop esi
    ret 0
_main ENDP
```

所以,sizeof(SYSTEMTIME) = 16, 这正是malloc所分配的字节数。它返回了刚刚分配的地址空间,这个指针存在EAX寄存器里。然后,这个指针会被移动到ESI结存器中, GetSystemTime()会用它来存储返回值,这也就是为什么这里分配完之后并没有把EAX放到某个地方保存起来,而是直接使用它的原因。

新指令:MOVZX(Move with Zero eXtent, 0扩展移动)。它可以说是和MOVSX基本一样(13.1.1节),但是,它把其他位都设置为0。这是因为printf()需要一个32位的整数,但是我们的结构体里面是WORD,这只有16位厂。这也就是为什么从WORD复制到INT时第16~31位必须清零的原因了。因为,如果不清除的话,剩余位可能有之前操作留下来的干扰数据。

在下面这个例子里面,我可以用WORD数组来重现这个结构:

```
#include <windows.h>
#include <stdio.h>
void main()
{
    WORD *t;
    t=(WORD *)malloc (16);
    GetSystemTime (t);
    printf ("%04d-%02d-%02d %02d:%02d:%02d",
      t /* wYear */, t /* wMonth */, t /* wDay */,
      t /* wHour */, t /* wMinute */, t /* wSecond */);
    free (t);
    return;
};
```

我们得到:

```
$SG78594 DB ’%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H
_main PROC
    push esi
    push 16 ; 00000010H
    call _malloc
    add esp, 4
    mov esi, eax
    push esi
    call DWORD PTR __imp__GetSystemTime@4
    movzx eax, WORD PTR
    movzx ecx, WORD PTR
    movzx edx, WORD PTR
    push eax
    movzx eax, WORD PTR
    push ecx
    movzx ecx, WORD PTR
    push edx
    movzx edx, WORD PTR
    push eax
    push ecx
    push edx
    push OFFSET $SG78594
    call _printf
    push esi
    call _free
    add esp, 32 ; 00000020H
    xor eax, eax
    pop esi
    ret 0
_main ENDP
```

同样,我们可以看到编译结果和之前一样。个人重申一次,你不应该在写代码的时候用这么晦涩的方法来表达它。

## 18.3 结构体tm

### 18.3.1 linux

在Linux下,我们看看time.h中的tm结构体是什么样子的:

```
#include <stdio.h>
#include <time.h>
void main()
{
    struct tm t;
    time_t unix_time;
    unix_time=time(NULL);
    localtime_r (&unix_time, &t);
    printf ("Year: %d", t.tm_year+1900);
    printf ("Month: %d", t.tm_mon);
    printf ("Day: %d", t.tm_mday);
    printf ("Hour: %d", t.tm_hour);
    printf ("Minutes: %d", t.tm_min);
    printf ("Seconds: %d", t.tm_sec);
};
```

在GCC 4.4.1下编译得到:

清单18.6:GCC 4.4.1

```
main proc near
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 40h
    mov dword ptr , 0 ; first argument for time()
    call time
    mov , eax
    lea eax, ; take pointer to what time() returned
    lea edx, ; at ESP+10h struct tm will begin
    mov , edx ; pass pointer to the structure begin
    mov , eax ; pass pointer to result of time()
    call localtime_r
    mov eax, ; tm_year
    lea edx, ; edx=eax+1900
    mov eax, offset format ; "Year: %d"
    mov , edx
    mov , eax
    call printf
    mov edx, ; tm_mon
    mov eax, offset aMonthD ; "Month: %d"
    mov , edx
    mov , eax
    call printf
    mov edx, ; tm_mday
    mov eax, offset aDayD ; "Day: %d"
    mov , edx
    mov , eax
    call printf
    mov edx, ; tm_hour
    mov eax, offset aHourD ; "Hour: %d"
    mov , edx
    mov , eax
    call printf
    mov edx, ; tm_min
    mov eax, offset aMinutesD ; "Minutes: %d"
    mov , edx
    mov , eax
    call printf
    mov edx,
    mov eax, offset aSecondsD ; "Seconds: %d"
    mov , edx ; tm_sec
    mov , eax
    call printf
    leave
    retn
main endp
```

可是,IDA并没有为本地栈上变量建立本地变量名。但是因为我们已经学了汇编了,我们也不需要在这么简单的例子里面如此依赖它。

请也注意一下lea edx, ,这个指令把eax的值加上0x76c,但是并不修改任何标记位。请也参考LEA的相关章节(B.6.2节)

为了表现出结构体只是一个个的变量连续排列的东西,让我们重新测试一下这个例子,我们看看time.h: 清单18.7 time.h

```
struct tm
{
    int tm_sec;
    int tm_min;
    int tm_hour;
    int tm_mday;
    int tm_mon;
    int tm_year;
    int tm_wday;
    int tm_yday;
    int tm_isdst;
};
#include <stdio.h>
#include <time.h>
void main()
{
    int tm_sec, tm_min, tm_hour, tm_mday, tm_mon, tm_year, tm_wday, tm_yday, tm_isdst;
    time_t unix_time;
    unix_time=time(NULL);
    localtime_r (&unix_time, &tm_sec);
    printf ("Year: %d", tm_year+1900);
    printf ("Month: %d", tm_mon);
    printf ("Day: %d", tm_mday);
    printf ("Hour: %d", tm_hour);
    printf ("Minutes: %d", tm_min);
    printf ("Seconds: %d", tm_sec);
};
```

注:指向tm_sec的指针会传递给localtime_r,或者说第一个“结构体”元素。 编译器会这么警告我们

清单18.8 GCC4.7.3

```
GCC_tm2.c: In function ’main’:
GCC_tm2.c:11:5: warning: passing argument 2 of ’localtime_r’ from incompatible pointer type [
enabled by default]
In file included from GCC_tm2.c:2:0:
/usr/include/time.h:59:12: note: expected ’struct tm *’ but argument is of type ’int *’
```

但是至少,它会生成这段代码:

清单18.9 GCC 4.7.3

```
main proc near
    var_30 = dword ptr -30h
    var_2C = dword ptr -2Ch
    unix_time = dword ptr -1Ch
    tm_sec = dword ptr -18h
    tm_min = dword ptr -14h
    tm_hour = dword ptr -10h
    tm_mday = dword ptr -0Ch
    tm_mon = dword ptr -8
    tm_year = dword ptr -4
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 30h
    call __main
    mov , 0 ; arg 0
    mov , eax
    lea eax,
    mov , eax
    lea eax,
    mov , eax
    call localtime_r
    mov eax,
    add eax, 1900
    mov , eax
    mov , offset aYearD ; "Year: %d"
    call printf
    mov eax,
    mov , eax
    mov , offset aMonthD ; "Month: %d"
    call printf
    mov eax,
    mov , eax
    mov , offset aDayD ; "Day: %d"
    call printf
    mov eax,
    mov , eax
    mov , offset aHourD ; "Hour: %d"
    call printf
    mov eax,
    mov , eax
    mov , offset aMinutesD ; "Minutes: %d"
    call printf
    mov eax,
    mov , eax
    mov , offset aSecondsD ; "Seconds: %d"
    call printf
    leave
    retn
main endp
```

这个代码和我们之前看到的一样,依然无法分辨出源代码是用了结构体还是只是数组而已。

当然这样也是可以运行的,但是实际操作中还是不建议用这种晦涩的方法。因为通常,编译器会在栈上按照声明顺序分配变量空间,但是并不能保证每次都是这样。

还有,其他编译器可能会警告tm_year,tm_mon, tm_mday, tm_hour, tm_min变量而不是tm_sec使用时未初始化。事实上,计算机并不知道调用localtime_r()的时候他们会被自动填充上。

我选择了这个例子来解释是因为他们都是int类型的,而SYSTEMTIME的所有成员是16位的WORD,如果把它们作为本地变量来声明的话,他们会按照32位的边界值来对齐,因此什么都用不了了(因为由于数据对齐,此时GetSystemTime()会把它们错误的填充起来)。请继续读下一节的内容:“结构体的成员封装”。

所以,结构体只是把一组变量封装到一个位置上,数据是一个接一个的。我可以说结构体是一个语法糖,因为它只是用来让编译器把一组变量保存在一个地方。但是,我不是编程方面的专家,所以更有可能的是,我可能会误读这个术语。还有,在早期(1972年以前)的时候,C是不支持结构体的。

### 18.3.2 ARM+优化Keil+thumb模式

同样的例子: 清单18.10: 优化Keil+thumb模式

```
var_38 = -0x38
var_34 = -0x34
var_30 = -0x30
var_2C = -0x2C
var_28 = -0x28
var_24 = -0x24
timer = -0xC
    PUSH {LR}
    MOVS R0, #0 ; timer
    SUB SP, SP, #0x34
    BL time
    STR R0,
    MOV R1, SP ; tp
    ADD R0, SP, #0x38+timer ; timer
    BL localtime_r
    LDR R1, =0x76C
    LDR R0,
    ADDS R1, R0, R1
    ADR R0, aYearD ; "Year: %d"
    BL __2printf
    LDR R1,
    ADR R0, aMonthD ; "Month: %d"
    BL __2printf
    LDR R1,
    ADR R0, aDayD ; "Day: %d"
    BL __2printf
    LDR R1,
    ADR R0, aHourD ; "Hour: %d"
    BL __2printf
    LDR R1,
    ADR R0, aMinutesD ; "Minutes: %d"
    BL __2printf
    LDR R1,
    ADR R0, aSecondsD ; "Seconds: %d"
    BL __2printf
    ADD SP, SP, #0x34
    POP {PC}
```

### 18.3.3 ARM+优化Xcode(LLVM)+thumb-2模式

IDA“碰巧知道”tm结构体(因为IDA“知道”例如localtime_r()这些库函数的参数类型),所以他把这里的结构变量的名字也显示出来了。

```
var_38 = -0x38
var_34 = -0x34
    PUSH {R7,LR}
    MOV R7, SP
    SUB SP, SP, #0x30
    MOVS R0, #0 ; time_t *
    BLX _time
    ADD R1, SP, #0x38+var_34 ; struct tm *
    STR R0,
    MOV R0, SP ; time_t *
    BLX _localtime_r
    LDR R1,
    MOV R0, 0xF44 ; "Year: %d"
    ADD R0, PC ; char *
    ADDW R1, R1, #0x76C
    BLX _printf
    LDR R1,
    MOV R0, 0xF3A ; "Month: %d"
    ADD R0, PC ; char *
    BLX _printf
    LDR R1,
    MOV R0, 0xF35 ; "Day: %d"
    ADD R0, PC ; char *
    BLX _printf
    LDR R1,
    MOV R0, 0xF2E ; "Hour: %d"
    ADD R0, PC ; char *
    BLX _printf
    LDR R1,
    MOV R0, 0xF28 ; "Minutes: %d"
    ADD R0, PC ; char *
    BLX _printf
    LDR R1,
    MOV R0, 0xF25 ; "Seconds: %d"
    ADD R0, PC ; char *
    BLX _printf
    ADD SP, SP, #0x30
    POP {R7,PC}
...
00000000 tm struc ; (sizeof=0x2C, standard type)
00000000 tm_sec DCD ?
00000004 tm_min DCD ?
00000008 tm_hour DCD ?
0000000C tm_mday DCD ?
00000010 tm_mon DCD ?
00000014 tm_year DCD ?
00000018 tm_wday DCD ?
0000001C tm_yday DCD ?
00000020 tm_isdst DCD ?
00000024 tm_gmtoff DCD ?
00000028 tm_zone DCD ? ; offset
0000002C tm ends
```

清单18.11: ARM+优化Xcode(LLVM)+thumb-2模式

## 18.4 结构体的成员封装

结构体做的一个重要的事情就是封装了成员,让我们看看简单的例子:

```
#include <stdio.h>
struct s
{
    char a;
    int b;
    char c;
    int d;
};
void f(struct s s)
{
    printf ("a=%d; b=%d; c=%d; d=%d", s.a, s.b, s.c, s.d);
};
```

如我们所看到的,我们有2个char成员(每个1字节),和两个int类型的数据(每个4字节)。

### 18.4.1 x86

编译后得到:

```
_s$ = 8 ; size = 16
?f@@YAXUs@@@Z PROC ; f
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _s$
    push eax
    movsx ecx, BYTE PTR _s$
    push ecx
    mov edx, DWORD PTR _s$
    push edx
    movsx eax, BYTE PTR _s$
    push eax
    push OFFSET $SG3842
    call _printf
    add esp, 20 ; 00000014H
    pop ebp
    ret 0
?f@@YAXUs@@@Z ENDP ; f
_TEXT ENDS
```

如我们所见,每个成员的地址都按4字节对齐了,这也就是为什么char也会像int一样占用4字节。为什么?因为对齐后对CPU来说更容易读取数据。

但是,这么看明显浪费了一些空间。 让我们能用/Zp1(/Zp代表结构体边界值为n字节)来编译它:

清单18.12: MSVC /Zp1

```
_TEXT SEGMENT
_s$ = 8 ; size = 10
?f@@YAXUs@@@Z PROC ; f
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _s$
    push eax
    movsx ecx, BYTE PTR _s$
    push ecx
    mov edx, DWORD PTR _s$
    push edx
    movsx eax, BYTE PTR _s$
    push eax
    push OFFSET $SG3842
    call _printf
    add esp, 20 ; 00000014H
    pop ebp
    ret 0
?f@@YAXUs@@@Z ENDP ; f
```

现在,结构体只用了10字节,而且每个char都占用1字节。我们得到了最小的空间,但是反过来看,CPU却无法用最优化的方式存取这些数据。 可以容易猜到的是,如果这个结构体在很多源代码和对象中被使用的话,他们都需要用同一种方式来编译起来。 除了MSVC /Zp选项,还有一个是#pragma pack编译器选项可以在源码中定义边界值。这个语句在MSVC和GCC中均被支持。 回到SYSTEMTIME结构体中的16位成员,我们的编译器怎么才能把它们按1字节边界来打包? WinNT.h有这么个代码:

清单18.13:WINNT.H

`#include "pshpack1.h"`

和这个:

清单18.14:WINNT.H

`#include "pshpack4.h" // 4 byte packing is the default`

文件PshPack1.h看起来像

清单18.15: PSHPACK1.H

```
#if ! (defined(lint) || defined(RC_INVOKED))
#if ( _MSC_VER >= 800 && !defined(_M_I86)) || defined(_PUSHPOP_SUPPORTED)
#pragma warning(disable:4103)
#if !(defined( MIDL_PASS )) || defined( __midl )
#pragma pack(push,1)
#else
#pragma pack(1)
#endif
#else
#pragma pack(1)
#endif
#endif /* ! (defined(lint) || defined(RC_INVOKED)) */
```

这就是#pragma pack处理结构体大小的方法。

### 18.4.2 ARM+优化Keil+thumb模式

清单18.16

```
.text:0000003E exit ; CODE XREF: f+16
.text:0000003E 05 B0 ADD SP, SP, #0x14
.text:00000040 00 BD POP {PC}


.text:00000280 f
.text:00000280
.text:00000280 var_18 = -0x18
.text:00000280 a = -0x14
.text:00000280 b = -0x10
.text:00000280 c = -0xC
.text:00000280 d = -8
.text:00000280
.text:00000280 0F B5 PUSH {R0-R3,LR}
.text:00000282 81 B0 SUB SP, SP, #4
.text:00000284 04 98 LDR R0, ; d
.text:00000286 02 9A LDR R2, ; b
.text:00000288 00 90 STR R0,
.text:0000028A 68 46 MOV R0, SP
.text:0000028C 03 7B LDRB R3, ; c
.text:0000028E 01 79 LDRB R1, ; a
.text:00000290 59 A0 ADR R0, aADBDCDDD ; "a=%d; b=%d; c=%d; d=%d
"
.text:00000292 05 F0 AD FF BL __2printf
.text:00000296 D2 E6 B exit
```

我们可以回忆到的是,这里它直接用了结构体而不是指向结构体的指针,而且因为ARM里函数的前4个参数是通过寄存器传递的,所以结构体其实是通过R0-R3寄存器传递的。

LDRB指令将内存中的一个字节载入,然后把它扩展到32位,同时也考虑它的符号。这和x86架构的MOVSX(参考13.1.1节)基本一样。这里它被用来传递结构体的a、c两个成员。

还有一个我们可以容易指出来的是,在函数的末尾处,这里它没有使用正常的函数尾该有的指令,而是直接跳转到了另一个函数的末尾! 的确,这是一个相当不同的函数,而且跟我们的函数没有任何关联。但是,他却有着相同的函数结尾(也许是因为他也有5个本地变量(5 x 4 = 0x14))。而且他就在我们的函数附近(看看地址就知道了)。事实上,函数结尾并不重要,只要函数好好执行就行了嘛。显然,Keil决定要重用另一个函数的一部分,原因就是为了优化代码大小。普通函数结尾需要4字节,而跳转指令只要2个字节。

### 18.4.3 ARM+优化XCode(LLVM)+thumb-2模式

清单18.17: 优化的Xcode (LLVM)+thumb-2模式

```
var_C = -0xC
    PUSH {R7,LR}
    MOV R7, SP
    SUB SP, SP, #4
    MOV R9, R1 ; b
    MOV R1, R0 ; a
    MOVW R0, #0xF10 ; "a=%d; b=%d; c=%d; d=%d
"
    SXTB R1, R1 ; prepare a
    MOVT.W R0, #0
    STR R3, ; place d to stack for printf()
    ADD R0, PC ; format-string
    SXTB R3, R2 ; prepare c
    MOV R2, R9 ; b
    BLX _printf
    ADD SP, SP, #4
    POP {R7,PC}
```

SXTB(Singned Extend Byte,有符号扩展字节)和x86的MOVSX(见13.1.1节)差不多,但是它不是对内存操作的,而是对一个寄存器操作的,至于剩余的——都一样。

## 18.5 嵌套结构

如果一个结构体里定义了另一个结构体会怎么样?

```
#include <stdio.h>
struct inner_struct
{
    int a;
    int b;
};
struct outer_struct
{
    char a;
    int b;
    struct inner_struct c;
    char d;
    int e;
};
void f(struct outer_struct s)
{
    printf ("a=%d; b=%d; c.a=%d; c.b=%d; d=%d; e=%d", s.a, s.b, s.c.a, s.c.b, s.d, s.e);
};
```

在这个例子里,我们把inner_struct放到了outer_struct的abde中间。 让我们在MSVC 2010中编译:

清单18.18: MSVC 2010

```
_s$ = 8 ; size = 24
_f PROC
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _s$ ; e
    push eax
    movsx ecx, BYTE PTR _s$ ; d
    push ecx
    mov edx, DWORD PTR _s$ ; c.b
    push edx
    mov eax, DWORD PTR _s$ ; c.a
    push eax
    mov ecx, DWORD PTR _s$ ; b
    push ecx
    movsx edx, BYTE PTR _s$ ;a
    push edx
    push OFFSET $SG2466
    call _printf
    add esp, 28 ; 0000001cH
    pop ebp
    ret 0
_f ENDP
```

一个令我们好奇的事情是,看看这个反汇编代码,我们甚至不知道它的体内有另一个结构体!因此,我们可以说,嵌套的结构体,最终都会转化为线性的或者一维的结构。 当然,如果我们把struct inner_struct c;换成struct inner_struct *c(因此这里其实是定义个了一个指针),这个情况下状况则会大为不同。

## 18.6 结构体中的位

### 18.6.1 CPUID 的例子

C/C++中允许给结构体的每一个成员都定义一个准确的位域。如果我们想要节省空间的话,这个对我们来说将是非常有用的。比如,对BOOL来说,1位就足矣了。但是当然,如果我们想要速度的话,必然会浪费点空间。 让我们以CPUID指令为例,这个指令返回当前CPU的信息和特性。 如果EAX在指令执行之前就设置为了1,CPUID将会返回这些内容到EAX中。

![](./mdimg/Chapter-18/1.png)

MSVC 2010有CPUID的宏,但是GCC 4.4.1没有,所以,我们就手动的利用它的内联汇编器为GCC写一个吧。

```
#include <stdio.h>
#ifdef __GNUC__
static inline void cpuid(int code, int *a, int *b, int *c, int *d) {
    asm volatile("cpuid":"=a"(*a),"=b"(*b),"=c"(*c),"=d"(*d):"a"(code));
}
#endif
#ifdef _MSC_VER
#include <intrin.h>
#endif
struct CPUID_1_EAX
{
    unsigned int stepping:4;
    unsigned int model:4;
    unsigned int family_id:4;
    unsigned int processor_type:2;
    unsigned int reserved1:2;
    unsigned int extended_model_id:4;
    unsigned int extended_family_id:8;
    unsigned int reserved2:4;
};
int main()
{
    struct CPUID_1_EAX *tmp;
    int b;
    #ifdef _MSC_VER
    __cpuid(b,1);
    #endif
    #ifdef __GNUC__
    cpuid (1, &b, &b, &b, &b);
    #endif
    tmp=(struct CPUID_1_EAX *)&b;
    printf ("stepping=%d", tmp->stepping);
    printf ("model=%d", tmp->model);
    printf ("family_id=%d", tmp->family_id);
    printf ("processor_type=%d", tmp->processor_type);
    printf ("extended_model_id=%d", tmp->extended_model_id);
    printf ("extended_family_id=%d", tmp->extended_family_id);
    return 0;
};
```

之后CPU会填充EAX,EBX,ECX,EDX,这些寄存器的值会通过b[]数组显现出来。接着我们用一个指向CPUID_1_EAX结构体的指针,把它指向b[]数组的EAX值。 换句话说,我们将把32位的INT类型的值当作一个结构体来看。 然后我们就能从结构体中读取数据。 让我们在MSVC 2008用/Ox编译一下:

清单18.19: MSVC 2008

```
_b$ = -16 ; size = 16
_main PROC
    sub esp, 16 ; 00000010H
    push ebx
    xor ecx, ecx
    mov eax, 1
    cpuid
    push esi
    lea esi, DWORD PTR _b$
    mov DWORD PTR , eax
    mov DWORD PTR , ebx
    mov DWORD PTR , ecx
    mov DWORD PTR , edx
    mov esi, DWORD PTR _b$
    mov eax, esi
    and eax, 15 ; 0000000fH
    push eax
    push OFFSET $SG15435 ; ’stepping=%d’, 0aH, 00H
    call _printf
    mov ecx, esi
    shr ecx, 4
    and ecx, 15 ; 0000000fH
    push ecx
    push OFFSET $SG15436 ; ’model=%d’, 0aH, 00H
    call _printf
    mov edx, esi
    shr edx, 8
    and edx, 15 ; 0000000fH
    push edx
    push OFFSET $SG15437 ; ’family_id=%d’, 0aH, 00H
    call _printf
    mov eax, esi
    shr eax, 12 ; 0000000cH
    and eax, 3
    push eax
    push OFFSET $SG15438 ; ’processor_type=%d’, 0aH, 00H
    call _printf
    mov ecx, esi
    shr ecx, 16 ; 00000010H
    and ecx, 15 ; 0000000fH
    push ecx
    push OFFSET $SG15439 ; ’extended_model_id=%d’, 0aH, 00H
    call _printf
    shr esi, 20 ; 00000014H
    and esi, 255 ; 000000ffH
    push esi
    push OFFSET $SG15440 ; ’extended_family_id=%d’, 0aH, 00H
    call _printf
    add esp, 48 ; 00000030H
    pop esi
    xor eax, eax
    pop ebx
    add esp, 16 ; 00000010H
    ret 0
_main ENDP
```

SHR指令将EAX寄存器的值右移位,移出去的值必须被忽略,例如我们会忽略右边的位。 AND指令将清除左边不需要的位,换句话说,它处理过后EAX将只留下我们需要的值。 让我们在GCC4.4.1下用-O3编译。

清单18.20: GCC 4.4.1

```
main proc near ; DATA XREF: _start+17
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    push esi
    mov esi, 1
    push ebx
    mov eax, esi
    sub esp, 18h
    cpuid
    mov esi, eax
    and eax, 0Fh
    mov , eax
    mov dword ptr , offset aSteppingD ; "stepping=%d"
    mov dword ptr , 1
    call ___printf_chk
    mov eax, esi
    shr eax, 4
    and eax, 0Fh
    mov , eax
    mov dword ptr , offset aModelD ; "model=%d"
    mov dword ptr , 1
    call ___printf_chk
    mov eax, esi
    shr eax, 8
    and eax, 0Fh
    mov , eax
    mov dword ptr , offset aFamily_idD ; "family_id=%d"
    mov dword ptr , 1
    call ___printf_chk
    mov eax, esi
    shr eax, 0Ch
    and eax, 3
    mov , eax
    mov dword ptr , offset aProcessor_type ; "processor_type=%d"
    mov dword ptr , 1
    call ___printf_chk
    mov eax, esi
    shr eax, 10h
    shr esi, 14h
    and eax, 0Fh
    and esi, 0FFh
    mov , eax
    mov dword ptr , offset aExtended_model ; "extended_model_id=%d"
    mov dword ptr , 1
    call ___printf_chk
    mov , esi
    mov dword ptr , offset unk_80486D0
    mov dword ptr , 1
    call ___printf_chk
    add esp, 18h
    xor eax, eax
    pop ebx
    pop esi
    mov esp, ebp
    pop ebp
    retn
main endp
```

几乎一样。只有一个需要注意的地方就是GCC在调用每个printf()之前会把extended_model_id和extended_family_id的计算联合到一块去,而不是把它们分开计算。

### 18.6.2 将浮点数当作结构体看待

我们已经在FPU(15章)中注意到了float和double两个类型都是有符号的,他们分为符号、有效数字和指数部分。但是我们能直接用上这些位嘛?让我们试一试float。

![](./mdimg/Chapter-18/2.png)

```
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <memory.h>
struct float_as_struct
{
    unsigned int fraction : 23; // fractional part
    unsigned int exponent : 8; // exponent + 0x3FF
    unsigned int sign : 1; // sign bit
};
float f(float _in)
{
    float f=_in;
    struct float_as_struct t;
    assert (sizeof (struct float_as_struct) == sizeof (float));
    memcpy (&t, &f, sizeof (float));
    t.sign=1; // set negative sign
    t.exponent=t.exponent+2; // multiple d by 2^n (n here is 2)
    memcpy (&f, &t, sizeof (float));
    return f;
};
int main()
{
    printf ("%f", f(1.234));
};
```

float_as_struct结构占用了和float一样多的内存空间,也就是4字节,或者说,32位。 现在我们给输入值设置一个负值,然后指数加2,这样我们就能把整个数按照22的值来倍乘,也就是乘以4。 让我们在MSVC2008无优化模式下编译它。

清单18.21: MSVC 2008

```
_t$ = -8 ; size = 4
_f$ = -4 ; size = 4
__in$ = 8 ; size = 4
?f@@YAMM@Z PROC ; f
    push ebp
    mov ebp, esp
    sub esp, 8
    fld DWORD PTR __in$
    fstp DWORD PTR _f$
    push 4
    lea eax, DWORD PTR _f$
    push eax
    lea ecx, DWORD PTR _t$
    push ecx
    call _memcpy
    add esp, 12 ; 0000000cH
    mov edx, DWORD PTR _t$
    or edx, -2147483648 ; 80000000H - set minus sign
    mov DWORD PTR _t$, edx
    mov eax, DWORD PTR _t$
    shr eax, 23 ; 00000017H - drop significand
    and eax, 255 ; 000000ffH - leave here only exponent
    add eax, 2 ; add 2 to it
    and eax, 255 ; 000000ffH
    shl eax, 23 ; 00000017H - shift result to place of bits 30:23
    mov ecx, DWORD PTR _t$
    and ecx, -2139095041 ; 807fffffH - drop exponent
    or ecx, eax ; add original value without exponent with new calculated exponent
    mov DWORD PTR _t$, ecx
    push 4
    lea edx, DWORD PTR _t$
    push edx
    lea eax, DWORD PTR _f$
    push eax
    call _memcpy
    add esp, 12 ; 0000000cH
    fld DWORD PTR _f$
    mov esp, ebp
    pop ebp
    ret 0
?f@@YAMM@Z ENDP ; f
```

有点多余。如果用/Ox编译的话,这里就没有memcpy调用了。f变量会被直接使用,但是没有优化的版本看起来会更容易理解一点。 GCC 4.4.1的-O3选项会怎么做?

清单18.22: Gcc 4.4.1

```
; f(float)
public _Z1ff
_Z1ff proc near
var_4 = dword ptr -4
arg_0 = dword ptr 8
    push ebp
    mov ebp, esp
    sub esp, 4
    mov eax,
    or eax, 80000000h ; set minus sign
    mov edx, eax
    and eax, 807FFFFFh ; leave only significand and exponent in EAX
    shr edx, 23 ; prepare exponent
    add edx, 2 ; add 2
    movzx edx, dl ; clear all bits except 7:0 in EAX
    shl edx, 23 ; shift new calculated exponent to its place
    or eax, edx ; add new exponent and original value without exponent
    mov , eax
    fld
    leave
    retn
    _Z1ff endp
    public main
    main proc near
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 10h
    fld ds:dword_8048614 ; -4.936
    fstp qword ptr
    mov dword ptr , offset asc_8048610 ; "%f
"
    mov dword ptr , 1
    call ___printf_chk
    xor eax, eax
    leave
    retn
main endp
```

f()函数基本可以理解,但是有趣的是,GCC可以在编译阶段就通过我们这堆大杂烩一样的代码计算出f(1.234)的值,从而会把他当作参数直接给printf()。
页: [1]
查看完整版本: C++反汇编入门17.结构体