系統級程序設計筆記(中)
來自專欄 文卿的學習筆記
該學習筆記來自於對武漢大學計算機學院軟體工程專業大三上學期的專業必修課《系統級程序設計》的學習,這門必修課對應卡內基梅隆大學的ssd6課程,教材主要為深入理解計算機系統CSAPP,涉及的編程語言全部為C語言和C++語言。
本篇文章包括第2單元和第4單元的學習筆記,其中第2單元的主要內容是堆棧原理的介紹、指針和數組、變數和地址等,部分內容來自《深入理解計算機系統》的第三章的部分內容,部分內容來自《C專家編程》。對應ssd6課程的lecture3;第4單元的主要內容是對堆棧的再認識、動態內存分配、堆的介紹、隱式空閑鏈表、垃圾回收、C語言中與內存有關的常見錯誤等,部分內容來自《深入理解計算機系統》的第三章第七節的內容和第九章第九節的內容。對應ssd6課程的lecture5。
unit2——程序的機器級表示
1.The Wonder of Program Execution
(1)觀察下面的代碼
#define ARRAY_SIZE 10void natural_numbers (void) { int i; int array[ARRAY_SIZE]; i = 1; while (i <= ARRAY_SIZE) { array[i] = i - 1; i = i + 1; }}
該代碼會陷入一種死循環的狀態,這是因為在內存中,局部變數的位置。數組a[9]結束後,是變數i,因此,對a[10]進行操作時,實際上是對變數i進行操作,所以變數i的值被修改為9,再次滿足進入循環的條件。i的值一直在9和10之間擺動,while條件總成立,造成了死循環。
(2)C語言中的抽象層次
C編程模型本身就是抽象的,體現在以下的方面: 使用變數名,而不是直接通過地址值進行訪問變數。 使用array[i]的形式訪問數組元素,而不需要自己計算元素的地址。 使用c=a+b,這樣的代碼實現加法,而不需要直接向CPU下達命令編譯器負責將C程序翻譯成機器代碼。我們考慮變數和數據類型,而不是內存晶元。
我們考慮實現的演算法,而不是在這些晶元之間移動數據。 我們考慮程序語句,而不是存儲這些語句的位置和方式。 我們不太考慮執行過程,因為沒有必要 。計算機系統使用了多種不同形式的抽象,利用更簡單的抽象模型來隱藏實現的細節。對於機器級編程來說,兩種抽象很重要,第一種是由指令集體系結構(Instruction Set Architecture,ISA)來定義機器級程序的格式和行為,第二種抽象是機器級程序使用的內存地址是虛擬地址,提供的內存模型看上去是一個非常大的位元組數組。
(3)對以下代碼的解釋:
#include <stdio.h>#include <string.h>#define MAXLINE_LENGTH 80char Buffer[MAXLINE_LENGTH];char * readString(void){ int nextInChar; int nextLocation; printf("Input> "); nextLocation = 0; while ((nextInChar = getchar()) !=
&& nextInChar != EOF) { Buffer[nextLocation++] = nextInChar; } return Buffer;}int main(int argc, char * argv[]){ char * newString; do { newString = readString(); printf("%s
", newString); } while (strncmp(newString, "exit", 4)); return 0;}
這段代碼並不能達到預期的效果,這是因為Buffer變數在這裡是全局的。在接連讀取的情況下。第二次讀取並不會清空第一次讀取中的數據。如果第二次讀取的數據比第一次短,那麼第一次比第二次多的那部分仍然會留在Buffer之中。例如
Input>Hello
Hello Input>Hello world Hello world Input>Hello Hello world
可以看到第三次輸入的是hello,但是依然保留了第二次輸出的world
2.變數和地址
(1)程序計數器(Program Counter)指示要執行的下一個指令的內存中的地址。
整數寄存器文件包含16個命名的位置,分別存儲64位的值。這些寄存器可以存儲地址(對應於C語言的指針)或整數數據。有些寄存器被用來記錄某些重要的程序狀態,而其他的寄存器用來保存臨時數據,例如過程的參數和局部變數,以及函數的返回值。條件碼寄存器保存著最近執行的算術或邏輯指令的狀態信息。它們用來實現控制或數據流中的條件變化,比如if和while語句。
一組向量寄存器可以存放一個或多個整數或浮點數值。(2)在硬體中,所有的數據都存儲在內存中。內存是一個從0開始的位元組序列。CPU執行的機器代碼在內存位置上運行,僅由它們的地址標識。編譯器負責將我們的程序在變數上執行的操作轉換為在地址上執行的操作,但無論是變數名稱還是變數的類型都不存在於此轉化中。
在大多數情況下,程序員不關心處理變數的地址。 &運算符返回存儲變數或表達式的地址。而*操作符做相反的操作,它返回存儲在它的地址中的值。這就引出了一個棘手的問題:C程序員可以使兩個變數名指向內存中相同的位置,而硬體無法區分。上述問題在源代碼中表現並不明顯。 (3)這幅圖我們在unit1中已經見過了,現在詳細解釋一下:
從低地址向上: 程序代碼和數據:對所有的進程來說,代碼是從一個固定的地址開始的,緊接著是和C全局變數對應的數據位置。代碼和數據區是直接按照可執行目標文件的內容初始化的。堆。代碼和數據區後緊隨著的是運行時堆。代碼和數據區在進程一開始運行時就被指定了大小,與此不同,調用像malloc和free這樣的C標準庫函數時,堆可以在運行時動態地擴展和收縮。
共享庫。大約在地址空間的中間部分是一塊用來存放像C標準庫和數學庫這樣的共享庫的代碼和數據的區域。 棧。位於用戶虛擬地址空間頂部的是用戶棧,編譯器用它來實現函數調用。和堆一樣,用戶棧在程序執行期間可以動態地擴展和收縮。調用函數時棧增長,從一個函數返回時棧收縮。 內核虛擬內存。地址空間頂部是為內核保留的。不允許應用程序讀寫這個區域的內容或者直接調用內核代碼定義的函數。 地址分為三組:所有局部變數都聚集在一起。在全局變數中,聲明時初始化的變數在一個集群中,而未初始化的則在另一個集群中。我們看到所有的數據都有一個地址,而地址只是一個整數。由於內存能夠存儲整數,我們不僅可以將數據存儲在內存中,還可以將數據地址存儲在內存中。在後一種情況下,我們存儲地址的位置本身就有一個地址。C允許我們給出這些存儲地址名稱的位置,也就是說,C允許我們聲明保存數據地址的變數,而不是直接保存數據。您可能已經熟悉這種變數類型:它們通常稱為指針或引用。
可執行程序包括BSS段、數據段、代碼段(也稱文本段)。
BSS(Block Started by Symbol)通常是指用來存放程序中未初始化的全局變數和靜態變數的一塊內存區域。特點是:可讀寫的,在程序執行之前BSS段會自動清0。所以,未初始的全局變數在程序執行之前已經成0了。 注意和數據段的區別,BSS段存放的是未初始化的全局變數和靜態變數,數據段存放的是初始化後的全局變數和靜態變數。3.數組和指針
(1)帶下標的數組和指針引用之間的等效性:
下面表格中每一行的兩者都是等效的。
(2)需要注意的是,雖然一個數組可以用指針引用來進行操作,但是我們也不能認為任何一個指針也可以被想像成為數組,觀察下面這個例子。
#include <stdio.h>void Initialize (char * a, char * b){ a[0] = T; a[1] = h; a[2] = i; a[3] = s; a[4] = ; a[5] = i; a[6] = s; a[7] = ; a[8] = A; a[9] =