【CSAPP筆記】8. 匯編語言——數據存儲


下面介紹一些C語言中常見的特殊的數據存儲方式,以及它們在匯編語言中是如何表示的。

數組

數組是一種將標量數據聚集成更大數據類型的方式。實現數組的方式其實十分簡單,也非常容易翻譯成機器代碼。C語言的一個特點是可以產生指向數組元素的指針,然后可以對這些指針進行運算。

數組的基本原則如下:

對於數組的聲明:T A[N];,這句語句有兩個效果。首先,它在存儲器中分配一個 L * N 個字節的連續區域,L 是數據類型 T 單位為字節的大小,用 $ X_A $ 來表示起始位置。其次,它引入標識符 A ,可以用 A 作為指向數組開頭的指針,指針的值就是$ X_A $。

匯編代碼:假設 E 是一個 int 型數組,我們想計算 E[i],數組的地址存儲在寄存器 %edx 中, 下標 i 存放在寄存器 %ecx 中。下列指令會計算地址 $ X_E + 4i $,讀取這個存儲器的值,然后將結果放到寄存器 %eax 中。

movl    (%edx, %ecx, 4), %eax

指針運算

C語言允許對指針進行運算,計算的過程中,值會根據該指針引用的類型的大小進行伸縮(指針類型的作用)。下面這些例子給出一些關於數組 E 的表達式,以及每個表達式的匯編代碼實現。

表達式 結果類型 匯編代碼
E int * $ X_E $ movl %edx, %eax
E[0] int $ M[X_E] $ movl (%edx), %eax
E[i] int $ M[X_E + 4i] $ movl (%edx, %ecx, 4), %eax
&E[2] int * $ X_E + 8 $ leal 8(%edx), %eax
E+i-1 int * $ X_E + 4i - 4 $ leal -4(%edx, %ecx, 4), %eax
*(E+i-3) int $ M[X_E + 4i - 12] $ movl -12(%edx, %ecx, 4), %eax
&E[i]-E int i movl %ecx, %eax

嵌套數組

當我們創建數組的數組時,數組的分配和引用的一般原則也是成立的。例如有聲明:

int A[5][3];

則相當於下面的聲明:

typedef int row3_t[3];
row3_5 A[5];

A 數組是一個包含 5 個元素的數組,每一個元素是一個含有 3 個整數的數組。也可以看成是一個 5 行 3 列的二維數組。用 A[0][0]A[4][2] 來引用。二維數組是以行優先的順序來排列的。第 0 行的所有元素,可以寫作 A[0]

對於數組 T[R][C]; 來說,他的元素 T[i][j] 的存儲器地址為:$ X_D + L(C * i + j) $

異質的數據結構

C語言提供了兩種結合不同的對象來創建數據類型的機制:結構(struct),將多個對象集合到一個單位中;聯合(union),允許用幾種不同的類型引用一個對象。

結構體

C語言的 struct 創建一種數據類型,將可能不同的數據類型的對象聚合到一個對象中。結構體中各個組成部分由名字來引用。類似於數組的實現,結構體的所有組成部分都存放在存儲器中的一段連續的區域內,指向結構體的指針就是結構體第一個字節的地址。編譯器維護每個結構類型的信息,指示每個字段(field)的字節偏移。以這些偏移作為存儲器引用中指令的位移,從而產生對結構體元素的引用。

假設有下面的結構體聲明:

struct rec{
int i;
int j;
int a[3];
int *p;
};

那么在內存中的排列是:

為了訪問字段,編譯器產生的代碼要將結構體的地址加上適當的偏移。例如指向結構體 rec 的指針變量 r 存放在寄存器 %edx 中,下面的代碼將元素 r->i 復制到 r->j

movl    (%edx), %eax        # 將 r->i 放到寄存器中
movl %eax, 4(%edx) # 將寄存器的值復制到 r->j 中

