1. 程式人生 > >計算機原理學習(5)-- x86-16 CPU和記憶體管理

計算機原理學習(5)-- x86-16 CPU和記憶體管理

前言

前面我們已經瞭解了計算機硬體的工作原理,以及作業系統的發展。我們知道是記憶體把計算機硬體和軟體聯絡了起來。不誇張的說,瞭解了軟體在記憶體中的結構,就基本瞭解了程式最底層的執行原理。所以從這一篇開始,將深入的討論計算機中記憶體管理和佈局。記憶體的管理同計算機硬體以及擦做系統是分不開的。這一篇我們主要討論早期x86 CPU和DOS系統對於記憶體的管理。

1. 8086 CPU

說到CPU,我們第一個想到的應該就是Intel。 1971年11月15號,Intel釋出了全球第一款微處理器Intel 4004,這是一個主頻只有108KHz的4bit處理器。而後又釋出了8bit的8008處理器。而我們最熟悉的應該就是8086,為什麼?因為隨便找一本彙編的書籍看看,都會有8086四個大字。因為8086標誌著Intel x86體系結構的CPU的開始。而且8086/8088開始用於便攜電腦,所以我們就從8086開始介紹。80186除8086核心,另外包括了中斷控制器、定時器、DMA、I/O、UART、片選電路等外設。

1.1 8086/8088記憶體訪問

8086是x86體系結構的開始,他採用了16bit,但是地址線卻用了20位。前面介紹CPU工作原理的時候哦我們知道,CPU內部有一個PC計數器,用來儲存下一個要執行的實體地址。但是16位的暫存器如何儲存20位的地址呢?

不僅僅是8086,我們發現之前的CPU的位寬和可定址範圍都不是對應的關係,而且4004和8008也找不到地址線位寬。對於8080來說,地址有16位,而它內部有1個主累加器和5個次累加器,所以它使用2個暫存器組合來訪問16位地址。而對於8086,並沒有採用相同的方式,而是參考了PDP-11小型機,設計出了分段定址技術

因為CPU一次能送出的地址是16位,要訪問20位地址的儲存器就需要使用2個16位的地址計算表示一個20位的地址。這裡採用的辦法是,將記憶體分為不同邏輯段,每段段有自己的段地址(16位),而段內資料地址則是相對於段首地址的偏移地址(16位)。而且段之間是可以相鄰或者重疊的。

因為偏移地址是16位,所以最大的範圍是64K,而對於1M記憶體來說,最少有16個邏輯段。而因為段暫存器也是16位,所以段的實體地址需要是16的倍數,表示為0xXXXX0。這樣的地址可以壓縮為0xXXXX,所以段首地址的高16位表示段值。所以段首的實體地址 = 段值 * 0x10。那麼偏移地址是相對於段首地址來說的,那麼要訪問的實體地址公式為:實體地址 = 段值*0x10 + 偏移地址

這裡介紹一下邏輯地址的概念,邏輯地址指的是機器語言指令中,用來指定一個運算元或者是一條指令的地址。Intel中段式管理中,對邏輯地址要求,“一個邏輯地址,是由一個段識別符號加上一個指定段內相對地址的偏移量“,表示為 [段識別符號:段內偏移量]。

1.2 8086暫存器

關於8086CPU的架構圖,我們在前面的文章中出現了多次。這裡我們只看看8086的暫存器。我們知道暫存器對於CPU來時是非常的重要,無論是取指令還是做運算都需要暫存器來存放資料。我們在多執行緒程式設計中經常聽到一個詞是切換上下文,這裡所指的上下文就包含CPU暫存器的值,當然這個是後話,後面會介紹。

8086一共有14個16位暫存器,具體分類如上圖:

  • AX,BX,CX,DX 是主暫存器,又叫通用暫存器, 為了相容8位CPU,採用2個8位暫存器組合而成,這4個暫存器主要用作資料暫存器,節省從暫存器存取操作的時間。
  • SI, DI是變址暫存器,主要用於操作字串,當然也可以作為通用暫存器;而BP,SP是指標暫存器,主要用於堆疊操作。
  • CS,DS,ES,SS是段暫存器,是為了記憶體分段而設定的,用來存放邏輯段的段值。
  • IP 是指令指標暫存器,類似前面提到的PC計數器,這裡用作表示下一條指令相對於CS段的偏移
  • Flags標誌暫存器,主要用於表示CPU計算的運算結果和狀態。

