前言:《***軟體程式設計規範》中提到:「在定義結構資料型別時,為了提高系統效率,要注意4位元組對齊原則……」。本文解釋x86上位元組對齊的機制,其他架構讀者可自行試驗。同時,本文對c/c++的函式呼叫方式進行了討論。
感謝幾位同事。以及carrot。呵呵……
下面言歸正傳。
1.先看下面的例子:
struct aa;
struct bb;
結構a沒有遵守位元組對齊原則(為了區分,我將它叫做對齊聲明原則),結構b遵守了。我們來看看在x86上會出現什麼結果。先列印出a和b的各個成員的位址。會看到a中,各個成員間的間距是4個位元組。b中,i和j,j和s都間距4個位元組,但是s和c1間距2個位元組。所以:
sizeof(a) = 16
sizeof(b) = 12
為什麼會有這樣的結果呢?這就是x86上位元組對齊的作用。為了加快程式執行的速度,一些體系結構以對齊的方式設計,通常以字長作為對齊邊界。對於一些結構體變數,整個結構要對齊在內部成員變數最大的對齊邊界,如b,整個結構以4為對齊邊界,所以sizeof(b)為12,而不是11。
對於a來講,雖然宣告的時候沒有對齊,但是根據列印出的位址來看,編譯器已經自動為其對齊了,所以每個成員的間距是4。在x86下,宣告a與b唯一的差別,僅在於a多浪費了4個位元組記憶體。(是不是某些特定情況下,b比a執行更快,這個還需要討論。比如緊挨的兩條分別取s和c1的指令)
如果體系結構是不對齊的,a中的成員將會乙個挨乙個儲存,從而sizeof(a)為11。顯然對齊更浪費了空間。那麼為什麼要使用對齊呢?
體系結構的對齊和不對齊,是在時間和空間上的乙個權衡。對齊節省了時間。假設乙個體繫結構的字長為w,那麼它同時就假設了在這種體系結構上對寬度為w的資料的處理最頻繁也是最重要的。它的設計也是從優先提高對w位資料操作的效率來考慮的。比如說讀寫時,大多數情況下需要讀寫w位資料,那麼資料通道就會是w位。如果所有的資料訪問都以w位對齊,那麼訪問還可以進一步加快,因為需要傳輸的位址位減少,定址可以加快。大多數體系結構都是按照字長來對齊訪問資料的。不對齊的時候,有的會出錯,比如mips上會產生bus error,而x86則會進行多次訪問來拼接得到的結果,從而降低執行效率。
有些體系結構是必須要求對齊的,如sparc,mips。它們在硬體的設計上就強制性的要求對齊。不是因為它們作不到對齊的訪問,而是它們認為這樣沒有意義。它們追求的是速度。
上面講了體系結構的對齊。在ia-32上面,sizeof(a)為16,就是對齊的結果。下面我們來看,為什麼變數宣告的時候也要盡量對齊。
我們看到,結構a的宣告並不對齊,但是它的成員位址仍是以4為邊界對齊的(成員間距為4)。這是編譯器的功勞。因為我所用的編譯器gcc,預設是對齊的。而x86可以處理不對齊的資料訪問,所以這樣宣告程式並不會出錯。但是對於其他結構,只能訪問對齊的資料,而編譯器又不小心設定了不對齊的選項,則**就不能執行了。如果按照b的方式宣告,則不管編譯器是否設定了對齊選項,都能夠正確的訪問資料。
目前的開發普遍比較重視效能,所以對齊的問題,有三種不同的處理方法:
1) 採用b的方式宣告
2) 對於邏輯上相關的成員變數希望放在靠近的位置,就寫成a的方式。有一種做法是顯式的插入reserved成員:
struct aa;
3) 隨便怎麼寫,一切交給編譯器自動對齊。
**中關於對齊的隱患,很多是隱式的。比如在強制型別轉換的時候。下面舉個例子:
unsigned int ui_1=0x12345678;
unsigned char *p=null;
unsigned short *us_1=null;
p=&ui_1;
*p=0x00;
us_1=(unsigned short *)(p+1);
*us_1=0x0000;
最後兩句**,從奇數邊界去訪問unsigned short型變數,顯然不符合對齊的規定。在x86上,類似的操作只會影響效率,但是在mips或者sparc上,可能就是乙個bus error(我沒有試)。
有些人喜歡通過移動指標來操作結構中的成員(比如在linux操作struct sk_buff的成員),但是我們看到,a中(&c1+1) 決不等於&i。不過b中(&s+2)就是 &c1了。所以,我們清楚了結構中成員的存放位置,才能編寫無錯的**。同時切記,不管對於結構,陣列,或者普通的變數,在作強制型別轉換時一定要多看看:)不過為了不那麼累,還是遵守宣告對齊原則吧!(這個原則是說變數盡量宣告在它的對齊邊界上,而且在節省空間的基礎上)
2.c/c++函式呼叫方式
我們當然早就知道,c/c++中的函式呼叫,都是以值傳遞的方式,而不是引數傳遞。那麼,值傳遞是如何實現的呢?
函式呼叫前的典型彙編碼如下:
push %eax
call 0x401394
add $0x10,%esp
首先,入棧的是實參的位址。由於被調函式都是對位址進行操作,所以就能夠理解值傳遞的原理和引數是引用時的情況了。
call ***, 是要呼叫函式了,後面的位址,就是函式的入口位址。call指令等價於:
push ip
jmp ***
首先把當前的執行位址ip壓棧,然後跳轉到函式執行。
執行完後,被調函式要返回,就要執行ret指令。ret等價於pop ip,恢復call之前的執行位址。所以一旦使用call指令,堆疊指標sp就會自動減2,因為ip的值進棧了。
函式的引數進棧的順序是從右到左,這是c與其它語言如pascal的不同之處。函式呼叫都以以下語句開始:
push %ebp
mov %esp,%ebp
首先儲存bp的值,然後將當前的堆疊指標傳遞給bp。那麼現在bp+2就是ip的值(16位register的情況),bp+4放第乙個引數的值,bp+6放第二個引數……。函式在結束前,要執行pop bp。
c/c++語言預設的函式呼叫方式,都是由主呼叫函式進行引數壓棧並且恢復堆疊,實參的壓棧順序是從右到左,最後由主調函式進行堆疊恢復。由於主呼叫函式管理堆疊,所以可以實現變參函式。
對於winapi和callback函式,在主呼叫函式中負責壓棧,在被呼叫函式中負責彈出堆疊中的引數,並且負責恢復堆疊。因此不能實現變參函式。
(哪位對編譯原理和編譯器比較了解的,可以將這個部分寫完善,謝謝。可以加入編譯時的處理。不然只有等偶繼續學習了)
位元組對齊和C C 函式呼叫方式學習總結
前言 軟體程式設計規範 中提到 在定義結構資料型別時,為了提高系統效率,要注意4位元組對齊原則 本文解釋x86上位元組對齊的機制,其他架構讀者可自行試驗。同時,本文對c c 的函式呼叫方式進行了討論。1 先看下面的例子 view code cpp 123 4567 891011 1213struct...
位元組對齊(c c ) (僅供學習參考)
概述 對於結構體 位元組對齊準則 1 結構體變數的首位址能夠被其最寬基本型別成員大小所整除 2 結構體每個成員相對於結構體首位址的偏移量 offset 都是該成員大小的整數倍,如有需要,編譯器會在成員之間加上中間填充位元組 3 結構體總大小為結構體最寬基本型別成員大小的整數倍,如有需要,編譯器會在最...
C C 函式呼叫方式
cdecl 是c declaration的縮寫 declaration,宣告 表示c語言預設的函式呼叫方法 所有引數從右到左依次入棧,這些引數由呼叫者清除,稱為手動清棧。被呼叫函式不會要求呼叫者傳遞多少引數,呼叫者傳遞過多或者過少的引數,甚至完全不同的引數都不會產生編譯階段的錯誤。stdcall 是...