手写操作系统(九)-获取物理内存容量

代码参考自于几本经典著作,包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

1.概述

linux2.6内核是用detect_memory函数来获取内存容量的。 其函数在本质上是通过调用BIOS中断0x15实现的,分别是BIOS中断0x15的3个子功能,子功能号要存放到寄存器EAX或AX中,如下。

EAX=0xE820:遍历主机上全部内存。

AX=0xE801: 分别检测低 15MB 和 16MB~4GB 的内存,最大支持 4GB。

AH=0x88:最多检测出 64MB 内存,实际内存超过此容量也按照 64MB 返回。

 

2.利用0xe820 获取内存

这种方式BIOS只返回一种类型的内存信息,直到将所有内存类型返回完毕。

子功能0xE820返回的内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(Address Range Descriptor Structure,ARDS),共20字节

结构如下:

每次int 0x15之后,BIOS就返回这样一个结构的数据。 ARDS结构中用64位宽度的属性来描述这段内存基地址(起始地址)及其长度,所以表中的基地址和长度都分为低32位和高32位两部分。

其中的 Type 字段用来描述这段内存的类型

使用此中断的方法

此中断的调用步骤如下:

  1. 填写好“调用前输入”中列出的寄存器。
  2. 执行中断调用 int 0x15。
  3. 在CF位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果。

 

3.利用0xe801 获取内存

这个方法只能识别 4GB 内存

使用方法如下:

低于15MB的内存以1KB为单位大小来记录,单位数量在寄存器AX和CX中记录,其中AX和CX的值是一样的,所以在15MB空间以下的实际内存容量=AX*1024。即0x3c00*1024=15MB

16MB~4GB是以64KB为单位大小来记录的,单位数量在寄存器BX和DX中记录,其中BX和DX的值是一样的,所以16MB以上空间的内存实际大小=BX*64*1024。

区分16MB以上即以下是为了兼容,80286拥有24位地址线,其寻址空间是16MB。当时有一些ISA设备要用到地址15MB以上的内存作为缓冲区,也就是此缓冲区为1MB大小,所以硬件系统就把这部分内存保留下来,操作系统不可以用此段内存空间。

此中断的调用步骤如下:

  1. 将AX寄存器写入0xE801。
  2. 执行中断调用 int 0x15。
  3. 在CF位为0的情况下, “返回后输出”中对应的寄存器便会有对应的结果

 

4.利用0x88 获取内存

这个方法只能识别最大64MB 的内存,即使内存容量大于64MB,不过只会显示63MB,因为此中断只会显示1MB之上的内存,不包括这1MB。所以使用时要加入1MB

中断返回后, AX寄存器中的值,其单位是1KB。

此中断的调用步骤如下:

  1. 将AX寄存器写入0x88。
  2. 执行中断调用 int 0x15。
  3. 在CF位为0的情况下, “返回后输出”中对应的寄存器便会有对应的结果。

 

5.代码更改

更改loader.asm

%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   ; 第一个选择子
SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0   ; 第二个选择子
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0   ; 第三个选择子


total_mem_bytes dd 0       ; 保存内存容量,以字节为单位

; 以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr  dw  GDT_LIMIT
         dd  GDT_BASE



ards_buf times 244 db 0     ; 人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_nr dw 0               ; 用于记录ards结构体数量

loader_start:
; 获取内存容量,int 15, ax = E820h
    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 15, ax = E801h
.e820_failed_so_try_e801:
    mov ax,0xe801
    int 0x15
    jc .e801_failed_so_try88       ;若当前e801方法失败,就尝试0x88方法

    ; 先算出低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寄存器备份

    ; 再将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 15, ah = 0x88
.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


;将内存换为byte单位后存入total_mem_bytes处。
.mem_get_ok:
    mov [total_mem_bytes], edx

.error_hlt:          ;出错则挂起
    hlt
; 进入保护模式三步骤

