操作系统真象还原:第五章 保护模式进阶-加载内核

修改loader.S

在分页机制开启之前将内核从磁盘加载到内存中
;------------- 加载内核 ----------------
;将内核从磁盘的9号扇区加载的内存的KERNEL_BIN_BASE_ADDR地址处
    mov eax,KERNEL_START_SECTOR
    mov ebx,KERNEL_BIN_BASE_ADDR
    mov ecx,200 
    call rd_disk_m_32

 rd_disk_m_32读取磁盘函数需要增加,在mbr读取磁盘函数rd_disk_m_16的基础上,主要是修改一下bx基址寄存器为ebx,进入保护模式后偏移地址也要用32位的地址

增加加载内核代码kernel_init,将内核代码根据程序头表的描述加载至各段内存中
enter_kernel:
	call kernel_init
	mov esp,0xc009f000
	jmp KERNEL_ENTRY_POINT
;-----------------------------
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 = e_phentsize:偏移文件42字节处的属性是e_phentsize,表示program header的大小
    mov ebx,[KERNEL_BIN_BASE_ADDR + 28]    ;ebx = e_phoff:偏移文件开始部分28字节的地方是e_phoff,表示第一个program header在文件中的偏移量。这里是将e_phoff给ebx而不是KERNEL_BIN_BASE_ADDR + 28的地址
    add ebx,KERNEL_BIN_BASE_ADDR           ;ebx = KERNEL_BIN_BASE_ADDR + e_phoff = 程序头表的物理地址
    mov cx,[KERNEL_BIN_BASE_ADDR + 44]     ;cx = e_phnum:偏移文件开始部分44字节的地方是e_phnum,表示有几个program header

.each_segment:                      ;分析每个段,如果不是空程序类型,则将其拷贝到编译的地址中
    cmp byte [ebx + 0],PT_NULL      ;程序先判断下段类型是不是PT_NULL,表示空段类型
    je .PTNULL                      ;如果p_type = PTNULL(空程序类型),说明此program header未使用,则跳转到下一个段头

    ;为函数mem_cpy(dst,src,size)压入参数,参数从右往左依次压入
    push dword [ebx + 16]           ;push f_filesz:program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数size
    mov eax,[ebx + 4]               ;eax = p_offset:距程序头偏移量为4字节的位置是p_offset
    add eax,KERNEL_BIN_BASE_ADDR    ;eax = KERNEL_BIN_BASE_ADDR + p_offset = 该段的物理地址:加上kernel.bin被加载到的物理地址,eax为该段的物理地址
    push eax                        ;push 该段的物理地址:压入memcpy的第二个参数:源地址
    push dword [ebx + 8]            ;push p_vaddr:压入memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr
    call mem_cpy                    ;调用mem_cpy完成段复制
    add esp,12                      ;清理栈中压入的三个参数,每个4B

.PTNULL:
    add ebx,edx                     ;edx为program header大小,即e_phentsize;每遍历一个段头,就跳转到下一个段头处
    loop .each_segment              ;在此ebx指向下一个program header
    ret

修改启动配置文件boot.inc

 第一行LOADER_STACK_TOP原来是在loader.S中,现在移动到了boot.inc中,方便统一管理

另外下面三个地址标号是为加载我们的内核定义的宏

PT_NULL宏为了后面根据程序头表加载内核进行判断,确定是否应该加载该段

LOADER_STACK_TOP equ LOADER_BASE_ADDR

KERNEL_BIN_BASE_ADDR equ 0x70000     ;内核文件加载到内存中的位置
KERNEL_START_SECTOR equ 0x9          ;内核文件在磁盘中的起始盘区
KERNEL_ENTRY_POINT equ 0xc0001500    ;定义内核可执行代码的入口地址
;-----  程序段的类型定义  ---------
PT_NULL equ 0

loader.S全部代码如下

%include "boot.inc"
section loader vstart=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
				dd	DESC_VIDEO_HIGH4
				
	GDT_SIZE equ $ - GDT_BASE	
	GDT_LIMIT equ GDT_SIZE - 1
	times 60 dq 0
;构建选择子
	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_ptr dw GDT_LIMIT
			dd GDT_BASE
	;人工对齐
	ards_buf times 244 db 0
	ards_nr dw 0

loader_start:
	;int 15h eax=0000e820h,edx=534D4150h获取内存布局
	xor ebx,ebx
	mov edx,0x534d4150
	mov di,ards_buf
.e820_mem_get_loop:
	mov eax,0x0000e820
	mov ecx,20
	int 0x15
	jc .e820_failed_so_try_e801
	
	add di,cx
	inc word [ards_nr]
	cmp ebx,0
	jnz .e820_mem_get_loop
	
	mov cx,[ards_nr]
	mov ebx,ards_buf
	xor edx,edx
.find_max_mem_area:
	mov eax,[ebx]
	add eax,[ebx+8]
	add ebx,20
	cmp edx,eax
	jge .next_ards
	mov edx,eax
	
.next_ards:
	loop .find_max_mem_area
	jmp .mem_get_ok
	
