实验7.加载内核

简介

实验:加载内核,分析 kernel.bin 的 elf 文件头,找到各个段,然后将各个段复制到内存指定位置

代码

boot/mbr.s

省略,和上一个实验一样

boot/loader.s

; boot/loader.s
; 功能:完成在内存里写好3个段描述符(代码段、数据段、显存段)、赋值好GDTR寄存器、创建好表示3个选择子的字段后。
    ;   读取物理内存。
    ;   进入保护模式
    ;   开启分页
    ;   分析kernel.bin的elf文件头,找到各个段,然后将各个段复制到内存指定位置

%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

; ---全局段描述符表 begin---
GDT_BASE:          ; 一个8字节为段描述符结构体 注意:第一次4字节是基地址+段限长
    dd 0x0000_0000 ; dd define dword(4字节)
    dd 0x0000_0000

CODE_DESC:
    dd 0x0000_ffff
    dd DESC_CODE_HIGH4

DATA_STARK_DESC:
    dd 0x0000_ffff
    dd DESC_DATA_HIGH4

VEDIO_DESC:
    dd 0x8000_0007 ; limit=(0xbffff-0xb8000)/4k=0x7
    dd DESC_VIDEO_HIGH4

    GDT_SIZE equ $ - GDT_BASE
    GDT_LIMIT equ GDT_SIZE - 1
    times 60 dq 0 ; 预留60个段描述的空间
; ---全局段描述符表 end---

    ; 用来保存内存容量
    total_mem_bytes dd 0 ; 此处的地址为0x900 + (64 * 8 = 0x200) = 0xb00,偏移地址为0x200

    ; 定义选择子
    SELECTOR_CODE equ  (0x0001<<3) + TI_GDT + RPL0 
    SELECTOR_DATA equ  (0x0002<<3) + TI_GDT + RPL0
    SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

    ; 赋值GDTR寄存器
    gdt_ptr dw GDT_LIMIT 
            dd GDT_BASE 

    ; ARDS结构体缓冲区,缓冲区用来装0xe820子功能返回的ARDS结构体
    ; 一个ARDS结构体20字节,本次实验测试一共返回了6个
    ards_buf times 244 db 0
    ards_nr dw 0 ; 大小2个字节,用于记录 ARDS 结构体数量
    ; 人工对齐:total_mem_bytes+gdt_ptr+ards_buf+ards_nr=4+6+244+2=256,共256=0x100字节


;---bios读取内存容量 begin---
loader_start: ; loader_start地址为0x900+0x300,偏移地址为0x300
    ;jmp .e820_failed_so_try_e801 ; 直接测试第2种
    ;jmp .e801_failed_so_try88 ; 直接测试第3种

    ; ---方法1 int 15h eax = 0000e820h ,edx = 534D4150h ('SMAP')获取内存布局 begin---
    xor ebx,ebx         ; 异或运算,第一次使用0xe820子功能ebx要清0
    mov edx, 0x534d4150 ; edx 只赋值一次,循环体中不会改变
    mov di,ards_buf     ; ES:DI是ards结构缓冲区的指针,es已在mbr.s赋值完毕了,这里不用再赋值.
.e820_mem_get_loop:     ; 循环获取每个ARDS内存范围描述结构
    mov eax,0x0000e820  ; 执行int0x15后,eax值为0x534d4150, 所以每次执行int前都要更新为子功能号
    mov ecx, 20         ; 1个ards结构体大小为20字节
    int 0x15;
    jc .e820_failed_so_try_e801 ;cf 位为 1则 有错误发生,尝试 Oxe801子功能
    
    add di, cx         ; 使di 增加 20 字节指向缓冲区中新的 ARDS 结构位置
    inc word [ards_nr] ; 记录 ARDS 数量
    cmp ebx, 0
    jnz .e820_mem_get_loop ; 若ebx不等于0,继续调用e820子功能返回ards结构体

    ; 在所有的ARDS结构体中,找到(base_addr_low + length_low)的最大值,即内存的容量
    mov cx, [ards_nr] ; 遍历每一个 ARDS 结构体,循环次数是 ARDS 的数量
    mov ebx, ards_buf ; ards缓冲区首地址
    xor edx, edx      ; edx为最大容量,在此先清0
