1. 8086匯編中的段
段地址
8086對內存尋址的方式是通過段地址*16+偏移地址的方式實現的,而在16位的8086CPU下,段地址和偏移地址也都是16位的。這意味着,對於任意一個段,段的起始地址必定為16的倍數(段地址*16)。
對於同一個內存地址,存在多種不同的內存尋址方式:
例如:段地址1000H+偏移地址2345H,與段地址1234H+偏移地址0005H都可以對內存地址12345H進行尋址(段地址1204H+偏移地址0305H等等也可行,非常自由)。
段的最大空間為2^16bit=64KB(例如:將段地址設為1000H,將段的大小設置為64KB,則段的內存范圍為10000H-1FFFFH)。
段的最小空間為2^4bit=16bit (例如:將段地址設為1234H,將段的大小設置為16bit,則段的內存范圍為12340H-1234FH)。
段的邏輯意義
需要注意,內存段的概念並不是內存硬件所固有的,而是從CPU尋址的角度出發,將內存中的物理連續區域邏輯上分隔為不同的區域。內存段這一概念的提出有利於在復雜程序、多程序系統中對同一程序的不同邏輯部分以及不同程序的內存進行更好的訪問和管理。
段寄存器
內存段的存在能夠划分同一程序中的不同邏輯部分,進行更有效的管理和更簡單的訪問。匯編程序的內存通常可以被划分為三部分:代碼、棧以及數據。
通常為了避免混淆,會將這三種不同的邏輯部分分別存放在三個不同的內存段中,便於理解(當然也能將同一部分的內存存放在不同的段中,這主要取決於程序的復雜程度)。
8086CPU提供了對代碼、棧、數據三種內存段訪問的段寄存器,分別是代碼段寄存器CS、棧段寄存器SS、數據段寄存器DS以及附加段寄存器ES。
代碼段寄存器CS
代碼段寄存器CS在前面的博客中已經有過介紹,CPU在運行時會將CS:IP指向的內存中的數據當作指令來執行。
棧段寄存器SS
執行棧相關的指令時,CPU通過SS:SP獲得當前棧頂指針。通過設置寄存器SS的值,可以將某一段內存當作邏輯上的棧來使用。有關棧的內容,會在博客的后半段進行介紹。
數據段寄存器DS
內存尋址時,可以通過段地址:偏移地址的方式進行指定內存的訪問(例如:mov cx 1000H:2345H)。
但由於大多數時候程序中對內存的訪問都主要集中在某一邏輯段中,8086匯編提供了另一種更加簡單的內存尋址方式:只需要指定偏移地址就能進行訪問。(例如:mov cx [2345H];其中2345H為偏移地址)
但事實上,8086CPU依然是通過段地址:偏移地址的方式來尋址的,那么CPU是如何知道[address]指令形式的段地址的呢?答案是通過數據段寄存器DS來獲取。當使用[address]這種僅指定了偏移地址的尋址方式時,CPU默認從寄存器DS中獲取當前的段地址。
想要將地址12345H內存數據送入寄存器CX,[address]形式的指令序列為:mov ax 1000H; mov DS AX; mov cx [2345H]。
這里之所以需要通過通用寄存器AX間接的將段地址1000H送入DS,是因為8086CPU不允許直接對段寄存器進行賦值,具體的原因可以參考: https://www.zhihu.com/question/43608287。
附加段寄存器ES
附加段寄存器也可以視為額外的數據段寄存器,用於指定附加數據段的段地址。當同時需要操作兩個段內存中的數據時,ES的存在可以避免反復的修改寄存器DS的,有效的簡化代碼和提高機器執行效率。
一段內存,既可以是程序的存儲空間,可以是數據的存儲空間,也可以作為棧空間來使用,甚至可以什么都不是。這里的關鍵在於CPU中相關寄存器CS/IP,SS/SP以及DS、ES的值。
2. 8086匯編中的棧
棧這個概念在計算機相關的領域內十分常見,其所具有的的后進先出的特性被很多程序廣泛利用。
在匯編語言程序開發的過程中,在內存中數據的交換、子程序跳轉進行變量保存等場景下,開發人員希望能夠將一段內存當作邏輯上后進先出的棧來使用,比起通過每次都指明特定的內存地址去操作存儲器,使用棧操作一段連續內存能讓程序更加簡單明了。
通過軟件來實現一個棧,機器效率是十分低下的。為此,8086CPU的設計者提供了硬件層面的棧機制:通過特定的指令,CPU能夠像對待棧一樣來訪問指定的一段內存。
棧有兩種獨特的操作,一是入棧(PUSH),二是出棧(POP),一個棧只有棧頂指針指向的元素才可以被訪問。入棧和出棧時,棧頂指針也會相應的移動以指向新的棧頂處。
8086匯編提供了對應的指令PUSH和POP來對內存進行入棧、出棧操作。同時8086CPU是16位的,因此POP和PUSH所操作的棧中元素大小為兩個字節。
棧相關指令
PUSH [寄存器],將指定寄存器中的值傳送到棧頂指針處,棧頂指針上移
PUSH [內存單元],將指定內存單元處的值傳送到棧頂指針處,棧頂指針上移
POP [寄存器],將棧頂指針處的內存數據傳送到指定寄存器中,棧頂指針下移
POP [內存單元],將棧頂指針處的內存數據傳送到指定內存單元處,棧頂指針下移
可以這么理解,PUSH是入棧操作,那么指令中的寄存器或是內存單元自然就是指向入棧數據所處的位置。而POP是出棧操作,指令中的寄存器或是內存單元則是用來承載出棧數據的。
棧頂指針
隨之而來的問題是,CPU該如何知道在棧操作指令中沒有明確聲明的棧頂指針呢?
前面提到過,CPU是通過CS:IP來獲得當前所應該執行的指令;類似的,CPU通過另外兩個寄存器SS(stack-segment 棧段寄存器)和SP(stack-point 堆棧指針寄存器)來確定棧頂指針。在執行PUSH/POP指令時SS:SP所指向的內存單元就是棧頂指針所在的位置,SS標識棧內存段,而SP標識內存段中棧頂指針的內存地址。8086的一段連續棧內存地址中,將高位內存視為棧底,低位視為棧頂。
PUSH入棧:SS不變而SP自減2(空出兩個字節共16位的內存空間),標識新的棧頂,隨后將數據送入棧頂
POP出棧:先將棧頂元素送入目的地(寄存器/內存),隨后SS不變而將SP自增2(縮小棧),標識新的棧頂
棧的越界問題
這里只提到了棧的頂部指針,而沒有提到棧底指針。如果不斷的出棧導致sp指針越過棧的底部會怎樣?不斷的入棧導致SP越過了棧的頂部又會發生什么呢?
匯編語言給了程序員難以言喻的自由,將幾乎一切都交給了開發者自己控制,因此上述的棧訪問越界問題必須由程序員自己避免。
在出棧入棧過程中,棧段寄存器SS是不變的,這意味着8086能直接支持的最大棧就是64KB(段的最大值),而SP在極端情況下會發生棧的上溢或者下溢;或者一個物理段被分成了許多的邏輯段,對棧頂指針的控制出現失誤,會導致不同邏輯段間的數據被覆蓋。
由於棧指針訪問越界造成的問題在編程時不易察覺,卻會帶來嚴重后果,需要加以小心。
