第21章 结构体

在C/C++的数据结构里结构体(structure)是由一系列数据简单堆积而成的数据类型。结构体中的各项数据元素,可以是相同类型的数据、也可以是不同类型的数据。[1]

21.1 MSVC: systemtime

本节以Win32描述系统时间的SYSTEMTIME结构体为例。

库函数对它的定义如下[2]

指令清单21.1 WinBase.h

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

根据上述声明,获取系统时间的程序大致如下:

#include <windows.h>
#include <stdio.h>
void main() 
{
    SYSTEMTIME t;
    GetSystemTime (&t);

    printf ("%04d-%02d-%02d %02d:%02d:%02d\n",
        t.wYear, t.wMonth, t.wDay,
        t.wHour, t.wMinute, t.wSecond);

    return;
};

使用MSVC 2010(启用/GS-选项)编译这个程序,可得如下所示的代码。

指令清单21.2 MSVC 2010 /GS-

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

函数在栈里为这个结构体申请了16字节空间。这个结构体由8个WORD型数据构成,每个WORD型数据占用2字节,所以整个结构体正好需要16字节的存储空间。

这个结构体的第一个字段是wYear。根据MSDN有关SYSTEMTIME结构体的相关声明[3],在使用GetSystemTime()函数时,传递给函数的是SYSTEMTIME结构体的指针。但是换个角度看,这也是wYear字段的指针。GetSystemTime()函数首先会在结构体的首地址写入年份信息,然后再把指针调整2个字节并写入月份信息,如此类推写入全部信息。

21.1.1 OllyDbg

我们使用MSVC 2010(指定/GS- /MD 选项)编译上述程序,并用OllyDbg打开MSVC生成的可执行文件。找到传递给GetSystemTime()函数的指针地址,然后在数据观察窗口里观察这部分数据。此时数据如图21.1所示。

..\TU\2101.tif{}

图21.1 OllyDbg:执行GetSystemTime()

在执行函数时精确的系统时间是“9 december 2014, 22:29:52”如图21.2所示。

..\TU\2102.tif

图21.2 OllyDbg:printf()函数的输出结果

我们在数据窗口看到这个地址开始的16字节空间的值是:

DE 07 0C 00 02 00 09 00 16 00 1D 00 34 00 D4 03

这段空间的每2个字节代表结构体的一个字段。由于采用了小端字节序,所以就同一个WORD型数据而言,数权较小的一个字节在前,数权较大的字节在后。我们将其整理一下,看看它们的实际涵义:

十六进制数

十进制含义

字段名

0x07DE

2014

wYear

0x000C

12

wMonth

0x0002

2

wDayOfWeek

0x0009

9

wDay

0x0016

22

wHour

0x001D

29

wMinute

0x0034

52

wSecond

0x03D4

980

wMilliSeconds

在栈窗口里看到的值与此相同,不过栈窗口以32位数据的格式组织数据。

而后printf()函数从结构体中获取所需数据,并在屏幕上进行相应输出。

printf()函数并没有将所有的字段都打印出来。wDayOfWeek 和wMilliSeconds都未被输出,但是内存中确实有它们对应的值。

21.1.2 以数组替代结构体

结构体中的各个元素,在内存里依次排列。为了验证它在内存中的存储状况和数组相同,我用数组替代了SYSTEMTIME结构体:

#include <windows.h>
#include <stdio.h>
void main() 
{
    WORD array[8];
    GetSystemTime (array);
    printf ("%04d-%02d-%02d %02d:%02d:%02d\n",
        array[0] /* wYear */, array[1] /* wMonth */, array[3] /* wDay */, 
        array[4] /* wHour */, array[5] /* wMinute */, array[6] /* wSecond */);
    return; 
};

编译器会提示警告信息:

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

即使如此,MSVC 2010仍能够进行编译。它生成的代码如下所示。

指令清单21.3 Non-optimizing 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
        lea     eax, DWORD PTR _array$[ebp]
        push    eax
        call    DWORD PTR __imp__GetSystemTime@4
        movzx   ecx, WORD PTR _array$[ebp+12] ; wSecond
        push    ecx
        movzx   edx, WORD PTR _array$[ebp+10] ; wMinute
        push    edx
        movzx   eax, WORD PTR _array$[ebp+8] ; wHoure
        push    eax
        movzx   ecx, WORD PTR _array$[ebp+6] ; wDay
        push    ecx
        movzx   edx, WORD PTR _array$[ebp+2] ; wMonth
        push    edx
        movzx   eax, WORD PTR _array$[ebp] ; wYear
        push    eax
        push    OFFSET $SG78573
        call    _printf
        add     esp, 28
        xor     eax, eax
        mov     esp, ebp
        pop     ebp
        ret     0
_main   ENDP

即使调整了数据类型,编译器生成的程序仍然没有什么变化。

这个现象说明,即使我们使用数组替代了原有结构体,编译器生成的汇编指令依旧完全相同。仅仅从汇编指令分析,很难判断出到底源程序使用的是多变量的结构体还是数组。

好在正常人不会做这种别扭的替换。毕竟结构体的可读性、易用性都比数组强,也方便编程人员替换结构体中的字段。

因为这个程序和前面的程序完全相同,本书就不再演示OllyDbg的调试过程了。

21.2 用malloc()分配结构体的空间

在某些情况下,使用堆(heap)来存储结构体要比栈(stack)容易一些:

#include <windows.h>
#include <stdio.h>

void main() 
{
    SYSTEMTIME *t;
    t=(SYSTEMTIME *)malloc (sizeof (SYSTEMTIME));
    GetSystemTime (t);
    printf ("%04d-%02d-%02d %02d:%02d:%02d\n",
        t->wYear, t->wMonth, t->wDay,
        t->wHour, t->wMinute, t->wSecond);
    free (t);
    return; 
};

现在启用MSVC的优化选项/Ox,编译上述程序,得到的代码如下所示。

指令清单21.4 Optimizing MSVC

_main      PROC
    push   esi
    push   16
    call   _malloc
    add    esp, 4
    mov    esi, eax
    push   esi
    call   DWORD PTR __imp__GetSystemTime@4
    movzx  eax, WORD PTR [esi+12] ; wSecond
    movzx  ecx, WORD PTR [esi+10] ; wMinute
    movzx  edx, WORD PTR [esi+8] ; wHour
    push   eax
    movzx  eax, WORD PTR [esi+6] ; wDay
    push   ecx
    movzx  ecx, WORD PTR [esi+2] ; wMonth
    push   edx
    movzx  edx, WORD PTR [esi] ; wYear
    push   eax
    push   ecx
    push   edx
    push   OFFSET $SG78833
    call   _printf
    push   esi
    call   _free
    add    esp, 32
    xor    eax, eax
    pop    esi
    ret    0
_main      ENDP

16就是sizeof(SYSTEMTIME),即malloc()分配空间的确切大小。malloc()函数根据参数指定的大小分配一块空间,并把空间的指针传递给EAX寄存器。而后ESI寄存器获取了这个指针。Win32的GetSystemTime()函数把返回值的各个项存储到esi指针指向的对应空间里,接下来几个寄存器依次读取这些返回值并将之依次推送入栈、给printf()函数调用。

此处的“MOVZX(Move with Zero eXtent)”是前文没有介绍过的指令。这个指令的作用和MOVSX(参见第14章)的相似。与MOVSX的不同之处是,MOVZX在进行格式转换的时候会将其他bit位清零。因为printf()函数需要的数据类型是32 位整型数据,而我们的结构体SYSTEMTIME里对应的字段是WORD型数据,所以此处要转换数据类型。WORD型数据是16位无符号数据,因而要把WORD型数据照抄到int型数据空间的低地址位,并把高地址位(第16位到第31位)清零。高地址位必须要清零,否则转换的int型数据很可能会受到脏数据问题的不良影响。

本例中,我们可以使用8个WORD型数组重新构造上述结构体:

#include <windows.h>
#include <stdio.h>

void main() 
{
    WORD *t;

    t=(WORD *)malloc (16);

    GetSystemTime (t);

    printf ("%04d-%02d-%02d %02d:%02d:%02d\n",
        t[0] /* wYear */, t[1] /* wMonth */, t[3] /* wDay */,
        t[4] /* wHour */, t[5] /* wMinute */, t[6] /* wSecond */);

    free (t);

    return; 
};

使用MSVC(启用优化选项)编译上述程序,可得到如下所示的代码。

