一、4.主引导程序与内核

image-20230802144005579

内核的结构:

  1. 内核程序的统计信息
  2. 内核的公用例程段(各种可重复使用的子过程)
  3. 内核的数据段
  4. 内核的代码段(内核自己执行、控制内存和用户程序)

主引导程序

常数和数据空间分配

core_base_address equ 0x00040000   ;常数,内核加载的起始内存地址 
core_start_sector equ 0x00000001   ;常数,内核的起始逻辑扇区号 

pgdt	dw 0 ;GDT的界限值
		dd 0x00007e00 ;GDT的物理地址

计算GDT逻辑段地址

 ;计算GDT所在的逻辑段地址
 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位物理地址 
 xor edx,edx
 mov ebx,16
 div ebx ;分解成16位逻辑地址 

mov ds,eax ;令DS指向该段以进行操作
mov ebx,edx ;段内起始偏移地址 

创建描述符表

;跳过0#号描述符的槽位 

;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xFFFFF
mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符 

;创建2#描述符,保护模式下主引导程序代码段描述符
mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,界限0x1FF 
mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符 

;建立3#描述符,保护模式下的堆栈段描述符     
mov dword [ebx+0x18],0x7c00fffe  ;基地址为0x00007C00,界限0xFFFFE 
mov dword [ebx+0x1c],0x00cf9600  ;粒度为4KB 

;建立4#描述符,保护模式下的文本显示缓冲区描述符   
mov dword [ebx+0x20],0x80007fff    ;基地址为0x000B8000,界限0x07FFF 
mov dword [ebx+0x24],0x0040920b    ;粒度为字节

mov word [cs: pgdt+0x7c00],39      ;描述符表的界限  

;初始化描述符表寄存器GDTR
lgdt [cs: pgdt+0x7c00]

打开A20地址线

in al,0x92                         ;南桥芯片内的端口 
or al,0000_0010B
out 0x92,al                        ;打开A20

进入保护模式

cli ;中断机制尚未工作

mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位

jmp dword 0x0010:flush ;16位的描述符选择子:
bits 32

设置数据段和栈段

flush:                                  
    mov eax,0x0008 ;加载数据段(0..4GB)选择子
    mov ds,eax

    mov eax,0x0018 ;加载堆栈段选择子 
    mov ss,eax
    xor esp,esp ;堆栈指针置0 

加载内核程序的起始扇区信息

mov edi,core_base_address 
mov eax,core_start_sector ; 内核程序所在起始扇区号
mov ebx,edi ;起始地址 
call read_hard_disk_0 ;以下读取程序的起始部分(一个扇区) 

判断内核程序大小

mov eax,[edi] ;取出core_length
xor edx,edx 
mov ecx,512 ;512字节每扇区
div ecx ;商为内核程序所占的扇区数

or edx,edx
jnz @1 ;未除尽,因此结果比实际扇区数少1 
dec eax ;已经读了一个扇区,扇区总数减1 
@1:
    or eax,eax ;考虑实际长度≤512个字节的情况 
    jz setup ;EAX=0 ?

读取内核程序的剩余扇区

mov ecx,eax ;32位模式下的LOOP使用ECX
mov eax,core_start_sector
inc eax ;从下一个逻辑扇区接着读
@2:
    call read_hard_disk_0
    inc eax
    loop @2 ;循环读,直到读完整个内核 

获取GDT基地址

image-20230802184624483

setup:
    mov esi,[0x7c00+pgdt+0x02] ;不可以在代码段内寻址pgdt,但可以通过4GB的段来访问

建立内核中各段描述符

image-20230802184746647

;建立5#描述符,公用例程段描述符
mov eax,[edi+0x04] ;公用例程代码段起始汇编地址
mov ebx,[edi+0x08] ;核心数据段汇编地址
sub ebx,eax ; 核心数据段起始汇编地址 - 公用例程段起始汇编地址 = 公用例程段长度
dec ebx ;公用例程段界限 
add eax,edi ;公用例程段基地址
mov ecx,0x00409800 ;字节粒度的代码段描述符
call make_gdt_descriptor ;构建描述符 EDX:EAX
mov [esi+0x28],eax ;安装描述符低位
mov [esi+0x2c],edx ;高位

;建立6#描述符,核心数据段描述符
mov eax,[edi+0x08] ;核心数据段起始汇编地址
mov ebx,[edi+0x0c] ;核心代码段汇编地址 
sub ebx,eax
dec ebx ;核心数据段界限
add eax,edi ;核心数据段基地址
mov ecx,0x00409200 ;字节粒度的数据段描述符 
call make_gdt_descriptor
mov [esi+0x30],eax
mov [esi+0x34],edx 

