位元組對齊和C C 函式呼叫方式學習總結

2021-09-30 02:25:38 字數 3030 閱讀 2122

前言:《***軟體程式設計規範》中提到:「在定義結構資料型別時,為了提高系統效率,要注意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 是...