指令清单21.5 Optimizing MSVC

$SG78594 DB     '%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H

_main   PROC
        push    esi
        push    16
        call    _malloc
        add     esp, 4
        mov     esi, eax
        push    esi
        call    DWORD PTR __imp__GetSystemTime@4
        movzx   eax, WORD PTR [esi+12]
        movzx   ecx, WORD PTR [esi+10]
        movzx   edx, WORD PTR [esi+8]
        push    eax
        movzx   eax, WORD PTR [esi+6]
        push    ecx
        movzx   ecx, WORD PTR [esi+2]
        push    edx
        movzx   edx, WORD PTR [esi]
        push    eax
        push    ecx
        push    edx
        push    OFFSET $SG78594
        call    _printf
        push    esi
        call    _free
        add     esp, 32
        xor     eax, eax
        pop     esi
        ret     0
_main   ENDP

这个代码和结构体生成的代码完全相同。我再次强调,这种“用数组替代结构体”的做法没有什么实际意义。除非有必要,否则不必做这种替换。

21.3 UNIX: struct tm

21.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\n", t.tm_year+1900);
    printf ("Month: %d\n", t.tm_mon);
    printf ("Day: %d\n", t.tm_mday);
    printf ("Hour: %d\n", t.tm_hour);
    printf ("Minutes: %d\n", t.tm_min);
    printf ("Seconds: %d\n", t.tm_sec);
};

使用GCC 4.4.1 编译,可得如下所示的代码。

指令清单21.6 GCC 4.4.1

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

可惜的是IDA未能在局部栈里将这些局部变量逐一识别出来,因此未能标记变量的别名。但是读到这里的读者都是有经验的逆向工程分析人员了,不再需要辅助信息仍然可以分析出数据的作用。

需要注意的是“lea edx, [eax+76Ch]”指令。它给EAX寄存器里的值加上0x76C(1900),而不会修改任何标志位。有关LEA指令的更多信息,请参见附录A.6.2。

GDB

我们使用GDB调试这个程序,可得到如下所示的代码。[4]

指令清单21.7 GDB

dennis@ubuntuvm:~/polygon$ date
Mon Jun  2 18:10:37 EEST 2014
dennis@ubuntuvm:~/polygon$ gcc GCC_tm.c -o GCC_tm
dennis@ubuntuvm:~/polygon$ gdb GCC_tm
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/GCC_tm...(no debugging symbols found)...done.
(gdb) b printf
Breakpoint 1 at 0x8048330
(gdb) run
Starting program: /home/dennis/polygon/GCC_tm
Breakpoint 1, __printf (format=0x80485c0 "Year: %d\n") at printf.c:29
29      printf.c: No such file or directory.
(gdb) x/20x $esp
0xbffff0dc:     0x080484c3      0x080485c0      0x000007de      0x00000000
0xbffff0ec:     0x08048301      0x538c93ed      0x00000025      0x0000000a
0xbffff0fc:     0x00000012      0x00000002      0x00000005      0x00000072
0xbffff10c:     0x00000001      0x00000098      0x00000001      0x00002a30
0xbffff11c:     0x0804b090      0x08048530      0x00000000      0x00000000
(gdb)

在数据栈中,结构体的构造非常清晰。首先我们看看time.h里的定义。

指令清单21.8 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;
};

Linux的tm结构体的每个元素都是int型数据。在数据类型上它就和Windows 的SYSTEMTIME结构体采用的WORD型数据不同。

我们继续分析局部栈的情况:

0xbffff0dc:0x080484c3       0x080485c0       0x000007de        0x00000000
0xbffff0ec:0x08048301       0x538c93ed       0x00000025 秒     0x0000000a 分
0xbffff0fc:0x00000012时     0x00000002 mday  0x00000005 月     0x00000072 年
0xbffff10c:0x00000001 wday  0x00000098 yday  0x00000001 isdst  0x00002a30
0xbffff11c:0x0804b090       0x08048530       0x00000000        0x00000000

整理一下,结果如下表所示。

十六进制数

十进制

字段

0x00000025

37

tm_sec

0x0000000a

10

tm_min

0x00000012

18

tm_hour

0x00000002

2

tm_mday

0x00000005

5

tm_mon

0x00000072

114

tm_year

0x00000001

1

tm_wday

0x00000098

152

tm_yday

0x00000001

1

tm_isdst

这个结构体比SYSTEMTIME多了一些字段,例如tm_wday/ tm_yday/ tm_isdst字段,不过本例用不到这些字段。

21.3.2 ARM

Optimizing Keil 6/2013 (Thumb mode)

使用Keil 6(启用优化选项)编译上述结构体程序,可得到Thumb模式的程序如下所示。