.find_max_mem_area:
    ; 无需判断 type 是否为1,最大的内存块一定是可被使用的
    mov eax, [ebx]   ; 拿到结构体的 base_addr_low
    add eax, [ebx+8] ; base_addr_low + length_low
    add ebx, 20      ; 指向缓冲区中下-个 ARDS 结构
    cmp edx,eax 
    jge .next_ards   ; edx大于eax就访问下一个ards结构体
    mov edx, eax     ; 小于就记录在dex中
.next_ards:
    loop .find_max_mem_area ; cx记录了ards结构体数量,cx为0说明已经循环结束了
    jmp .mem_get_ok         ; 将最大内存容量放入total_mem_byte中
    ; ---方法1 int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP')获取内存布局 end---

    ;---方法2 int 15h ax = e801h 获取内存大小,最大支持4G begin---
    ; 返回后, 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用做乘数 0x400 = 1024
    mul cx
    shl edx,16
    and eax,0x0000FFFF
    or edx,eax
    add edx, 0x100000 ; ax只是15MB,故要加1MB
    mov esi,edx
    ;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
    ;---方法2 int 15h ax = e801h 获取内存大小,最大支持4G end---

    ;---方法3 int 15h ah = 0x88 获取内存大小,只能获取64MB以内 begin---
.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
    add edx,0x100000  ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB
    ;---方法3 int 15h ah = 0x88 获取内存大小,只能获取64MB以内 end---

.mem_get_ok:
    mov [total_mem_bytes ], edx

    ; ---进入保护模式 begin---
    ; 打开A20
    in al,0x92
    or al,0000_0010B
    out 0x92,al
    ; 加载gdt
    lgdt [gdt_ptr]
    ; cr0第0位给1
    mov eax,cr0
    or eax,0x0000_0001
    mov cr0,eax
    ; ---进入保护模式 end---

    jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线

.error_hlt: ; 出错则挂起
    hlt
;---bios读取内存容量 end---

[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 eax, KERNEL_START_SECTOR  ; kernel.bin所在的扇区号
    mov ebx, KERNEL_BIN_BASE_ADDR ; 从磁盘读出后,写入到ebx指定的地址
    mov ecx, 200                  ; 读入的扇区数
    call rd_disk_m_32

    ; 创建页目录及页表
    call setup_page
    sgdt [gdt_ptr]                         ; 存储到原来gdt所有的位置

    ; 把视频段的基地址+3G
    mov ebx, [gdt_ptr + 2]                 ; [gdt_ptr + 2]为gdt的起始地址
    or dword [ebx + 0x18 + 4], 0xc000_0000 ; [ebx + 0x18 + 4]为视频段描述符的后4个字节的首地址 3*8=24=0x18
                                           ; 高8位是基地址的高8位,0xc000_0000(3G)的高8位给到基地址的高8位
                                           ; 实现的效果就是: 基地址 = 高8位的3G + 低24位的基地址值

    add dword [gdt_ptr + 2],0xc000_0000    ; 映射之后 GDTR寄存器的全局描述符表地址也要加上3G
    add esp,0xc000_0000                    ; 将栈指针也要加上3G

    ;---开启分页机制 begin---
    ; 把页目录地址赋给cr3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax
    ; 打开cr0的pg位(第31位)
    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax
    ;---开启分页机制 end---

    
    lgdt [gdt_ptr]                         ; 赋值GDTR寄存器新的段描述符表
    jmp SELECTOR_CODE:enter_kernel         ; 强制刷新流水线
enter_kernel:
    call kernel_init
    mov esp, 0xc009f000
    jmp KERNEL_ENTRY_POINT


;---读取硬盘n个扇区 begin---
    ; eax=LBA扇区号
    ; ebx=数据的写入的内存地址
    ; ecx=读取的扇区数
rd_disk_m_32:
    mov esi,eax ; 备份eax
    mov di,cx   ; 备份扇区数到di
    
    ; 第1步:设置要读取的扇区数
    mov dx,0x1f2
    mov al,cl
    out dx,al            ;读取的扇区数
    mov eax,esi          ;恢复ax

    ; 第2步:将LBA地址存入0x1f3 ~ 0x1f6
    ; LBA地址7~0位写入端口0x1f3
    mov dx,0x1f3
    out dx,al

    ; LBA地址15~8位写入端口0x1f4
    mov cl,8
    shr eax,cl
    mov dx,0x1f4
    out dx,al

    ; LBA地址23~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:        ; 测试0x1f7端口(status寄存器)的的BSY位
    nop
    in al,dx
    and al,0x88    ; 第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
    cmp al,0x08
    jnz .not_ready ; 若未准备好,继续等。

    ; 第5步:从0x1f0端口读数据
    mov ax, di     ; di是备份的扇区数
    mov dx, 256	   ; 一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
    mul dx
    mov cx, ax
    mov dx, 0x1f0
.go_on_read:
    in ax,dx
    mov [ebx], ax
    add ebx, 2
    loop .go_on_read
    ret
;---读取硬盘n个扇区 end---


;---创建页目录及页表 begin---
setup_page:
    ; 1.先把页目录的(1024个页目录项)4096字节清0
    mov ecx, 4096
    mov esi, 0
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS + esi], 0
    inc esi
    loop .clear_page_dir


    ; 2;把页目录项0和页目录项0xc00(768)指向第0个页表,在把第0个页表的前256个页表项绑定物理地址0-1M
