15-棧和棧的初始化
計算機工程師們為了設計出更高效,更易於控制管理的程式,把記憶體分成一些不同的區域(詳細見4-計算機的啟動過程中的圖3),其中有一塊區域就是“棧”,本質上棧是一段記憶體空間,在計算機裡領域裡代表:資料臨時儲存的地方。
同時棧也是一種資料儲存結構,這對於學過資料結構這門課的同學來說並不難理解,我們知道“棧”在程式設計中是經常使用的,在早期,設計8086處理器的計算機工程師為了更方便的管理“棧”這段記憶體,設計了“三個”專門負責管理“棧”的暫存器。
和程式碼段,資料段一樣,棧也被定義成一個記憶體段,叫做棧段(Stack Segement),由段暫存器SS指向棧的段地址。針對棧的操作主要有壓棧(push)和出棧(pop),進棧和出棧只能在一端進行,因此需要使用棧指標暫存器SP指向棧頂的地址,表示下一個資料應該壓入棧的位置或者從哪裡出棧。而棧指標暫存器BP則用於指向棧底的地址。
在定義棧的時候,需要從記憶體中找到一塊可用的區域,我們再來回憶一下8086的記憶體分佈圖(不記得同學趕快複習一下:4-計算機的啟動過程)。首先,0xF0000-0xFFFFF區域的記憶體和0xB8000-0xB8FFF區域的記憶體已經固定了不可用,因此我們可以選擇區域就是0x00000-0x9FFFF區域了,那麼這裡我們把棧的段地址定為0x0000,偏移地址為0xFFFF。
下面給出彙編程式碼:
mov ax,0x0000 mov ss,ax ;指定棧的段地址為0x0000 mov bp,0xFFFF ;指定棧底的地址 mov sp,0xFFFF ;指定棧頂的地址 ;定義資料 mov ax,0x1234 mov bx,0x2345 mov cx,0x3456 mov dx,0x4567 ;通過push指令依次把資料入棧 push ax push bx push cx push dx End: jmp near End times 510-($-$$) db 0x00 db 0x55,0xAA
啟動虛擬機器除錯,執行結果如下:
在初始化棧時,暫存器SP和BP都指向了0xFFFF地址(棧底),從棧的記憶體區域來看,資料入棧是從0xFFFE地址開始儲存資料的,當資料全部入棧完畢,我們知道最後一個數據肯定是在棧頂的,而暫存器SP正好就指向了0xFFF7(棧頂地址),當然在資料入棧的過程中,暫存器SP的值是一直在不斷變化的,大家可以每執行一次push指令時,觀察暫存器SP的變化。
ESP和EBP兩個暫存器是針對32位的,SP和BP暫存器是16位的。
前面說過暫存器SP也表示下一個要入棧的資料的位置,那麼有的小夥伴肯定很好奇push指令是怎麼把資料入棧的?
以push ax為例,我們可以理解為push指令做了這些事情:
sub sp,2 ;將暫存器sp的值減2
mov word[ss:sp],ax ;然後把暫存器ax的值存到暫存器sp指向的地址,完成資料入棧
當然,以上這些只是為了方便理解push指令,實際上在彙編程式中這樣寫是不會編譯通過的。
對於出棧,x86彙編提供了pop指令來完成出棧操作,如果你理解了push指令,那麼pop指令對你來說,就很容易理解了。
彙編程式碼如下:
mov ax,0x0000
mov ss,ax ;指定棧的段地址為0x0000
mov bp,0xFFFF ;指定棧底的地址
mov sp,0xFFFF ;指定棧頂的地址
;定義資料
mov ax,0x1234
mov bx,0x2345
mov cx,0x3456
mov dx,0x4567
;把資料依次入棧
push ax
push bx
push cx
push dx
;依次出棧
pop dx
pop cx
pop bx
pop ax
End:
jmp near End
times 510-($-$$) db 0x00
db 0x55,0xAA
程式執行結果:
注意觀察棧的記憶體區域,暫存器SP和BP的變化。出棧的時候要遵守先進後出,後進先出的原則,當然你也可以不遵守,按ax,bx,cx,dx的順序出棧,但是這樣可能會導致程式的最終執行結果並不是你想要的。因為我們現在所寫的程式已經是沒有了作業系統的限制,所以你想做怎麼都可以,如果是基於作業系統上這樣寫程式是肯定不行的,普通使用者程式無法直接訪問記憶體,必須藉助作業系統才能訪問。
總結:
棧其實是計算機工程師們在記憶體的某塊區域抽象虛擬出來的一個“箱子”,這個箱子的上端是開口,可以依次往存放東西,但是要遵守先進後出,後進先出的原則。所以不要把棧想象的有多神祕,在組合語言中就是用ss,sp,bp,以及push和pop指令來管理控制,讀寫的一塊記憶體區域而已(說白了,棧就是從記憶體抽象出來的)。