指令清单21.9 Optimizing Keil 6/2013 (Thumb mode)

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, [SP,#0x38+timer]
       MOV      R1, SP          ; tp
       ADD      R0, SP, #0x38+timer ; timer
       BL       localtime_r
       LDR      R1, =0x76C
       LDR      R0, [SP,#0x38+var_24]
       ADDS     R1, R0, R1
       ADR      R0, aYearD      ; "Year: %d\n"
       BL        __2printf
       LDR      R1, [SP,#0x38+var_28]
       ADR      R0, aMonthD     ; "Month: %d\n"
       BL       __2printf
       LDR      R1, [SP,#0x38+var_2C]
       ADR      R0, aDayD       ; "Day: %d\n"
       BL       __2printf
       LDR      R1, [SP,#0x38+var_30]
       ADR      R0, aHourD      ; "Hour: %d\n"
       BL       __2printf
       LDR      R1, [SP,#0x38+var_34]
       ADR      R0, aMinutesD   ; "Minutes: %d\n"
       BL       __2printf
       LDR      R1, [SP,#0x38+var_38]
       ADR      R0, aSecondsD   ; "Seconds: %d\n"
       BL       __2printf
       ADD      SP, SP, #0x34
       POP      {PC}

Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

通过分析被调用方函数的函数名称(例如本例的localtime_r()函数),IDA能够“识别”出返回值为结构体型数据,并能给结构体中的字段重新标注名称。

指令清单21.10 Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

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, [SP,#0x38+var_38]
       MOV  R0, SP  ; time_t *
       BLX  _localtime_r
       LDR  R1, [SP,#0x38+var_34.tm_year]
       MOV  R0, 0xF44 ; "Year: %d\n"
       ADD  R0, PC  ; char *
       ADDW R1, R1, #0x76C
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34.tm_mon]
       MOV  R0, 0xF3A ; "Month: %d\n"
       ADD  R0, PC  ; char *
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34.tm_mday]
       MOV  R0, 0xF35 ; "Day: %d\n"
       ADD  R0, PC  ; char *
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34.tm_hour]
       MOV  R0, 0xF2E ; "Hour: %d\n"
       ADD  R0, PC  ; char *
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34.tm_min]
       MOV  R0, 0xF28 ; "Minutes: %d\n"
       ADD  R0, PC  ; char *
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34]
       MOV  R0, 0xF25 ; "Seconds: %d\n"
       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
21.3.3 MIPS

指令清单21.11 Optimizing GCC 4.4.5 (IDA)

 1 main:
 2
 3 ; IDA is not aware of structure field names, we named them manually:
 4
 5 var_40          = -0x40
 6 var_38          = -0x38
 7 seconds         = -0x34
 8 minutes         = -0x30
 9 hour            = -0x2C
10 day             = -0x28
11 month           = -0x24
12 year            = -0x20
13 var_4           = -4
14
15                 lui     $gp, (__gnu_local_gp >> 16)
16                 addiu   $sp, -0x50
17                 la      $gp, (__gnu_local_gp & 0xFFFF)
18                 sw      $ra, 0x50+var_4($sp)
19                 sw      $gp, 0x50+var_40($sp)
20                 lw      $t9, (time & 0xFFFF)($gp)
21                 or      $at, $zero ; load delay slot, NOP
22                 jalr    $t9
23                 move    $a0, $zero ; branch delay slot, NOP
24                 lw      $gp, 0x50+var_40($sp)
25                 addiu   $a0, $sp, 0x50+var_38
26                 lw      $t9, (localtime_r & 0xFFFF)($gp)
27                 addiu   $a1, $sp, 0x50+seconds
28                 jalr    $t9
29                 sw      $v0, 0x50+var_38($sp) ; branch delay slot
30                 lw      $gp, 0x50+var_40($sp)
31                 lw      $a1, 0x50+year($sp)
32                 lw      $t9, (printf & 0xFFFF)($gp)
33                 la      $a0, $LC0 # "Year: %d\n"
34                 jalr    $t9
35                 addiu   $a1, 1900 ; branch delay slot
36                 lw      $gp, 0x50+var_40($sp)
37                 lw      $a1, 0x50+month($sp)
38                 lw      $t9, (printf & 0xFFFF)($gp)
39                 lui     $a0, ($LC1 >> 16) # "Month: %d\n"
40                 jalr    $t9
41                 la      $a0, ($LC1 & 0xFFFF) # "Month: %d\n" ; branch delay slot
42                 lw      $gp, 0x50+var_40($sp)
43                 lw      $a1, 0x50+day($sp)
44                 lw      $t9, (printf & 0xFFFF)($gp)
45                 lui     $a0, ($LC2 >> 16) # "Day: %d\n"
46                 jalr    $t9
47                 la      $a0, ($LC2 & 0xFFFF) # "Day: %d\n" ; branch delay slot
48                 lw      $gp, 0x50+var_40($sp)
49                 lw      $a1, 0x50+hour($sp)
50                 lw      $t9, (printf & 0xFFFF)($gp)
51                 lui     $a0, ($LC3 >> 16) # "Hour: %d\n"
52                 jalr    $t9
53                 la      $a0, ($LC3 & 0xFFFF) # "Hour: %d\n" ; branch delay slot
54                 lw      $gp, 0x50+var_40($sp)
55                 lw      $a1, 0x50+minutes($sp)
56                 lw      $t9, (printf & 0xFFFF)($gp)
57                 lui     $a0, ($LC4 >> 16) # "Minutes: %d\n"
58                 jalr    $t9
59                 la      $a0, ($LC4 & 0xFFFF) # "Minutes: %d\n" ; branch delay slot
60                 lw      $gp, 0x50+var_40($sp)
61                 lw      $a1, 0x50+seconds($sp)
62                 lw      $t9, (printf & 0xFFFF)($gp)
63                 lui     $a0, ($LC5 >> 16) # "Seconds: %d\n"
64                 jalr    $t9
65                 la      $a0, ($LC5 & 0xFFFF) # "Seconds: %d\n" ; branch delay slot
66                 lw      $ra, 0x50+var_4($sp)
67                 or      $at, $zero ; load delay slot, NOP
68                 jr      $ra
69                 addiu   $sp, 0x50
70
71                 $LC0: .ascii "Year: %d\n"<0>
72                 $LC1: .ascii "Month: %d\n"<0>
73                 $LC2: .ascii "Day: %d\n"<0>
74                 $LC3: .ascii "Hour: %d\n"<0>
75                 $LC4: .ascii "Minutes: %d\n"<0>
76                 $LC5: .ascii "Seconds: %d\n"<0>

这个案例再次演示了延时槽影响人工分析的严重程度。例如,位于第35行的“addiu $a1, 1900”指令,把返回值加上1900。千万别忘记它的执行顺序先于第34行的JALR指令。

21.3.4 数组替代法

在内存中,结构体是依次排列的一系列数据。为了演示它的这一特色,我们对指令清单21.8的程序进行少许修改,以使用数组替代tm结构体:

#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\n", tm_year+1900);
    printf ("Month: %d\n", tm_mon);
    printf ("Day: %d\n", tm_mday);
    printf ("Hour: %d\n", tm_hour);
    printf ("Minutes: %d\n", tm_min);
    printf ("Seconds: %d\n", tm_sec);
};

注解:指向结构体的指针,实际上指向结构体的第一个元素。

使用GCC 4.7.3编译的时候,会看到编译器提示如下所示。

指令清单21.12 GCC 4.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 *’

不过这些问题不影响正常编译,所得可执行程序的具体指令如下所示。

指令清单21.13 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     [esp+30h+var_30], 0 ; arg 0
           call    time
           mov     [esp+30h+unix_time], eax
           lea     eax, [esp+30h+tm_sec]
           mov     [esp+30h+var_2C], eax
           lea     eax, [esp+30h+unix_time]
           mov     [esp+30h+var_30], eax
           call    localtime_r
           mov     eax, [esp+30h+tm_year]
           add     eax, 1900
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aYearD ; "Year: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_mon]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aMonthD ; "Month: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_mday]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aDayD ; "Day: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_hour]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aHourD ; "Hour: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_min]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aMinutesD ; "Minutes: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_sec]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aSecondsD ; "Seconds: %d\n"
           call    printf
           leave
           retn
main       endp

从汇编代码上看,采用数组的程序与先前使用结构体的程序最终竟会如此一致,以至于我们无法从汇编代码上区分出源程序的区别。

虽然上述程序可以照常运行,但是使用数组替换结构体的做法并不值得推荐。一般来说,“不启用优化编译选项”的编译器通常以代码声明变量的次序,在局部栈里分配变量的空间,但是无法保证每次编译的结果都严丝合缝。

如果使用GCC以外的编译器的话,部分编译器可能警告除变量tm_sec之外的tm_year、tm_mon、tm_mday、tm_hour、tm_min没有被初始化。这是因为编译器并不能分析出这些变量将被localtime_r()函数赋值。

在这个程序里,结构体各字段都是int型数据,所以本例十分直观。如果源程序中结构体的字段都是 16 位 WORD型数据,且采取了SYSTEMTIME那样的数据结构,因为局部变量向32位边界对齐的缘故,这将使GetSystemTime()无法正常赋值。有关结构体的字段封装问题,请参考21.4节。

由此可见,结构体就是一连串变量的封装体。在内存中结构体的各字段依次排列。我认为结构体就是语体上的糖块,使其内各个变量像糖分一样粘成一个统一体,以便编译器把它们分配到连续空间里。即使别人可能认为我是编程专家,但是我毕竟不是,所以这种说法很可能不够确切。顺便提一下,早期的(1972年之前)C语言不支持结构体structure。[5]

这个可执行程序完全和前面的程序一样,本书就不演示相关调试过程了。

21.3.5 替换为32位words
#include <stdio.h>
#include <time.h>

void main() 
{
    struct tm t;
    time_t unix_time;
    int i;

    unix_time=time(NULL);

    localtime_r (&unix_time, &t);

    for (i=0; i<9; i++)
    {
        int tmp=((int*)&t)[i];
        printf ("0x%08X (%d)\n", tmp, tmp);
    };
};

上述程序把同一地址(指针)的数据分别当作结构体和整型数据、分别进行写和读的访问操作,完全可以正常工作。在当前时间为“23:51:45 26-July-2014”的时刻,内存中的数据如下:

0x0000002D (45)
0x00000033 (51)
0x00000017 (23)
0x0000001A (26)
0x00000006 (6)
0x00000072 (114)
0x00000006 (6)
0x000000CE (206)
0x00000001 (1)

这些变量的排列数据,与结构体在源代码中的声明顺序相同。详细内容请参见本书第21章第8节。

编译而得的程序如下所示。

指令清单21.14 Optimizing GCC 4.8.1

main            proc near
                push     ebp
                mov      ebp, esp
                push     esi
                push     ebx
                and      esp, 0FFFFFFF0h
                sub      esp, 40h
                mov      dword ptr [esp], 0 ; timer
                lea      ebx, [esp+14h]
                call     _time
                lea      esi, [esp+38h]     ;tp
                mov      [esp+4], ebx
                mov      [esp+10h], eax
                lea      eax, [esp+10h]
                mov      [esp], eax        ; timer
                call     _localtime_r
                nop
                lea      esi, [esi+0]      ; nop

loc_80483D8:
; EBX here is pointer to structure, ESI is the pointer to the end of it.
                mov      eax, [ebx]      ; get 32-bit word from array
                add      ebx, 4          ; next field in structure
                mov      dword ptr [esp+4], offset a0x08xD ; "0x%08X (%d)\n"
                mov      dword ptr [esp], 1
                mov      [esp+0Ch], eax  ; pass value to printf()
                mov      [esp+8], eax    ; pass value to printf()
                call     ___printf_chk
                cmp      ebx, esi        ; meet structure end?
                jnz      short loc_80483D8 ; no - load next value then
                lea      esp, [ebp-8]
                pop      ebx
                pop      esi
                pop      ebp
                retn
main            endp

栈空间内的数据依次被看作两种数据:先被当作结构体、再被当作数组。

本例表明,我们可以借助指针修改结构体中的各别字段。

本文再次强调:若不是以hack目的研究代码,就不必做这种处理。在编写生产环境下的程序时,不建议使用这种处理手段。

21.3.6 替换为字节型数组

更进一步的实验表明,也可以让时间结构体与字节型数组共用一个指针。

#include <stdio.h>
#include <time.h>

void main() 
{
    struct tm t;
    time_t unix_time;
    int i, j;

    unix_time=time(NULL);

    localtime_r (&unix_time, &t);

    for (i=0; i<9; i++)
    {
        for (j=0; j<4; j++)
            printf ("0x%02X ", ((unsigned char*)&t)[i*4+j]);
        printf ("\n");
    };
};
 
0x2D 0x00 0x00 0x00
0x33 0x00 0x00 0x00
0x17 0x00 0x00 0x00
0x1A 0x00 0x00 0x00
0x06 0x00 0x00 0x00
0x72 0x00 0x00 0x00
0x06 0x00 0x00 0x00
0xCE 0x00 0x00 0x00
0x01 0x00 0x00 0x00

假如此时的系统时间和21.3.5节那个程序同时启动,它肯定会提示“23:51:45 26-July-2014”。需要注意的是,因为采用了小端字节序(参见第31章),数权较小的字节反而排在数权较大的字节之前。

指令清单21.15 Optimizing GCC 4.8.1

main             proc near
                 push     ebp
                 mov      ebp, esp
                 push     edi
                 push     esi
                 push     ebx
                 and      esp, 0FFFFFFF0h
                 sub      esp, 40h
                 mov      dword ptr [esp], 0 ; timer
                 lea      esi, [esp+14h]
                 call     _time
                 lea      edi, [esp+38h]  ; struct end
                 mov      [esp+4], esi    ; tp
                 mov      [esp+10h], eax
                 lea      eax, [esp+10h]
                 mov      [esp], eax      ; timer
                 call     _localtime_r
                 lea      esi, [esi+0]    ; NOP
; ESI here is the pointer to structure in local stack. EDI is the pointer to structure end.
loc_8048408: 
                 xor      ebx, ebx        ; j=0
loc_804840A:
                 movzx    eax, byte ptr [esi+ebx] ; load byte
                 add      ebx, 1          ; j=j+1
                 mov      dword ptr [esp+4], offset a0x02x ; "0x%02X "
                 mov      dword ptr [esp], 1
                 mov      [esp+8], eax   ; pass loaded byte to printf()
                 call     ___printf_chk
                 cmp      ebx, 4
                 jnz      short loc_804840A
; print carriage return character (CR)
                 mov      dword ptr [esp], 0Ah ; c
                 add      esi, 4
                 call     _putchar
                 cmp      esi, edi       ; meet struct end?
                 jnz      short loc_8048408 ; j=0
                 lea      esp, [ebp-0Ch]
                 pop      ebx
                 pop      esi
                 pop      edi
                 pop      ebp
                 retn
main             endp

21.4 结构体的字段封装

结构体的字段封装方法构成了这种数据类型的一个重要特性。[6]

我们以下面的例子来加以说明。

#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\n", s.a, s.b, s.c, s.d);
};
int main() 
{
    struct s tmp;
    tmp.a=1;
    tmp.b=2;
    tmp.c=3;
    tmp.d=4;
    f(tmp);
};

