环境:
virtual-box:版本 6.0.10 r132072 (Qt5.6.2)运行的的ubuntu18.04系统。
nasm汇编器:NASM version 2.13.02
执行效果如下:
bootsect.s代码如下:
; boot.s 程序
; 首先利用 BIOS 中断把内核代码( head 代码)加载到内存 0x10000 处,然后移动到内存 0 处。
; 最后进入保护模式,并跳转到内存 0( head 代码)开始处继续运行。
BOOTSEG equ 0x07c0 ; 引导扇区(本程序)被 BIOS 加载到内存 0x7c00 处。
SYSSEG equ 0x1000 ; 内核( head)先加载到 0x10000 处,然后移动到 0x0 处。
SYSLEN equ 17 ; 内核占用的最大磁盘扇区数。
start:
jmp BOOTSEG:go ; 段间跳转至 0x7c0:go 处。当本程序刚运行时所有段寄存器值
go:
mov ax,cs ; 均为 0。该跳转语句会把 CS 寄存器加载为 0x7c0(原为 0)。
mov ds,ax ; 让 DS 和 SS 都指向 0x7c0 段。
mov ss,ax
mov sp,0x400 ; 设置临时栈指针。其值需大于程序末端并有一定空间即可。
; 加载内核代码到内存 0x10000 开始处。
load_system:
mov dx,0x0000 ; 利用 BIOS 中断 int 0x13 功能 2 从启动盘读取 head 代码。
mov cx,0x0002 ; DH - 磁头号; DL - 驱动器号; CH - 10 位磁道号低 8 位;
mov ax,SYSSEG ; CL - 位 7、 6 是磁道号高 2 位,位 5-0 起始扇区号(从 1 计)。
mov es,ax ; ES:BX - 读入缓冲区位置( 0x1000:0x0000) 。
xor bx,bx ; AH - 读扇区功能号; AL - 需读的扇区数( 17)。
mov ax,0x200+SYSLEN
int 0x13
jnc ok_load ; 若没有发生错误则跳转继续运行,否则死循环。
die:
jmp die
; 把内核代码移动到内存 0 开始处。共移动 8KB 字节(内核长度不超过 8KB)。
ok_load:
cli ; 关中断。
mov ax, SYSSEG ; 移动开始位置 DS:SI = 0x1000:0;目的位置 ES:DI=0:0。
mov ds, ax
xor ax, ax
mov es, ax
mov cx, 0x1000 ; 设置共移动 4K 次,每次移动一个字(dw) 。
sub si,si
sub di,di
rep movsw ; 执行重复移动指令。
; 加载 IDT 和 GDT 基地址寄存器 IDTR 和 GDTR。
mov ax, BOOTSEG
mov ds, ax ; 让 DS 重新指向 0x7c0 段。
lidt [idt_48] ; 加载 IDTR。 6 字节操作数: 2 字节表长度, 4 字节线性基地址。
lgdt [gdt_48] ; 加载 GDTR。 6 字节操作数: 2 字节表长度, 4 字节线性基地址。
; 设置控制寄存器 CR0(即机器状态字),进入保护模式。段选择符值 8 对应 GDT 表中第 2 个段描述符。
mov ax,0x0001 ; 在 CR0 中设置保护模式标志 PE(位 0)。
lmsw ax ; 然后跳转至段选择符值指定的段中,偏移 0 处。
jmp 8:0 ; 注意此时段值已是段选择符。该段的线性基地址是 0。
; 下面是全局描述符表 GDT 的内容。其中包含 3 个段描述符。第 1 个不用,另 2 个是代码和数据段描述符。
;---------------------------------------------------------------------------------------------
; |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
;high |_____________________word3_____________________|__________________word2______________|
;low |_____________________word1_____________________|__________________word0______________|
;
;---------------------------------------------------------------------------------------------
;
;---------------------------------------------------------------------------------------------
; |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
;high |____Base_addr(31~24)___|___________|limit(19~16|___________|___TYPE__|__addr(23~16)__|
;low |_______________Base_addr(15~0)_________________|____________Seg_limit(15~0)__________|
gdt:
dw 0,0,0,0 ; 段描述符 0,不用。每个描述符项占 8 字节。
dw 0x07FF ; 段描述符 1。 8Mb - 段限长值=2047 (2048*4096=8MB)。
dw 0x0000 ; 段基地址=0x00000。
dw 0x9A00 ; 是代码段,可读/执行。
dw 0x00C0 ; 段属性颗粒度=4KB, 80386。
dw 0x07FF ; 段描述符 2。 8Mb - 段限长值=2047 (2048*4096=8MB)。
dw 0x0000 ; 段基地址=0x00000。
dw 0x9200 ; 是数据段,可读写。
dw 0x00C0 ; 段属性颗粒度=4KB, 80386。
; 下面分别是 LIDT 和 LGDT 指令的 6 字节操作数。
idt_48:
dw 0 ; IDT 表长度是 0。
dw 0,0 ; IDT 表的线性基地址也是 0。
gdt_48:
dw 0x7ff ; GDT 表长度是 2048 字节,可容纳 256 个描述符项。
dw 0x7c00+gdt,0 ; GDT 表的线性基地址在 0x7c0 段的偏移 gdt 处。
end:
times 510-($-$$) db 0
dw 0xAA55 ; 引导扇区有效标志。必须处于引导扇区最后 2 字节处。
head.s代码如下:
; head.s 包含 32 位保护模式初始化设置代码、时钟中断代码、系统调用中断代码和两个任务的代码。
; 在初始化完成之后程序移动到任务 0 开始执行,并在时钟中断控制下进行任务 0 和 1 之间的切换操作。
LATCH equ 11930 ; 定时器初始计数值,即每隔 10 毫秒发送一次中断请求。
;0x08 ;00001 0 00b --->GDT1 ; 是代码段选择符.
;0x10 ;00010 0 00b --->GDT2 ; 是数据段描述符.
SCRN_SEL equ 0x18 ;00011 0 00b --->GDT3 ; 屏幕显示内存段选择符。
TSS0_SEL equ 0x20 ;00100 0 00b --->GDT4 ; 任务 0 的 TSS 段选择符。
LDT0_SEL equ 0x28 ;00101 0 00b --->GDT5 ; 任务 0 的 LDT 段选择符。
TSS1_SEL equ 0X30 ;00110 0 00b --->GDT6 ; 任务 1 的 TSS 段选择符。
LDT1_SEL equ 0x38 ;00111 0 00b --->GDT7 ; 任务 1 的 LDT 段选择符。
bits 32 ;nasm instruction:32 bits model.
global startup_32,write_char1
extern main
startup_32:
; 首先加载数据段寄存器 DS、堆栈段寄存器 SS 和堆栈指针 ESP。所有段的线性基地址都是 0。
mov eax,0x10 ; 0x10 是 GDT 中数据段选择符。00010 0 00b --->GDT2
mov ds,ax
lss esp,[init_stack]
; 在新的位置重新设置 IDT 和 GDT 表。
call setup_gdt ; 设置 GDT。
mov eax,0x10 ; 在改变了 GDT 之后重新加载所有段寄存器。
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
lss esp,[init_stack]
after_page_tables:
push 0 ;# These are the parameters to main :-)
push 0
push 0
push L6 ;# return address for main, if it decides to.
push main
jmp setup_paging
L6:
jmp L6
align 4
setup_paging:
ret
; 以下是设置 GDT 和 IDT 中描述符项的子程序。
setup_gdt: ; 使用 6 字节操作数 lgdt_48 设置 GDT 表位置和长度。
lgdt [lgdt_48]
ret
;------------------------------------------------------------------------------------------
; 显示字符子程序。取当前光标位置并把 AL 中的字符显示在屏幕上。整屏可显示 80 X 25 个字符。
;------------------------------------------------------------------------------------------
write_char1:
push ax
push gs ; 首先保存要用到的寄存器, EAX 由调用者负责保存。
push ebx
push ecx
mov ax,'A'
again: mov ecx,0xFFFFF
mov ebx,SCRN_SEL ; 然后让 GS 指向显示内存段( 0xb8000) 。
mov gs,bx
mov bx,[scr_loc] ; 再从变量 scr_loc 中取目前字符显示位置值。
shl ebx,1 ; 因为在屏幕上每个字符还有一个属性字节,因此字符
mov byte [gs:ebx],al ; 实际显示位置对应的显示内存偏移地址要乘 2。
delay: loop delay
shr ebx,1 ; 把字符放到显示内存后把位置值除 2 加 1,此时位置值对
inc ebx ; 应下一个显示位置。如果该位置大于 2000,则复位成 0。
cmp ebx,2000
jb .1
mov ebx,0
inc ax
.1:
mov [scr_loc],ebx ; 最后把这个位置值保存起来( scr_loc),
mov ecx,2000
loop again
pop ebx ; 并弹出保存的寄存器内容,返回。
pop gs
pop ax
ret
scr_loc:
dd 0 ; 屏幕当前显示位置。按从左上角到右下角顺序显示。
align 4
lidt_48:
dw 256*8-1 ; 加载 IDTR 寄存器的 6 字节操作数:表长度和基地址。
dd idt
lgdt_48:
dw (end_gdt-gdt)-1 ; 加载 GDTR 寄存器的 6 字节操作数:表长度和基地址。
dd gdt
align 8
; LDT
;---------------------------------------------------------------------------------------------
; |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
;high |_____________________word3_____________________|__________________word2______________|
;low |_____________________word1_____________________|__________________word0______________|
;
;---------------------------------------------------------------------------------------------
;
;---------------------------------------------------------------------------------------------
; |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
;high |___________entry_offset_addr(31~16)____________|_P|_DPL_|____________________________|
;low |_______________Seg_select______________________|_______entry_offset_addr(15~0)_______|
idt:
times 256 dq 0 ; IDT 空间。共 256 个门描述符,每个 8 字节,占用 2KB。
; 下面是全局描述符表 GDT 的内容。其中包含 3 个段描述符。第 1 个不用,另 2 个是代码和数据段描述符。
;---------------------------------------------------------------------------------------------
; |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
;high |_____________________word3_____________________|__________________word2______________|
;low |_____________________word1_____________________|__________________word0______________|
;
;---------------------------------------------------------------------------------------------
;
;---------------------------------------------------------------------------------------------
; |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
;high |____Base_addr(31~24)___|___________|limit(19~16|___________|___TYPE__|__addr(23~16)__|
;low |_______________Base_addr(15~0)_________________|____________Seg_limit(15~0)__________|
gdt:
dq 0x0000000000000000 ; GDT 表。第 1 个描述符不用。
dq 0x00c09a00000007ff ; 第 2 个是内核代码段描述符。其选择符是 0x08。
dq 0x00c09200000007ff ; 第 3 个是内核数据段描述符。其选择符是 0x10。
dq 0x00c0920b80000002 ; 第 4 个是显示内存段描述符。其选择符是 0x18。
end_gdt:
times 128 dd 0 ; 初始内核堆栈空间。
init_stack: ; 刚进入保护模式时用于加载 SS:ESP 堆栈指针值。
dd init_stack ; 堆栈段偏移位置。
dw 0x10 ; 堆栈段同内核数据段。
main.c代码如下:
extern void write_char1();
int main(int argc, char **argv)
{
while(1) {
write_char1();
}
return 0;
}
Makefile如下:
LDFLAGS=-s -x -M -nostartfiles -m elf_i386 -Ttext 0 -e startup_32
CFLAGS=-m32 -nostdinc -Wall -O
all:
nasm -f bin bootsect.s -o bootsect
nasm -f elf head.s -o head.o
gcc $(CFLAGS) -c main.c -o main.o
ld $(LDFLAGS) -o system head.o main.o > System.map
dd if=bootsect of=boot.img
dd ibs=160 skip=1 seek=1 if=system of=boot.img
clean:
rm bootsect head head.o main.o system System.map -f
这里ibs=160,是从二进制文件看出的偏移值。
objdump -D system反汇编system文件,地址0处的内容为:b8 10 00 00 00
使用UltraEdit编辑器打开system,看到b8 10 00 00 00在a0h处,即160.去掉system的文件头,拷贝到boot.img 的512KB位置后。不同的ld选项命令,这个值可能不同。