📄 head1.txt
字号:
;2005.10.27
;这是被引导扇区代码读取到物理地址0x10000H处的代码,他被编译后写在第2个扇区开始的地方,
; 在这里写代码就不用顾忌512字节的限制了,随便写几个扇区,嘿嘿!
; 从这时候开始已经是在保护模式下了。
; Linus从这里开始就使用了AT&T的汇编语法,我就比照着还是用NASM的语法实现吧!
KRN_BASE equ 0x10000
;注意这里是物理地址,不要少写一个零哦!:)
TSS0_SEL equ 0x20
LDT0_SEL equ 0x28
TSS1_SEL equ 0x30
LDT1_SEL equ 0x38
;全局描述符表在引导扇区的实现中只有三项,第一项当然是空选择符,第二项是基址为0x10000、长度为8M的代码段,
; 第三项是基址为0x10000、长度为8M的数据段,也就是别名技术的代码段了!
; 而在这里的代码中重新设置了全局描述符表,里面留了两个TSS和LDT的位置,
; 现在这里的TSS0_SEL就将要指向新的全局描述符表中的偏移位置为0x20也就是第0个、第1个......第4个描述符!
; (直接说第4个的话,总会误解为从1开始的第4个:) )同样地,LDT0_SEL指向了第5个描述符,随后就是TSS1和LDT1了。
[BITS 32]
mov eax,0x10
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
lss esp,[stack_ptr]
;0x10也就是第2个全局描述符(从0开始算起),这里指向的是基址为KRN_BASE长度为8M的数据段
; 堆栈段也指向这里,根据后面的定义可知,堆底和GDT之间有128个双字的空间!
mov ebx,KRN_BASE
mov ecx,gdt
lea eax,[tss0]
mov edi,TSS0_SEL
call set_base
;设置第0个任务的TSS段描述符的基址,在后面的GDT中这些个描述符是未完全初始化的!
lea eax,[ldt0]
mov edi,LDT0_SEL
call set_base
;同上,这是第0个任务的LDT段!
lea eax,[tss1]
mov edi,TSS1_SEL
call set_base
;同上,设置第1个任务的TSS段描述符的基址!
lea eax,[ldt1]
mov edi,LDT1_SEL
call set_base
;同上,这是第0个任务的LDT段!
;好了,以上操作完成后,GDT中的8个描述符就完整了,可以正常使用了!
call setup_idt
;引导扇区代码中曾有过一个IDT,那是假的,这里重新搞了一个,先执行这个调用把256个中断描述符表通通初始化,
; 使之指向同一个处理过程,那就是ignore_int!然后用lidt指令装入IDT寄存器。
call setup_gdt
;后面的GDT中留了8个全局描述符的位置,
; 第0个是不能用的;
; 第1个指向了基址为0x10000限长为8M的代码段;
; 第2个指向了基址为0x10000限长为8M的数据段;
; 第3个很特别,指向了显存的位置,即基址为0xB80000处限长为8K,准备用在直接屏幕显示的新中断调用上;
; 第4个用于TSS0;
; 第5个用于LDT0;
; 第6个用于TSS1;
; 第7个用于LDT1;
;这里的setup_gdt实现很简单的功能,就是装入GDT寄存器:
; lgdt [lgdt_opcode]
;刚刚装入了两个重要的寄存器GDTR和IDTR,现在当然得重新设置一下各个段寄存器了!
mov eax,0x10
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
lss esp,[stack_ptr]
;现在又来了难懂的代码,设置时钟芯片8253!好象是设置成每秒产生100次中断吧...
mov al,0x36
mov edx,0x43
out dx,al
mov eax,11930
mov edx,0x40
out dx,al
mov al,ah
out dx,al
;设置0x20时钟中断和0x80系统调用中断描述符...
mov eax,0x00080000
mov ax,timer_interrupt
mov dx,0x8E00
mov ecx,0x20
lea esi,[idt+0x100]
;第0x20号中断(也就是时钟中断)在中断描述符表中的位置= 0x20*8=0x100!
; Linus的源代码在这里用AT&T的汇编语法是这样写的: lea idt(,ecx,8),esi ,但我不知道怎么用 NASM 的语法实现,
; 所以干脆就直接把乘的结果写出来了 :)
mov [esi],eax
mov [esi+4],edx
mov ax,system_interrupt
mov dx,0xEF00
mov ecx,0x80
lea esi,[idt+0x400]
;同上,第0x80号中断(也就是系统中断)在中断描述符表中的位置= 0x80*8=0x400!
mov [esi],eax
mov [esi+4],edx
;umask the timer interrupt,难懂的代码,不知道什么意思,和硬件有关,启动实时时钟???
mov edx,0x21
in al,dx
and al,0xFE
out dx,al
;用模拟中断返回的方式转到级别3的任务0中去运行!为什么这样呢?可能是因为核心不算是一个完整的任务,所以无法使用任务切换吧...
; 这里的代码也有点像手动切换的意思...
pushF
and dword [esp],0xFFFFBFFF
popF
mov eax,TSS0_SEL
ltr ax
mov eax,LDT0_SEL
lldt ax
mov dword [current],0
sti
;开中断,此时实时时钟的中断已经可以响应了...
push 0x17
push stack0_ptr
pushF
push 0x0F
push task0
iret
;这里使用的是核心自身的堆栈 stack_ptr ,里面的数据如下:
; 116C <=== 堆顶,是EIP,任务0的代码段的偏移地址
; 000F <=== CS ,任务0的代码段选择子
; 0282 <=== 在任务0中用的EFLAGS寄存器的值
; 1380 <=== 在任务0中用的堆栈段的ESP,也就是 stack0_ptr
; 0017 <=== 在任务0中用的堆栈段的SS,换成二进制 0001 0111,是指向当前局部描述符表中的第0x10偏移处(第2位为1嘛),也就是任务0的数据段
;
;执行 iret 指令,模拟了中断返回的效果.结果是SS、ESP、CS、EIP、EFLAGS被设置,且CS中指示了当前运行在3这个级别上,
; 这不同于任务切换,任务切换是要进行一系列操作的,譬如把当前寄存器的值写入TSS中、把下一条代码的地址写入TSS的EIP域中等等.
setup_gdt:
lgdt [lgdt_opcode]
ret
setup_idt:
lea edx,[ignore_int]
mov eax,0x00080000
mov ax,dx
;EAX寄存器中高16位存放了下面要用到的段选择子0x0008,也就是指向GDT中的第1个代码段描述符,
; 基址为0x10000,限长为8M
;EAX寄存器中低16位存放了下面要用到的偏移地址 ignore_int 。
mov dx,0x8E00
;这里是下面要用到的属性字节,0x8E00的意思是"有效的386中断门,特权级为0"。
lea edi,[idt]
;EDI指向了中断描述符表
mov ecx,256
;循环256次,设置256个中断描述符
rp_sidt:
mov [edi],eax
mov [edi+4],edx
;中断描述符表中的每一个描述符的格式是:
; 1、每个描述符占8个字节;
; 2、第0、1个字节是偏移地址的低8位;
; 3、第2、3个字节是段选择子;
; 4、第4、5个字节是属性字节;
; 5、第6、7个字节是偏移地址的高8位。
add edi,8
dec ecx
jne rp_sidt
lidt [lidt_opcode]
ret
set_base:
add eax,ebx
;EAX中现在存放的是将要放入描述符中的32位基址
add edi,ecx
;EDI中指向要设置的那个描述符
;描述符中第0到第7个字节中,基址的低16位在第2、3个字节中,
; 高16位中的低8个字节在第4个字节中,
; 高16位中的高8个字节在第7个字节中,
mov [edi+2],ax
;设置了低16位
ror eax,16
;把EAX中的高16位转移到AX中
mov [edi+4],al
;设置了高16位中的低8位
mov [edi+7],ah
;设置了高16位中的高8位
ror eax,16
;还原了EAX,整个调用结束后,EAX寄存器中的值未变!
ret
write_char:
;调用本程序前,把将要显示的字符放到AL寄存器中,同时DS选择子要指向全局描述符表中第2个数据段,
; 因为本子程序中用到了变量 scr_loc ,该变量是在这个段中的!
push gs
push ebx
push eax
;保存在这个子过程中将要用到的寄存器
mov ebx,0x18
;这是第3个全局描述符,指向显存0xB80000处
mov gs,bx
;现在GS段指向了显存!
mov ebx,[scr_loc]
;scr_loc在后面定义为了一个 long 型的数据,初始值为0。
; Linus的源代码在这里是 movl scr_loc,%bx, 但是scr_loc是LONG啊,
; 难道他不担心数据溢出么?奇怪,这里我为保险起见,还是用了 EBX!
shl ebx,1
mov [gs:ebx],al
shr ebx,1
;把要显示的字符(在AL寄存器中)写入了scr_loc处,因为在显存中一个字符要2个字节来说明,
; 第一个字节是显示的属性,第二个字节才是要显示的字符,这里跳过了属性字节不改变,而只是改变字符字节,很有意思!
inc ebx
;因为显示了一个字节,所以位置要加1,以便后面的显示
;上面使用了 SHL 和 SHR 指令,所以要显示第1个字符,就把字符写入显存中第0*2的位置,
; 显示第2个字符,就把字符写入显存中第1*2的位置,
; 显示第3个字符,就把字符写入显存中第2*2的位置,以此类推。
cmp ebx,2000
jb x1
;小于2000则跳转,显示超过了2000个字符后,光标位置清零,不然显存的那个段(0xB8000)的 8K 长度可能不够的喔,呵呵! :)
; 另外,从这里可以看出,2000=0x7D0,所以长度是超出了BX的范围的,
; Linus是故意这样的么,还是疏忽了呢?呵呵...:)
mov ebx,0
x1:
mov [scr_loc],ebx
pop eax
pop ebx
pop gs
ret
align 2
ignore_int:
;这是缺省的中断处理程序,256个中断描述符均指向这里
push ds
push eax
mov eax,0x10
mov dx,ax
mov eax,67
;以上设置了调用write_char的两个参数: AL和DS。
; 67=字符C的 ASCII码。
call write_char
pop eax
pop ds
iret
align 2
timer_interrupt:
;这是时钟中断处理程序,本系统的关键的调度程序
push ds
push edx
push ecx
push ebx
push eax
mov eax,0x10
mov ds,ax
mov al,0x20
out 0x20,al
;开中断,因为一进入这个中断程序,系统自动屏蔽了这个中断!没办法,这是 x86 体系规定了的! :(
mov eax,1
cmp [current],eax
je x2
mov [current],eax
jmp TSS1_SEL:0
jmp y2 ;在调用上面一条指令进行切换时,CPU自动把这条指令的地址写入了任务0的TSS中的EIP域中,
; 当切换回任务0的时候将在这里继续进行...
x2:
mov dword [current],0
jmp TSS0_SEL:0
y2:
pop eax
pop ebx
pop ecx
pop edx
pop ds
iret
align 2
system_interrupt:
;这是本系统中唯一的一个系统调用的实现代码,
push ds
push edx
push ecx
push ebx
push eax
mov edx,0x10
mov ds,dx
call write_char
;这里没有设置要打印的字符,因为这是系统调用,调用者会在 AL 寄存器中存放调用的参数,也就是要显示的字符
pop eax
pop ebx
pop ecx
pop edx
pop ds
iret
current dd 0
scr_loc dd 0
align 2
dw 0
;很显然,以上指令是为了下面的数据能双字对齐。
lidt_opcode:
;这里定义的6个字节是给 IDT寄存器 用的,使用的指令是 lidt lidt_opcode
dw 256*8-1
dd idt+KRN_BASE
align 2
dw 0
;同样地,以上指令是为了下面的数据能双字对齐。
lgdt_opcode:
;这里定义的6个字节是给 GDT寄存器 用的,使用的指令是 lgdt lgdt_opcode
dw (end_gdt-gdt)-1
dd gdt+KRN_BASE
align 3
idt:
times 256 dd 0
times 256 dd 0
gdt:
dw 0x0000,0x0000,0x0000,0x0000 ;空选择子
dw 0x07FF,0x0000,0x9A01,0x00C0 ;基址 0x10000 限长 8M 的代码段
dw 0x07FF,0x0000,0x9201,0x00C0 ;基址 0x10000 限长 8M 的数据段
dw 0x0002,0x8000,0x920B,0x00C0 ;基址 0xB8000 限长 12K 的显存数据段
dw 0x0068,0x0000,0xE901,0x0000 ;对应于TSS0的描述符,基址暂定0x10000,但会被设置为指向 tss0 处,限长为0x68,即102个字节。
dw 0x0040,0x0000,0xE201,0x0000 ;对应于LDT0的描述符,基址暂定0x10000,但会被设置为指向 ldt0 处,限长为0x40,即64个字节。
dw 0x0068,0x0000,0xE901,0x0000 ;对应于TSS1的描述符,基址暂定0x10000,但会被设置为指向 tss1 处,限长为0x68,即102个字节。
dw 0x0040,0x0000,0xE201,0x0000 ;对应于LDT1的描述符,基址暂定0x10000,但会被设置为指向 ldt1 处,限长为0x40,即64个字节。
end_gdt:
times 128 dd 0
;给堆栈使用
stack_ptr:
dd stack_ptr
dw 0x10
align 3
;
ldt0:
dw 0x0000,0x0000,0x0000,0x0000
dw 0x03FF,0x0000,0xFA01,0x00C0 ;基址为0x10000、限长为4M字节、DPL为3的代码段
dw 0x03FF,0x0000,0xF201,0x00C0 ;基址为0x10000、限长为4M字节、DPL为3的数据段
tss0:
dd 0
dd stack0_krn_ptr,0x10 ;任务0的核心态使用的堆栈,就紧跟在 TSS0 的后面!好像 Linux0.11也和这个类似...
dd 0,0
dd 0,0
dd 0
dd task0 ;确保第一次切换到任务0的时候EIP从这里取值,所以就从 task0 处开始运行,
; 因为任务0的基址为 0x10000,和核心一样,不指定这个的话,
; 第一次切换进来就会跑去执行 0x10000处的核心代码了,
; 除非ldt0中的代码段描述符中的基址改成 task0 处的线性地址,
; 那这里就可以设为0.
dd 0x200
dd 0,0,0,0
dd stack0_ptr,0,0,0
dd 0x17,0x0F,0x17,0x17,0x17,0x17
dd LDT0_SEL
dd 0x08000000
times 128 dd 0
stack0_krn_ptr:
dd 0
align 3
;
ldt1:
dw 0x0000,0x0000,0x0000,0x0000
dw 0x03FF,0x0000,0xFA01,0x00C0
dw 0x03FF,0x0000,0xF201,0x00C0
tss1:
dd 0
dd stack1_krn_ptr,0x10
dd 0,0
dd 0,0
dd 0
dd task1
dd 0x200
dd 0,0,0,0
dd stack1_ptr,0,0,0
dd 0x17,0x0F,0x17,0x17,0x17,0x17
dd LDT1_SEL
dd 0x08000000
times 128 dd 0
stack1_krn_ptr:
dd 0
task0:
mov eax,0x17
mov ds,ax
mov al,'X'
int 0x80
mov ecx,0xFFF
x3:
loop x3
jmp task0
times 128 dd 0
stack0_ptr:
dd 0
task1:
mov eax,0x17
mov ds,ax
mov al,'M'
int 0x80
mov ecx,0xFFF
x4:
loop x4
jmp task1
times 128 dd 0
stack1_ptr:
dd 0
;2005.10.30 晚上23点47分终于完成了调试!哈哈!真是开心,Linus 当年也是这样的心情么?呵呵! :)
; 通过调试学到了很多细节,这几天没有白忙!调试代码很繁琐,但确实很有乐趣! :)
;数据结构
;GDT[里面有8个全局描述符]:
;
; 第0个[0x00]描述符==>>空选择子
; 第1个[0x08]描述符==>>基址 0x10000 限长 8M 的代码段
; 第2个[0x10]描述符==>>基址 0x10000 限长 8M 的数据段
; 第3个[0x18]描述符==>>基址 0xB8000 限长 12K 的显存数据段
;
; 第4个[0x20]描述符==>>指向一个系统段描述符,也就是第0个任务的TSS描述符,基址设置为 tss0 处,限长为0x68,即102个字节。
; 第5个[0x28]描述符==>>指向一个系统段描述符,也就是第0个任务的LDT描述符,基址设置为 ldt0 处,限长为0x40,即64个字节。
; 第6个[0x30]描述符==>>指向一个系统段描述符,也就是第1个任务的TSS描述符,基址设置为 tss1 处,限长为0x68,即102个字节。
; 第7个[0x38]描述符==>>指向一个系统段描述符,也就是第1个任务的LDT描述符,基址设置为 ldt1 处,限长为0x40,即64个字节。
;
;LDT0[第0个任务的局部描述符表,里面有3个描述符]:
;
; 第0个[0x00]描述符==>>空选择子
; 第1个[0x08]描述符==>>基址为0x10000、限长为4M字节、DPL为3的代码段
; 第2个[0x10]描述符==>>基址为0x10000、限长为4M字节、DPL为3的数据段
;TSS0[第0个任务的任务状态段,紧跟着放了一个长度为128个双字的核心堆栈,供0级使用]
;
;LDT1[第1个任务的局部描述符表,和LDT0一样]:
;
;TSS1[第1个任务的任务状态段,紧跟着放了一个长度为128个双字的核心堆栈,供0级使用]
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -