X86-64和ARM64用戶棧的結構 (1) ---背景介紹

背景

主要基於Linux,介紹X86-64和ARM64的用戶棧結構。斷斷續續的學了不少和棧相關的知識,今天打算整理用戶棧相關的知識,廢話少說,下面進入正題。python

棧的定義和類別

棧有時也稱堆棧,是一種受限的線性表,只能在線性表的一端按序進行插入(進棧)和刪除(出棧),所以先進棧的數據會後出棧。爲了便於描述,咱們習慣將在線性表進行插入和刪除的一端稱爲棧頂,另外一端稱爲棧底。棧頂會隨着插入和刪除而發生變化,棧底則保持不變。ide

其實,棧在計算機中就是一塊連續的存儲區域(至少虛擬地址是連續的),只不過在這塊連續的存儲區域寫入和刪除數據按照先進後出的規則進行,在計算機中使用兩個指針就能夠徹底描述一個棧,bp(base pointer)指向棧底,sp(stack pointer)指向棧頂,以下圖所示。
X86-64和ARM64用戶棧的結構 (1) ---背景介紹函數

上面主要講了棧的定義,在上面棧的定義中至少有兩個地方沒有說清楚,一是往棧中增長數據時,棧是往高地址增長仍是往低地址增長;二是棧頂指針SP指向的地方是否存放數據。向高地址增加的棧稱爲遞增棧(Ascendant Stack),向低地址增加的棧稱爲遞減棧(Decendant Stack)。SP指向棧頂元素(即SP指向的地方存放數據)的棧爲滿棧(Full Stack),SP指向下一個棧頂元素位置(即SP指向的地方不存放數據)的棧爲空棧(Empty Stack)。很顯然一個棧不能同時爲遞增棧和遞減棧,也不能同時爲滿棧和空棧。所以,存在4種類型的棧,即空增棧(Empty Ascendant Stack,EA)、空減棧(Empty Descendant Stack,ED)、滿增棧(Full Ascendant Stack,FA)和滿減棧(Full Descendant Stack,FD)。設計

空增棧(Empty Ascendant Stack,EA)

在對空增棧中壓入數據時,先把數據放入SP所指的位置處,而後SP=SP+1。對這種棧的壓入操做,至關於C語言的 memory[SP++]=data;或者至關於ARM64的彙編語言str x1,[SP],#8。出棧操做至關於C語言的data=memory[--SP];或者ARM64的彙編語言ldr x1,[SP,#-8]!
X86-64和ARM64用戶棧的結構 (1) ---背景介紹指針

空減棧(Empty Descendant Stack,ED)

在對空減棧中壓入數據時,先把數據放入SP所指的位置處,而後SP=SP-1。對這種棧的壓入操做,至關於C語言的 memory[SP--]=data;或者至關於ARM64的彙編語言str x1,[SP],#-8。出棧操做至關於C語言的data=memory[++SP];或者ARM64的彙編語言ldr x1,[SP,#8]!
X86-64和ARM64用戶棧的結構 (1) ---背景介紹code

滿增棧(Full Ascendant Stack,FA)

在對滿增棧中壓入數據時,先對SP操做騰出位置SP=SP+1,而後數據放入SP指向的位置。對這種棧的壓入操做,至關於C語言的 memory[++SP]=data;或者至關於ARM64的彙編語言str x1,[SP,#8]!。出棧操做至關於C語言的data=memory[SP--];或者ARM64的彙編語言ldr x1,[SP],#-8
X86-64和ARM64用戶棧的結構 (1) ---背景介紹生命週期

滿減棧(Full Descendant Stack,FD)

在對滿減棧中壓入數據時,先對SP操做騰出位置SP=SP-1,而後數據放入SP指向的位置。對這種棧的壓入操做,至關於C語言的 memory[--SP]=data;或者至關於ARM64的彙編語言str x1,[SP,#-8]!。出棧操做至關於C語言的data=memory[SP++];或者ARM64的彙編語言ldr x1,[SP],#8。或者X86-64的彙編指令push r1x86-64的彙編指令push和pop操做棧是按照滿減棧的規則進行。默認狀況下,ARM64也使用滿減棧的規則操做棧。
X86-64和ARM64用戶棧的結構 (1) ---背景介紹進程

棧的生命週期

棧的生命週期是和進程的生命週期保持一致的,進程在則棧在,進程亡則棧亡。所以,不妨從進程的生命週期探討棧的生命週期。一個用戶進程從無到開始運行,須要通過幾個重要的步驟:內存

  • Linux首先建立一個task_struct用於管理進程的方方面面。這裏只是有了進程的「草圖」,進程尚未被建立。
  • 創建進程的虛擬地址空間,也即創建頁表,創建虛擬地址到物理地址的映射,到這時一個用戶進程所需的基本元素已經具有,一個進程被建立完成,在建立進程的過程當中,進程的內核棧也被建立,內核棧不在本文的說明範圍內。
  • 接下來就須要可執行文件自己的參與,讀取可執行文件頭,解析文件頭,文件頭的前幾個字節會指出當前文件是何種類型,若是是#!/bin/sh或 #!/bin/python 則該文件是腳本文件,有負責腳本文件的加載程序,本文只關注可執行文件。創建虛擬地址和可執行文件之間的映射。
  • 初始化進程環境,其中比較重要的一項即是初始化用戶棧
  • 跳轉到可執行文件的入口,執行可執行文件,運行到用戶程序main函數,這其中主要右libc對進行管理。
  • main()函數經過切換棧幀調用其它子函數,子函數也能經過切換棧幀調用其子函數。
  • mian()函數返回,整個進程結束,釋放棧佔的內存,棧消失

結合上面所述以及下圖所示,棧的生命週期能夠分爲4個部分:編譯器

  • Linux Kernel建立用戶棧,爲棧分配內存空間,處理傳遞給用戶的參數,將參數壓入棧中,壓入指向參數的argv,計算出argc並將其壓棧。
  • libc的_start函數將 Linux Kernel建立的棧和libc庫函數接上頭,由體系結構相關的彙編語言編寫,核心做用是將棧頂地址賦值給SP,還將Linux設置的棧傳遞、參數傳遞以及一些庫函數的函數指針傳遞給C語言編寫的函數__libc_start_mian_start函數只是起到一個過渡做用,根據CPU的體系結構將Linux Kernel初始化好的棧傳遞給後續的C語言編寫的函數。
  • libc的__libc_start_mian函數是一個C語言寫的函數,運行到該函數時用戶棧的結構已是編譯器設計的了,同時因爲_start函數已經設置好了SP的值,各類壓棧、出找操做都在不斷調整SP的值。該函數的功能主要有,main調用前的初始化工做;調用main;main函數返回後的清尾工做。
  • 編譯器設計main函數及其調用的子函數的棧。
    X86-64和ARM64用戶棧的結構 (1) ---背景介紹

    用戶棧在系統中的位置

    對於Linux內核而言,將整個內存空間劃分爲兩個部分,Kernel Space 和User Space,前者用於支撐Linux Kenrel自己的運行所需空間,後者就是用於支持用戶程序所需的運行空間。用戶棧就是位於用戶空間,通常位於用戶空間的最高部分,向低地址處增加。
    X86-64和ARM64用戶棧的結構 (1) ---背景介紹

相關文章
相關標籤/搜索