1.3 8086段暫存器的引用

從8086開始,採用了分段式的記憶體管理,於是在訪問記憶體時不在像以前那樣,拿到地址直接送到匯流排進行訪問,而是需要通過計算得到的。這就涉及到使用那個段暫存器和偏移量。一般來說程式碼不需要指定要訪問的段,匯流排可以自行判斷,當然也可以顯示的指定。

比如我們在獲取下一條指令地址時,就是用CS*16+IP,而當執行一條取資料指令時,就是用DS*16+有效地址來訪問,上表就定義了不同操作時是用到的暫存器和偏移。

這裡可能有一個疑問:程式是如何被載入到不同記憶體段呢?這個其實和程式的編譯,可執行檔案的結構以及作業系統有關,有此可見一項新技術的使用是需要硬體和軟體相互配合的。這些會在後面介紹。

1.4 8086定址方式

表示指令中運算元所在的方法稱為定址方式。

1.4.1 立即定址

運算元直接包含在指令中,比如MOV AX, 1234H, 一般用於給儲存器或暫存器賦值。

1.4.2 暫存器定址

運算元存放在暫存器中,而指令中存放的是暫存器號,比如MOV SI, AX。這裡可以用到的暫存器有AX,BX,CX,DX,SI,DI,SP,BP.

1.4.3 直接定址

運算元在儲存器中,指令中直接包含儲存器的有效地址。比如MOV AX, [1234H]。從前面知道,這裡的有效地址並不是真正的實體地址而是偏移地址,因為這是一條取資料操作指令,所以在沒指定段的時候,預設訪問的是DS資料段。最後得到真正的實體地址。

當然也可以指定要訪問的段比如: MOV AX ES:[1234H],這種定址方式只適用於段小於64K的情況,在程式中儲存器的有效地址一般用變量表示。

1.4.4 暫存器間接定址

運算元存放在暫存器中,運算元的有效地址存放在SI, DI, BX, BP這4個暫存器中。在一般情況下如果有效地址在SI,DI,BX中則訪問DS段,而當在BP中時則訪問SS段。比如MOV AX,[SI]

同樣,也可以指定有效地址要訪問的段,比如MOV AX, CS:[BX]。

1.4.5 暫存器相對定址

運算元在儲存器中,操作時的有效地址存放在SI, DI, BX, BP暫存器並加上一個8位或16位的位移量中。比如MOV AX, [DI+1234H],計算方法和暫存器間接定址相同,只是多加上一個偏移量。

這種定址方式有利於實現高階語言中對結構型別資料所實施的操作。

1.4.6 基址加變址定址

運算元在儲存器中,操作時的有效地址是由基址暫存器BX或BP和變址暫存器SI或DI相加得到的。比如MOV AX, [BX+DI]

這種定址方式一般用來處理陣列的訪問,基址暫存器存放陣列首地址,而變址暫存器來定位陣列的每個元素。也可以寫作MOV AX,[BX][DI]。

1.4.7 相對基址加變址定址

運算元存放在儲存器,運算元有效地址由基址暫存器BX或BP,變址暫存器SI或DI, 以及一個8位或16位位移量相加得到的。比如MOV AX, [BX+DI-2]

2. 程式的記憶體結構

前面我們介紹過程式編譯的過程,當代碼被編譯成可執行檔案後,當執行程式時,通過裝入程式把我們的可執行檔案從磁碟裝載到記憶體中,然後指定程式入口地址,CPU變開始順序的執行。這裡就涉及到CPU如何定位程式的地址的問題。其實對於早期程式編寫,裝入我也不是很瞭解,資料也挺少的。

2.1 早期程式的裝入

我們知道,早期的計算機中,CPU並沒有對記憶體分段,所以程式執行時,獲取的下一條指令或資料的地址就是真實的記憶體地址。於是早期的程式在編譯時就需要確定裝載後在了在記憶體中的絕對位置。比如裝載到記憶體的0x00000010處,那麼在編譯時生成的指定和資料,就是基於0x00000010向上擴充套件。

當程式被裝入記憶體後,裝載程式把PC計數器設定為0x00000010,然以CPU開始執行我們的程式。這種方式需要對記憶體使用情況非常熟悉。

2.2 8086程式的裝入和執行