其中有2个单字节型char字段和2个4字节的int型变量。

21.4.1 x86

使用MSVC 2012(启用选项/GS- /Ob0)编译,可得如下所示的代码。

指令清单21.16 MSVC 2012/GS-/O60

 1 _tmp$ = -16
 2 _main    PROC
 3     push   ebp
 4     mov    ebp, esp
 5     sub    esp, 16
 6     mov    BYTE PTR _tmp$[ebp], 1       ; set field a
 7     mov    DWORD PTR _tmp$[ebp+4], 2    ; set field b
 8     mov    BYTE PTR _tmp$[ebp+8], 3     ; set field c
 9     mov    DWORD PTR _tmp$[ebp+12], 4   ; set field d
10     sub    esp, 16                      ; allocate place for temporary structure
11     mov    eax, esp
12     mov    ecx, DWORD PTR _tmp$[ebp]    ; copy our structure to the temporary one
13     mov    DWORD PTR [eax], ecx
14     mov    edx, DWORD PTR _tmp$[ebp+4]
15     mov    DWORD PTR [eax+4], edx
16     mov    ecx, DWORD PTR _tmp$[ebp+8]
17     mov    DWORD PTR [eax+8], ecx
18     mov    edx, DWORD PTR _tmp$[ebp+12]
19     mov    DWORD PTR [eax+12], edx
20     call    _f
21     add    esp, 16
22     xor    eax, eax
23     mov    esp, ebp
24     pop    ebp
25     ret    0
26 _main   ENDP
27
28 _s$ = 8                ; size = 16
29 ?f@@YAXUs@@@Z PROC     ; f
30     push  ebp
31     mov   ebp, esp
32     mov   eax, DWORD PTR _s$[ebp+12]
33     push  eax
34     movsx ecx, BYTE PTR _s$[ebp+8]
35     push  ecx
36     mov   edx, DWORD PTR _s$[ebp+4]
37     push  edx
38     movsx eax, BYTE PTR _s$[ebp]
39     push  eax
40     push  OFFSET $SG3842
41     call  _printf
42     add   esp, 20
43     pop   ebp
44     ret   0
45 ?f@@YAXUs@@@Z ENDP ; f
46 _TEXT     ENDS

虽然我们在代码里一次性分配了结构体tmp,并依次给它的四个字段赋值,但是可执行程序的指令有些不同:它将结构体的指针复制到临时地址(第10行指令分配的空间里),然后通过临时的中间变量把结构体的四个值赋值给临时结构体(第12~19行的指令),还把指针也复制出来供f()调用。这主要是因为编译器无法判断f()函数是否会修改结构体的内容。借助中间变量,编译器可以保证main()函数里tmp结构体的值不受被调用方函数的影响。我们也可以改动源程序、使用指针传递数据,那样编译器生成的汇编指令也基本相同,但是不会再复制数据了。

另外,这个程序里结构体的字段向4字节边界对齐。也就是说它的char型数据也和int型数据一样占4字节存储空间。这主要是为了方便CPU从内存读取数据,提高读写和缓存的效率。

这样做的缺点是浪费存储空间。

接下来我们启用编译器的/Zp1 (/Zp[n]表示向n个字节的边界对齐)选项。

使用MSVC 2012(启用/GS- /Zp1选项)编译上述程序,可得到如下所示的代码。

指令清单21.17 MSVC 2012 /GS- /Zp1

 1 _main    PROC
 2     push   ebp
 3     mov    ebp, esp
 4     sub    esp, 12
 5     mov    BYTE PTR _tmp$[ebp], 1       ; set field a
 6     mov    DWORD PTR _tmp$[ebp+1], 2    ; set field b
 7     mov    BYTE PTR _tmp$[ebp+5], 3     ; set field c
 8     mov    DWORD PTR _tmp$[ebp+6], 4    ; set field d
 9     sub    esp, 12                      ; allocate place for temporary structure
10     mov    eax, esp
11     mov    ecx, DWORD PTR _tmp$[ebp]    ; copy 10 bytes
12     mov    DWORD PTR [eax], ecx
13     mov    edx, DWORD PTR _tmp$[ebp+4]
14     mov    DWORD PTR [eax+4], edx
15     mov    cx, WORD PTR _tmp$[ebp+8]
16     mov    WORD PTR [eax+8], cx
17     call   _f
18     add    esp, 12
19     xor    eax, eax
20     mov    esp, ebp
21     pop    ebp
22     ret    0
23 _main    ENDP
24
25 _TEXT    SEGMENT
26 _s$ = 8      ; size = 10
27 ?f@@YAXUs@@@Z PROC       ; f
28     push   ebp
29     mov    ebp, esp
30     mov    eax, DWORD PTR _s$[ebp+6]
31     push   eax
32     movsx  ecx, BYTE PTR _s$[ebp+5]
33     push   ecx
34     mov    edx, DWORD PTR _s$[ebp+1]
35     push   edx
36     movsx  eax, BYTE PTR _s$[ebp]
37     push   eax
38     push   OFFSET $SG3842
39     call   _printf
40     add    esp, 20
41     pop    ebp
42     ret    0
43 ?f@@YAXUs@@@Z ENDP                    ; f