; 1.打开A20地址线
open_A20:
    in   al,0x92
    or   al,0000_0010B
    out  0x92,al

; 2.加载gdt描述符
load_gdt:
    lgdt [gdt_ptr]

; 3.修改cr0标志寄存器的PE位
change_cr0_PE:
    mov  eax, cr0
    or   eax, 0x00000001
    mov  cr0, eax

jmp SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响
                           ; 远跳将导致之前做的预测失效,从而起到了刷新的作用。

; 下面就是保护模式下的程序了
[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 $

 

代码解释:

之前loader.asm需要jmp loader_start跳转,而mbr.asm会跳转指令jmp LOADER_BASE_ADDR,经过这两个跳转才执行到loader_start处的代码。

不过现在mbr.S中的跳转指令已经变成了jmp LOADER_BASE_ADDR + 0x300,多加了个0x300字节,跨过前面的数据部分,直接跳到loader_start。之所以这样做,是为对齐代码,因为在程序开头加个跳转指令,其机器码要占用3字节空间,原本在它之后定义的数据,其地址未对齐到偶数,这会影响硬件执行的速度。

mbr.asm的更改如下即可:

 

total_mem_bytes此变量用于存储获取到的内存容量,以字节为单位,加载到内存中的地址是0xb00。它前面有4个段描述符的定义,还有预留60个段描述槽位times 60 dq 0,段描述符大小是8字节,dq也是8字节,所以偏移量是(4+60)*8=512=0x200字节。loader的加载地址是0x900, 0x900+0x200=0xb00。

total_mem_bytes dd 0       ; 保存内存容量,以字节为单位

 

ards_buf times用来人工对齐,total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节,即0x100,total_mem_bytes的地址0x200,加上后所以loader_start在文件内的偏移地址是0x100+0x200=0x300。

ards_nr dw 0用于记录ards结构体数量

ards_buf times 244 db 0     ; 人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_nr dw 0               ; 用于记录ards结构体数量

 

BIOS 中断是实模式下的方法,只能在进入保护模式前调用。在实模式下用这三种方法检测完内存容量后再进入保护模式。如果一种方法获取失败,尝试另一种方法,若三种方法都失败了,由于无法获取内存信息,后续程序无法加载,只好停止运行。

 

这里是利用0xe820方法。此方法需要提前准备好一块数据缓冲区用于存放返回的ARDS结构,就是ards_buf。按照0xe820的调用方法, es: di存放缓冲区地址,由于es在mbr中已经赋值了,所以在第47行”mov di, ards_buf”,只为di赋值便可,每执行一次int 0x15后,寄存器eax,ebx,ecx都会更新。eax的值由之前的子功能号变成了字符串SMAP的ASCI码,ebx为新的后续值,ecx为实际写入缓冲区中的字节数。其中ebx咱们不用干涉,原方不动地作为输入即可。 eax和ecx寄存器每次调用前都要更新为正确的输入参数,所以放在了循环体中。接下来每得到一个ARDS结构后,便将di增加一个ARDS结构大小(这里是20字节),以指向缓冲区中的下一个ARDS存放的位置,然后将变量ards nr加1,以记录ARDS的个数,用于在后面的代码中遍历所有ARDS,找出最大内存块。

.find_max_mem_area是找出最大的内存块。思路是对每一个ARDS结构中的BaseAddrLow与LengthLow相加求和,遍历完所有ARDS,值最大的则为内存容量,由于BaseAddrLow+LengthLow的单位是字节而无需转换,之后便直接跳转到.mem_get_ok,将此容量数写入变量total_mem_bytes,具体代码在后面mov [total_mem_bytes], edx。在此说明下,三种方法探测到的内存容量都是统一跳转到.mem_get_ok处后以字节形式写入到变量total_mem_bytes,所以三种方法中内存容量都要用 edx 来保存。

loader_start:
; 获取内存容量,int 15, ax = E820h
    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

 

这里是利用0xe801探测内存容量。 在int 0x15执行中断后,先计算出低15MB内存空间的容量。 这里面用到了乘法指令mul,在16位乘法中,由于mul指令固定的乘数是寄存器AX,所以只给提供另一个乘数就行了,于是乘法指令格式是mul 16 位内存或16位寄存器。结果(积)是 32 位,高 16在 DX寄存器,低16位在AX寄存器。 由于寄存器AX和CX的单位是KB,这里要将获得的结果转换成字节,所以寄存器AX或CX要乘以1024,寄存器AX和CX值相同,随便用哪个做乘数皆可,由于固定的操作数AX,所以把CX的值覆盖为0x400,即1024。 这里用的是16位操作数乘法,积的高16位在DX寄存器,低16位在AX寄存器。 所以将EDX左移16位后再与AX做或运算便得到了完整32位的积。 在第85行将edx加了1MB,原因是获取到的内存总比实际大小少1MB,故在此“补偿”。 后面的乘法指令会破坏寄存器EDX的值,所以第86行将结果备份到寄存器ESI。

然后计算16MB之上的内存容量,结果存放在寄存器BX和DX,单位是64KB,所以也要将其转换为字节。 这里面用到了32位操作数的乘法, 32位乘法固定的乘数是EAX,同样也是再提供一个操作数就行了。 所以其指令格式为mul 32位内存或32位寄存器。 积为64位结果,高32位在EDX寄存器,低32位在EAX寄存器。

之后用寄存器BX初始化EAX寄存器的低16位AX (EAX的高16位已经由xor eax,eax清0),EAX为固定的乘数,另一个乘数是0x10000,即64KB。 由于0xe801子功能只能测出4GB之内的内存容量,所以只需要乘积的低32位结果,也就是寄存器EAX的值就够了,最后再将备份在寄存器ES1中的低15MB空间的内存容量同EAX相加,存入变量total_mem_bytes中。

; 获取内存容量,int 15, ax = E801h
.e820_failed_so_try_e801:
    mov ax,0xe801
    int 0x15
    jc .e801_failed_so_try88       ;若当前e801方法失败,就尝试0x88方法

    ; 先算出低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寄存器备份

    ; 再将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

 

这里是用子功能0x88方法探测内存容量,解释如下:

  • mov ah, 0x88: 设置寄存器 AH 的值为 0x88,表示要调用 BIOS 中断 15h 的子功能,以获取内存容量。
  • int 0x15: 触发 BIOS 中断 15h,根据 AH 寄存器中的值执行相应的功能。
  • jc .error_hlt: 检查进位标志 CF,如果设置(表示操作失败),则跳转到标签 .error_hlt 处处理错误。
  • and eax, 0x0000FFFF: 清除 EAX 寄存器中的高16位,保留低16位,这是以 KB 为单位的内存容量。
  • mov cx, 0x400 和 mul cx: 使用 CX 寄存器中的值 0x400(1024)来将 AX 中的内存容量从 KB 转换为字节。
  • shl edx, 16 和 or edx, eax: 将 DX 寄存器中的值左移 16 位,并与 AX 中的值进行逻辑或操作,以合并成一个32位的值,表示以字节为单位的内存容量。
  • add edx, 0x100000: 因为子功能 0x88 只返回 1MB 以上的内存,所以实际内存大小要加上 1MB
; 获取内存容量,int 15, ah = 0x88
.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

 

编译

nasm -I common/ -o bin/mbr.bin asm/mbr.asm
nasm -I common/ -o bin/loader.bin asm/loader.asm 
sudo dd if=bin/mbr.bin of=/bochs/bin/dreams.img bs=512 count=1 conv=notrunc
sudo dd if=bin/loader.bin of=/bochs/bin/dreams.img bs=512 count=2 seek=2 conv=notrunc

运行

sudo /bochs/bin/bochs -f /bochs/bin/bochsrc.disk

在控制台

ctrl + c 
xp 0xb00

最后可以看到确实是32MB

 

6.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