8086CPU將記憶體分割成了不同的段,於是指令和資料的有效地址並不是真正的實體地址而是相對於段首地址的偏移地址。CPU在取地址時會進行計算,所以我們在編譯程式時無法確定程式在記憶體中絕對的位置。而且前面介紹了8086的定址方式,我們知道在不指定段暫存器的時候,如果是取指定會使用CS,而如果是取運算元則是使用DS。所以我們程式的資料和指令必須裝載到不同的段中。

我們知道,記憶體並沒有被真正的分段,而是通過CPU中段暫存器來存放不同段的首地址。所以最好的辦法是我們對程式也進行分段,把資料放在程式的資料段中,而程式碼則放在程式碼段中。這樣在編譯的時候,每個資料會每條指定的地址都是相對於段的偏移量,我們只需要設定CPU的CS,DS端寄存的地址。8086組合語言中就有段定義語句,就是為了和儲存器結構對應。

上圖顯示了彙編程式編譯後背載入到記憶體的情況:

  1. 8086的彙編支援定義邏輯段,這裡定義了邏輯段和程式碼段。(我們知道組合語言是CPU指令的一種翻譯形式,和硬體密切相關,所以對於8086來說有特定的組合語言)
  2. 在程式編譯時,給每條指令和資料分配一個偏移地址。
  3. 在載入程式之前,載入程式會在記憶體中找到可以存放每個段的地址,並且把段值寫入到執行檔案中,可以理解為給CSEG和DSEG賦值。
  4. 正式載入程式時,會把CSEG,DSEG載入到之前找到的實體地址,並且會去更新CS暫存器為CSEG的值。
  5. 載入程式把IP暫存器設定為第一條指令的偏移地址。
  6. 把控制權交給我們的程式,此時如圖CS = 0x0002(開始執行CS*16 + IP)
  7. 執行第一條語句,把DSEG的段值寫入到AX,然後執行第二條,把AX的值寫入到DS, 也就我們設定了DS暫存器的段值。
  8. 當執行到MOV AX, VAR1時,因為是取資料操作,所以從DS段取資料,此時DS=0x0001。 最終VAR1在記憶體中的地址 = 0x0001*0x10 + 0x0000 = 0x00010。

這裡CS和DS的值不是編譯時確定的,而是在分配段記憶體時獲得的。但是DS的值是從DSEG寫入的,那麼DSEG的值是不是載入程式寫入到執行檔案中的呢?不是很確定。另外或許的是實體記憶體地址,而CS中並不是實體記憶體,是不是要用實體記憶體/0x10來計算出CS和DS的值呢?

2.3 8086的多種模式

前面說過了,段的大小最大為64位,那麼如果我們的程式的段大於64K或者是對於之前的8位程式,又是如何執行的呢?實際上8086根據分段的記憶體結構,有六種執行方式。對於8位機上的程式可以不考慮段地址直接以.com可執行檔案以“微模式”在8086上執行。這是當時8086與MS-DOS作為新平臺取得市場成功的關鍵原因——大量已存的CP/M應用程式能很快得到利用。而對於大於64k的段則執行在大模式中。這塊內容完全不懂,有興趣就自己研究吧。

3. MS-DOS記憶體結構

前面討論完了CPU的記憶體訪問方式,最後討論一下作業系統的記憶體管理。這裡選用了MS-DOS作業系統。這裡並沒有指定是那一個版本,只是從大體上去介紹記憶體管理方式。MS-DOS是一個單任務的批處理作業系統,同時只能有一個.COM或.EXE檔案被執行,所做系統沒有任務排程功能,使用.BAT檔案可以實現批處理功能。而早期的Windows1.x也是基於MS-DOS,只是增加了圖形介面。

3.1 早期MS-DOS的記憶體佈局

IBM和微軟在設計DOS作業系統時,當時CPU主要是8086,採用了20位地址線,所以最大記憶體訪問只1M。但是在在當時普片採用8bitCPU,最大記憶體訪問64K的CPU來說,這個記憶體空間已經相當大了,所以MS-DOS就基於這個進行了設計。

DOS把記憶體劃分問了2個區域:

  • 常規記憶體: 也叫基本記憶體,是記憶體低端的640K。DOS系統本身、中斷向量表、系統資料、驅動程式都會常駐在這一段記憶體中,剩餘的就是使用者程式可以使用的空間,大概600K,而隨著MS-DOS功能越來越多,DOS本身所佔用的空間也越來越大。而MS-DOS提供的記憶體管理,也主要是管理常規記憶體塊。
  • 上位記憶體:這一塊記憶體區域是從640K-1M,是給外接卡裝置的資料緩衝區以及ROM-BIOS使用的。 在這段記憶體空間中有一些空閒的空間,稱為UMB(Upper Memory Block)。這些記憶體DOS系統無法進行管理。EMM386.exe可以管理這一部分記憶體。