经过这种处理之后,结构体只占用10字节空间,其中的char型数据占用1字节。这将提高代码的空间利用效率,不过这样做会同时降低CPU的IO读取效率。

main()函数里同样使用临时结构体复制了传入参数的信息,再把临时结构体传递给其他函数。不过这10字节数据并不是一个字段一个字段地、按变量声明的那样分4次复制过去的,编译器分配3对MOV指令复制它们。为什么不是4对赋值指令?这是因为编译器认为,在复制10字节数据时,3个MOV指令对的效率,比4对MOV指令对(按字段赋值)的效率要高。这是编译器常用的优化手段。编译器使用MOV指令直接实现(替代)memcpy()函数的情况十分普遍,主要就是因为在复制小型数据时memcpy()函数没有MOV指令的效率高。如需了解这方面详细知识,请参见本书43.1.5节。

当然,如果某个结构体被多个源文件、目标文件(object files)调用,那么在编译这些程序时,结构封装格式和数据对其规范(/Zp[n])必须完全匹配。

MSVC编译器有指定结构体字段对齐标准的/Zp选项。此外,还可以通过在源文件里设定#pragma pack的方法来指定这个选项。MSVC和GCC都支持这种代码级的宏指令。[7]

我们回顾一下使用16位型数据的结构体SYSTEMTIME。编译器如何知道要将它的字段向1字节边界对齐呢?

文件WinNT.h有如下声明。

指令清单21.18 WinNT.h

#include "pshpack1.h"

而且还有下述声明。

指令清单21.19 WinNT.h

#include "pshpack4.h"//默认情况下进行4字节边界对齐

PshPack1.h文件有下列内容。

指令清单21.20 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的信息,编译器会在封装结构体时向相应的边界对齐。

OllyDbg + 默认封装格式

现在打开OllyDbg,加载上述以默认封装格式(4字节边界对齐)的程序,如图21.3所示。

..\TU\2103.tif{}

图21.3 OllyDbg:调用printf()函数之前

我们在数据窗口可以找到四个字段的值。但是,第一个字段(变量a)和第三个字段(变量c)空间之后的数据工具出现了随机值(0x30,0x37, 0x01)。它们是怎么产生的?在指令清单21.16的源程序中,变量a和变量c都是char型单字节数据。程序的第6、第8行,分别给它们赋值1、3。它们在内存里占用了4个字节,而其他32位存储空间里有3个字节并没有被赋值!在这3个字节空间里的数据,就是随机脏数据。因为在给printf()函数传递参数时,编译器会使用的是单字节数据赋值指令MOVSX(参见指令清单21.16的第34行和第38行)传递数据,所以这些脏数据并不会影响printf()函数的输出结果。

另外,因为ac是char型数据,char型数据属于有符号(signed)型数据,所以复制操作所对应的汇编指令是MOVSX(SX是sign-extending的缩写)。如果它们是unsigned char或uint8_t型数据,那么此处就会是MOVZX指令。

OllyDbg + 字段向单字节边界对齐

这种情况下,整个结构体的各个变量在内存里依次排列,如图21.4所示。

..\TU\2104.tif{}

图21.4 OllyDbg:在调用printf()函数之前

21.4.2 ARM

Optimizing Keil 6/2013 (Thumb mode)

使用Keil 6/2013(开启优化选项)、以Thumb模式编译上述程序可得如下所示的代码。

指令清单21.21 Optimizing Keil 6/2013 (Thumb mode)

