《操作系统真相还原》6-3代码,kernel实现put_char函数(含mbr,loader文件)

《操作系统真相还原》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);
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值