可以看到,為了訪問字段 j,代碼將 r 的地址加上偏移量 4。

聯合體

聯合體提供了一種方式,能夠規避C語言的類型系統,允許以多種類型來引用同一個對象。聯合體的語法和結構體一樣,但語義差別很大——聯合體用不同的字段來引用相同的內存

考慮下面的聲明:

struct S{
char c;
int i[2];
double v;
}

union U{
char c;
int i[2];
double v;
}

在一台 IA32 Linux 機器上編譯時,字段的偏移量、數據類型的完整大小如下:

類型 偏移量:c i v 大小
S 0 4 12 20
U 0 0 0 8

一個聯合體的總的大小總是等於它的最大字段的大小。對於類型結構體 U 的指針 p,p->cp->i[0]p->v 都是引用數據結構的起始位置

在一些上下文中,聯合體十分有用,但是它也引起一些討厭的錯誤,因為它繞過了C語言類型系統提供的安全措施。

舉一個聯合體應用的具體例子:現在我們要設計一個特殊的二叉樹的數據結構,它有一個特點:每個葉子節點有一個 double 類型的值。每個內部節點有左右兒子的指針,但是沒有數據。如果聲明如下:

struct Node{
struct Node * left;
struct Node * right;
double data;
};

如果用上述結構體,那么每個節點都需要 16 個字節。然而,葉子結點是沒有左右兒子的,內部節點是用不着 double data 的,所以每個類型的節點都要浪費一半的內存。如果我們這樣聲明:

union Node{
struct
{
union Node *left;
union Node *right;
}internal;
double data;
};

那么每個節點就只需要 8 個字節。如果 n 是一個指向 union Node 的指針,那么可以用 n->data 來表示葉子結點的數據,n->internal.leftn->internal.right來表示內部節點的左右兒子。

然而這樣表示,我們是沒辦法確定一個給定的節點到底是葉子節點,還是內部節點。有一個方法,是引入一個枚舉類型,定義這個聯合中可能的不同選擇,再用一個結構體將兩者包含起來:

typedef enum {LEAF, INTERNAL} nodetype;

struct Node_T{
nodetype type;
union Node{
struct
{
union Node *left;
union Node *right;
}internal;
double data;
}info;
};

可以發現,這樣的聯合體只需要12個字節。

相對於編碼帶來的麻煩問題,其實使用聯合體帶來的節省是很小的。除非說對於有較多字段的數據結構,結構體帶來的節省就會更加吸引人。

數據對齊

struct S{
char c;
int i[2];
double v;
};
類型 偏移量:c i v 大小
S 0 4 12 20

為什么 i 的偏移量不是 1 呢?char 類型的大小不是只有 1 個字節嗎?

要解答這個問題,我們必須對數據對齊有一定的了解。計算機系統對於基本的數據類型的合法地址做出了一定的限制,要求地址必須是某個值的倍數(例如4、8、16)。這種對齊限制簡化了處理器系統和存儲器系統之間的接口硬件設計。

對於上面那個結構體,畫出來的圖是這樣的:

它不可能滿足對齊要求。假設我們的對齊要求是 4 字節對齊的話,編譯器會在字段 c 之后插入一個 3 字節的間隙(用灰色表示),對齊后的結構體如下:

可以看到后面的字段的偏移量發生了改變,整個結構體的大小變成了 20 字節。

另外,編譯器結構的末尾可能為了滿足對齊要求而需要一些填充。這樣,結構體數組的每個元素都能滿足對齊要求。例如將上述結構體稍作修改:

struct S{
int i[2];
double v;
char c;
};

那么編譯器為了使結構體數組的每個元素都滿足對齊要求,不得不在末尾插入多余的 3 個字節。

編譯這段代碼,可以發現,這個編譯器的對齊要求應該是為 8 的倍數。了解到對齊方式之后,我們在寫結構體的時候,注意一下元素的排列順序,可以更有效地利用內存

參考資料


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com