修改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=notruncnasm -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
- gcc -m32 -c -o kernel/main.o kernel/main.c
- ld -m elf_i386 kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
- 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函数堆栈框架的起始)