;建立7#描述符,核心代码段描述符
mov eax,[edi+0x0c] ;核心代码段起始汇编地址
mov ebx,[edi+0x00] ;程序总长度
sub ebx,eax
dec ebx ;核心代码段界限
add eax,edi ;核心代码段基地址
mov ecx,0x00409800 ;字节粒度的代码段描述符
call make_gdt_descriptor
mov [esi+0x38],eax
mov [esi+0x3c],edx
mov word [0x7c00+pgdt],63 ;修改描述符表的界限
lgdt [0x7c00+pgdt] ;修改GDTR

image-20230802200421838

跳转到内核执行

jmp far [edi+0x10] ;定位到内核入口点程序

子过程

读取硬盘扇区
read_hard_disk_0:   ;从硬盘读取一个逻辑扇区
                    ;EAX=逻辑扇区号
                    ;DS:EBX=目标缓冲区地址
                    ;返回:EBX=EBX+512 记录下一次该存放的偏移地址
	push eax 
	push ecx
	push edx

    push eax

    mov dx,0x1f2
    mov al,1
    out dx,al ;读取的扇区数

    inc dx ;0x1f3
    pop eax
    out dx,al ;LBA地址7~0

    inc dx ;0x1f4
    mov cl,8
    shr eax,cl
    out dx,al ;LBA地址15~8

    inc dx ;0x1f5
    shr eax,cl
    out dx,al ;LBA地址23~16

    inc dx ;0x1f6
    shr eax,cl
    or al,0xe0 ;第一硬盘  LBA地址27~24
    out dx,al

    inc dx ;0x1f7
    mov al,0x20 ;读命令
    out dx,al

    .waits:
    in al,dx
    and al,0x88
    cmp al,0x08
    jnz .waits ;不忙,且硬盘已准备好数据传输 

    mov ecx,256 ;总共要读取的字数
    mov dx,0x1f0
    .readw:
    in ax,dx
    mov [ebx],ax
    add ebx,2
    loop .readw

    pop edx
    pop ecx
    pop eax

    ret
构造描述符
make_gdt_descriptor:    ;构造描述符
                        ;输入:EAX=线性基地址
                        ;      EBX=段界限
                        ;      ECX=属性(各属性位都在原始位置,其它没用到的位置0) 
                        ;返回:EDX:EAX=完整的描述符
    mov edx,eax
    shl eax,16
    or ax,bx ;或上段界限,描述符前32位(EAX)构造完毕

    and edx,0xffff0000 ;清除基地址中无关的位
    rol edx,8 ;将段基地址分割成两段
    bswap edx ;装配基址的31~24和23~16  (80486+)
    ; A  B  C  D 每个字母都是一个8位数据
    ; D  C  B  A

    xor bx,bx ; 清零低16位,剩下16~19位的段界限
    or edx,ebx ;装配段界限的高4位

    or edx,ecx ;装配属性

    ret

内核程序

常数和数据空间分配

core_code_seg_sel     equ  0x38    ;内核代码段选择子
core_data_seg_sel     equ  0x30    ;内核数据段选择子 
sys_routine_seg_sel   equ  0x28    ;系统公共例程代码段的选择子 
video_ram_seg_sel     equ  0x20    ;视频显示缓冲区的段选择子
core_stack_seg_sel    equ  0x18    ;内核堆栈段选择子
mem_0_4_gb_seg_sel    equ  0x08    ;整个0-4GB内存的段的选择子

头部信息

core_length      dd core_end ;内核程序总长度#00

sys_routine_seg  dd section.sys_routine.start ;系统公用例程段位置#04

core_data_seg    dd section.core_data.start ;内核数据段位置#08

core_code_seg    dd section.core_code.start ;内核代码段位置#0c

core_entry        dd start ;内核代码段入口点#10
				dw core_code_seg_sel ;内核代码段选择子

加载用户程序一个扇区

mov esi,50 ;用户程序位于逻辑50扇区 
call load_relocate_program
load_relocate_program: ;加载并重定位用户程序
                       ;输入:ESI=起始逻辑扇区号
                       ;返回:AX=指向用户程序头部的选择子 
        push ebx
        push ecx
        push edx
        push esi
        push edi

        push ds
        push es

        mov eax,core_data_seg_sel ;内核数据段选择子
        mov ds,eax ;切换DS到内核数据段

        mov eax,esi ;读取用户程序头部数据 
        mov ebx,core_buf ;数据段中的一个缓冲区
        call sys_routine_seg_sel:read_hard_disk_0

计算程序大小

条件传送指令

;若EAX和EDX的值不同,则将EDX的值传送到EAX
;传统方法
cmp eax, edx
jz next ;使用跳转,大大降低程序运行速度,流水线编译的指令有一分支会被遗弃
mov eax, edx
next:
	...