.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, [SP,#16] ; d
.text:00000286 02 9A              LDR     R2, [SP,#8]  ; b
.text:00000288 00 90              STR     R0, [SP]
.text:0000028A 68 46              MOV     R0, SP
.text:0000028C 03 7B              LDRB    R3, [R0,#12] ; c
.text:0000028E 01 79              LDRB    R1, [R0,#4]  ; a
.text:00000290 59 A0              ADR     R0, aADBDCDDD ; "a=%d; b=%d; c=%d; d=%d\n"
.text:00000292 05 F0 AD FF        BL      __2printf
.text:00000296 D2 E6              B       exit

本例向被调用方函数传递的是结构体型数据,不是结构体的指针。因为ARM会利用寄存器传递函数所需的前4个参数,所以编译器利用3R0~R3寄存器向printf()函数传递结构体的全部字段。

LDRB从内存中加载1个字节并转换为32位有符号数据,用来从结构体中读取字段a和字段c。它相当于x86指令集中的MOVSX指令。

请注意,在函数退出时,它借用了另一个函数的函数尾声!这种借助B指令跳转到其他完全不相干的函数、共用同一个函数尾声的现象,应当是因为两个函数的局部变量的存储空间完全相同。或许因为这两个函数在启动时分配的栈大小相同(都分配了4×5=0x14的数据栈),导致退出语句也完全相同的缘故,再加上它们在内存中的地址相近的因素,所以编译器进行了这种处理。确实,使用同一组退出语句不会影响程序的任何功能。这明显是Keil编译器出于经济因素而进行的指令复用。JMP指令只占用2个字节,而标准的函数尾声要占用4字节。

ARM + Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

指令清单21.22 Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

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\n" 
      SXTB      R1, R1 ;制备a
      MOVT.W    R0, #0
      STR       R3, [SP,#0xC+var_C] ; 推送d 入栈,供 printf() 调用
      ADD       R0, PC ; 格式化字符串
      SXTB      R3, R2 ; 制备c
      MOV       R2,R9;制备b
      BLX       _printf
      ADD       SP, SP, #4
      POP       {R7,PC}

SXTB(Signed Extend Byte)对应x86的MOVSX指令,但是它只能处理寄存器的数据,不能直接对内存进行操作。程序中的其余指令与前例一样,本文不再进行重复说明。

21.4.3 MIPS

指令清单21.23 Optimizing GCC 4.4.5 (IDA)

 1 f:
 2
 3 var_18       = -0x18
 4 var_10       = -0x10
 5 var_4        = -4
 6 arg_0        = 0
 7 arg_4        = 4
 8 arg_8        = 8
 9 arg_C        = 0xC
10
11 ; $a0=s.a
12 ; $a1=s.b
13 ; $a2=s.c
14 ; $a3=s.d
15              lui       $gp, (__gnu_local_gp >> 16)
16              addiu     $sp, -0x28
17              la        $gp, (__gnu_local_gp & 0xFFFF)
18              sw        $ra, 0x28+var_4($sp)
19              sw        $gp, 0x28+var_10($sp)
20 ; prepare byte from 32-bit big-endian integer:
21              sra       $t0, $a0, 24
22              move      $v1, $a1
23 ; prepare byte from 32-bit big-endian integer:
24              sra       $v0, $a2, 24
25              lw        $t9, (printf & 0xFFFF)($gp)
26              sw        $a0, 0x28+arg_0($sp)
27              lui       $a0, ($LC0 >> 16) # "a=%d; b=%d; c=%d; d=%d\n"
28              sw        $a3, 0x28+var_18($sp)
29              sw        $a1, 0x28+arg_4($sp)
30              sw        $a2, 0x28+arg_8($sp)
31              sw        $a3, 0x28+arg_C($sp)
32              la        $a0, ($LC0 & 0xFFFF) # "a=%d; b=%d; c=%d; d=%d\n"
33              move      $a1, $t0
34              move      $a2, $v1
35              jalr      $t9
36              move      $a3, $v0 ; branch delay slot
37              lw        $ra, 0x28+var_4($sp)
38              or        $at, $zero ; load delay slot, NOP
39              jr        $ra
40              addiu     $sp, 0x28 ; branch delay slot
41
42 $LC0:        .ascii "a=%d; b=%d; c=%d; d=%d\n"<0>

结构体各字段首先被安置于$A0~$A3寄存器,然后又被重新安放于$A1~$A3寄存器以传递给printf()函数。较为特殊的是,上述程序使用了两次SRA(Shift Word Right Arithmetic)指令,而SRA指令用于制备char型数据的字段。这是为什么?MIPS默认采用大端字节序(big-endian,参见本书第31章),此外我用的Debian Linux也使用大端字节序。当使用32位空间存储字节型变量时,数据占用第31~24位。因此,当把char型数据扩展为32位数据时,必须右移24位。再加上char型数据属于有符号型数据,所以此处必须用算术位移指令而不能使用逻辑位移指令。

21.4.4 其他

在向被调用方函数传递结构体时(不是传递结构体的指针),传递参数的过程相当于依次传递结构体的各字段。即是说,如果结构体各字段的定义不变,那么f()函数的源代码可改写为:

void f(char a, int b, char c, int d)
{
    printf ("a=%d; b=%d; c=%d; d=%d\n", a, b, c, d);
};

即使经过上述改动,编译器生成的可执行程序也完全不会发生变化。

21.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\n",
        s.a, s.b, s.c.a, s.c.b, s.d, s.e);
};

int main() 
{
    struct outer_struct s;
    s.a=1;
    s.b=2;
    s.c.a=100;
    s.c.b=101;
    s.d=3;
    s.e=4;
    f(s);
};

这个程序把结构体inner_struct当作另一个结构体outer_struct 的字段来用,它和outer_struct的a、b、d、e一样,都是一个字段。

我们使用MSVC 2010(启用/Ox /Ob0选项)编译上述程序,可得到如下所示的代码。

指令清单21.24 Optimizing MSVC 2010 /Ob0

$SG2802 DB    ’a=%d; b=%d; c.a=%d; c.b=%d; d=%d; e=%d’, 0aH, 00H

_TEXT    SEGMENT
_s$ = 8
_f    PROC
      mov    eax, DWORD PTR _s$[esp+16]
      movsx  ecx, BYTE PTR _s$[esp+12]
      mov    edx, DWORD PTR _s$[esp+8]
      push   eax
      mov    eax, DWORD PTR _s$[esp+8]
      push   ecx
      mov    ecx, DWORD PTR _s$[esp+8]
      push   edx
      movsx  edx, BYTE PTR _s$[esp+8]
      push   eax
      push   ecx
      push   edx
      push   OFFSET $SG2802 ; ’a=%d; b=%d; c.a=%d; c.b=%d; d=%d; e=%d’
      call   _printf
      add    esp, 28
      ret    0
_f       ENDP

_s$ = -24
_main    PROC
    sub    esp, 24
    push   ebx
    push   esi
    push   edi
    mov    ecx, 2
    sub    esp, 24
    mov    eax, esp
    mov    BYTE PTR _s$[esp+60], 1
    mov    ebx, DWORD PTR _s$[esp+60]
    mov    DWORD PTR [eax], ebx
    mov    DWORD PTR [eax+4], ecx
    lea    edx, DWORD PTR [ecx+98]
    lea    esi, DWORD PTR [ecx+99]
    lea    edi, DWORD PTR [ecx+2]
    mov    DWORD PTR [eax+8], edx
    mov    BYTE PTR _s$[esp+76], 3
    mov    ecx, DWORD PTR _s$[esp+76]
    mov    DWORD PTR [eax+12], esi
    mov    DWORD PTR [eax+16], ecx
    mov    DWORD PTR [eax+20], edi
    call   _f
    add    esp, 24
    pop    edi
    pop    esi
    xor    eax, eax
    pop    ebx
    add    esp, 24
    ret    0
_main    ENDP

在汇编代码中,我们找不到内嵌结构体的影子。所以,我们可以断定,嵌套模式的结构体会被编译器展开,最终形成一维结构体。

当然,如果使用“struct inter_stuct *c”替代源程序中的“struct iner_struct c”,汇编指令会大不相同。

OllyDbg

我们使用OllyDbg加载上述程序,观测outer_struct。如图21.5所示。

..\TU\2105.tif{}

图21.5 OllyDbg:在执行printf()之前

它在内存中的构造如下:

21.6 结构体中的位操作

21.6.1 CPUID

C/C++语言可以精确操作结构体中的位域。这能够帮助程序员大幅度地节省程序的内存消耗。例如,bool型数据就需要1位空间。但是时间开销和空间效率不可两全,如果要节约内存开销,程序的性能就会下降。

以CPUID指令为例。该指令用于获取CPU及其特性信息。[8]

如果在调用指令之前设置EAX寄存器的值为1,那么CPUID指令将会按照下列格式在EAX寄存器里存储CPU的特征信息。

3:0 (4 bits)
7:4 (4 bits)
11:8 (4 bits)
13:12(2 bits)
19:16(4 bits)
27:20(8 bits)
Stepping
Model
Family
Processor Type
Extended Model
Extended Family

MSVC 2010 有CPUID宏,而GCC 4.4.1没有这个宏。所以,我们自己写一个函数,再用GCC编译它。[9]

#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[4];

#ifdef _MSC_VER
    __cpuid(b,1);
#endif

#ifdef __GNUC__
    cpuid (1, &b[0], &b[1], &b[2], &b[3]);
#endif

    tmp=(struct CPUID_1_EAX *)&b[0];

    printf ("stepping=%d\n", tmp->stepping);
    printf ("model=%d\n", tmp->model);
    printf ("family_id=%d\n", tmp->family_id);
    printf ("processor_type=%d\n", tmp->processor_type);
    printf ("extended_model_id=%d\n", tmp->extended_model_id);
    printf ("extended_family_id=%d\n", tmp->extended_family_id);

    return 0; 
};

在CPUID将返回值存储到EAX/EBX/ECX/EDX之后,程序使用数组b[]收集相关信息。然后,我们通过指向结构体CPUID_1_EAX的指针,从数组b[]中获取EAX寄存器里的值。

换而言之,我们把32位int型数据分解为结构体型数据,再从结构体中读取各项数值。

MSVC

使用MSVC 2008(启用/Ox 选项)编译上述程序,可得如下所示的代码。

指令清单21.25 Optimizing MSVC 2008

b$ = -16 ;size=16 
_main   PROC
    sub    esp, 16
    push   ebx
    xor    ecx, ecx
    mov    eax, 1
    cpuid
    push   esi
    lea    esi, DWORD PTR _b$[esp+24]
    mov    DWORD PTR [esi], eax
    mov    DWORD PTR [esi+4], ebx
    mov    DWORD PTR [esi+8], ecx
    mov    DWORD PTR [esi+12], edx

    mov    esi, DWORD PTR _b$[esp+24]
    mov    eax, esi
    and    eax, 15
    push   eax
    push   OFFSET $SG15435 ; ’stepping=%d’, 0aH, 00H
    call   _printf

    mov    ecx, esi
    shr    ecx, 4
    and    ecx, 15
    push   ecx
    push   OFFSET $SG15436 ; ’model=%d’, 0aH, 00H
    call   _printf

    mov    edx, esi
    shr    edx, 8
    and    edx, 15
    push   edx
    push   OFFSET $SG15437 ; ’family_id=%d’, 0aH, 00H
    call   _printf

    mov    eax, esi
    shr    eax, 12
    and    eax, 3
    push   eax
    push   OFFSET $SG15438 ; ’processor_type=%d’, 0aH, 00H
    call   _printf

    mov    ecx, esi
    shr    ecx, 16
    and    ecx, 15
    push   ecx
    push   OFFSET $SG15439 ; ’extended_model_id=%d’, 0aH, 00H
    call   _printf

    shr    esi, 20
    and    esi, 255
    push   esi
    push   OFFSET $SG15440 ; ’extended_family_id=%d’, 0aH, 00H
    call   _printf
    add    esp, 48
    pop    esi

    xor    eax, eax
    pop    ebx

    add    esp, 16
    ret    0
_main    ENDP

SHR指令用于过滤位于EAX寄存器中右侧那些不参与运算的bit位。

而AND指令用于过滤位于寄存器左侧且不参与运算的相关位。换句话说,这两条指令用于筛选寄存器的特定bit位。

MSVC+OllyDbg

现在使用OllyDbg调试这个程序。如图21.6所示,在执行CPUID之后,EAX/EBX/ECX/EDX寄存器保存着返回值。

..\TU\2106.tif{}

图21.6 OllyDbg:执行CPUID之后的寄存器情况

因为我使用的是Xeon E3-1200 CPU,所以EAX的值是0x000206A7 。它的二进制数值为00000000000000100000011010100111。运行结果如图29.7所示。

..\TU\2107.tif

图21.7 OllyDbg:运行结果

各字段涵义如下表所示。

字段

二进制值

十进制值

reserved2

0000

0

Extended_family_id

00000000

0

Extended_model_id

0010

2

reserved1

00

0

Processor_id

00

0

Family_id

0110

6

model

1010

10

stepping

0111

7

GCC

使用GCC 4.4.1(启用优化选项–O3)编译上述程序,可得如下所示的代码。

指令清单21.26 Optimizing 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     [esp+8], eax
    mov     dword ptr [esp+4], offset aSteppingD ; "stepping=%d\n"
    mov     dword ptr [esp], 1
    call    ___printf_chk
    mov     eax, esi
    shr     eax, 4
    and     eax, 0Fh
    mov     [esp+8], eax
    mov     dword ptr [esp+4], offset aModelD ; "model=%d\n"
    mov     dword ptr [esp], 1
    call    ___printf_chk
    mov     eax, esi
    shr     eax, 8
    and     eax, 0Fh
    mov     [esp+8], eax
    mov     dword ptr [esp+4], offset aFamily_idD ; "family_id=%d\n"
    mov     dword ptr [esp], 1
    call    ___printf_chk
    mov     eax, esi
    shr     eax, 0Ch
    and     eax, 3
    mov     [esp+8], eax
    mov     dword ptr [esp+4], offset aProcessor_type ; "processor_type=%d\n"
    mov     dword ptr [esp], 1
    call    ___printf_chk
    mov     eax, esi
    shr     eax, 10h
    shr     esi, 14h
    and     eax, 0Fh
    and     esi, 0FFh
    mov     [esp+8], eax
    mov     dword ptr [esp+4], offset aExtended_model ; "extended_model_id=%d\n"
    mov     dword ptr [esp], 1
    call    ___printf_chk
    mov     [esp+8], esi
    mov     dword ptr [esp+4], offset unk_80486D0
    mov     dword ptr [esp], 1
    call    ___printf_chk
    add     esp, 18h
    xor     eax, eax
    pop     ebx
    pop     esi
    mov     esp, ebp
    pop     ebp
    retn
main            endp

GCC生成的汇编指令与MSVC库函数基本相同。二者的主要区别是:GCC编译的程序在调用printf()指令之前把extended_model_id和extended_family_id 放在连续的内存块里一并处理了,而没有像MSVC编译的程序那样、在每次调用printf()之前逐一进行计算。

21.6.2 用结构体构建浮点数

前文已经指出,每个单精度float或双精度double型浮点数,都由符号、小数和指数三部分组成。到底能否直接利用结构体构建浮点型数据呢?

符号位(第31位)
指数部分(23~30位)
小数部分(0~11位)
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <memory.h>

struct float_as_struct
{
    unsigned int fraction : 23; // 小数
    unsigned int exponent : 8;  // 指数 + 0x3FF
    unsigned int sign : 1;// 符号位
};
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; // multiply d by 2^n (n here is 2)

    memcpy (&f, &t, sizeof (float));

    return f;
};

int main() 
{
    printf ("%f\n", f(1.234));
};

通过结构体构建而成的float_as_struct和单精度浮点数据占用相同大小的内存空间,即32位/4字节。

在程序里,我们设置符号位为负(1),并把指数增加2,以进行乘以4(2的n次方(n=2))的乘法运算。

使用MSVC 2008(不开启任何优化选项)编译上述程序,可得如下所示的代码。

指令清单21.27 Non-optimizing 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$[ebp]
    fstp   DWORD PTR _f$[ebp]

    push   4
    lea    eax, DWORD PTR _f$[ebp]
    push   eax
    lea    ecx, DWORD PTR _t$[ebp]
    push   ecx
    call   _memcpy
    add    esp, 12
    mov    edx, DWORD PTR _t$[ebp]
    or     edx, -2147483648 ; 80000000H - set minus sign
    mov    DWORD PTR _t$[ebp], edx

    mov    eax, DWORD PTR _t$[ebp]
    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$[ebp]
    and    ecx, -2139095041 ; 807fffffH - drop exponent

; add original value without exponent with new calculated exponent
    or     ecx, eax
    mov    DWORD PTR _t$[ebp], ecx

    push   4
    lea    edx, DWORD PTR _t$[ebp]
    push   edx
    lea    eax, DWORD PTR _f$[ebp]
    push   eax
    call   _memcpy
    add    esp, 12
 
    fld    DWORD PTR _f$[ebp]
 
    mov    esp, ebp
    pop    ebp
    ret    0
?f@@YAMM@Z ENDP ; f

这个程序略微臃肿。如果使用优化选项/Ox 程序就不会调用memcpy(),转而直接使用变量f。不过,不启用优化选项而编译出来的代码易于理解。

如果启用GCC 4.4.1的-O3选项又会如何?

指令清单21.28 Optimizing 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, [ebp+arg_0]
       or       eax, 80000000h  ; set minus sign
       mov      edx, eax
       and      eax, 807FFFFFh  ; leave onlySign and significand 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        ; join new exponent and original value without exponent
       mov      [ebp+var_4], eax
       fld      [ebp+var_4]
       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 [esp+8]
       mov      dword ptr [esp+4], offset asc_8048610 ; "%f\n"
       mov      dword ptr [esp], 1
       call     ___printf_chk
       xor      eax, eax
       leave
       retn
main   endp

f()函数的指令几乎可以自然解释。有趣的是,尽管结构体的各个字段如同大杂烩一样复杂,但是GCC能够在编译阶段就计算出函数表达式f(1.234)的值,并且把这个结果传递给printf()函数!

21.7 练习题

21.7.1 题目1

Linux程序[10]

请参见http://beginners.re/exercises/per_chapter/struct_exercise_Linux86.tar

MIPS程序[11]

请参见http://beginners.re/exercises/per_chapter/struct_exercise_MIPS.tar

这个Linux x86程序能够打开文件并在屏幕上打印数字。请问它打印的是什么?

21.7.2 题目2

这个函数的输入变量是结构体。请尝试逆向推出结构体的各个字段,不必推敲函数的具体功能。

由MSVC 2010 /Ox 选项编译而得的代码如下所示。

指令清单21.29 Optimizing MSVC 2010

$SG2802 DB      '%f', 0aH, 00H
$SG2803 DB      '%c, %d', 0aH, 00H
$SG2805 DB      'error #2', 0aH, 00H
$SG2807 DB      'error #1', 0aH, 00H

__real@405ec00000000000 DQ 0405ec00000000000r ; 123
__real@407bc00000000000 DQ 0407bc00000000000r ; 444

_s$ = 8
_f      PROC
        push    esi
        mov     esi, DWORD PTR _s$[esp]
        cmp     DWORD PTR [esi], 1000
        jle     SHORT $LN4@f
        cmp     DWORD PTR [esi+4], 10
        jbe     SHORT $LN3@f
        fld     DWORD PTR [esi+8]
        sub     esp, 8
        fmul    QWORD PTR __real@407bc00000000000
        fld     QWORD PTR [esi+16]
        fmul    QWORD PTR __real@405ec00000000000
        faddp   ST(1), ST(0)
        fstp    QWORD PTR [esp]
        push    OFFSET $SG2802 ; ’%f’
        call    _printf
        movzx   eax, BYTE PTR [esi+25]
        movsx   ecx, BYTE PTR [esi+24]
        push    eax
        push    ecx
        push    OFFSET $SG2803 ; ’%c, %d’
        call    _printf
        add     esp, 24
        pop     esi
        ret     0
$LN3@f:
        pop     esi
        mov     DWORD PTR _s$[esp-4], OFFSET $SG2805 ; ’error #2’
        jmp     _printf
$LN4@f:
        pop     esi
        mov     DWORD PTR _s$[esp-4], OFFSET $SG2807 ; ’error #1’
        jmp     _printf
_f      ENDP

指令清单21.30 Non-optimizing Keil 6/2013 (ARM mode)

f  PROC
        PUSH    {r4-r6,lr}
        MOV     r4,r0
        LDR     r0,[r0,#0]
        CMP     r0,#0x3e8
        ADRLE   r0,|L0.140|
        BLE     |L0.132|
        LDR     r0,[r4,#4]
        CMP     r0,#0xa
        ADRLS   r0,|L0.152|
        BLS     |L0.132|
        ADD     r0,r4,#0x10
        LDM     r0,{r0,r1}
        LDR     r3,|L0.164|
        MOV     r2,#0
        BL      __aeabi_dmul
        MOV     r5,r0
        MOV     r6,r1
        LDR     r0,[r4,#8]
        LDR     r1,|L0.168|
        BL      __aeabi_fmul
        BL      __aeabi_f2d
        MOV     r2,r5
        MOV     r3,r6
        BL      __aeabi_dadd
        MOV     r2,r0
        MOV     r3,r1
        ADR     r0,|L0.172|
        BL      __2printf
        LDRB    r2,[r4,#0x19]
        LDRB    r1,[r4,#0x18]
        POP     {r4-r6,lr}
        ADR     r0,|L0.176|
        B       __2printf
|L0.132|
        POP     {r4-r6,lr}
        B       __2printf
        ENDP
|L0.140|
        DCB     "error #1\n",0
        DCB     0
        DCB     0
|L0.152|
        DCB     "error #2\n",0
        DCB     0
        DCB     0
|L0.164|
        DCB     0x405ec000
|L0.168|
        DCB     0x43de0000
|L0.172|
        DCB     "%f\n",0
|L0.176|
        DCB     "%c, %d\n",0

指令清单21.31 Non-optimizing Keil 6/2013 (Thumb mode)

f  PROC
        PUSH     {r4-r6,lr}
        MOV      r4,r0
        LDR      r0,[r0,#0]
        CMP      r0,#0x3e8
        ADRLE    r0,|L0.140|
        BLE      |L0.132|
        LDR      r0,[r4,#4]
        CMP      r0,#0xa
        ADRLS    r0,|L0.152|
        BLS      |L0.132|
        ADD      r0,r4,#0x10
        LDM      r0,{r0,r1}
        LDR      r3,|L0.164|
        MOV      r2,#0
        BL       __aeabi_dmul
        MOV      r5,r0
        MOV      r6,r1
        LDR      r0,[r4,#8]
        LDR      r1,|L0.168|
        BL       __aeabi_fmul
        BL       __aeabi_f2d
        MOV      r2,r5
        MOV      r3,r6
        BL       __aeabi_dadd
        MOV      r2,r0
        MOV      r3,r1
        ADR      r0,|L0.172|
        BL       __2printf
        LDRB     r2,[r4,#0x19]
        LDRB     r1,[r4,#0x18]
        POP      {r4-r6,lr}
        ADR      r0,|L0.176|
        B        __2printf
|L0.132|
        POP      {r4-r6,lr}
        B        __2printf
        ENDP
|L0.140|
        DCB      "error #1\n",0
        DCB      0
        DCB      0
|L0.152|
        DCB      "error #2\n",0
        DCB      0
        DCB      0
|L0.164|
        DCD      0x405ec000
|L0.168|
        DCD      0x43de0000
|L0.172|
        DCB      "%f\n",0
|L0.176|
        DCB      "%c, %d\n",0

指令清单21.32 Optimizing GCC 4.9 (ARM64)

f:
        stp     x29, x30, [sp, -32]!
        add     x29, sp, 0
        ldr     w1, [x0]
        str     x19, [sp,16]
        cmp     w1, 1000
        ble     .L2
        ldr     w1, [x0,4]
        cmp     w1, 10
        bls     .L3
        ldr     s1, [x0,8]
        mov     x19, x0
        ldr     s0, .LC1
        adrp    x0, .LC0
        ldr     d2, [x19,16]
        add     x0, x0, :lo12:.LC0
        fmul    s1, s1, s0
        ldr d0, .LC2
        fmul    d0, d2, d0
        fcvt    d1, s1
        fadd    d0, d1, d0
        bl      printf
        ldrb    w1, [x19,24]
        adrp    x0, .LC3
        ldrb    w2, [x19,25]
        add     x0, x0, :lo12:.LC3
        ldr     x19, [sp,16]
        ldp     x29, x30, [sp], 32
        b       printf
.L3:
        ldr     x19, [sp,16]
        adrp    x0, .LC4
        ldp     x29, x30, [sp], 32
        add     x0, x0, :lo12:.LC4
        b       puts
.L2:
        ldr     x19, [sp,16]
        adrp    x0, .LC5
        ldp     x29, x30, [sp], 32
        add     x0, x0, :lo12:.LC5
        b       puts
        .size   f, .-f
.LC1:
        .word   1138622464
.LC2:
        .word 0
        .word   1079951360
.LC0:
        .string "%f\n"
.LC3:
        .string "%c, %d\n"
.LC4:
        .string "error #2"
.LC5:
        .string "error #1"

指令清单21.33 Optimizing GCC 4.4.5 (MIPS) (IDA)

f:

var_10          = -0x10 
var_8           =-8 
var_4           =-4

                lui     $gp, (__gnu_local_gp >> 16)
                addiu   $sp, -0x20
                la      $gp, (__gnu_local_gp & 0xFFFF)
                sw      $ra, 0x20+var_4($sp)
                sw      $s0, 0x20+var_8($sp)
                sw      $gp, 0x20+var_10($sp)
                lw      $v0, 0($a0)
                or      $at, $zero
                slti    $v0, 0x3E9
                bnez    $v0, loc_C8
                move    $s0, $a0
                lw      $v0, 4($a0)
                or      $at, $zero
                sltiu   $v0, 0xB
                bnez    $v0, loc_AC
                lui     $v0, (dword_134 >> 16)
                lwc1    $f4, $LC1
                lwc1    $f2, 8($a0)
                lui     $v0, ($LC2 >> 16)
                lwc1    $f0, 0x14($a0)
                mul.s   $f2, $f4, $f2
                lwc1    $f4, dword_134
                lwc1    $f1, 0x10($a0)
                lwc1    $f5, $LC2
                cvt.d.s $f2, $f2
                mul.d   $f0, $f4, $f0
                lw      $t9, (printf & 0xFFFF)($gp)
                lui     $a0, ($LC0 >> 16)  # "%f\n"
                add.d   $f4, $f2, $f0
                mfc1    $a2, $f5
                mfc1    $a3, $f4
                jalr    $t9
                la      $a0, ($LC0 & 0xFFFF)  # "%f\n"
                lw      $gp, 0x20+var_10($sp)
                lbu     $a2, 0x19($s0)
                lb      $a1, 0x18($s0)
                lui     $a0, ($LC3 >> 16)  # "%c, %d\n"
                lw      $t9, (printf & 0xFFFF)($gp)
                lw      $ra, 0x20+var_4($sp)
                lw      $s0, 0x20+var_8($sp)
                la      $a0, ($LC3 & 0xFFFF)  # "%c, %d\n"
                jr      $t9
                addiu   $sp, 0x20
loc_AC:                                 # CODE XREF: f+38
                lui     $a0, ($LC4 >> 16)  # "error #2"
                lw      $t9, (puts & 0xFFFF)($gp)
                lw      $ra, 0x20+var_4($sp)
                lw      $s0, 0x20+var_8($sp)
                la      $a0, ($LC4 & 0xFFFF)  # "error #2"
                jr      $t9
                addiu   $sp, 0x20
loc_C8:                                 # CODE XREF: f+24
                lui     $a0, ($LC5 >> 16)  # "error #1"
                lw      $t9, (puts & 0xFFFF)($gp)
                lw      $ra, 0x20+var_4($sp)
                lw      $s0, 0x20+var_8($sp)
                la      $a0, ($LC5 & 0xFFFF)  # "error #1"
                jr      $t9
                addiu   $sp, 0x20

$LC0:           .ascii "%f\n"<0>
$LC3:           .ascii "%c, %d\n"<0>
$LC4:           .ascii "error #2"<0>
$LC5:           .ascii "error #1"<0>

                .data # .rodata.cst4
$LC1:           .word 0x43DE0000

                .data # .rodata.cst8
$LC2:           .word 0x405EC000
dword_134:      .word 0

[1] 又称为“异构容器/heterogeneous container”。

[2] https://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx。

[3] https://msdn.microsoft.com/en-us/library/ee488017.aspx。

[4] 为了便于演示,作者略微调整了date的返回结果。在实际情况下,手动输入GDB指令的速度不会那么快,多次操作的返回结果不可能分毫不差。

[5] 请参见Dennis M. Ritchie撰写的<< The development of the c language>>,“SIGPLAN Not”的第28章第3节:201-208页,您也可以在作者的网站下载:http://yurichev.com/mirrors/C/ dmr-The%20Development%20of%20the%20C%20Language-1993.pdf.。

[6] 更多内容请参见https://en.wikipedia.org/wiki/Data_structure_alignment。

[7] 请参阅https://msdn.microsoft.com/en-us/library/ms253935.aspx和https://gcc.gnu.org/ onlinedocs/gcc/Structure-Packing-Pragmas.html。

[8] 请参见http://en.wikipedia.org/wiki/CPUID。

[9] 请参见http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html。

[10] GCC 4.8.1 -O3。

[11] GCC 4.4.5-O3。