;int 15h ax=e801获取内存大小
.e820_failed_so_try_e801:
	mov ax,0xe801
	int 0x15
	jc .e801_failed_so_try88
	;算出低端15MB内存
	mov cx,0x400
	mul cx
	shl edx,16
	and eax,0x0000FFFF
	or edx,eax
	add edx,0x100000;加上不可探测的1MB
	mov esi,edx
	;16MB以上内存
	xor eax,eax
	mov ax,bx
	mov ecx,0x10000
	mul ecx
	
	add esi,eax
	mov edx,esi
	jmp .mem_get_ok	;edx为总内存大小
	
;int 15h ah=0x88获取内存大小,只能获取64MB之内
.e801_failed_so_try88:
	mov ah,0x88
	int 0x15
	jc .error_hlt
	and eax,0x0000FFFF
	
	mov cx,0x400
	mul cx
	shl edx,16
	or edx,eax
	add edx,0x100000;0x88只返回1MB以上内存
	
.mem_get_ok:
	mov [total_mem_bytes],edx
	
;-------------------------准备进入保护模式-----------------------
;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
	
.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	
;-----------------------------加载kernel-----------------------------
mov eax,KERNEL_START_SECTOR	
mov ebx,KERNEL_BIN_BASE_ADDR
mov ecx,200

call rd_disk_m_32
;----------------------	创建页目录表及页表并初始化页内存位图-----------------------
;创建页目录表及页表并初始化页内存位图
	call setup_page
	
	sgdt [gdt_ptr]	;先备份描述符表地址,一会儿需要用新地址重新加载
	
	;gdt中视频端描述符段基址+0xc0000000
	mov ebx,[gdt_ptr+2]
	or dword [ebx+0x18 +4],0xc0000000
	
	;gdt的基址+0xc0000000
	add dword [gdt_ptr+2],0xc0000000
	
	;栈指针同样映射到内核地址
	add esp,0xc0000000
	
	;页目录地址赋值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]
	
;-----------------以防万一,刷新流水线--------------------------------
jmp SELECTOR_CODE:enter_kernel

enter_kernel:
	call kernel_init
	mov esp,0xc009f000
	jmp KERNEL_ENTRY_POINT
;-----------------------------
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 = e_phentsize:偏移文件42字节处的属性是e_phentsize,表示program header的大小
    mov ebx,[KERNEL_BIN_BASE_ADDR + 28]    ;ebx = e_phoff:偏移文件开始部分28字节的地方是e_phoff,表示第一个program header在文件中的偏移量。这里是将e_phoff给ebx而不是KERNEL_BIN_BASE_ADDR + 28的地址
    add ebx,KERNEL_BIN_BASE_ADDR           ;ebx = KERNEL_BIN_BASE_ADDR + e_phoff = 程序头表的物理地址
    mov cx,[KERNEL_BIN_BASE_ADDR + 44]     ;cx = e_phnum:偏移文件开始部分44字节的地方是e_phnum,表示有几个program header

.each_segment:                      ;分析每个段,如果不是空程序类型,则将其拷贝到编译的地址中
    cmp byte [ebx + 0],PT_NULL      ;程序先判断下段类型是不是PT_NULL,表示空段类型
    je .PTNULL                      ;如果p_type = PTNULL(空程序类型),说明此program header未使用,则跳转到下一个段头

    ;为函数mem_cpy(dst,src,size)压入参数,参数从右往左依次压入
    push dword [ebx + 16]           ;push f_filesz:program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数size
    mov eax,[ebx + 4]               ;eax = p_offset:距程序头偏移量为4字节的位置是p_offset
    add eax,KERNEL_BIN_BASE_ADDR    ;eax = KERNEL_BIN_BASE_ADDR + p_offset = 该段的物理地址:加上kernel.bin被加载到的物理地址,eax为该段的物理地址
    push eax                        ;push 该段的物理地址:压入memcpy的第二个参数:源地址
    push dword [ebx + 8]            ;push p_vaddr:压入memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr
    call mem_cpy                    ;调用mem_cpy完成段复制
    add esp,12                      ;清理栈中压入的三个参数,每个4B

.PTNULL:
    add ebx,edx                     ;edx为program header大小,即e_phentsize;每遍历一个段头,就跳转到下一个段头处
    loop .each_segment              ;在此ebx指向下一个program header
    ret

;------------------------------逐字节拷贝mem_cpy(dst,src,size)---------------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;--------------------------------------------------------------------------------
mem_cpy:
    cld                 ;clean direction,将eflags寄存器中的方向标志位DF置0,这样rep在循环执行后面的字符串指令时,esi和edi根据使用的字符串搬运指令,自动加上所搬运数据的字节大小。
    push ebp
    mov ebp,esp         ;esp是栈顶指针
    push ecx            ;rep指令用到了ecx,但ecx对于外层段的循环还有用,所以先入栈备份
    mov edi,[ebp + 8]   ;dst
    mov esi,[ebp + 12]  ;src
    mov ecx,[ebp + 16]  ;size
    rep movsb           ;逐字节拷贝:movs表示mov string,b表示byte,w表示word,d表示dword。将DS:EI/SI指向的地址处的字节搬运到ES:DI/EI指向的地址去。
                        ;16位环境下源地址指针用SI寄存器,目的地址指针用DI寄存器;32位环境下源地址用SI寄存器,目的地址用EDI寄存器。

    ;恢复环境
    pop ecx
    pop ebp
    ret                 ;在调用ret时,栈顶处的数据是正确的返回地址。一般情况下,我们在函数体中保持push和pop配对使用。