;新的方法
cmp eax, edx ;影响标志位ZF
cmovne eax, edx ; 不相等则传送 edx->eax
;cmov... r, r/m
        mov eax,[core_buf] ;用户程序头部信息中的程序尺寸
        mov ebx,eax
        and ebx,0xfffffe00 ;低9位置0,使之512字节对齐(能被512整除的数,低9位都为0)
        add ebx,512 ;低9位置1
        test eax,0x000001ff ;测试eax低9位是否是0,即程序的大小正好是512的倍数吗? 
        cmovnz eax,ebx ;不是。使用凑整(低9位置1)的结果 

内存分配并加载用户程序

        mov ecx,eax ;实际需要申请的内存数量
        call sys_routine_seg_sel:allocate_memory
        
        mov ebx,ecx ;申请到的内存首地址ecx
        push ebx ;保存该首地址 
        xor edx,edx
        mov ecx,512
        div ecx ;用户程序的总字节数eax(已对齐512) / 512 = 用户程序的总扇区数
        mov ecx,eax ;总扇区数 

        mov eax,mem_0_4_gb_seg_sel ;切换DS到0-4GB的段
        mov ds,eax

        mov eax,esi ;ESI=起始逻辑扇区号,调用read_hard_disk_0过程需要eax=起始逻辑扇区号
    .b1:
        call sys_routine_seg_sel:read_hard_disk_0
        inc eax
        loop .b1 ;循环读,直到读完整个用户程序

创建描述符

        ;建立程序头部段描述符
        pop edi ;恢复程序装载的首地址 
        mov eax,edi ;程序头部起始线性地址
        mov ebx,[edi+0x04] ;段长度
        dec ebx ;段界限 
        mov ecx,0x00409200 ;字节粒度的数据段描述符
        call sys_routine_seg_sel: make_seg_descriptor
        call sys_routine_seg_sel: set_up_gdt_descriptor
        mov [edi+0x04],cx ;将头部段选择子写入用户程序头部段中记录头部长度的位置   

        ;建立程序代码段描述符
        mov eax,edi
        add eax,[edi+0x14] ;程序起始位置+代码段偏移位置=代码起始物理地址
        mov ebx,[edi+0x18] ;段长度
        dec ebx ;段界限
        mov ecx,0x00409800 ;字节粒度的代码段描述符
        call sys_routine_seg_sel: make_seg_descriptor
        call sys_routine_seg_sel: set_up_gdt_descriptor
        mov [edi+0x14],cx ;程序代码段选择子覆盖代码段起始地址

        ;建立程序数据段描述符
        mov eax,edi
        add eax,[edi+0x1c] ;数据段起始线性地址
        mov ebx,[edi+0x20] ;段长度
        dec ebx ;段界限
        mov ecx,0x00409200 ;字节粒度的数据段描述符
        call sys_routine_seg_sel: make_seg_descriptor
        call sys_routine_seg_sel: set_up_gdt_descriptor
        mov [edi+0x1c],cx

        ;建立程序堆栈段描述符
        mov eax, edi
        add eax, [edi+0x08]; stack_seg
        mov ebx, [edi+0x0c]
        dec ebx
        mov ecx, 0x00409200 ;字节粒度,向上扩展的数据段描述符
        call sys_routine_seg_sel: make_seg_descriptor
        call sys_routine_seg_sel: set_up_gdt_descriptor
        mov [edi+0x08],cx
        ; PS:这里向上扩展的栈段代码我无法运行,用向下扩展的就可以
        ;建立程序堆栈段描述符
        ;mov ecx,[edi+0x0c]                 ;4KB的倍率 
        ;mov ebx,0x000fffff
        ;sub ebx,ecx                        ;得到段界限
        ;mov eax,4096                        
        ;mul dword [edi+0x0c]                         
        ;mov ecx,eax                        ;准备为堆栈分配内存 
        ;call sys_routine_seg_sel:allocate_memory
        ;add eax,ecx                        ;得到堆栈的高端物理地址 
        ;mov ecx,0x00c09600                 ;4KB粒度的堆栈段描述符
        ;call sys_routine_seg_sel:make_seg_descriptor
        ;call sys_routine_seg_sel:set_up_gdt_descriptor
        ;mov [edi+0x08],cx
        
        ;向下`00CF9690_1000F7FF`  0000_0000_1100_1111_1001_0110_1001_0000__0001_0000_0000_0000_1111_0111_1111_1111

		;向上`00409210_076807FF`  0000_0000_0100_0000_1001_0010_0001_0000__0000_0111_0110_1000_0000_0111_1111_1111

        mov ax,[edi+0x04] ;取出用户程序头部的选择子,用于返回

        pop es ;恢复到调用此过程前的es段 
        pop ds ;恢复到调用此过程前的ds段

        pop edi
        pop esi
        pop edx
        pop ecx
        pop ebx

        ret

