標籤:

操作系統 向內核邁進(四)

內核的一個重要工作就是做好內存管理工作,首先要知道有多少物理內存。

Linux有多方法獲取內存容量

1.detect_memory函數獲取內存容量,本質上調用BIOS中斷0X15實現,分別是三個子功能,子功能號要存放到EAX或者AX中,如EAX=0XE820,歷遍主機上所有內存。AX=0XE801,分別檢測低15MB和16MB~4GB內存,最大支持4GB。AH=0X88,最多檢測出64MB內存。

BIOS中斷可以返回已安裝的硬體信息,由於BIOS以及其中斷也只是一組軟體,訪問硬體本質上要依靠硬體提供介面(Application Program Interface,API)。

子功能OxE820 的強大之處是返回的內存信息較豐富,包括多個屬性欄位,所以需要一種格式結構來組織這些數據。內存信息的內容是用地址範圍描述符來描述的,用於存儲這種描述符的結構稱之為地址範圍描述符( Address Range Descriptor Structure, ARDS )。

地址範圍描述符結構ARDS位元組偏移量 屬性名稱 描述0 BaseAddrLow 基地址的低32 位4 BaseAddrHigh 基地址的高32 位8 LenggthLow 內存長度的低32 位,以位元組為單位12 LengthHigh 內存長度的高32 位,以位元組為單位16 Type 本段內存的類型地址範圍描述符結構的Type 欄位Type值 名稱 捕述1 AddressRangeMemory 這段內存可以被操作系統使用2 AddressRangeReserved 內存使用中或者被系統保留,操作系統不可以用此內存其他 未定義 未定義,將來會用到,目前保留,但是需要操作系統一樣將其視為ARR

為什麼BIOS 會按類型來返回內存信息呢?原因是這段內存可能是。

? 系統的ROM 。

? ROM 用到了這部分內存。

? 設備內存映射到了這部分內存。

? 由於某種原因,這段內存不適合標準設備使用。

由於我們在32 位環境下工作,所以在ARDS 結構屬性中,我們只用到低32 位屬性。

BaseAddrLow+LengthLow 是一片內存區域上限,單位是位元組。正常情況下,不會出現較大的內存區域不可用的情況,除非安裝的物理內存極其小。這意味著,在所有返回的ARDS 結構里,此值最大的內存塊一定是操作系統可使用的部分,即主板上配置的物理內存容量。

下面實用這三種方法