create_pde: 			; Page Directory Entry
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000		; 0x1000=4096 eax+4096页目录后面挨着的第0个页表的地址
    mov ebx, eax
    
    ; 把页第0个和第0xc00(768)个目录项的内容写入第0个页表的地址
    or eax, PG_US_U | PG_RW_W | PG_P       ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
    mov [PAGE_DIR_TABLE_POS + 0x0], eax
    mov [PAGE_DIR_TABLE_POS + 0xc00], eax
    
    sub eax, 0x1000                       ; 此时eax是页目录的起始地址
    mov [PAGE_DIR_TABLE_POS + 4092], eax  ; 使最后一个目录项指向页目录表的地址
    
    ; 下面是把 第0个页表的前256个页表项的内容 绑定到 物理地址0-1M
    mov ecx, 256                      ; 1M低端内存 / 每页大小4k = 256
    mov esi, 0
    mov edx, PG_US_U | PG_RW_W | PG_P ; edx表示页表项结构体的内容
.create_pte:                          ; Page Table Entry
    mov [ebx + esi * 4],edx           ; 此时ebx是第0个页表的地址
    add edx,0x1000                    ; 每一个页表项的内容的第31-12位表示物理页地址的31-12位,所以递增0x1000(4096)的效果是把第31-12位递增1,相当于物理地址+4096
    inc esi
    loop .create_pte


    ; 3.把页目录项769-1022指向第1-255个页表,第1-255页表没有绑定物理地址
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000                   ; 0x2000=4096*2 eax+4096*2就是第1个页表的地址
    or eax, PG_US_U | PG_RW_W | PG_P  ; eax表示页目录项结构体的内容
    mov ebx, PAGE_DIR_TABLE_POS       ; ebx位页目录起始地址
    mov ecx, 254                      ; 循环次数254 范围为第769~1022的所有目录项数量
    mov esi, 769                      ; 起始索引
.create_kernel_pde:
    mov [ebx + esi * 4], eax
    inc esi
    add eax, 0x1000                   ; 页目录项结构体的第31-12位是页表物理地址的31-12位 所以递增0x1000(4096)的效果是把第31-12位递增1,相当于移动到下一个页表的位置
    loop .create_kernel_pde
    ret
;---创建页目录及页表 end---

;---把kernel.bin中段拷贝到编译的地址 begin---
kernel_init:
    xor eax, eax
    xor ebx, ebx    ; ebx记录程序头表地址
    xor ecx, ecx    ; cx记录程序头表中的program header数量
    xor edx, edx    ; dx 记录program header尺寸,即e_phentsize

    mov dx, [KERNEL_BIN_BASE_ADDR + 42]  ; dx是Size of program headers 偏移文件42字节处的属性是e_phentsize
    mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; ebx是Start of program headers 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量,其实该值是0x34 = 52
    add ebx, KERNEL_BIN_BASE_ADDR        ; ebx是第一个program header的地址
    mov cx, [KERNEL_BIN_BASE_ADDR + 44]  ; cx是Number of program headers 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header

