操作系统:保护模式 (四)内核的加载
用 GNU C 套件开发生成的内核本质上是一个 ELF 文件头可执行文件。这意味着:
- 生成内核时,需要手动将可重定向文件链接时指定代码段所在线性地址 (高位 0xc0000000-0xc03fffff)
- Loader 将原始 kernel 映像从磁盘读出,存放到内核可用内存的 (较高处) 内核本身不会触及的位置。
- Loader 根据 ELF 文件格式,将 ELF 中的各个段展开到内核空间。
- Loader 将控制权转移到内核。
内核加载地址的策略
- 内核加载地址可以任意在内核空间选定,但不能破坏 loader (0x900 - 0x1500) 所在区域,扩展 BIOS 数据区 (0x9FC00-0x9FFFF), 原始 kernel 映像所在区域。
- 内核加载完毕后可以选择覆盖原始kernel映像,但还是不能破坏 loader (0x900 - 0x1500) 所在区域,扩展 BIOS 数据区 (0x9FC00-0x9FFFF)
注意:此处策略与书中不同
示例:一个完整的 Loader 引导程序:
; Loader
%include "boot.inc"
SECTION LOADER VSTART=LOADER_BASE_ADDRESS
LOADER_STACK_TOP EQU LOADER_BASE_ADDRESS
jmp loader_main
times 16 db 0
; -------------------------
; GDT start from 0x910
; -------------------------
GDT_BASE:
dd 0x00000000
dd 0x00000000
FLAT_CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4
FLAT_DATA_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 ; Reserve space for future use
; ^^^ LOADER_BASE_ADDRESS + 0x200 = 0xb00 ^^^
; -------------------------
; Data
; -------------------------
TOTAL_MEMORY_BYTES_ADDR EQU $
LOADER_DATA_ADDR EQU $
TotalMemoryBytes dd 0
GdtPtr:
dw GDT_LIMIT
dd GDT_BASE
Message db "Failed to get memory", 0
MessageLength equ $ - Message
ArdsBuf:
times 320 db 0 ; 20 * 16
ArdsCnt dw 0
; -------------------------
; Selectors
; -------------------------
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
loader_main:
mov sp, LOADER_STACK_TOP
; -------------------------
; Memory
; -------------------------
xor ebx, ebx
mov edx, 0x534D4150 ; "SMAP"
mov di, ArdsBuf
.try_e820:
mov eax, 0x0000E820
mov ecx, 20
int 0x15
jc .e820_fail
add di, cx
inc word [ArdsCnt]
cmp ebx, 0
jnz .try_e820
mov cx, word [ArdsCnt]
mov ebx, ArdsBuf
xor edx, edx
.find_usable_memory:
mov eax, [ebx]
add eax, [ebx + 8]
add ebx, 20
cmp edx, eax
jge .next_ard
mov edx, eax
.next_ard:
loop .find_usable_memory
jmp .success_get_memory
.e820_fail:
mov ax, Message
mov bx, 0x07
mov cx, MessageLength
xor dx, dx
call printstr
.success_get_memory:
mov [TotalMemoryBytes], edx
; -------------------------
; Protected mode
; -------------------------
; Open A20 gate
in al, 92h
or al, 00000010b
out 92h, al
; Load GDT
lgdt [GdtPtr]
; Enter protected mode
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
jmp dword SELECTOR_CODE:pmain
; ----------------------------------------------------
; printstr: Print a string
; ----------------------------------------------------
; ax: string address
; bx: color
; cx: string length
; dx: cursor position (zero to use next line)
printstr:
cmp dx, 0
jnz .print
mov ah, 3
mov bx, 0
int 10h
mov dl, 0
inc dh
.print:
mov bp, ax
mov ax, 0x1301
int 10h
ret
[bits 32]
pmain:
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
; copy kernel file to KERNEL_BIN_ADDR
mov eax, KERNEL_BIN_SECTOR
mov ebx, KERNEL_BIN_ADDR
mov ecx, 200
call rd_disk_m_32
call startup_page
; 修改 GdtPtr, 重新映射 Gdt 到内核空间
; 并修改显存地址使其位于内核空间
sgdt [GdtPtr]
mov ebx, [GdtPtr + 2]
or dword [ebx + 0x18 + 4], 0xC0000000
add dword [GdtPtr + 2], 0xC0000000
; 将栈指针移到内核空间
add esp, 0xC0000000
mov eax, PAGE_DIR_TABLE_BASE
mov cr3, eax
; open cr0 pg bit
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
lgdt [GdtPtr]
jmp SELECTOR_CODE:enter_kernel
enter_kernel:
call kernel_init
mov esp, KERNEL_STACK_BOTTOM
jmp KERNEL_ENTRY_POINT
hlt
startup_page:
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_BASE + esi], 0
inc esi
loop .clear_page_dir
.create_pde:
mov eax, PAGE_DIR_TABLE_BASE
add eax, 0x1000
mov ebx, eax
or eax, PG_US_U | PG_RW_W | PG_P
; 第 0 PDE 和 第 768 PDE 都指向同一个页表(第0PTE)
; 第 0 PDE 是为了将 0x00000000 - 0x003FFFFF 映射到 0x00000000 - 0x003FFFFF.
; 第 768 PDE 是为了将 0x00000000 - 0x003FFFFF 映射到 0xC0000000 - 0xC03FFFFF.
; 因为我们内核和 loader 位于 低端 4 MB 之内, 而我们规定内核将会映射到虚拟地址的高 3GB 以上 (0xC0000000 - 0xFFFFFFFF)
; 至于 0 PDE 是为了保证,对于 loader 代码 (0 - 0xfffff) ,线性地址和物理地址是一样的。
mov dword [PAGE_DIR_TABLE_BASE + 0x0], eax
mov dword [PAGE_DIR_TABLE_BASE + 0xc00], eax
; 将最后 PDE 设为页目录表的物理地址,这是为了动态操作页表
sub eax, 0x1000
mov dword [PAGE_DIR_TABLE_BASE + 4092], eax
; 创建第 0 PDE 内的所有 PTE
mov ecx, 1024 ; map 4MB
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
mov dword [ebx + esi * 4], edx
add edx, 4096
inc esi
loop .create_pte
; 创建内核其它 PDE
mov eax, PAGE_DIR_TABLE_BASE
add eax, 0x2000
or eax, PG_RW_W | PG_US_U | PG_P
mov ebx, PAGE_DIR_TABLE_BASE
mov ecx, 254 ; 769 - 1022 PDE
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:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
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
;;;;;;; 至此,硬盘控制器便从指定的lba地址(eax)处,读出连续的cx个扇区,下面检查硬盘状态,不忙就能把这cx个扇区的数据读出来
;第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 ;以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,
;在此先用这种方法,在后面内容会用到insw和outsw等
mov dx, 256 ;di为要读取的扇区数,一个扇区有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
; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。
; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位,
; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时,
; 从硬盘上读出的数据会把0x0000~0xffff的覆盖,
; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址,
; 故程序出会错,不知道会跑到哪里去。
; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。
; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式.
; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时,
; 也会认为要执行的指令是32位.
; 当我们在其中任意模式下用了另外模式的寻址方式或操作数大小(姑且认为16位模式用16位字节操作数,
; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67,
; 临时改变当前cpu模式到另外的模式下.
; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位.
; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位.
; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址
; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址.
loop .go_on_read
ret
kernel_init:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
mov dx, [KERNEL_BIN_ADDR + 42]
mov ebx, [KERNEL_BIN_ADDR + 28]
add ebx, KERNEL_BIN_ADDR
mov cx, [KERNEL_BIN_ADDR + 44]
.each_segment:
cmp byte [ebx], PT_NULL
je .PTNULL
; 准备mem_cpy参数
push dword [ebx + 16]
mov eax, [ebx + 4]
add eax, KERNEL_BIN_ADDR
push eax
push dword [ebx + 8]
call mem_cpy
add esp, 12
.PTNULL:
add ebx, edx
loop .each_segment
ret
mem_cpy:
cld
push ebp
mov ebp, esp
push ecx
mov edi, [ebp + 8]
mov esi, [ebp + 12]
mov ecx, [ebp + 16]
rep movsb
pop ecx
pop ebp
ret