函式呼叫與空間分配

劉小緒同學發表於2018-11-18

    我們在程式設計序的時候,都會把某一個特定功能封裝在一個函式裡面,對外暴露一個介面,而隱藏了函式行為的具體實現,一個大型的複雜系統裡面包含了很多這樣的小函式,我們稱之為過程

    過程是相對獨立的小模組,系統的執行需要這些過程的緊密合作,這種合作就是函式呼叫。

    在一個函式執行時呼叫別的函式,比如 P 呼叫 Q,需要執行一些特定的動作。傳遞控制,在呼叫 Q 之前,控制權在 P 的手裡,既然要呼叫 Q,那麼就需要把控制權交給 Q;傳遞資料,就是函式傳參;分配與釋放記憶體,在開始時,Q 可能需要位區域性變數分配空間,結束時又必須釋放這些儲存空間。

    大多數語言都使用棧提供的先進後出機制來管理記憶體,x86-64 可以通過通用暫存器傳遞最多 6 個整數值(整數或地址),如果超過 6 個,那就需要在棧中分配記憶體,並且通過棧傳遞引數時,所有資料的大小都要向 8 的倍數對齊。將控制權從 P 轉交給 Q,只需要將 PC(程式計數器)的值置為 Q 程式碼的起始位置,並記錄好 P 執行的位置,方便 Q 執行完了,繼續執行 P 剩餘的程式碼。

    在函式的傳參、執行中,多多少少都需要空間來儲存變數,區域性資料能儲存在暫存器中就會儲存在暫存器中,如果暫存器不夠,將會儲存在記憶體中。除了暫存器不夠用的情況,還有陣列、結構體和地址等區域性變數都必須儲存在記憶體中。分配記憶體很簡單,只需要減小棧指標的值就行了,同樣釋放也只需要增加棧指標。

    在函式執行過程中,處理棧指標%rsp,其它暫存器都被分類為被呼叫者儲存暫存器,即當過程 P 呼叫過程 Q 時,Q 必須儲存這些暫存器的值,保證它們的值在 Q 返回到 P 時與 Q 被呼叫時是一樣的。

    所以遞迴也就不難理解了,初學演算法總覺得遞迴有點奇妙,怎麼自己呼叫自己,而實際上對於計算機來說,它和呼叫其它函式沒什麼區別,在計算機眼裡,沒有自身與其它函式的區別,所有被呼叫者都是其它人。

    陣列是程式設計中不可或缺的一種結構,“陣列是分配在連續的記憶體中”這句話已經爛熟於心了,歷史上,C 語言只支援大小在編譯時就能確定的多維陣列,這個多多少少有一些不便利,所以在ISO C99標準中就引入了新的功能,允許陣列的維度是表示式。

int A[expr1][expr2]

    因為陣列是連續的記憶體,所以很容易就能訪問到指定位置的元素,它通過首地址加上偏移量即可計算出對應元素的地址,這個偏移量一定意義上就是由索引給出。

    比如現在有一個陣列A,那麼A[i]就等同於表示式* (A + i),這是一個指標運算。C 語言的一大特性就是指標,既是優點也是難點,單操作符&*可以產生指標和簡介引用指標,也就是,對於一個表示某個物件的表示式expr&expr給出該物件地址的一個指標,而對於一個表示地址的表示式Aexpr*Aexpr給出該地址的值。

    即使我們建立巢狀(多維)陣列,上面的一般原則也是成立的,比如下面的例子。

int A[5][3];

// 上面宣告等價於下面
typedef int row3_t[3];
row3_t A[5];

    這個陣列在記憶體的中就是下面那個樣子的。

image

    還有一個重要的概念叫做資料對齊,即很多計算機系統要求某種型別的物件的地址必須是某個值 K(一般是2、4 或 8)的倍數,這種限制簡化了處理器和記憶體介面之間的設計,甚至有的系統沒有進行資料對齊,程式就無法正常執行。

    比如現在有一個如下的結構體。

struct S1 {
    int i;
    char c;
    int j;
}

    如果編譯器用最小的 9 位元組分配,那麼將是下面的這個樣子。

image

    但是上面這種結構無法滿足 i 和 j 的 4 位元組對齊要求,所以編譯器會在 c 和 j 之間插入 3 個位元組的間隙。

image

    在極客時間專欄中有這樣一段程式碼。

int main(int argc, char *argv[]){
    int i = 0;
    int arr[3] = {0};
    for(; i <= 3; i++){
        arr[i] = 0;
        printf("Hello world!\n");
    }
    return 0;
}

    這段程式碼神奇的是在某種情況下會一直迴圈的輸出Hello world,並不會結束,在計算機系統漫遊(補充)中也提到過。

    造成上面這種結果是因為函式體內的區域性變數存在棧中,並且是連續壓棧,而 Linux 中棧又是從高向低增長。陣列arr中是 3 個元素,加上 i 是 4 個元素,剛好滿足 8 位元組對齊(編譯器 64 位系統下預設會 8 位元組對齊),變數i在陣列arr之前,即i的地址與arr相鄰且比它大。

    程式碼中很明顯訪問陣列時越界了,當i為 3 時,實際上正好訪問到變數i的地址,而迴圈體中又有一句arr[i] = 0;,即又把i的值設定為了 0,由此就導致了死迴圈。

相關文章