3.2 MS-DOS 擴充記憶體(EMS)

隨著PC的發展,越來越多硬體支援MS-DOS, 越來越多的軟體開始在MS-DOS上被使用,於是640K記憶體中,作業系統,驅動程式,防毒程式,常駐程式的體積越來越來,而應用程式可以使用的空間越來越小。為了能讓8086使用更大的記憶體,Intel和MS聯合推出了EMS(Expanded Memory Specification)擴充記憶體。通過主機板上的擴充套件槽,最多可以支援32M記憶體。

但是CPU並不不能直接訪問擴充的記憶體,通過擴充記憶體管理程式,使用了上位記憶體空間中的64K空餘記憶體(UMB),這64K記憶體被分成4個頁,每頁16K,這部分頁稱為“頁框架”,EMS記憶體也分成一個個16K的頁,總數可達2000個。使用EMS的程式最多允許同時訪問4個頁,當程式要訪問到某個頁時,記憶體控制板就把相應EMS頁的內容複製到頁框架中讓程式讀寫,讀寫完後把頁框架中頁的內容複製回相應的EMS記憶體頁,再把別的EMS頁內容複製到頁框架中讓程式讀寫。所以也被稱為“調頁式擴充記憶體“。

3.3 真實模式和高位記憶體(HMA)

1982年,Intel釋出了新的80286 CPU,址線擴充套件到24位,最多可以訪問16M記憶體。 但是在80286無法相容8086上的程式,所以Intel提出了真實模式和保護模式兩種方式來解決這個問題。在真實模式下,80286依然只使用20位的地址線,最多訪問1M記憶體,以前的8086程式可以正常執行,而在保護模式下,程式可以使用全部的16M記憶體。所以真實模式,其實就是8086的執行模式。

在8086記憶體訪問時,當段值+偏移都為最大時:FFFF0h+FFFFh=10FFEFh=1M+64K,得到的地址超出了1M的範圍,這一塊地址稱為高位地址。但是因為只有20根地址線,所以會採用一種wrap-around的技術,將地址對1M求模,得到記憶體地址。但是80286開始擁有了24條地址線,於是在真實模式下,當使用10FFEFh地址訪問時,因為A20地址線的存在,所以會直接訪問記憶體這一塊的地址。

IBM為了解決這個問題,採用了用鍵盤控制器來控制A20,稱為A20 Gate,當開啟的時候,可以訪問到高位記憶體,而禁用時則和8086行為一樣。IBM-PC大部分禁用了A20 Gate,現在大多PC通過BIOS呼叫來控制A20 Gate。 關於A20可以參考:對A20 GATE的思考

3.4 真實模式和擴充套件記憶體(XMS)

80286有24根地址線,最大記憶體容量可以達到16M,但是現在的問題在於MS-DOS本身是運行於真實模式下的,所以即便處理器支援更大的記憶體,也無法使用。所以DOS上的應用程式最多隻能使用640K的記憶體,這個也就是我們經常聽到DOS程式的640K限制的問題。但這並不是8086時期硬體導致640K限制。

為了解決這個問題就使用看擴充套件記憶體XMS(Extended Memory Specification)。當然只有在80286和更高的處理器才才能支援。幾乎所有使用DOS的機器上超過1M的記憶體都是擴充套件記憶體。擴充套件記憶體同樣不能被DOS直接使用,DOS5.0以後提供了Himem.sys這個擴充套件記憶體管理程式,可以通過它來管理擴充套件記憶體。 Emm386.exe可以把擴充套件記憶體(XMS)模擬成擴充記憶體(EMS),以滿足一些要求使用擴充記憶體的程式。

3.5 MS-DOS和保護模式

在DOS作業系統下,無論CPU支援多大的記憶體空間,程式都只能使用常規記憶體空間的640K記憶體,執行在真實模式中。但是80286之後的CPU可以支援保護模式,於是就有一些程式可以通過DPMI(DOS Protocted Mode Interface), DOS擴充套件器程式比如DOS4GW.exe使得CPU進入到保護模式,從而直接訪問擴充套件的記憶體,但是此時,已經不是在DOS環境下了。而且對於80286來說一旦切換到保護模式就無法回到真實模式,只能reset CPU。