;-----------------------------------创建页目录及页表----------------------------------
setup_page:
;页目录占用的空间逐字节清0
	mov ecx,4096
	mov esi,0
.clear_page_dir:
	mov byte [PAGE_DIR_TABLE_POS + esi],0
	inc esi
	loop .clear_page_dir

;----------------------------页目录项----------	
;创建页目录项(PDE)
;先创建低端4M内存的页目录项,其中第768项和第0项相同,第1023项指向页目录表自己
.create_pde:
	mov eax,PAGE_DIR_TABLE_POS
	add eax,0x1000	;eax为第一个页表的位置及属性
	mov ebx,eax
	
	;将页目录项0和0xc00(第768个)都存为第一个页表的地址
	or eax, PG_US_U | PG_RW_W | PG_P
	mov [PAGE_DIR_TABLE_POS + 0x0],eax	
	mov [PAGE_DIR_TABLE_POS + 0xc00],eax
	
	;使最后一个页目录项指向页目录表自己的地址
	sub eax,0x1000
	mov [PAGE_DIR_TABLE_POS + 4092],eax
	
;-------------------------------页表项----------	
;下面创建页表项(PTE)
	;低端1M内存页表项,1M/4K=256
	mov ecx,256
	mov esi,0
	mov edx,PG_US_U | PG_RW_W | PG_P
.create_pte:
	mov [ebx+esi*4],edx	;ebx=0x101000
	
	add edx,4096
	inc esi
	loop .create_pte

;--------------------------其他内核页目录项----------	
	;创建内核其他页表的PDE
	mov eax,PAGE_DIR_TABLE_POS
	add eax,0x2000	;eax为第二个页表位置
	or eax,PG_US_U | PG_RW_W | PG_P
	mov ebx,PAGE_DIR_TABLE_POS
	mov ecx,254	;769~1022
	mov esi,769
.create_kernel_pde:
	mov [ebx+esi*4],eax
	inc esi
	add eax,0x1000
	loop .create_kernel_pde
	
	ret
	
;--------------------------------------------------
;功能为读取硬盘n个扇区
rd_disk_m_32:
;--------------------------------------------------
	mov esi,eax
	mov di,cx
	;设置要读取的扇区数
	mov dx,0x1f2
	mov al,cl
	out dx,al

	mov eax,esi

	;将LBA地址存入0x1f3-0x1f6
	;LBA地址7-0位
	mov dx,0x1f3
	out dx,al
	;LBA地址15-8位
	mov cl,8
	shr eax,cl
	mov dx,0x1f4
	out dx,al
	;LBA地址23-16位
	shr eax,cl
	mov dx,0x1f5
	out dx,al
	;LBA地址24-27位
	shr eax,cl
	and al,0x0f
	or al,0xe0	;设置7-4位为1110,表示lba模式
	mov dx,0x1f6
	out dx,al

	;向0x1f7端口写入读命令,0x20
	mov dx,0x1f7
	mov al,0x20
	out dx,al

.not_ready:
	nop
	in al,dx
	and al,0x88

	cmp al,0x08
	jnz .not_ready

	;读数据
	mov ax,di
	mov dx,256
	mul dx
	mov cx,ax	;di*512/2,di*256

	mov dx,0x1f0	;读数据端口
.go_on_read:
	in ax,dx
	mov [ebx],ax	;;与rd_disk_m_16相比,主要是基址寄存器bx换成了32位模式ebx
	add ebx,2
	loop .go_on_read

	ret

编译代码并写进磁盘

注意:32位和64位的elf文件中有些数据结构的长度不一样,所以在编译链接中要添加类型参数-m32和-m elf_i386

测试可执行文件,里面就放了个死循环。

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

nasm -I include/ -o mbr.bin mbr.S
dd if=./mbr.bin of=/home/xingchendahai01/bochs/hd60M.img bs=512 count=1 conv=notrunc

nasm -I include/ -o loader.bin loader.S
dd if=./loader.bin of=/home/xingchendahai01/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc

  1. gcc -m32 -c -o kernel/main.o kernel/main.c 
  2. ld -m elf_i386 kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
  3. dd if=kernel/kernel.bin of=/home/xingchendahai01/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

运行代码,启动bochs

 bin/bochs -f bochsrc.disk

可知,程序运行正常,最后一条指令是我们自己写的死循环指令(不放心的话,通过xp命令看下程序入口的代码,如上图 55是push ebp  89e5是mov ebp,esp是main函数堆栈框架的起始)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值