%include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR ;構建gdt及其內部的描述符 GDT_BASE: dd 0x00000000 dd 0x00000000 CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4 DATA_STACK_DESC: dd 0x0000FFFF dd DESC_DATA_HIGH4 VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7 dd DESC_VIDEO_HIGH4 ; 此時dpl為0 GDT_SIZE equ $ - GDT_BASE GDT_LIMIT equ GDT_SIZE - 1 times 60 dq 0 ; 此處預留60個描述符的空位(slot) SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相當於(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上 ; total_mem_bytes用於保存內存容量,以位元組為單位,此位置比較好記。 ; 當前偏移loader.bin文件頭0x200位元組,loader.bin的載入地址是0x900, ; 故total_mem_bytes內存中的地址是0xb00.將來在內核中咱們會引用此地址 total_mem_bytes dd 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;以下是定義gdt的指針,前2位元組是gdt界限,後4位元組是gdt起始地址 gdt_ptr dw GDT_LIMIT dd GDT_BASE ;人工對齊:total_mem_bytes4位元組+gdt_ptr6位元組+ards_buf244位元組+ards_nr2,共256位元組 ards_buf times 244 db 0 ards_nr dw 0 ;用於記錄ards結構體數量 loader_start: ;------- int 15h eax = 0000E820h ,edx = 534D4150h (SMAP) 獲取內存布局 ------- xor ebx, ebx ;第一次調用時,ebx值要為0 mov edx, 0x534d4150 ;edx只賦值一次,循環體中不會改變 mov di, ards_buf ;ards結構緩衝區.e820_mem_get_loop: ;循環獲取每個ARDS內存範圍描述結構 mov eax, 0x0000e820 ;執行int 0x15後,eax值變為0x534d4150,所以每次執行int前都要更新為子功能號。 mov ecx, 20 ;ARDS地址範圍描述符結構大小是20位元組 int 0x15 jc .e820_failed_so_try_e801 ;若cf位為1則有錯誤發生,嘗試0xe801子功能 add di, cx ;使di增加20位元組指向緩衝區中新的ARDS結構位置 inc word [ards_nr] ;記錄ARDS數量 cmp ebx, 0 ;若ebx為0且cf不為1,這說明ards全部返回,當前已是最後一個 jnz .e820_mem_get_loop;在所有ards結構中,找出(base_add_low + length_low)的最大值,即內存的容量。 mov cx, [ards_nr] ;遍歷每一個ARDS結構體,循環次數是ARDS的數量 mov ebx, ards_buf xor edx, edx ;edx為最大的內存容量,在此先清0.find_max_mem_area: ;無須判斷type是否為1,最大的內存塊一定是可被使用 mov eax, [ebx] ;base_add_low add eax, [ebx+8] ;length_low add ebx, 20 ;指向緩衝區中下一個ARDS結構 cmp edx, eax ;冒泡排序,找出最大,edx寄存器始終是最大的內存容量 jge .next_ards mov edx, eax ;edx為總內存大小.next_ards: loop .find_max_mem_area jmp .mem_get_ok;------ int 15h ax = E801h 獲取內存大小,最大支持4G ------; 返回後, ax cx 值一樣,以KB為單位,bx dx值一樣,以64KB為單位; 在ax和cx寄存器中為低16M,在bx和dx寄存器中為16MB到4G。.e820_failed_so_try_e801: mov ax,0xe801 int 0x15 jc .e801_failed_so_try88 ;若當前e801方法失敗,就嘗試0x88方法;1 先算出低15M的內存,ax和cx中是以KB為單位的內存數量,將其轉換為以byte為單位 mov cx,0x400 ;cx和ax值一樣,cx用做乘數 mul cx shl edx,16 and eax,0x0000FFFF or edx,eax add edx, 0x100000 ;ax只是15MB,故要加1MB mov esi,edx ;先把低15MB的內存容量存入esi寄存器備份;2 再將16MB以上的內存轉換為byte為單位,寄存器bx和dx中是以64KB為單位的內存數量 xor eax,eax mov ax,bx mov ecx, 0x10000 ;0x10000十進位為64KB mul ecx ;32位乘法,默認的被乘數是eax,積為64位,高32位存入edx,低32位存入eax. add esi,eax ;由於此方法只能測出4G以內的內存,故32位eax足夠了,edx肯定為0,只加eax便可 mov edx,esi ;edx為總內存大小 jmp .mem_get_ok;----------------- int 15h ah = 0x88 獲取內存大小,只能獲取64M之內 ----------.e801_failed_so_try88: ;int 15後,ax存入的是以kb為單位的內存容量 mov ah, 0x88 int 0x15 jc .error_hlt and eax,0x0000FFFF ;16位乘法,被乘數是ax,積為32位.積的高16位在dx中,積的低16位在ax中 mov cx, 0x400 ;0x400等於1024,將ax中的內存容量換為以byte為單位 mul cx shl edx, 16 ;把dx移到高16位 or edx, eax ;把積的低16位組合到edx,為32位的積 add edx,0x100000 ;0x88子功能只會返回1MB以上的內存,故實際內存大小要加上1MB.mem_get_ok: mov [total_mem_bytes], edx ;將內存換為byte單位後存入total_mem_bytes處。;----------------- 準備進入保護模式 -------------------;1 打開A20;2 載入gdt;3 將cr0的pe位置1 ;----------------- 打開A20 ---------------- in al,0x92 or al,0000_0010B out 0x92,al ;----------------- 載入GDT ---------------- lgdt [gdt_ptr] ;----------------- cr0第0位置1 ---------------- mov eax, cr0 or eax, 0x00000001 mov cr0, eax jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水線,避免分支預測的影響,這種cpu優化策略,最怕jmp跳轉, ; 這將導致之前做的預測失效,從而起到了刷新的作用。.error_hlt: ;出錯則掛起 hlt[bits 32]p_mode_start: mov ax, SELECTOR_DATA mov ds, ax mov es, ax mov ss, ax mov esp,LOADER_STACK_TOP mov ax, SELECTOR_VIDEO mov gs, ax mov byte [gs:160], P jmp $

代碼5-1 是為了演示BIOS 中斷Ox15 的用法,咱們把以上介紹的三種獲取內存的方法都用上了。代碼中的注釋還是很全的,再結合前面介紹的用法,大家應該很容易看懂。不過我己經厚道慣了,多多少少還是要給大家說一下,代碼雖然非常簡單,但畢竟揣摩別人的思想還是要花時間的。

本代碼第28 行定義了4 位元組的變數total_mem_bytes,此變數用於存儲獲取到的內存容量,以位元組為單位。也是啊,經過千辛萬苦才獲取到的內存大小當然要趕緊找個地方藏起來了,哈哈,有點誇張了,總之先存起來留著以後用。

在total_mem_bytes 定義的地方上面有幾行注釋,闡述了total_mem_b嚴囚的地址是OxbOO 。理由是它前面有4 個段描述符的定義,還有預留60 個段描述槽位陽es60dq0。段描述符大小是8 宇節, dq 也是8 位元組,所以偏移量是(4+60)*8=512=0x200 位元組。本程序的載入地址是Ox900, Ox900+0x200=0xb00 ,所以OxbOO便是變數total_mem_b es 載入到內存中的地址。將來在內核中實現內存分配系統時還會用到此地址。

從代碼的第41 行開始,採用BIOS 中斷Ox15 的三種子功能來檢測內存。在檢測內存時,必然是先使用功能最全、檢測功能最強大的方法,功能最弱的、檢測能力有限的方法應該是在「萬般無奈」下才用,要放在最後,以避免無法檢測出真實的內存容量。從強到弱的子方法依次是0xe820 、0xe801 、0x880.

第41 ~69 行用的子功能0xe820 方法。此方法需要提前準備好一塊數據緩衝區用於存放返回的ARDS結構,此緩衝區我們在上面已經準備好了,就是缸ds_buf。按照0xe820 的調用方法, es:「存放緩衝區地址,由於es 在mbr 中己經賦值了,所以在第47 行「 movdi ads_buf」,只為di 賦值便可。

第56~69 行是找出最大的內存塊。思路是對每一個ARDS 結構中的BaseAddrLow 與Len目止ow 相加求和,遍歷完所有ARDS ,值最大的則為內存容量.

第71 ~95 行是利用子功能Oxe801 探測內存容量。由於方法本身比較簡單,所以代碼量很短。在76行執行中斷後,第79~ 86 行先計算出低lSMB 內存空間的容量。這裡面用到了乘法指令mul ,在16 位乘法中,由於mul 指令固定的乘數是寄存器AX,所以只給提供另一個乘數就行了,於是乘法指令格式是16 位內存或16 位寄存器。結果(積)是32 位,高16 在DX 寄存器,低16 位在AX 寄存器。


推薦閱讀:

深入計算機底層,從幾本靠譜的書開始
計算機中的存儲器們
CPU怎樣對存儲器們進行讀寫?
純UEFI+GPT實現Win7 Win10雙系統
系統突發性地磁碟佔有100%,資源管理器無限重啟

TAG:操作系統 |