實際上大多介紹的保護模式是指80386的32位保護模式,而非80286的16位保護模式。而80386之後,保護模式基本沒有大的變化,後面將會詳細介紹32位保護模式下的記憶體結構和管理。

4. MS-DOS 記憶體管理

瞭解了MS-DOS的記憶體結構,最後我們看看MS-DOS是怎麼管理記憶體的。這一部分主要是看DOS如何管理常規記憶體的640K。

4.1 MS-DOS系統模組

我們可以看到在640K的常規記憶體中,一些系統模組專用了一部分記憶體。MS-DOS主要由一個載入程式好3個模組程式完成啟動

  • BOOT: 這是一個位於磁碟0扇區上的載入程式,主要是檢查DOS系統盤,並把IO.SYS載入的
  • IO.SYS是系統的輸入輸出模組,ROM-BIOS 是固化早BIOS中的裝置驅動; 系統啟動時IO.SYS接收到命令轉換為裝置控制命令,由ROM-BIOS中的驅動程式完成裝置操作。
  • MSDOS.SYS 這個是DOS的核心,主要進行記憶體,磁碟,外設的管理 
  • COMMOND.COM: 這個是MS-DOS和使用者之間的介面程式,用於接收使用者輸入的命令並執行。

4.2 MS-DOS 程序

MS-DOS是一個單任務的作業系統,所以不存在任務排程(80286也不支援多工,80386支援任務切換)。當代碼編寫好生成可執行檔案之後,被載入到記憶體空間時,而在程式記憶體空間前有一個256位元組的程式段字首(PSP)。

而在DOS中,還有一個環境塊EVB用來記錄環境變數,可以把他看做是PSP的擴充套件。

4.3 記憶體管理

當程式被載入的時候,需要向實體記憶體申請空間並載入程式。那麼如何知道那些記憶體可以被使用呢? MS-DOS採用了記憶體控制塊(MCB)來標識實體記憶體塊。DOS的記憶體塊以節為單位,一節等於16個位元組,每個記憶體塊的前面都有一個一節的MCB來描述這個記憶體塊。

  • 標誌位: 一個位元組,Z標識是最後一個分割槽,M標識不是最後一個分割槽
  • 擁有者:當這個欄位為0時,標識是一個沒有使用記憶體塊,否則存放的是擁有此記憶體塊的程序的PSP段地址。
  • 記憶體塊大小: 以節為單位,不包括MCB塊。

所以通過一個MCB塊,可以使用MCB塊地址+記憶體塊大小+1 就能知道下一個MCB塊的地址。這樣整個記憶體就被串聯起來。下圖展示了MS-DOS 3.3啟動後記憶體的情況。

記憶體一共被分成了3部分:

  1. 第一部分中的塊主要是系統使用;
  2. 第二部分是COMMAND.CMD程式,一共使用了3個塊,其中程式資料和PSP還有環境各站一個塊,構成了COMMAND的程序實體。
  3. 最後一部分記憶體塊是給暫駐程式TPA(Transient Program Area),也就是我們程式使用的使用者記憶體空間。

4.4 使用者程式的裝載

當我們要把一個EXE程式裝載到記憶體時,裝載程式會檢查EXE的頭部資訊,檢查TAP的容量並確裝載的段的地址。而裝載時可以分為低位載入和高位載入。

而在載入一個COM檔案時,因為COM檔案沒有頭部資訊,並且COM檔案限制不能大於64K。

我們知道,在程式載入後,需要設定段暫存器的值,程式才能正確的被執行,下面列出了執行檔案被載入後段暫存器的情況

總結:

這一篇主要介紹了x86-16 處理器的記憶體結構和訪問方式,後面還介紹了MS-DOS作業系統是如何管理記憶體的。 因為這一部分很久遠,並且我也沒怎麼接觸過,所以查閱了很多料,費了好多時間。但是可以找到的資料並不是很多。 但是我們的主要目的是瞭解早期的CPU和作業系統是如何管理記憶體的,程式是如何載入執行的。

後面我們會介紹x86-32 CPU的記憶體管理,有了這裡瞭解到的知識就能更好的理解為什麼現在的電腦是這樣執行的,為什麼使用保護模式,為什麼使用虛擬記憶體。

參考:

《80x86組合語言程式設計教程》