保存栈指针寄存器并进入用户程序

mov [esp_pointer],esp ;临时保存堆栈指针

mov ds,ax ;用户程序头部段选择子

jmp far [0x08] ;控制权交给用户程序(start入口点)
			   ;堆栈可能切换 
			   ;不使用call far,避免内核代码段地址压栈,内核和用户程序隔离

用户程序返回点

return_point: ;用户程序返回点
         mov eax,core_data_seg_sel ;使ds指向内核数据段
         mov ds,eax

         mov eax,core_stack_seg_sel ;切换回内核自己的堆栈
         mov ss,eax 
         mov esp,[esp_pointer]

         mov ebx,message_6
         call sys_routine_seg_sel:put_string

         ;这里可以放置清除用户程序各种描述符的指令
         ;也可以加载并启动其它程序
       
         hlt

子过程

内存分配
;简单的内存分配,每次分配是接着上次分配的位置接着分配
allocate_memory: ;分配内存
                 ;输入:ECX=希望分配的字节数
                 ;输出:ECX=这次分配的起始线性地址 
         push ds
         push eax
         push ebx
      
         mov eax,core_data_seg_sel ;设置ds为内核程序数据段,为了访问ram_alloc存放下次内存分配时的起始地址
         mov ds,eax
      
         mov eax,[ram_alloc]
         add eax,ecx ;下一次分配时的起始地址为eax
      
         ;这里应当有检测可用内存数量的指令
          
         mov ecx,[ram_alloc] ;返回分配的起始地址

		;因为地址线设计,长度为双字的数据建议保存在能被4整除的地址上
		;让eax的值与4字节对齐,这样能提高内存访问效率
         mov ebx,eax ;备份
         and ebx,0xfffffffc ;清零低3位
         add ebx,4 ;强制与4对齐,使得可以被4整除
         test eax,0x00000003 ;检测是否是4字节对齐,下次分配的起始地址最好是4字节对齐
         cmovnz eax,ebx ;如果没有对齐,则选择强制对齐的 
         
         mov [ram_alloc],eax ;下次从该地址分配内存
                             ;cmovcc指令可以避免控制转移 
         pop ebx
         pop eax
         pop ds

         retf
构造段描述符
make_seg_descriptor: ;构造存储器和系统的段描述符
                     ;输入:EAX=线性基地址
                     ;      EBX=段界限
                     ;      ECX=属性。各属性位都在原始位置,无关的位清零 
                     ;返回:EDX:EAX=描述符
         mov edx,eax
         shl eax,16
         or ax,bx                           ;描述符前32位(EAX)构造完毕

         and edx,0xffff0000                 ;清除基地址中无关的位
         rol edx,8
         bswap edx                          ;装配基址的31~24和23~16  (80486+)

         xor bx,bx
         or edx,ebx                         ;装配段界限的高4位

         or edx,ecx                         ;装配属性

         retf
安装段描述符
set_up_gdt_descriptor: ;在GDT内安装一个新的描述符
                       ;输入:EDX:EAX=描述符 
                       ;输出:CX=描述符的选择子
         push eax
         push ebx
         push edx
      
         push ds
         push es
      
         mov ebx,core_data_seg_sel ;切换到内核数据段
         mov ds,ebx

         sgdt [pgdt] ;从GDTR中读出描述符表地址(32位)和界限值(16位),以便开始处理GDT

         mov ebx,mem_0_4_gb_seg_sel ;0~4G内存段选择子
         mov es,ebx

         movzx ebx,word [pgdt] ;将右操作数扩充适当位数后传送到左操作数寄存器内,GDT界限 
         inc bx ;GDT总字节数,也是下一个描述符偏移 
         add ebx,[pgdt+2] ;下一个描述符的线性地址 
      
         mov [es:ebx],eax
         mov [es:ebx+4],edx
      
         add word [pgdt],8 ;增加一个描述符的大小
      
         lgdt [pgdt] ;对GDT的更改生效 
       
         mov ax,[pgdt] ;得到GDT界限值
         xor dx,dx
         mov bx,8
         div bx ;除以8,去掉余数,商为索引号
         mov cx,ax
         shl cx,3 ;将选择子的索引号移到正确位置 

         pop es
         pop ds

         pop edx
         pop ebx
         pop eax
      
         retf 

用CPUID指令取得处理器品牌信息并显示:

mov eax,0x80000002 ;功能号
cpuid
mov [cpu_brand + 0x00],eax
mov [cpu_brand + 0x04],ebx
mov [cpu_brand + 0x08],ecx
mov [cpu_brand + 0x0c],edx
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值