對棧進行push 和 pop
?程序運行時,會在內存上申請分配一個稱為 「棧」 的數據空間。
?
在棧中,數據在存儲時是從內存的下層(大的地址編號
)逐漸往上層(小的地址編號
)累積,讀出時則是按照從上往下的順序進行。
棧是 「存儲臨時數據的區域」 ,它的特點是通過push
指令和pop
指令進行數據的存儲和讀出。push
指令和pop
指令中只有一個操作數。該操作數表示的是 「push的是什么及pop的是什么」 ,而不需要指定”對哪一個地址編號的內存進行push
或pop
“。
這是因為,對棧進行讀寫的內存地址是有esp
寄存器(棧指針)進行管理的。push
指令和pop
指令運行后,esp
寄存器的值會 「自動進行更新」 (push
指令是-4
,pop
指令是+4
),因而就沒有必要指定內存地址了。
函數調用機制
假設存在如下的C語言
代碼片段。
// 返回兩個參數值之和的函數
int AddNum(int a,int b){
return a + b;
}
// 調用AddNum函數的函數
void MyFunc(){
int c;
c = AddNum(123,456);
}
轉換成對應的匯編語言的代碼如下。
這里我們先介紹(3)~(6)
的部分,這對了解函數調用的機制很重要。
(3)
和(4)
表示的是將傳遞給AddNum
函數的參數通過push
入棧。在C語言
中,雖然記述為函數AddNum(123,456)
,但入棧的則會按照456
、123
這樣的順序,也就是位于 「后面的數值先入棧」 。
(5)
的call
指令,把程序流程跳轉到了操作數中指定的AddNum
函數所在的內存地址處。在匯編語言中, 「函數名表示的是函數所在的內存地址」 。AddNum
函數處理完畢后,程序流程必須要返回到編號(6)
這一行。call
指令運行后,call
指令的下一行((6)
這一行)的內存地址會 「自動」 push
入棧。該值會在AddNum
函數處理的最后通過ret
指令pop
出棧,然后程序流程就會返回到(6)
這一行。
(6)
部分會把棧中存儲的兩個參數(456
和123
)進行銷毀處理,也就是 「棧清理處理」 。雖然通過使用兩次pop
指令也可以實現,不過 「采用esp
寄存器加8的方式更有效率」 (處理一次)。對棧進行數值的輸入輸出時,數值的單位是4字節。因此,通過在棧地址管理的esp
寄存器加上4的2倍8,就可以達到和運行兩次pop
命令同樣的效果。
AddNum函數調用前后棧的狀態變化
函數內部的處理
繼續分析執行AddNum
函數的源代碼部分。
ebp
寄存器的值在(1)
中入棧,在(5)
中出棧。這主要是為了把函數中用到的ebp
寄存器的內容,恢復到函數調用前的狀態。CPU
擁有的寄存器是有數量的限制的。在函數調用前,調用源有可能已經在使用ebp
寄存器了。因而, 「在函數內部用的寄存器,要盡量返回到函數調用前的狀態」 。
(2)
中負責管理棧地址的esp
寄存器的值賦值到了ebp
寄存器中。這是因為,在mov
指令中方括號內的參數,是不允許指定esp
寄存器的。因此,這里就采用了不直接通過esp
,而是用ebp
寄存器來讀寫棧內容的方法。
(3)
是用[ebp+8]
指定棧中存儲的第1個參數123
,并將其讀出到eax
寄存器中。eax
寄存器是負責運算的累加寄存器
通過(4)
的add
指令,把當前eax
寄存器的值同第2個參數相加后的結果存儲在eax
寄存器中。 「函數的參數是通過棧來傳遞,返回值是通過寄存器來返回的」 。
(6)
中ret
指令運行后,函數返回目的地的內存地址會自動出棧。
AddNum函數內部的棧狀態變化
全局變量用的內存空間
在一些高級編程語言中,在函數外部定義的變量稱為 「全局變量」 ,在函數內部定義的變量稱為 「局部變量」 。全局變量可以在源代碼的任意部分被引用,而局部變量只能在定義該變量的函數內進行引用。
高級程序語言被編譯后,會被歸類到名為 「段」 定義的組。
- 初始化的全局變量被匯總到名為
_DATA
的段定義中 - 沒有初始化的全局變量被匯總到名為
_BSS
的段定義中 - 指令被匯總到名為
_TEXT
的段定義中
局部變量的內存空間
「局部變量只能在定義該變量的函數內進行引用」 ,這是因為,局部變量是臨時保存在寄存器和棧中的。
函數內部利用的棧,在函數處理完畢后會恢復到初始狀態,因此局部變量的值也就會被銷毀,而寄存器也可能被用于其他目的。因此,局部變量只是在函數處理運行期間臨時存儲在寄存器和棧上。
用于局部變量的棧空間的申請分配和釋放
循環處理的實現方法
假設我們存在如下的代碼,將局部變量i
作為循環計數器連續進行10次循環的C
語言源代碼。
// 定義MySub函數
void MySub(){
// 省略部分處理
}
// 定義MyFunc函數
void MyFunc(){
int i;
for(i=0;i<10;i++){
// 重復調用MySub函數10次
MySub();
}
}
將上述的代碼轉換成匯編語言如下(僅展示for
片段)
C語言
的for
語句是通過在括號中指定 「循環計數器」 的初始值(i=0
)、循環的繼續條件(i<10
)、循環計數器的更新(i++
)這3種形式來進行循環處理。與此相對,
?在匯編語言的源代碼中,循環是通過 「比較指令」 (
cmp
)和 「跳轉指令」 (jl
)來實現。?
具體流程我們就不在這里贅述。這里挑選比較重要的點來分析下。
cmp
指令是用來對第一個操作數和第二個操作數的數值進行比較的指令。cmp ebx,10
就相當于C語言
的i<10
這一處理,意思是把ebx
寄存器的數值同10進行比較。匯編語言中比較指令的結果,會存儲在CPU
的 「標志寄存器」 中。
最后一行的jl
是jump on less than
(小于的話就跳轉)的意思。也就是說,jl short @4
的意思就是,前面運行的比較指令的結果,若 「小」 的話就跳轉到@4
這個 「標簽」 。
條件分支的實現方式
條件分支的實現方法同循環的實現方法類似,使用的也是cmp
指令和跳轉指令。
-
cpu
+關注
關注
68文章
10829瀏覽量
211194 -
計算機
+關注
關注
19文章
7430瀏覽量
87733 -
C語言
+關注
關注
180文章
7601瀏覽量
136251 -
編譯器
+關注
關注
1文章
1618瀏覽量
49057
發布評論請先 登錄
相關推薦
評論