《操作系统真相还原》6-3代码(含mbr,loader文件)
完整代码
我的文件层级目录如图
boot.inc
;----------------- loader 和 kernel -----------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
PAGE_DIR_TABLE_POS equ 0x100000
KERNEL_START_SECTOR equ 0x9
KERNEL_BIN_BASE_ADDR equ 0x70000
KERNEL_ENTRY_POINT equ 0xc0001500
PT_NULL equ 0
;------------------ gdt描述符属性 -------------------
DESC_G_4K equ 1_00000000000000000000000b ;第23位G 表示4K或者1MB位 段界限的单位值 此时为1则为4k
DESC_D_32 equ 1_0000000000000000000000b ;第22位D/B位 表示地址值用32位EIP寄存器 操作数与指令码32位
DESC_L equ 0_000000000000000000000b ;第21位 设置成0表示不设置成64位代码段 忽略
DESC_AVL equ 0_00000000000000000000b ;第20位 是软件可用的 操作系统额外提供的 可不设置
DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;第16-19位 段界限的最后四位 全部初始化为1 因为最大段界限*粒度必须等于0xffffffff
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;相同的值 数据段与代码段段界限相同
DESC_LIMIT_VIDEO2 equ 0000_0000000000000000b ;第16-19位 显存区描述符VIDEO2 书上后面的0少打了一位 这里的全是0为高位 低位即可表示段基址
DESC_P equ 1_000000000000000b ;第15位 P Present判断段是否存在于内存
DESC_DPL_0 equ 00_0000000000000b ;第13-14位 DPL Descriptor Privilege Level 0-3
DESC_DPL_1 equ 01_0000000000000b ;0为操作系统,权力最高;3为用户段,用于保护
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_sys equ 0_000000000000b ;第12位为0 则表示系统段 为1则表示数据段
DESC_S_CODE equ 1_000000000000b ;第12位与type字段结合 判断是否为系统段还是数据段
DESC_S_DATA equ DESC_S_CODE
DESC_TYPE_CODE equ 1000_00000000b ;第9-11位表示该段状态 1000 可执行 不允许可读 已访问位0
;x=1 e=0 w=0 a=0
DESC_TYPE_DATA equ 0010_00000000b ;第9-11位type段 0010 可写
;x=0 e=0 w=1 a=0
;代码段描述符高位4字节初始化 (0x00共8位 <<24 共32位初始化0)
;4KB为单位 Data段32位操作数 初始化的部分段界限 最高权限操作系统代码段 P存在表示 状态
DESC_CODE_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + \
DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0X00
;数据段描述符高位4字节初始化
DESC_DATA_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + \
DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X00
;显存段描述符高位4字节初始化
DESC_VIDEO_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + \
DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X0B
;-------------------- 选择子属性 --------------------------------
;第0-1位 RPL 特权级比较是否允许访问;第2位 TI 0表示GDT 1表示LDT;第3-15位索引值
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
;--------------------页表相关属性---------------------------
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b
mbr.s
;主引导程序
;------------------------------------------------------------------------------
%include "boot.inc" ;让编译器在编译之前,把boot.inc文件包含进来
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
;清屏
;利用0x06功能,上卷所有行,则可清屏
;-------------------------------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;-------------------------------------------------------------------------------
;输入;
;AH 功能号:0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值
mov ax,0600h
mov bx,0700h
mov cx,0 ;左上角:(0,0)
mov dx,184fh ;右下角:(80,25)
;因为VGA文本模式中,一行只能容纳80个字符,共25行
; 下标从0开始,所有0x18=24,0x4f=79
int 10h ;int 10h
;输出字符串MBR
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4 ;A表示绿色背景闪烁,4表示前景颜色为红色
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
mov eax,LOADER_START_SECTOR ;起始扇区lba地址,0x2
mov bx,LOADER_BASE_ADDR ;写入的地址,0x900
mov cx,4 ;待写入的扇区数,由于loader.bin超过了512个字节,可能是多个扇区
call rd_disk_m_16 ;以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR+0x300
;-------------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------------
;eax=LBA扇区号
;bx=将数据写入的内存地址
;cx=读入的扇区数
mov esi,eax ;备份eax,因为al在out命令中会使用,会影响到eax的低8位
mov di,cx ;备份cx,cx在读数据的时候会使用到
;读写硬盘
;第一步:设置要读取的扇区数
mov dx,0x1f2 ;虚拟硬盘属于ata0,是Primary通道,所以sector count 是由0x1f2访问
mov al,cl ;cl是cx的低8位,就读一个扇区,这样就能传过去了
out dx,al ;读取的扇区数,sector count 记录要读取的扇盘数量
mov eax,esi ;恢复eax,现在eax存的是其实扇区lba的地址,0x2,第二个扇区
;第二步:将LBA地址存入 0x1f3 ~ 0x1f6
;LBA地址 7~0 位写入端口 0x1f3
mov dx,0x1f3 ;LBA low
out dx,al ;eax的第8位,就是al
;LBA地址 15~8 位写入端口 0x1f4
mov cl,8
shr eax,cl ;eax右移8位,让al的数,变为eax中8位
mov dx,0x1f4 ;LBA mid
out dx,al
;LBA地址 23~16 位写入端口 0x1f5
shr eax,cl ;再右移8位
mov dx,0x1f5 ;LBA high
out dx,al
shr eax,cl ;这样al为0000
and al,0x0f ;lba第24~27位
or al,0xe0 ;设置7~4位为1110,表示lba模式
mov dx,0x1f6 ;就是拼凑出device寄存器的值
out dx,al
;第3步:向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al ;command:0x1f7,写入命令,写入的命令是读命令
;第四步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示写入硬盘的状态,所以不需要更换dx的值
nop ;减少打扰硬盘的工作
in al,dx ;将Status的寄存器的值读入到al中
and al,0x88 ;第四位为1表示硬盘控制器已准备好数据传输,第七位为1表示硬盘忙,保存第4位和第7位
cmp al,0x08 ;若第4位为1,表示数据已经准备好了,若第7位为1,表示硬盘处于忙
jnz .not_ready ;若未准备好,继续等,判断结果是否为0
;第5步,从0x1f0端口读数据
mov ax,di ;这个时候di存的是上面备份的cx,及时要读取的扇区的数量
mov dx,256 ;每次in操作只读取两个字节,根据读入的数据总量(扇区数*512字节)
mul dx ;dx*ax就是总数量/2,然后将值送到cx中,cx就是要in的次数
mov cx,ax ;di为要读取的扇区数,一个扇区有512个字节,每次读入一个字,共需要di*512/2次,所以di*256
mov dx,0x1f0
.go_on_read:
in ax,dx ;读入到ax中
mov [bx],ax ;读入到bx指向的内存
add bx,2 ;每次读入2个字节
loop .go_on_read ;cx是循环的次数
ret
times 510-($-$$) db 0
db 0x55,0xaa
loader.S
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR ; loader在保护模式下的栈指针地址,esp
; 构建GDT及其内部描述符, 每个描述符8个字节, 拆分为高低各4字节(32位)
GDT_BASE: dd 0x00000000 ; 第0个描述符,不可用
dd 0x00000000
CODE_DESC: dd 0x0000ffff ; 低32位31~16位为段基址15~0位, 15~0位为段界限15~0位
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000ffff ; 数据段(栈段)描述符
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007 ; 0xB8000 到0xBFFFF为文字模式显示内存 段界限:limit=(0xbffff-0xb8000) / 4k=0x7
dd DESC_VIDEO_HIGH4 ; 0xB
GDT_SIZE: equ $ - GDT_BASE ; 当前位置减去GDT_BASE的地址 等于GDT的大小
GDT_LIMIT: equ GDT_SIZE - 1 ; SIZE - 1即为最大偏移量
times 60 dq 0 ; 预留60个 四字型 描述符空位, 用于后续扩展
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 段选择子: 低3位为TI RPL状态, 其余为描述符索引值
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起始地址(共48位)
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
; 人工对齐:total_mem_bytes 4字节 + gdt_ptr 6字节 + ards_buf 244字节 + ards_nr 2字节 , 共256字节
ards_buf times 244 db 0 ; 记录内存大小的缓冲区
ards_nr dw 0 ; 记录 ARDS 结构体数量
loader_start:
; ------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------
xor ebx, ebx ; 第一次调用将eb置0
mov edx, 0x534d4150 ; edx只赋值一次,循环体中不会改变
mov di, ards_buf ; di指向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数量加1
cmp ebx, 0 ; 若 ebx 为 0 且 cf 不为 1, 这说明 ards 全部返回,当前已是最后一个
jnz .e820_mem_get_loop ; 不为0则循环获取
; 在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量
mov cx, [ards_nr] ; 遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ; 用edx记录最大值, 在这里先清零
.find_max_mem_area: ; 无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ; base_add_low
add eax, [ebx+8] ; base_add_low + length_low = 这块ADRS容量
add ebx, 20 ; 指向下一块ARDS
cmp edx, eax ; 找出最大,edx寄存器始终是最大的内存容量
jge .next_ards ; 如果edx>=eax, 继续遍历下一块
mov edx, eax ; 如果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 15
jc .e801_failed_so_try88 ; 若当前e801方法失败,就尝试0x88方法
; 1 先算出低15M的内存, ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
; 乘数在eax, 即内存数量, 积高16位在edx, 低16位在eax
mov cx, 0x400 ; 0x400 = 1024
mul cx
shl edx, 16 ; 左移16位, 将低16位放到edx高16位
and eax, 0x0000FFFF ; 0x0000FFFF = 1111 1111 1111 1111, 高16位置0
or edx, eax ; 获得完整的32位积
add edx, 0x100000 ; edx比实际少1M, 故要加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
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 gate
; 2 加载gdt
; 3 将cr0 的 pe位(第0位)置1
; ----------------- 打开A20 ----------------
in al, 0x92 ; 端口号0x92 中的第1位变成 1 即可
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 ; 刷新流水线
.error_hlt: ; 出错则挂起
hlt ; 处理器暂停, 直到出现中断或复位信号才继续
[bits 32]
p_mode_start:
; 初始化32位的段寄存器
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
;------------加载 kernel------------
mov eax, KERNEL_START_SECTOR ; kernel.bin所在的扇区号
mov ebx, KERNEL_BIN_BASE_ADDR ; 从硬盘读出后写入的地址
mov ecx, 200 ; 读入的扇区数
call rd_disk_m_32 ; 从硬盘读取文件到内存, 上面eax, ebx, ecx是参数
;------------启用 分页机制------------
; 创建页目录及页表并初始化页内存位图
call setup_page
; gdt需要放在内核里
; 将描述符表地址&偏移量写入内存gdt_ptr,一会用新的地址加载
sgdt [gdt_ptr] ; 取出GDT地址和偏移信息,存放在gdt_ptr这个内存位置上
; 视频段需要放在内核里与用户进程进行共享
; 将gdt描述符中视频段的段基址 + 0xc0000000
mov ebx, [gdt_ptr + 2] ; 这里gdt_ptr前2字节是偏移量,后4字节是GDT基址,先选中GDT
or dword [ebx + 0x18 + 4], 0xc0000000 ; 一个描述符8字节,0x18处是第3个段描述符也就是视频段, 修改段基址最高位为C, +4进入高4字节, 用or修改即可
; 将gdt的基址加上 0xc0000000 成为内核所在的地址
add dword [gdt_ptr + 2], 0xc0000000
add esp, 0xc0000000 ; 将栈指针同样map到内核地址
; 页目录地址赋值给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax
; 打开 cr0 的 pg位 (第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
; 开启分页后, 用 gdt 新的地址重新加载
lgdt [gdt_ptr]
;;;;;;;;;;;;;;;;;;;;;;;;;;;; 此时不刷新流水线也没问题 ;;;;;;;;;;;;;;;;;;;;;;;;
; 由于一直处在32位下,原则上不需要强制刷新, 经过实际测试没有以下这两句也没问题.
; 但以防万一, 还是加上啦, 免得将来出来莫句奇妙的问题.
jmp SELECTOR_CODE:enter_kernel ; 强制刷新流水线,更新 gdt
enter_kernel: ; 进入内核
call kernel_init
mov esp, 0xc009f000 ; 给栈选个高地址且不影响内存其他位置的地方
jmp KERNEL_ENTRY_POINT
;----------------- 将kernel.bin中的segment拷贝到编译的地址 -----------
; 此时,kernel.bin 已经被读取到内存 KERNEL_BIN_BASE_ADDR 位置上了
kernel_init:
xor eax, eax ; 清空通用寄存器
xor ebx, ebx ; ebx 记录程序头表文件内偏移地址,即e_phoff
xor ecx, ecx ; cx 记录程序头表中的 program header 数量
xor edx, edx ; dx 记录 program header 尺寸, 即 e_phentsize
mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移文件 42 字节处是 e_phentsize, 表示 program header 大小
mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移文件 28 字节处是 e_phoff, 表示第一个程序头在文件的偏移量
add ebx, KERNEL_BIN_BASE_ADDR ; 获取程序头表第一个程序头的地址(基地址 + 偏移量)
mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移文件 44 字节处是 e_phnum,表示程序头的数量
.each_segment:
cmp byte [ebx + 0], PT_NULL ; 若相等,则表示程序头没使用
je .PTNULL
; 为mem_cpy压入参数(从右往左)类似 memcpy(dst, src, size)
; 参数 size:待复制的大小
push dword [ebx + 16] ; 偏移程序头 16 字节处是 p_filesz, 即本段在文件内的大小
; 参数 src: 源地址
mov eax, [ebx + 4] ; 偏移程序头 4 字节处是 p_offset, 即本段在文件内的偏移大小
add eax, KERNEL_BIN_BASE_ADDR ; 加上基地址 = 物理地址
push eax
; 参数 dst: 目的地址
push dword [ebx + 8] ; 偏移程序头 8 字节处是 p_vaddr, 即本段在内存中的虚拟地址
call .mem_cpy
add esp, 12 ; 清理栈中压入的三个参数
.PTNULL:
add ebx, edx ; 程序头的地址 + 程序头的大小 = 下一个程序头的地址
loop .each_segment ; 复制下一个程序头
ret
;---------- 逐字节拷贝 mem_cpy(dst,src,size) ------------
; 输入:栈中三个参数(dst,src,size)
; 输出:无
;---------------------------------------------------------
.mem_cpy:
cld ; 控制进行字符串操作时esi和edi的递增方式, cld增大, sld减小
push ebp
mov ebp, esp
push ecx ; rep指令用到了ecx,外层指令也用到了ecx,所以备份
; 分析一下为什么是 8, 因为push了ebp, 所以相对应的都需要+4
; 并且进入函数时还 push了函数返回地址, 所以再+4, 所以一共+8
mov edi, [ebp + 8] ; dst
mov esi, [ebp + 12] ; src
mov ecx, [ebp + 16] ; size
rep movsb ; 逐字节拷贝
; 恢复环境
pop ecx
pop ebp
ret
; ------------- 创建页目录及页表 ---------------
setup_page:
; 先把页目录所占空间清 0
mov ecx, 4096 ; 1024 * 4 = 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
; 开始创建页目录项(Page Directory Entry)
.create_pde:
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ; 第一个页表的位置(仅次于页目录表,页目录表大小4KB)
mov ebx, eax ; 0x00101 000, 储存到ebx为创建PTE时使用
; 下面将页目录项0和OxcOO都存为第一个页表的地址 ,每个页表表示4MB内存
; 这样Oxc03fffff(3G-3G04M)以下的地址和Ox003fffff(0-4M)以下的地址都 指向相同的页表
; 这是为将地址映射为内核地址做准备
or eax, PG_US_U | PG_RW_W | PG_P ; 用户特权级,可读可写,存在内存
mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第一个目录项,0x00101 007
mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 第0xc00高10位0x300=768个页表占用的目录项,0xc00以上属于kernel空间
; 这里是把第768个目录页和第1个目录页指向同一个页表的物理地址:0x101000
; 系统实际位于0~0x100000内存地址中,将系统虚拟地址0xc00000000映射到这低1M的空间内,只需要让0xc0000000的地址指向和低1M相同的页表即可
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的位置, 4092 = 1023 * 4
; 创建页表项(Page Table Entry)
mov ecx, 256 ; 1M低端内存/每页大小4K = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; edx中地址为0x0,属性为7,即111b
.create_pte:
mov [ebx + esi * 4], edx ; ebx = 0x00101 000, 即第一个PTE起始地址, 每个PTE = 4 byte
add edx, 4096 ; edx + 4KB
inc esi
loop .create_pte ; 低端1M内存中,物理地址=虚拟地址,这里创建了1M空间的页表项
; 创建内核其他页表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; eax指向第二个页表(每个页表对应一个PDE, 含有1024个页表项)
or eax, PG_US_U | PG_RW_W | PG_P
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 769~1022的所有目录项数量, 1022 - 769 + 1 = 254
mov esi, 769
.create_kernel_pde:
mov [ebx + esi * 4], eax
inc esi
add eax, 0x1000 ; eax指向下一个页表
loop .create_kernel_pde
ret
;-------------------------------------------------------------------------------
; 功能:读取硬盘n个扇区
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
;-------------------------------------------------------------------------------
rd_disk_m_32:
mov esi ,eax ; 备份eax
mov di ,cx ; 备份cx
; 读写硬盘
; 1---设置要读取的扇区数
mov dx ,0x1f2 ; 设置端口号,dx用来存储端口号的,要写入待读入的扇区数量
mov al ,cl
out dx ,al ; 读取的扇区数
mov eax ,esi ; 恢复eax
; 2---将LBA地址存入0x1f3~0x1f6
; LBA 7~0位写入端口0x1f3
mov dx ,0x1f3
out dx ,al
; LBA 15~8位写入端口0x1f4
mov cl ,8
shr eax ,cl ; 逻辑右移8位,将eax的最低8位移掉,让最低8位al的值变成接下来8位
mov dx ,0x1f4
out dx ,al
; LBA 24~16位写入端口0x1f5
shr eax ,cl
mov dx ,0x1f5
out dx ,al
shr eax ,cl
and al ,0x0f ; 设置lba 24~27位
or al ,0xe0 ; 设置7~4位是1110表示LBA模式
mov dx ,0x1f6
out dx ,al
; 3---向0x1f7端口写入读命令0x20
mov dx ,0x1f7
mov al ,0x20
out dx ,al
; 4---检测硬盘状态
.not_ready:
; 同写入命令端口,读取时标示硬盘状态,写入时是命令
nop
in al ,dx
and al ,0x88 ; 第三位为1表示已经准备好了,第7位为1表示硬盘忙
cmp al ,0x08
jnz .not_ready
; 5---0x1f0端口读取数据
mov ax ,di ; 要读取的扇区数
mov dx ,256 ; 一个扇区512字节,一次读取2字节,需要读取256次
mul dx ; 结果放在ax里
mov cx ,ax ; 要读取的次数
mov dx ,0x1f0
.go_on_read:
in ax, dx
mov [ebx], ax ; bx是要读取到的内存地址
add ebx, 0x02
loop .go_on_read ; 循环cx次
ret
stdint.h
#ifndef __LIB_STDINT_H
#define __LTB_STDINT_H
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;
#endif
print.h
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci);
//void put_str(char* message);
#endif
print.S
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
[bits 32]
section .text
;------------------------------put_char---------------
;功能描述:把栈中的1个字符写入光标所在处
;-----------------------------------------------------
global put_char
put_char:
pushad ;备份32位寄存器环境
;需要保证gs为正确的视频段选择子
;为了保险起见,每次打印时都为gs赋值
mov ax, SELECTOR_VIDEO ;不能直接把立即数送入段寄存器
mov gs, ax
;;;;;;;;;;;;; 获取当前光标位置;;;;;;;;;;;;;;;;
;先获得高8位
mov dx,0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx,al
mov dx, 0x03d5 ;通过读写数据端口0x3d5 来获取 或 设置光标位置
in al, dx
mov ah, al
;再获取低8位
mov dx, 0x03d4
mov al,0x0f
out dx,al
mov dx, 0x03d5
in al, dx
;将光标存入bx
mov bx, ax
;下面这行是在栈中获取待打印字符
mov ecx, [esp + 36] ;pushad压入了4X8=32字节,
;加上主调函数4字节的返回地址,故esp+36字节
cmp cl, 0xd
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed
cmp cl, 0x8 ;BS(backspace) 的 asc码是8
jz .is_backspace
jmp .put_other
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.is_backspace:
;;;;;;;;;;; backspace的一点说明 ;;;;;;;;;;;;
;当为backspace时,,本质上只要将光标移到前一个现存位置,后续会自动覆盖,但是有可能输入backspace后不再输入新字符,就会显得很奇怪,所以此处添加了空格或空字符0
dec bx
shl bx, 1 ;光标左移一位等于乘以2
;表示光标对应显存中的偏移字节
mov byte [gs:bx], 0x20 ;将待删除字节补0
inc bx
mov byte [gs:bx], 0x07 ;
shr bx,1
jmp .set_cursor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.put_other:
shr bx, 1 ;光标位置2字节表示,将光标值*2
;表示光标对应显存中的偏移字节
mov [gs:bx], cl ;ASCII字符本身
inc bx
mov byte [gs:bx],0x07 ;字符属性
shr bx, 1 ;恢复旧的光标值
inc bx
cmp bx, 2000
jl .set_cursor ;光标值<2000,表示未写到显存末尾
;光标>2000,则换行处理
.is_line_feed: ;是换行符LF(\n)
.is_carriage_return: ;是回车符CR(\r)
;如果是CR,只要把光标移到行首即可
xor dx, dx ;dx是被除数的高16位,清0
mov ax, bx ;ax是被除数的低16位
mov si, 80 ;效仿linux,\n表示下一行的行首
div si
sub bx, dx
.is_carriage_return_end: ;回车符CR处理结束
add bx, 80
cmp bx, 2000
.is_line_feed_end: ;若是LF(\n) ,将光标+80
jl .set_cursor
;;;;;;;;;;;;;;屏幕行的范围是0-24,滚屏原理是将屏幕的1-24行搬到0-23行
;再将24行用空格补充
.roll_screen: ;超出屏幕大小就开始滚屏
cld
mov ecx, 960 ;2000-80=1920个字符要搬运,共1920*2=3840字节
;一次搬4个字节,共3840/4=960
mov esi, 0xc00b80a0 ;第一行行首
mov edi, 0xc00b8000 ;第0行行首
rep movsd
;;;;;;;;;;;;;;;;将最后一行填充为空白
mov ebx, 3840
mov ecx, 80
.cls:
mov word [gs:ebx], 0x0720; 0x0720是黑底白字的空格键
add ebx, 2
loop .cls
mov bx, 1920
.set_cursor:
;将光标设为bx
;;;;;;;;;;;1 先设置高8;;;;;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0e
out dx, al
mov dx, 0x03d5
mov al, bh
out dx, al
;;;;;;;;;;;;2 再设置低8位;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret
main.c
#include "print.h"
void main(void) {
put_char('k');
put_char('e');
put_char('r');
put_char('n');
put_char('e');
put_char('l');
put_char('\n');
put_char('1');
put_char('2');
put_char('\b');
put_char('3');
while(1);
}