# 第18章 結構體
C/C++的結構體可以這么定義:它是一組存儲在內存中的變量的集合,成員變量類型不要求相同。
## 18.1 SYSTEMTIME 的例子
讓我們看看Win32結構體SYSTEMTIME的定義:
清單18.1: WinBase.h
```
#!cpp
typedef struct _SYSTEMTIME {
WORD wYear;
WORD wMonth;
WORD wDayOfWeek;
WORD wDay;
WORD wHour;
WORD wMinute;
WORD wSecond;
WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;
```
讓我們寫一個獲取當前時間的C程序:
```
#!cpp
#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
```
#!bash
_t$ = -16 ; size = 16
_main PROC
push ebp
mov ebp, esp
sub esp, 16 ; 00000010H
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 ; 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結構體,代碼如下:
```
#!cpp
#include <windows.h>
#include <stdio.h>
void main()
{
WORD array[8];
GetSystemTime (array);
printf ("%04d-%02d-%02d %02d:%02d:%02d
",
array[0] /* wYear */, array[1] /* wMonth */, array[3] /* wDay */,
array[4] /* wHour */, array[5] /* wMinute */, array[6] /* wSecond */);
return;
};
```
編譯器會稍稍給出一點警告:
```
#!cpp
systemtime2.c(7) : warning C4133: ’function’ : incompatible types - from ’WORD [8]’ to ’LPSYSTEMTIME’
```
不過至少,它會產生如下代碼:
清單18.3: MSVC 2010
```
#!bash
$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$[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 ; 0000001cH
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
```
當然,它也能一樣正常工作! 一個很有趣的情況是這兩次編譯結果居然一樣,所以光看編譯結果,我們還看不出來到底別人用的結構體還是單單用的變量數組。 不過,沒幾個人會這么無聊的用這種方法寫代碼,因為這太麻煩了。還有,結構體也有可能會被開發者更改,交換,等等,所以還是用結構體方便。
## 18.2 讓我們用malloc()為結構體分配空間
但是,有時候把結構體放在堆中而不是棧上卻更簡單:
```
#!cpp
#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
```
#!bash
_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 [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 ; 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數組來重現這個結構:
```
#!cpp
#include <windows.h>
#include <stdio.h>
void main()
{
WORD *t;
t=(WORD *)malloc (16);
GetSystemTime (t);
printf ("%04d-%02d-%02d %02d:%02d:%02d
",
t[0] /* wYear */, t[1] /* wMonth */, t[3] /* wDay */,
t[4] /* wHour */, t[5] /* wMinute */, t[6] /* wSecond */);
free (t);
return;
};
```
我們得到:
```
#!bash
$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 [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 ; 00000020H
xor eax, eax
pop esi
ret 0
_main ENDP
```
同樣,我們可以看到編譯結果和之前一樣。個人重申一次,你不應該在寫代碼的時候用這么晦澀的方法來表達它。
## 18.3 結構體tm
### 18.3.1 linux
在Linux下,我們看看time.h中的tm結構體是什么樣子的:
```
#!cpp
#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
```
#!bash
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
"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+20h] ; tm_mon
mov eax, offset aMonthD ; "Month: %d
"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+1Ch] ; tm_mday
mov eax, offset aDayD ; "Day: %d
"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+18h] ; tm_hour
mov eax, offset aHourD ; "Hour: %d
"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+14h] ; tm_min
mov eax, offset aMinutesD ; "Minutes: %d
"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+10h]
mov eax, offset aSecondsD ; "Seconds: %d
"
mov [esp+4], edx ; tm_sec
mov [esp], eax
call printf
leave
retn
main endp
```
可是,IDA并沒有為本地棧上變量建立本地變量名。但是因為我們已經學了匯編了,我們也不需要在這么簡單的例子里面如此依賴它。
請也注意一下lea edx, [eax+76ch],這個指令把eax的值加上0x76c,但是并不修改任何標記位。請也參考LEA的相關章節(B.6.2節)
為了表現出結構體只是一個個的變量連續排列的東西,讓我們重新測試一下這個例子,我們看看time.h: 清單18.7 time.h
```
#!cpp
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
```
#!bash
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
```
#!bash
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
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
"
call printf
mov eax, [esp+30h+tm_mon]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aMonthD ; "Month: %d
"
call printf
mov eax, [esp+30h+tm_mday]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aDayD ; "Day: %d
"
call printf
mov eax, [esp+30h+tm_hour]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aHourD ; "Hour: %d
"
call printf
mov eax, [esp+30h+tm_min]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aMinutesD ; "Minutes: %d
"
call printf
mov eax, [esp+30h+tm_sec]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], 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模式
```
#!bash
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
"
BL __2printf
LDR R1, [SP,#0x38+var_28]
ADR R0, aMonthD ; "Month: %d
"
BL __2printf
LDR R1, [SP,#0x38+var_2C]
ADR R0, aDayD ; "Day: %d
"
BL __2printf
LDR R1, [SP,#0x38+var_30]
ADR R0, aHourD ; "Hour: %d
"
BL __2printf
LDR R1, [SP,#0x38+var_34]
ADR R0, aMinutesD ; "Minutes: %d
"
BL __2printf
LDR R1, [SP,#0x38+var_38]
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()這些庫函數的參數類型),所以他把這里的結構變量的名字也顯示出來了。
```
#!bash
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
"
ADD R0, PC ; char *
ADDW R1, R1, #0x76C
BLX _printf
LDR R1, [SP,#0x38+var_34.tm_mon]
MOV R0, 0xF3A ; "Month: %d
"
ADD R0, PC ; char *
BLX _printf
LDR R1, [SP,#0x38+var_34.tm_mday]
MOV R0, 0xF35 ; "Day: %d
"
ADD R0, PC ; char *
BLX _printf
LDR R1, [SP,#0x38+var_34.tm_hour]
MOV R0, 0xF2E ; "Hour: %d
"
ADD R0, PC ; char *
BLX _printf
LDR R1, [SP,#0x38+var_34.tm_min]
MOV R0, 0xF28 ; "Minutes: %d
"
ADD R0, PC ; char *
BLX _printf
LDR R1, [SP,#0x38+var_34]
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 結構體的成員封裝
結構體做的一個重要的事情就是封裝了成員,讓我們看看簡單的例子:
```
#!bash
#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
編譯后得到:
```
#!bash
_s$ = 8 ; size = 16
?f@@YAXUs@@@Z PROC ; f
push ebp
mov ebp, esp
mov eax, DWORD PTR _s$[ebp+12]
push eax
movsx ecx, BYTE PTR _s$[ebp+8]
push ecx
mov edx, DWORD PTR _s$[ebp+4]
push edx
movsx eax, BYTE PTR _s$[ebp]
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]代表結構體邊界值為n字節)來編譯它:
清單18.12: MSVC /Zp1
```
#!bash
_TEXT SEGMENT
_s$ = 8 ; size = 10
?f@@YAXUs@@@Z PROC ; f
push ebp
mov ebp, esp
mov eax, DWORD PTR _s$[ebp+6]
push eax
movsx ecx, BYTE PTR _s$[ebp+5]
push ecx
mov edx, DWORD PTR _s$[ebp+1]
push edx
movsx eax, BYTE PTR _s$[ebp]
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
```
#!cpp
#include "pshpack1.h"
```
和這個:
清單18.14:WINNT.H
```
#!cpp
#include "pshpack4.h" // 4 byte packing is the default
```
文件PshPack1.h看起來像
清單18.15: PSHPACK1.H
```
#!bash
#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
```
#!bash
.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
"
.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模式
```
#!bash
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, [SP,#0xC+var_C] ; 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 嵌套結構
如果一個結構體里定義了另一個結構體會怎么樣?
```
#!cpp
#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
```
#!bash
_s$ = 8 ; size = 24
_f PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _s$[ebp+20] ; e
push eax
movsx ecx, BYTE PTR _s$[ebp+16] ; d
push ecx
mov edx, DWORD PTR _s$[ebp+12] ; c.b
push edx
mov eax, DWORD PTR _s$[ebp+8] ; c.a
push eax
mov ecx, DWORD PTR _s$[ebp+4] ; b
push ecx
movsx edx, BYTE PTR _s$[ebp] ;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中。

MSVC 2010有CPUID的宏,但是GCC 4.4.1沒有,所以,我們就手動的利用它的內聯匯編器為GCC寫一個吧。
```
#!cpp
#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
", 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
```
#!bash
_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$[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 ; 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
```
#!cpp
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
"
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
"
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
"
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
"
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
"
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在調用每個printf()之前會把extended_model_id和extended_family_id的計算聯合到一塊去,而不是把它們分開計算。
### 18.6.2 將浮點數當作結構體看待
我們已經在FPU(15章)中注意到了float和double兩個類型都是有符號的,他們分為符號、有效數字和指數部分。但是我們能直接用上這些位嘛?讓我們試一試float。

```
#!cpp
#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
```
#!bash
_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 ; 0000000cH
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
or ecx, eax ; add original value without exponent with new calculated exponent
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 ; 0000000cH
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選項會怎么做?
清單18.22: Gcc 4.4.1
```
#!bash
; 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 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 [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
"
mov dword ptr [esp], 1
call ___printf_chk
xor eax, eax
leave
retn
main endp
```
F()函數基本可以理解,但是有趣的是,GCC可以在編譯階段就通過我們這堆大雜燴一樣的代碼計算出f(1.234)的值,從而會把他當作參數直接給printf()。
- 第一章 CPU簡介
- 第二章 Hello,world!
- 第三章? 函數開始和結束
- 第四章 棧
- Chapter 5 printf() 與參數處理
- Chapter 6 scanf()
- CHAPER7 訪問傳遞參數
- Chapter 8 一個或者多個字的返回值
- Chapter 9 指針
- Chapter 10 條件跳轉
- 第11章 選擇結構switch()/case/default
- 第12章 循環結構
- 第13章 strlen()
- Chapter 14 Division by 9
- chapter 15 用FPU工作
- Chapter 16 數組
- Chapter 17 位域
- 第18章 結構體
- 19章 聯合體
- 第二十章 函數指針
- 第21章 在32位環境中的64位值
- 第二十二章 SIMD
- 23章 64位化
- 24章 使用x64下的SIMD來處理浮點數
- 25章 溫度轉換
- 26章 C99的限制
- 27章 內聯函數
- 第28章 得到不正確反匯編結果
- 第29章 花指令
- 第30章 16位Windows
- 第31章 類
- 三十二 ostream