定址空間配置
每個行程都拿到自己一片廣大的虛擬定址空間,而那片空間不是一塊毫無特徵的平地——它被配置成數個區域,各有職責。從低位址到高位址,你通常會看到程式的程式碼,接著是它的全域資料,然後是一個往上長的堆積、一大段空隙,靠頂端則是一個往下長的堆疊,共享函式庫與記憶體對映則坐落在中間的空隙裡。認得這張圖,就是認得一個指標「在哪」,以及為什麼有兩個區域朝彼此生長。
在典型配置上具體地走一遍:最低的已對映區域是文字區段(text segment,機器碼,對映成可讀可執行、不可寫),接著是唯讀與可讀寫的資料區段(已初始化的全域變數,然後是清零的 bss)。其上堆積開始,並隨 malloc() 要更多而朝高位址生長(歷史上透過程式中斷點 program break)。遠遠靠頂端坐落著堆疊,它隨函式被呼叫、呼叫框被推入而朝低位址往下長。堆積與堆疊之間那片寬廣區域,就是共享函式庫與 mmap() 對映被放置之處。往上長的堆積與往下長的堆疊之間刻意留的空白,正是讓兩者各自擴張而不立刻相撞的關鍵。
還有一件值得點名而不深入的事:在現代系統上,這些區域的起始位置每次執行都會被隨機化——這就是定址空間配置隨機化(address space layout randomisation,ASLR)。目的在於安全:若攻擊者無法預測堆疊、堆積或函式庫會落在哪裡,那些倚賴寫死位址的攻擊就難施展得多。這也是為什麼同一支程式跑兩次,會從同樣的 &x 印出不同的位址。配置的形狀在概念上固定,但每次啟動都被一個隨機偏移量平移。
某次執行中,一個區域變數可能坐落在靠頂端(高的堆疊位址,例如 0x7ffd...),而 malloc() 回傳一個低得多的堆積位址(例如 0x55a3...)。兩者間巨大的數值落差,就是那片空白的中段——堆積往上長進它、堆疊往下長進它。
堆疊在高處往下長、堆積在低處往上長,中間留有空隙。
經典的「堆疊在頂、堆積在下」圖是形狀,但 ASLR 每次執行都隨機化實際的基底位址,所以別寫死或假設任何位址。也要注意這是虛擬配置——每個行程都有自己的一份,透過轉譯在實體 RAM 中彼此重疊。