.each_segment:
    cmp byte [ebx + 0] , PT_NULL ; 若p_type等于 PT_NULL,说明此program header未使用。
    je .PTNULL


    ;为函数memcpy压入参数,参数是从右往左依次压入,函数原型类似于memcpy(dst,src,size)
    push dword [ebx + 16]        ;参数3是MemSiz 偏移16字节的地方是p_filesz,0x0000_0094 0x00000005

    mov eax, [ebx + 4]           ; eax是Offset 距程序头偏移量为4字节的位置是p_offset 0x0000 0x0500
    add eax,KERNEL_BIN_BASE_ADDR ; 参数2是eax为该段的物理地址 加上kernel.bin被加载到的物理地址 (0000 + 0x7_0000) (0x500 + 0x7_0000)
    push eax

    push dword [ebx + 8]         ; 参数1是VirtAddr,偏移程序头8字节的位置是p_vaddr,这就是目的地址 0xc000_1000 0xc000_1500

    call mem_cpy
    add esp,12                   ; 清理栈中压入的三个参数
.PTNULL:
    add ebx, edx                 ; edx为program header的起始地址,加上program header的大小,等于下一个program header的地址
    loop .each_segment
    ret
;---把kernel.bin中段拷贝到编译的地址 end---


;---逐字节拷贝 mem_cpy(dst,src,size) begin---
mem_cpy:
    cld
    push ebp
    mov ebp, esp
    push ecx            ; rep指令用到了ecx,北非ecx
    mov edi, [ebp + 8]  ; dst
    mov esi, [ebp + 12] ; src
    mov ecx, [ebp + 16] ; size
    rep movsb           ; 逐字节拷贝

    ; 恢复环境
    pop ecx
    pop ebp
    ret
;---逐字节拷贝 mem_cpy(dst,src,size) end---

kernel/main.c

// kernel/main.c
// 内核从此处开始


int main(void)
{
    while (1)
        ;
    return;
}

编译

Makefile

BUILD_DIR   = build
ENTRY_POINT = 0xc0001500
TARGET     ?= ${BUILD_DIR}/kernel

AS = nasm
CC = gcc
LD = ld

# 头文件目录
INCDIRS   := include \
# 遍历头文件目录并且加上-I 前缀 include -> -I include
INCLUDE   := $(patsubst %, -I %, $(INCDIRS))

# 源文件目录
SRCDIRS   := kernel

# 遍历源文件目录里的.s文件
SFILES    := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.s))
# 遍历源文件目录里的.c文件
CFILES    := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))

# 去除源文件的目录
SFILENDIR := $(notdir  $(SFILES))
CFILENDIR := $(notdir  $(CFILES))

# 遍历源文件并添加上build/前缀  xxx.s -> build/xxx.o
SOBJS	  := $(patsubst %, ${BUILD_DIR}/%, $(SFILENDIR:.s=.o))
# 遍历源文件并添加上build/前缀  xxx.c -> build/xxx.o
COBJS     := $(patsubst %, ${BUILD_DIR}/%, $(CFILENDIR:.c=.o))
OBJS      := $(SOBJS) $(COBJS)

# 指定.c文件的目录
VPATH	  := $(SRCDIRS)

# 编译时的配置
CFLAGS = -Wall -m32 -fno-stack-protector -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
# 链接时的配置
LDFLAGS =  -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map

# 链接全部.o文件
$(TARGET).bin : $(OBJS)
	$(LD) $(LDFLAGS) $(INCLUDE) $^ -o $@
# 编译全部的.s
$(SOBJS) : obj/%.o : %.s

# 编译全部.c文件
$(COBJS) : ${BUILD_DIR}/%.o : %.c
	$(CC) $(CFLAGS) $(INCLUDE) -o $@ $<


.PHONY : mk_dir bootloader clean all kernel

mk_dir: #创建build目录
	if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi

bootloader: #编译启动内核的文件
	nasm -I include/ -o $(BUILD_DIR)/mbr.bin boot/mbr.s
	nasm -I include/ -o $(BUILD_DIR)/loader.bin boot/loader.s
	dd if=/home/c/tityos/${BUILD_DIR}/mbr.bin of=/home/c/tityos/hd60M.img bs=512 count=1 conv=notrunc
	dd if=/home/c/tityos/${BUILD_DIR}/loader.bin of=/home/c/tityos/hd60M.img bs=512 count=3 seek=2 conv=notrunc

kernel: ${BUILD_DIR}/kernel.bin
	dd if=$(BUILD_DIR)/kernel.bin of=hd60M.img bs=512 count=200 seek=9 conv=notrunc


clean: #删除build目录里的全部文件
	cd $(BUILD_DIR) && rm -f  ./*

# 创建build目录。编译启动内核的文件。
all: mk_dir bootloader kernel

运行

start.sh

# !/bin/bash
# 功能:启动bochs

bin/bochs -f bochsrc.disk

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值