操作系统真象还原——第5章 保护模式进阶,向内核迈进

第5章 保护模式进阶,向内核迈进

  1. BIOS中断利用0x15子功能0xe802获取内存

  2. 汇编语言子功能的调用

    • 填写调用前相关寄存器
    • 进行int中断调用
    • 获取返回结果输出到对应寄存器的值
  3. 80286 拥有24 位地址线,其寻址空间是16MB 。有一些ISA 只使用15MB,剩下的1MB作为缓冲区,为了兼容保留了下来,但是现在很少ISA设备,操作系统不可以用此段内存空间。所以成为了内存空洞memory hole。

  4. BIOS的0x15中断获取内存空间的三个子功能

    • eax=0xe820:遍历主机上全部内存
    • ax = 0xe801:分别检测出低15MB和16~4GB的内存
    • ah = 0x88:最多检测出64MB内存
  5. 查询内存信息的loader

    %include "boot.inc"
    section loader vstart=LOADER_BASE_ADDR
    LOADER_STACK_TOP equ LOADER_BASE_ADDR
    jmp start
    ; 定义三个GDT描述符,第0个无效初始化为0
    GDT_START:	dd 0x00000000
    			dd 0x00000000
    			
    CODE_DESC: 	dd 0x0000FFFF
    			dd DESC_CODE_HIGH4
    			
    STACK_DATA_DESC: 	dd 0x0000FFFF
    					dd DESC_DATA_HIGH4
    					
    VIDIO_DESC: dd 0x80000007
    			dd DESC_VIDEO_HIGH4
    
    GDT_SIZE equ $-GDT_START ; 当前地址-GDT起始地址是GDT的范围
    GDT_LIMIT equ GDT_SIZE-1 ; -1转化成物理地址
    times 60 dq 0 ; dp是8个字节,申请60个8字节的空间并初始化为0
    
    ;段选择子创建
    SELECT_CODE equ (0x0001<<3)+TI_GDT+RPL0 ; =0x1000 + 000 +00
    SELECT_DATA equ (0x0002<<3)+TI_GDT+RPL0
    SELECT_VIDIO equ (0x0003<<3)+TI_GDT+RPL0
    
    ;total_mem_bytes 用于保存内存容量,以字节为单位,此位置比较好记
    total_mem_bytes dd 0 ; 申请四个字节的空间初始化为0
    ards_buf times 241 db 0 ; 申请241个字节的空间初始化为0
    ards_nr dw 0 ; 申请四个字节的空间
    ; GDT的指针,前两个字节是gdt的界限,后四个字节是gdt的起始地址
    gdt_ptr dw GDT_LIMIT
    		dd GDT_START
    ; int 15h eax = 0000E820h,edx=534D4150h,则为获取内存布局
    start:
    	xor ebx,ebx	; 第一次调用时,ebx清0
    	mov edx,0x534d4150	; edx只赋值一次,循环体内不会改变
    	mov di,ards_buf		; ards结构缓冲区
    
    e820_mem_get_loop:	; 循环获取每个ARDS内存范围描述结构
    	mov eax,0x0000e820
    	; 执行int 0x15后,eax值变为0x534d4150,所以没此次执行int都要更新子功能号
    	mov ecx,20	; ARDS的地址范围描述符结构打消此为20字节
    	int 0x15
    	add di,cx	; 使di增加20字节指向缓冲区中新的ARDS 结构位置
    	inc word [ards_nr]	; 记录ards的数量
    	cmp ebx,0	; 若ebx为0且cf不为1,说明ards全部返回
    	jnz e820_mem_get_loop
    	; 在所有ards结构中,找出最大值,即内存的容量
    	mov cx,[ards_nr]
    	mov ebx,ards_buf
    	xor edx,edx	; edx存放最大内存容量,在此先清零
    ; 冒泡排序找最大的内存容量,并存放到edx中
    find_max_mem_loop:
    	mov eax,[ebx]
    	add eax,[ebx+8]
    	add ebx,20
    	cmp edx,eax
    	jge next
    	mov edx,eax
    next:
    loop find_max_mem_loop
    mov [total_mem_bytes],edx
    ; 打开A20
    in al,0x92
    or al,0000_0010B
    out 0x92,al
    ; 加载GDT
    lgdt [gdt_ptr]
    ; 将cr0的pe位置1
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax
    ; 刷新流水线
    jmp dword SELECT_CODE:mode_start
    
    [bits 32]
    mode_start:
    	mov ax,SELECT_DATA
    	mov ds,ax
    	mov es,ax
    	mov ss,ax
    	mov esp,LOADER_STACK_TOP
    	mov ax,SELECT_VIDIO
    	mov gs,ax
    	mov byte [gs:160],'P'
    	jmp $
    
  6. 成功截图
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hQ65SRj-1689072108287)(C:\Users\waterstop\AppData\Roaming\Typora\typora-user-images\image-20220226103917712.png)]

  7. 运行脚本改进

    • 增加&&,使得指令只能顺序执行
    • / 用于shell指令换行,清晰美观
    #!/bin/bash
    rm -rf ./hd.img &&\
    bin/bximage -hd -mode="flat" -size=60 -q hd.img &&\
    nasm -I include/ -o mbr.bin mbr.s &&\
    dd if=mbr.bin of=hd.img bs=512 count=1 conv=notrunc &&\
    nasm -I include/ -o loader.bin loader.s &&\
    dd if=loader.bin of=hd.img bs=512 count=4 seek=2 conv=notrunc &&\
    bin/bochs -f bochsrc
    
  8. 操作系统和硬件是相互依赖、相互推动、相互促进而发展起来的

  9. 内存和硬盘中的数据都是以二进制存储的

  10. 内存分页机制的基础原理:通过映射将连续的线性地址也任意的物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续

  11. 通过段基址+偏移地址获得的线性地址,再检查线性地址

    • 分页机制打开:使用线性地址在页表中查询
    • 分页机制关闭:将线性地址作为物理地址直接使用
  12. 分页机制的作用

    • 将线性地址转换成物理地址
    • 用大小相等的页代替大小不等的段
  13. 4G的线性地址空间属于所有进程的共享资源,其中标注为已分配页的内存块被分配给了其他进程,当前进程只能使用未分配页。

  14. 逻辑地址与物理地址的对应关系称为映射,而页表存储了这种映射关系

  15. 页表的地址转换:用线性地址的高20 位在页表中索引页表项,用线性地址的低12 位与页表项
    中的物理地址相加,所求的和便是最终线性地址对应的物理地址。

  16. 页表是用于管理内存的数据结构,也要占用内存

  17. 二级页表机制

    • 容量:页目录表中有1024个页表,每个页表中有1024个物理页,即总容量为1024*1024*4KB = 4GB
    • 作用:32位的虚拟地址中,高10 位在页目录表中索引一个页目录项PDE,页目录项中有页表物理地址。中间10 位在页表中索引到某个页表项PTE,页表项中有分配的物理页地址。用获得物理页地址和低12 位作为页内偏移量用于在已经定位到的物理页内寻址
    • 访问页表内的任何数据都要使用物理地址,页目录表项和页表项都是4字节,所以真正的表内物理地址需要*4
  18. 页目录项及页表项
    image-20220227110150755

    • P(Present),存在位,1表示该页存在于物理内存中,0表示该页不在物理内存中
    • RW(Read/Write),读写位,1表示可读可写,0表示可读不可写
    • US(User/Supervisor),用户权限位,1表示任意特权均可访问该页,0表示3特权不可访问而0,1,2特权可以访问
    • PWT(Page-level Write-Through),业级通写位,是否使用告诉缓存的通写改善该页的访问效率
    • PCD(Page-level Cache Disable),页级告诉缓存禁止位,1表示该页启用告诉缓存,0表示该页不可被告诉缓存
    • A(Accessed),访问位,由CPU进行设置,1表示被CPU访问过。系统定期清0,记录一段时间内的次数,表征内存页的使用率,来决定是否进行换页处理
    • D(Dirty),脏页位,CPU对一个页面执行写操作时,D位置1,仅对页表项有用
    • PAT(Page Attribute Table)页属性表位,用于设置内存属性
    • G(Global),全局位,1表示全局页可以一直存放在高速缓存TLB中,0表示不是全局页
    • AVL(Available),可用位
  19. 启用分页机制的步骤

    • 准备好页目录表及页表
    • 将页表地址写入控制寄存器cr3
    • 寄存器cr0的PG位置1(分页机制的开关)
  20. 控制寄存器cr3 用于存储页表物理地址,所以cr3 寄存器又称为页目录基址寄存器

  21. 控制寄存器和通用寄存器可以相互间进行数据的传递,所以可以使用mov进行处理

  22. 操作系统安全机制:用户进程必须运行在低特权级,当用户进程需要访问硬件相关的资源时,需要向操作系统申请,由操作系统去做,之后将结果返回给用户进程。

  23. 分页模式

    
    
    LOADER_BASE_ADDR equ 0x900 
    LOADER_START_SECTOR equ 0x2
    PAGE_DIR_TABLE_POS	equ 0x100000
    ;--------------   gdt描述符属性  -------------
    DESC_G_4K   equ	  1_00000000000000000000000b   
    DESC_D_32   equ	   1_0000000000000000000000b
    DESC_L	    equ	    0_000000000000000000000b	;  64位代码标记,此处标记为0便可。
    DESC_AVL    equ	     0_00000000000000000000b	;  cpu不用此位,暂置为0  
    DESC_LIMIT_CODE2  equ 1111_0000000000000000b
    DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2
    DESC_LIMIT_VIDEO2  equ 0000_000000000000000b
    DESC_P	    equ		  1_000000000000000b
    DESC_DPL_0  equ		   00_0000000000000b
    DESC_DPL_1  equ		   01_0000000000000b
    DESC_DPL_2  equ		   10_0000000000000b
    DESC_DPL_3  equ		   11_0000000000000b
    DESC_S_CODE equ		     1_000000000000b
    DESC_S_DATA equ	  DESC_S_CODE
    DESC_S_sys  equ		     0_000000000000b
    DESC_TYPE_CODE  equ	      1000_00000000b	;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.  
    DESC_TYPE_DATA  equ	      0010_00000000b	;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
    
    DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
    DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
    DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b
    
    ;--------------   选择子属性  ---------------
    RPL0  equ   00b
    RPL1  equ   01b
    RPL2  equ   10b
    RPL3  equ   11b
    TI_GDT	 equ   000b
    TI_LDT	 equ   100b
    ;--------页表相关属性----------
    PG_P	equ		1b
    PG_RW_R	equ		00b
    PG_RW_W	equ		10b
    PG_US_S	equ		000b
    PG_US_U	equ		100b
    
    %include "boot.inc" ; 导入自己写的宏文件
    section loader vstart=LOADER_BASE_ADDR ; 指明该段段内汇编的起始地址,值为0x900
    LOADER_STACK_TOP equ LOADER_BASE_ADDR
    jmp start ; 汇编语言是同时编译,顺序执行,但是下面的初始化也已经写入了内存中
    ; -------------------定义段描述符及其段选择子---------------------
    ; 定义三个GDT描述符,第0个无效初始化为0 
    ; dd表示四个字节,一个字节是8个二进制位数
    GDT_START:	dd 0x00000000
    			dd 0x00000000
    ; 根据段选择子位功能进行的拼凑		
    CODE_DESC: 	dd 0x0000FFFF
    			dd DESC_CODE_HIGH4
    			
    STACK_DATA_DESC: 	dd 0x0000FFFF
    					dd DESC_DATA_HIGH4
    					
    VIDIO_DESC: dd 0x80000007
    			dd DESC_VIDEO_HIGH4
    
    GDT_SIZE equ $-GDT_START ; 当前地址-GDT起始地址是GDT的范围
    GDT_LIMIT equ GDT_SIZE-1 ; -1转化成物理地址
    times 60 dq 0 ; dp是8个字节,申请60个8字节的空间并初始化为0
    
    ;段选择子创建 GDT中段描述符下标+TI+RPL
    SELECT_CODE equ (0x0001<<3)+TI_GDT+RPL0 ; =0x1000 + 000 +00
    SELECT_DATA equ (0x0002<<3)+TI_GDT+RPL0
    SELECT_VIDIO equ (0x0003<<3)+TI_GDT+RPL0
    ;---------获取内存容量-------------------------------------------
    ;total_mem_bytes 用于保存内存容量,以字节为单位,此位置比较好记
    total_mem_bytes dd 0 ; 申请四个字节的空间初始化为0
    ards_buf times 241 db 0 ; 申请241个字节的空间初始化为0
    ards_nr dw 0 ; 申请2个字节的空间
    ; GDT的指针,前两个字节是gdt的界限,后四个字节是gdt的起始地址
    gdt_ptr dw GDT_LIMIT
    		dd GDT_START
    ; int 15h eax = 0000E820h,edx=534D4150h,则为获取内存布局
    start:
    	xor ebx,ebx	; 第一次调用时,ebx清0
    	mov edx,0x534d4150	; edx只赋值一次,循环体内不会改变
    	mov di,ards_buf		; ards结构缓冲区
    
    e820_mem_get_loop:	; 循环获取每个ARDS内存范围描述结构
    	mov eax,0x0000e820
    	; 执行int 0x15后,eax值变为0x534d4150,所以没此次执行int都要更新子功能号
    	mov ecx,20	; ARDS的地址范围描述符结构打消此为20字节
    	int 0x15
    	add di,cx	; 使di增加20字节指向缓冲区中新的ARDS 结构位置
    	inc word [ards_nr]	; 记录ards的数量
    	cmp ebx,0	; 若ebx为0且cf不为1,说明ards全部返回
    	jnz e820_mem_get_loop
    	; 在所有ards结构中,找出最大值,即内存的容量
    	mov cx,[ards_nr]
    	mov ebx,ards_buf
    	xor edx,edx	; edx存放最大内存容量,在此先清零
    ; 冒泡排序找最大的内存容量,并存放到edx中
    find_max_mem_loop:
    	mov eax,[ebx]
    	add eax,[ebx+8]
    	add ebx,20
    	cmp edx,eax
    	jge next
    	mov edx,eax
    next:
    loop find_max_mem_loop
    mov [total_mem_bytes],edx
    ; 打开A20
    in al,0x92
    or al,0000_0010B
    out 0x92,al
    ; 加载GDT
    lgdt [gdt_ptr]
    ; 将cr0的pe位置1
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax
    ; 刷新流水线
    jmp dword SELECT_CODE:mode_start
    
    [bits 32]
    mode_start:
    	mov ax,SELECT_DATA
    	mov ds,ax
    	mov es,ax
    	mov ss,ax
    	mov esp,LOADER_STACK_TOP
    	mov ax,SELECT_VIDIO
    	mov gs,ax
    ; 创建页目录及页表并初始化页内存位图
       call setup_page
    
       ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
       sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置
    
       ;将gdt描述符中视频段描述符中的段基址+0xc0000000
       mov ebx, [gdt_ptr + 2]  
       or dword [ebx + 0x18 + 4], 0xc0000000  ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
    	;段描述符的高4字节的最高位是段基址的31~24位
    
       ;将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]             ; 重新加载
    
       mov  byte [gs:160], 'V';
       jmp $;
    
    
    
    ;-------------   创建页目录及页表   ---------------
    setup_page:
    ;先把页目录占用的空间逐字节清0,避免随机数据	
    ; 利用PAGE_DIR_TABLE_POS 作为基址, esi作为变址,然后通过188行的
    ; inc esi,每次使esi 自增1,逐步完成4096 字节的清0工作,ecx存储loop循环次数
       mov ecx, 4096	; 目录页表为4KB
       mov esi, 0
    .clear_page_dir:
       mov byte [PAGE_DIR_TABLE_POS + esi], 0 ; 大写的是页目录表物理地址的宏,值为0x100000
       inc esi
       loop .clear_page_dir
    
    ;开始创建页目录项(PDE)
    .create_pde:				     ; 创建Page Directory Entry
       mov eax, PAGE_DIR_TABLE_POS
       add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
       mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。
    
    ;   下面将页目录项0和0xc00都存为第一个页表的地址,
    ;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
    ;   这是为将地址映射为内核地址做准备
       or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
       mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(3)
       mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
    					     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
       sub eax, 0x1000
       mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址
    
    ;下面创建页表项(PTE)
       mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256
       mov esi, 0
       mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1
    .create_pte:				     ; 创建Page Table Entry
       mov [ebx+esi*4],edx			     ; 此时的ebx已经在上面通过eax赋值为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  ; 页目录项的属性RW和P位为1,US为0
       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
    
  24. 成功截图
    image-20220227170941159

  25. 汇编代码编译成机器码后,加载到内存中,在不进行编译优化的情况下,源码和对应地址的二进制编码相对应,即紧跟jmp指令后面的内存空间的定义(dd、dw···)不是jmp直接跳过的,可以认为无优化编译是一种静态的翻译,而CPU真正执行时,对应的内存空间已经存入对应的值
    image-20220227181831664

  26. PDE是页目录项,PTE是页表项

  27. 当物理内存不足时,操作系统的虚拟内存管理机制有可能会将该PDE 或PTE 指向的物理页框换出到磁盘上,此时PDE 或PTE 的P 位便被操作系统置为0,处理器访问该PDE 或PTE时会触发page_fault 缺页异常,操作系统为该异
    常注册了中断处理程序,该程序会将所缺的页从磁盘上重新加载到内存中,并将P 位置为1

  28. 为什么GDT中的第0个描述符总是为空

    ​ GDT中的第0个段描述符不可访问,因为未初始化的选择子值为0,避免错误访问到第0个描述符。

    ​ GDT和IDT是整个系统一张,而LDT可以每个任务独占一长,用于存储每个任务私有的段的信息,所以当任务发生切换时,LDT也要随之切换,CPU中专门用一个16位的寄存器LDTR来存储当前任务的LDT在GDT中的描述符的选择子,以此来定位当前任务的LDT。同时也存在这么一种情况,那就是一个任务使用的所有段都是系统全局的,它不需要用LDT来存储私有段信息,因此,当系统切换到这种任务时,会将LDTR寄存器赋值成一个空(全局描述符)选择子,选择子的描述符索引值为0,TI指示位为0,RPL可以为任意值,用这种方式表明当前任务没有 LDT。这里的空选择子因为TI为0,所以它实际上指向了GDT的第0项描述符,第0项的作用类似于C语言中NULL的用法,它虽然是一个描述符,但却只起到到了标志的作用,规定GDT的第0项描述符为空描述符,其8个字节全为0,就是这个原因。如果把前面的空描述符选择子的TI位改为1,使之指向LDT 中的0号描述符,这样的选择子就不是空选择子,它指向的LDT中的0号描述符是可以正常使用的,也就是LDT中没有空描述符一说

  29. 进入分页机制运行模式:数据在内存中最终以物理地址来访问,但访问任何物理地址都需要通过虚拟地址

  30. 二级页表可以通过增加或删除页表项或页目录项进行动态增减。

  31. 页表是将虚拟地址转换成物理地址的映射表,在分页机制下,如何用虚拟地址访问到页表自身呢?

    • 虚拟地址直接与物理地址一一对应(不用)

    • 虚拟地址与物理地址乱序映射

      先要从CR3 寄存器中获取页目录表物理地址,然后用虚拟地址的高10 位乘以4 的积作为在页目录表中的偏移量去寻址目录项pde ,从pde 中读出页表物理地址,然后再用虚拟地址的中间10 位乘以4 的积作为在该页表中的偏移量去寻址页表项pte,从该pte 中读出页框物理地址,用虚拟地址的低12 位作为该物理页框的偏移量(物理地址为基址,逻辑地址乘以4作为偏移量)

  32. 在虚拟机中使用info tab命令可以获取逻辑和物理地址的映射关系

  33. 用虚拟地址获取页表

    • 获取页目录表物理地址:让虚拟地址的高20 位为0xfffff,低12 位为0x000 ,即0xfffff000,这也是页目录表中第0 个页目录项自身的物理地址。
    • 访问页目录中的页目录项,即获取页表物理地址:要使虚拟地址为Oxffiffxxx ,其中xxx 是页目录项的索引乘以4 的积。
    • 访问页表中的页表项z 要使虚拟地址高10 位为0x3ff,目的是获取页目录表物理地址。中间10 位为页表的索引,因为是10 位的索引值,所以这里不用乘以4 低12 位为页表内的偏移地址,用来定位页表项,它必须是己经乘以4 后的值。
  34. 虚拟地址和物理地址的转换需要频繁进行内存访问,处理器中断等待资源被浪费。快表TLB(Translation Lookaside Buffer)用来专门存储虚拟地址页框和物理地址页框的映射关系,根据程序的局部性,减少到内存的访问,匹配高速的处理器速率和低速的内存访问速度。处理器在寻址之前优先访问TLB,会用虚拟地址的高20 位作为索引来查找TLB 中的相关条目,如果命中则返回虚拟地址所映射的物理页框地址,否则会查询内存中的页表,获得页框物理地址后再更新TLB。而且只有P 位为1 的页表项才有资格在TLB 中,如果TLB 被装满了,需要将很少使用的条目换出。

  35. TLB对方程序员透明,但是可以间接进行更新TLB

    • 重新加载CR3
    • 使用invlpg 虚拟地址,可以刷新TLB中的该虚拟地址表项
  36. 标准库程序是对于系统调用的效率和规范的平衡

  37. gcc编译后生成的.o文件知识一个目标文件,还需要进行重定位(给文件中的所有符号安排地址)

  38. 操作系统是给用户提供功能支持的平台

  39. BIOS 调用mbr, mbr 的地址是0x7c00, mbr 调用loader,loader 的地址是0x900。这
    两个地址是固定的

  40. 不同平台的c 编译器也会根据系统平台自动添加文件头,文件头用来描述程序的内存布局信息,通常有8个字节,前四个是程序的长度,后四个是程序的入口地址(控制信息)

  41. Windows下可执行文件的格式是PE(exe只是扩展名),Linux下可执行文件格式是ELF,是经过编译链接后可以直接运行的文件

  42. 执行的程序由段(segment)和节(section)组成,多个节经过链接后合并成了一个段

  43. 硬盘中的不同程序尽量不要完全相邻,隔开点不容易出现问题

  44. gcc常用参数

    • -c 的作用是编译、汇编到目标代码,不进行链接,也就是直接生成目标文件。
    • -o 的作用是将输出的文件以指定文件名来存储,有同名文件存在时直接覆盖。
  45. .o文件是一个待重定位文件,即文件中的符号地址需要其他目标文件进行地址编排,才能链接成为一个可执行文件

  46. 程序的开头常常有函数或数据的定义,所以入口地址通常不是函数的开始处,main 函数通常在运行库代码初始化完环境后才被调用。

  47. ELF文件开头必然是0x7f 45 4c 46,后三位为字符串ELF的ascii

    // ELF的头
    #define EI_NIDENT 16
    struct Elf32_Ehdr            //共52个字节    //Ehdr表示ELF header
    {
      unsigned char  e_ident[EI_NIDENT];
      Elf32_Half e_type;        //类型包括:可执行文件、可重定向文件、共享目标文件等
      Elf32_Half e_machine;     //有X86、arm之类
      Elf32_Word e_version;
      Elf32_Addr e_entry;       //可执行程序的入口地址
      Elf32_Off e_phoff;        //Program头表的偏移地址
      Elf32_Off e_shoff;        //Section头表的偏移地址
      Elf32_Word e_flags;
      Elf32_Half e_ehsize;      //本结构体的size
      Elf32_Half e_phentsize;   //单个Program头的size
      Elf32_Half e_phnum;       //Segment头表中Segment头的个数
      Elf32_Half e_shentsize;   //单个Section头的szie
      Elf32_Half e_shnum;       //Section头表中Section头的个数
      Elf32_Half e_shstrndx;    //储存Section名字集合的Section的下标,指".shstrtab"的下标
    };
    
  48. 内存空间的规划最好隔开一点,对于不再使用的进行无情的覆盖

内核运行完整代码

选的课太多了,两个月没搞,有点忘记了,把所有的程序代码整理注释了一遍,下面是运行程序和过程

  1. include/boot.inc

    ;------------------- 进入loader所需要的宏 --------------------------
    
    LOADER_START_SECTOR equ 2
    LOADER_BASE_ADDR equ 0x600 ;博客名字是Love 6 干脆就把Loader设置加载到0x600
    
    ;-------------------- gdt描述符属性 --------------------------------
    ;我查了查下划线的作用 其实没有任何作用 这里仅仅为了方便 确定哪些位为我们想要设置数而专门用的下划线分割
    ;上面的第多少位都是针对的高32位而言的 参照博客的图 
    
    DESC_G_4K equ 1_00000000000000000000000b ;第23位G 表示4K或者1MB位 段界限的单位值 此时为1则为4k 
    DESC_D_32 equ 1_0000000000000000000000b  ;第22位D/B位 表示地址值用32位EIP寄存器 操作数与指令码32位
    DESC_L    equ 0_000000000000000000000b   ;第21位 设置成0表示不设置成64位代码段 忽略
    DESC_AVL  equ 0_00000000000000000000b    ;第20位 是软件可用的 操作系统额外提供的 可不设置
                                             
    DESC_LIMIT_CODE2  equ  1111_0000000000000000b   ;第16-19位 段界限的最后四位 全部初始化为1 因为最大段界限*粒度必须等于0xffffffff
    DESC_LIMIT_DATA2  equ  DESC_LIMIT_CODE2         ;相同的值  数据段与代码段段界限相同
    DESC_LIMIT_VIDEO2 equ	0000_0000000000000000b	  ;第16-19位 显存区描述符VIDEO2 书上后面的0少打了一位 这里的全是0为高位 低位即可表示段基址
       
    DESC_P            equ 	1_000000000000000b	  ;第15位  P present判断段是否存在于内存  
    DESC_DPL_0        equ  00_0000000000000b         ;第13-14位 这两位更是重量级 Privilege Level 0-3
    DESC_DPL_1        equ  01_0000000000000b	  ;0为操作系统 权力最高 3为用户段 用于保护
    DESC_DPL_2        equ  10_0000000000000b
    DESC_DPL_3        equ  11_0000000000000b
    
    DESC_S_sys        equ  0_000000000000b           ;第12位为0 则表示系统段 为1则表示数据段
    DESC_S_CODE       equ  1_000000000000b           ;第12位与type字段结合 判断是否为系统段还是数据段
    DESC_S_DATA       equ  DESC_S_CODE
    
    
    DESC_TYPE_CODE    equ  1000_00000000b            ;第9-11位表示该段状态 1000 可执行 不允许可读 已访问位0
    ;x=1 e=0 w=0 a=0
    DESC_TYPE_DATA    equ  0010_00000000b            ;第9-11位type段   0010  可写  
    ;x=0 e=0 w=1 a=0
    
    
    ;代码段描述符高位4字节初始化 (0x00共8位 <<24 共32位初始化0) 
    ;4KB为单位 Data段32位操作数 初始化的部分段界限 最高权限操作系统代码段 P存在表示 状态 
    DESC_CODE_HIGH4   equ  (0x00<<24) + DESC_G_4K + DESC_D_32 + \
    DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + \
    DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0X00
    
    ;数据段描述符高位4字节初始化
    DESC_DATA_HIGH4   equ  (0x00<<24) + DESC_G_4K + DESC_D_32 + \
    DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + \
    DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X00
    
    ;显存段描述符高位4字节初始化
    DESC_VIDEO_HIGH4   equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \
    DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + \
    DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X0B  ;整挺好 我看书上写的0x00 结果我自己推算出来这里末尾是B 
    
    ;-------------------- 选择子属性 --------------------------------
    ;第0-1位 RPL 特权级比较是否允许访问  第2位TI 0表示GDT 1表示LDT    第3-15位索引值
    RPL0    equ 00b
    RPL1    equ 01b
    RPL2    equ 10b
    RPL3    equ 11b
    TI_GDT  equ 000b
    TI_LDT  equ 100b
    
    ;------------------ 开启页表所需要的宏 ---------------------------
    
    PAGE_DIR_TABLE_POS  equ 0x100000                          ;这里设置了页目录项的起始位置
    
    ;------------------ 页表相关属性 ---------------------------------
    
    PG_P 	 equ  1b                                            ;PG目录项的属性 Present存在于当前物理内存
    PG_RW_R  equ 00b                                            ;只可读不可写
    PG_RW_W  equ 10b                                            ;可写可读
    PG_US_S  equ 000b                                           ;Supervisor  超级用户
    PG_US_U  equ 100b                                           ;User        普通用户
    ;不是很清楚 Global位为什么宏先不定义 但是剩下的PWT PCD 我们用不到即设置为0 A位是cpu操控的 页表项就算是弄完
    
    ;-----------------  加载内核宏定义 -------------------------------
    
    KERNEL_BIN_SECTOR    equ  0x9
    KERNEL_BIN_BASE_ADDR equ  0x70000
    KERNEL_ENTER_ADDR    equ  0xc0001500
    
    PT_NULL              equ  0x0
    
    
    
  2. mbr.s

    ; 主引导程序MBR
    
    ; SS存放栈顶的段地址,SP存放栈顶的偏移地址。在任何时刻 ,SS:SP都是指向栈顶元素 
    ; CS存放内存中代码段入口的段基址,CS:IP表示下一条要运行的指令内存地址   
        
    ; 1.引入头文件,汇编时需要指定其所在目录,eg:`nasm -I include/ ···`
    %include "boot.inc"
    
    ; 2.地址定向
    ; SECTION是伪指令,cpu不运行,只是方便程序员规划程序分段使用
    ; `vstart=0x7c00`告知汇编器,本段将被加载到0x7c00
    SECTION MBR vstart=0x7c00 ; =前后不能有空格
    
    ; 3.初始化操作
    	mov ax,cs			; 由于BIOS是通过`jmp 0:Ox7c00(cs:ip)`跳转到MBR的
        mov ds,ax			; 段寄存器不能使用立即数进行赋值,可以使用通用寄存器ax
        mov es,ax           ; 将其他段寄存器初始化为0
        mov ss,ax  
        mov fs,ax
    	mov sp,0x7c00       ; SP是堆栈指针寄存器,存放着当前堆栈栈顶地址,程序都要使用堆栈而0x7c00以下是安全的(栈向低地址生长,0x7c00以上是MBR程序)
        mov ax,0xb800       ; OxB8000后32KB的内存区域是用于文本显示,所以该处输出的宇符即可通过显存打印在显示器屏幕上
        mov gs,ax
    
    
    ; 4.向显存输出字符        
    ; 使用10号中断的0x06功能号,进行窗口上卷的清屏操作,避免BIOS检测信息影响显示
    	mov ax,0600h		; ah存放将要调用的中断子功能号
        mov bx,0700h
        mov cx,0			; (CL,CH)=窗口左上角的(X,Y)位置
        mov dx,184fh		; (DL,DH)=窗口右下角的(X,Y)位置(80,25)
        int 10h				; 调用中断
    ; 输出背景色是绿色,前景色是红色,并且跳动的字符串为“1 MBR”
        mov byte [gs:0x00],'1'
        mov byte [gs:0x01],0xA4; A表示绿色背景闪烁,4表示前景色为红色
        
        mov byte [gs:0x02],' '
        mov byte [gs:0x03],0xA4
        
        mov byte [gs:0x04],'M'
        mov byte [gs:0x05],0xA4
        
        mov byte [gs:0x06],'B'
        mov byte [gs:0x07],0xA4
        
        mov byte [gs:0x08],'R'
        mov byte [gs:0x09],0xA4
    
    ; 5.将第二个扇区中的内核加载器程序loader读入到内存中
    ; 硬盘扇区的写入
     	mov eax,LOADER_START_SECTOR	; 起始扇区lba地址
        mov bx,LOADER_BASE_ADDR		; 写入的地址
        mov cx,4					; 待读入的扇区数(使用寄存器传递函数参数)
        call rd_disk_m_16			; 调用函数,以下读取程序的起始部分
        
        jmp LOADER_BASE_ADDR
            
    ; 功能:读取硬盘的n个扇区,在16位模式下
     rd_disk_m_16:
        mov esi,eax					; 备份eax
        mov di,cx					; 备份cx
            
    	; 读写硬盘
        ; 第一步:设置要读取的扇区数
        mov dx,0x1f2				; 存储端口号
        mov al,cl
        out dx,al					; 读取的扇区数(out指令用于向端口写数据)
        mov eax,esi					; 恢复ax
        ; 第二步:将LBA地址存入端口0x1f3 ~ 0x1f6
        ; LBA地址的0~7位写入端口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		; lab第24~27位
        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		; 第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
    	cmp al,0x08
    	jnz .not_ready	; 若未准备好,继续等待
    	; 第5步:从0x1f0端口读数据
        ; di为要读取的扇区数,一个扇区有512字节,每读取一个字,共需di*512/2次
    	mov ax,di
        mov dx,256
    	mul dx
    	mov cx,ax
        mov dx,0x1f0
     .go_on_read:
    	in ax,dx
    	mov [bx],ax
    	add bx,2
    	loop .go_on_read
    	ret    
    ; $表示本行指令所在的地址,$$表示本section的起始地址,$-$$表示执行代码行到段首的偏移量
        times 510-($-$$) db 0	; 将剩余字节用0进行填充
        db 0x55,0xaa			; 最后两个填充字节是MBR结尾的标识
    
    
  3. loader.s

    
    ;1. 初始化
    %include "boot.inc"
    SECTION loader vstart=LOADER_BASE_ADDR          ; 定义loader程序在内存存放中的偏移地址
    LOADER_STACK_TOP equ LOADER_BASE_ADDR           ; 程序运行需要堆栈区,这是保护模式下的栈基址为0x600
    jmp loader_start                     		   	; 存放跳跃到下面的代码区的指令,指令和数据最终都是二进制字符串(除了伪指令)
    ;2. 定义全局描述符表GDT,每隔8个字节是一个表项
    GDT_BASE : dd 0x00000000          	    ; 第0个段描述符不可用
       		   dd 0x00000000                ; dd定义四个字节的内存空间
    CODE_DESC  : dd 0x0000FFFF              ;FFFF是与其他的几部分相连接 形成0XFFFFF段界限
        		 dd DESC_CODE_HIGH4
    DATA_STACK_DESC : dd 0x0000FFFF         ; 数据段向上拓展,栈段向下拓展,由type中的e字段决定
      		          dd DESC_DATA_HIGH4
    VIDEO_DESC  : dd 0x80000007         	; 0xB8000 到0xBFFFF为文字模式显示内存 B只能在boot.inc中出现定义了 此处不够空间了 8000刚好够
                  dd DESC_VIDEO_HIGH4     	; 0x0007 (bFFFF-b8000)/4k = 0x7
    GDT_SIZE     equ $ - GDT_BASE           ; 当前位置减去GDT_BASE的地址 等于GDT的大小
    GDT_LIMIT    equ GDT_SIZE - 1   	    ; SIZE - 1即为最大偏移量
    ; times循环执行其后的表达式    
    times 59 dq 0                           ;预留59个描述符空位置
    times 5 db 0                            ;为了凑整数 0x800 导致前面少了三个
        
    total_mem_bytes  dd 0
        	               			        ;在此前经过计算程序内偏移量为0x200 我算了算 60*8+4*8=512 刚好是 0x200 说这里的之后还会用到
        							        ;我们刚开始程序设置的地址位置为 0x600 那这就是0x800
    gdt_ptr     dw GDT_LIMIT			    ;gdt指针 2字gdt界限放在前面 4字gdt地址放在后面 lgdt 48位格式 低位16位界限 高位32位起始地址
        		dd GDT_BASE
        		       
    ards_buf times 244 db 0                 ;buf  记录内存大小的缓冲区
    ards_nr dw 0					        ;nr 记录20字节结构体个数  计算了一下 4+2+4+244+2=256 刚好256字节
        
    SELECTOR_CODE        equ (0X0001<<3) + TI_GDT + RPL0    ;16位寄存器 4位TI RPL状态 GDT剩下的选择子
    SELECTOR_DATA	  equ (0X0002<<3) + TI_GDT + RPL0
    SELECTOR_VIDEO       equ (0X0003<<3) + TI_GDT + RPL0   
        
    loader_start:
        
        mov sp,LOADER_BASE_ADDR                                   ;先初始化了栈指针
        xor ebx,ebx                                               ;异或自己 即等于0
        mov ax,0                                       
        mov es,ax                                                 ;心有不安 还是把es给初始化一下
        mov di,ards_buf                                           ;di指向缓冲区位置
    .e820_mem_get_loop:
        mov eax,0x0000E820                                            ;每次都需要初始化
        mov ecx,0x14
        mov edx,0x534d4150
        
        int 0x15                                                  ;调用了0x15中断
        jc  .e820_failed_so_try_e801                              ;这时候回去看了看jc跳转条件 就是CF位=1 carry flag = 1 中途失败了即跳转
        add di,cx							;把di的数值增加20 为了下一次作准备
        inc word [ards_nr]
        cmp ebx,0
        jne .e820_mem_get_loop                                    ;直至读取完全结束 则进入下面的处理时间
        
        mov cx,[ards_nr]                                          ;反正也就是5 cx足以
        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
        
    .e820_failed_so_try_e801:                                       ;地址段名字取的真的简单易懂 哈哈哈哈 
        mov ax,0xe801
        int 0x15
        jc .e801_failed_so_try_88
       
    ;1 先算出来低15MB的内存    
        mov cx,0x400
        mul cx                                                      ;低位放在ax 高位放在了dx
        shl edx,16                                                  ;dx把低位的16位以上的书往上面抬 变成正常的数
        and eax,0x0000FFFF                                          ;把除了16位以下的 16位以上的数清零 防止影响
        or edx,eax                                                  ;15MB以下的数 暂时放到了edx中
        add edx,0x100000                                            ;加了1MB 内存空缺 
        mov esi,edx
        
    ;2 接着算16MB以上的内存 字节为单位
        xor eax,eax
        mov ax,bx
        mov ecx,0x10000                                              ;0x10000为64KB  64*1024  
        mul ecx                                                      ;高32位为0 因为低32位即有4GB 故只用加eax
        mov edx,esi
        add edx,eax
        jmp .mem_get_ok
     
    .e801_failed_so_try_88:
         mov ah,0x88
         int 0x15
         jc .error_hlt
         and eax,0x0000FFFF
         mov cx,0x400                                                 ;1024
         mul cx
         shl edx,16
         or edx,eax 
         add edx,0x100000
    
    .error_hlt:
         jmp $
    .mem_get_ok:
         mov [total_mem_bytes],edx
    ; --------------------------------- 设置进入保护模式 -----------------------------
    ; 1 打开A20 gate
    ; 2 加载gdt
    ; 3 将cr0 的 pe位置1
        
        in al,0x92                 ;端口号0x92 中 第1位变成1即可
        or al,0000_0010b
        out 0x92,al
       
        
        lgdt [gdt_ptr]
        
        
        mov eax,cr0                ;cr0寄存器第0位设置位1
        or  eax,0x00000001              
        mov cr0,eax
          
    ;-------------------------------- 已经打开保护模式 ---------------------------------------
        jmp dword SELECTOR_CODE:p_mode_start                       ;刷新流水线
     
     [bits 32]
     p_mode_start: 
        mov ax,SELECTOR_DATA
        mov ds,ax
        mov es,ax
        mov ss,ax
        mov esp,LOADER_STACK_TOP
        
    ;------------------------------- 加载内核到缓冲区 -------------------------------------------------
    
        mov eax, KERNEL_BIN_SECTOR
        mov ebx, KERNEL_BIN_BASE_ADDR
        
        mov ecx,200
        call rd_disk_m_32
    
    ;------------------------------- 启动分页 ---------------------------------------------------
        
        call setup_page
        							         ;这里我再把gdtr的格式写一下 0-15位界限 16-47位起始地址
        sgdt [gdt_ptr]                                             ;将gdt寄存器中的指 还是放到gdt_ptr内存中 我们修改相对应的 段描述符
        mov ebx,[gdt_ptr+2]                                        ;32位内存先倒出来 为的就是先把显存区域描述法的值改了 可以点开boot.inc 和 翻翻之前的段描述符
                                                                   ;段基址的最高位在高4字节 故
        or dword [ebx+0x18+4],0xc0000000
        add dword [gdt_ptr+2],0xc0000000                            ;gdt起始地址增加 分页机制开启的前奏
        
        add esp,0xc0000000                                         ;栈指针也进入高1GB虚拟内存区
        
        mov eax,PAGE_DIR_TABLE_POS
        mov cr3,eax
        
        mov eax,cr0
        or eax,0x80000000
        mov cr0,eax
        
        lgdt [gdt_ptr]
        
        mov eax,SELECTOR_VIDEO
        mov gs,eax
        mov byte [gs:160],'V'
        
        jmp SELECTOR_CODE:enter_kernel
        
    ;------------------------------ 跳转到内核区    
    
    enter_kernel:
        call kernel_init					          ;根据我们的1M以下的内存分布区 综合考虑出的数据
        mov  esp,0xc009f000
        jmp  KERNEL_ENTER_ADDR
    
    ;------------------------------- 创建页表 ------------------------------------------------    
    setup_page:
        mov ecx,0x1000                                             ;循环4096次 将页目录项清空 内存清0
        mov esi,0                                                   
     .clear_page_dir_mem:                                          ;dir directory 把页目录项清空
        mov byte [PAGE_DIR_TABLE_POS+esi],0
        inc esi
        loop .clear_page_dir_mem
        
     .create_pde: 
        mov eax,PAGE_DIR_TABLE_POS				  ;页目录项 起始位置
        add eax,0x1000                            ;页目录项刚好4k字节 add eax即得第一个页表项的地址
                                                  ;接下来我们要做的是 把虚拟地址1M下和3G+1M 两部分的1M内存在页目录项中都映射到物理地址0-0XFFFFF
        or  eax, PG_P | PG_RW_W | PG_US_U         ;哦 悟了 哈哈哈 这里设置为PG_US_U 是因为init在用户进程 如果这里设置成US_S 这样子连进内核都进不去了
         
        mov [PAGE_DIR_TABLE_POS+0x0],eax          ;页目录项偏移0字节与偏移0xc00 对应0x 一条页目录项对应2^22位4MB 偏移由前10位*4字节得到 可自己推算一下
        mov [PAGE_DIR_TABLE_POS+0xc00],eax                        
        sub eax,0x1000      
        
        mov [PAGE_DIR_TABLE_POS+4092],eax         ;虚拟内存最后一个目录项 指向页目录表自身 书上写的是为了动态操纵页表 我也不是很清楚 反正有用 先放放
    
    ;这里就创建了一页页表    
        mov eax,PAGE_DIR_TABLE_POS
        add eax,0x1000
        mov ecx,256
        mov esi,0
        mov ebx,PG_P | PG_RW_W | PG_US_U 
        
     .create_kernel_pte:           
        mov [eax+esi*4],ebx
        inc esi
        add ebx,0x1000
        loop .create_kernel_pte 
        
        
    ;这里对于我们这里填写的目录项所对应的页表 页表中我们还没填写的值
    ;为了实现 真正意义上的 内核空间被用户进程完全共享
    ;只是把页目录与页表的映射做出来了 
    
        mov eax,PAGE_DIR_TABLE_POS
        add eax,0x2000       					   ;eax此时处于第二个页表
        or  eax,PG_P | PG_RW_W | PG_US_U
    ;这里循环254次可以来分析一下 我们这里做的是 0xc0 以上部分的映射    0xc0 对应的是第768个页表项 页表项中一共有 2^10=1024项
    ;第1023项我们已经设置成 映射到页目录项本身位置了 即1022 - 769 +1 = 254
        mov ebx,PAGE_DIR_TABLE_POS
        mov ecx,254						  
        mov esi,769
            
     .create_kernel_pde:
        mov [ebx+esi*4],eax
        inc esi
        add eax,0x1000
        loop .create_kernel_pde 
        
        ret            
        
    ;----------------------- 初始化内核 把缓冲区的内核代码放到0x1500区域 ------------------------------------------
    ;这个地方主要对elf文件头部分用的很多
    ;可以参照着书上给的格式 来比较对比
    kernel_init:
        xor eax,eax   ;全部清零
        xor ebx,ebx
        xor ecx,ecx
        xor edx,edx
        
        ;这里稍微解释一下 因为0x70000 为64kb*7=448kb 而我们的内核映射区域是4MB 而在虚拟地址4MB以内的都可以当作1:1映射
        mov ebx,[KERNEL_BIN_BASE_ADDR+28]
        add ebx,KERNEL_BIN_BASE_ADDR                               ;ebx当前位置为程序段表
        mov dx,[KERNEL_BIN_BASE_ADDR+42]		         ;获取程序段表每个条目描述符字节大小
        mov cx,[KERNEL_BIN_BASE_ADDR+44]                         ;一共有几个段
        
         
     .get_each_segment:
        cmp dword [ebx+0],PT_NULL
        je .PTNULL                                                 ;空即跳转即可 不进行mem_cpy
        
        mov eax,[ebx+8]
        cmp eax,0xc0001500
        jb .PTNULL
        
            
        push dword [ebx+16]                                        ;ebx+16在存储的数是filesz  可以翻到Loader刚开始
                                                                   
        mov eax,[ebx+4]                                            
        add eax,KERNEL_BIN_BASE_ADDR
        push eax                                                   ;p_offset 在文件中的偏移位置    源位置         
        push dword [ebx+8]                                         ;目标位置
         
        call mem_cpy
        add esp,12                                                 ;把三个参数把栈扔出去 等于恢复栈指针
        
     .PTNULL:
        add  ebx,edx                                               ;edx是一个描述符字节大小
        loop .get_each_segment                                     ;继续进行外层循环    
        ret
                                            
    mem_cpy:
        cld                                                        ;向高地址自动加数字 cld std 向低地址自动移动
        push ebp                                                   ;保存ebp 因为访问的时候通过ebp 良好的编程习惯保存相关寄存器
        mov  ebp,esp 
        push ecx                                                   ;外层循环还要用 必须保存 外层eax存储着还有几个段
        
                                                                   ;分析一下为什么是 8 因为进入的时候又重新push了ebp 所以相对应的都需要+4
                                                                   ;并且进入函数时 还Push了函数返回地址 所以就那么多了
        mov edi,[ebp+8]                                            ;目的指针 edi存储的是目的位置 4+4
        mov esi,[ebp+12]                                           ;源指针   源位置             8+4
        mov ecx,[ebp+16]                                           ;与Movsb好兄弟 互相搭配      12+4
        
        
        rep movsb                                                  ;一个一个字节复制
           
        pop ecx 
        pop ebp
        ret
        
    ;------------------------ rd_disk_m_32  在mbr.S复制粘贴过来的 修改了点代码 ----------------------
    rd_disk_m_32:
    ;1 写入待操作磁盘数
    ;2 写入LBA 低24位寄存器 确认扇区
    ;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
    ;4 command 写指令
    ;5 读取status状态寄存器 判断是否完成工作
    ;6 完成工作 取出数据
     
     ;;;;;;;;;;;;;;;;;;;;;
     ;1 写入待操作磁盘数
     ;;;;;;;;;;;;;;;;;;;;;
        mov esi,eax   ; !!! 备份eax
        mov di,cx     ; !!! 备份cx
        
        mov dx,0x1F2  ; 0x1F2为Sector Count 端口号 送到dx寄存器中
        mov al,cl     ; !!! 忘了只能由ax al传递数据
        out dx,al     ; !!! 这里修改了 原out dx,cl
        
        mov eax,esi   ; !!!袄无! 原来备份是这个用 前面需要ax来传递数据 麻了
        
    ;;;;;;;;;;;;;;;;;;;;;
    ;2 写入LBA 24位寄存器 确认扇区
    ;;;;;;;;;;;;;;;;;;;;;
        mov cl,0x8    ; shr 右移8位 把24位给送到 LBA low mid high 寄存器中
    
        mov dx,0x1F3  ; LBA low
        out dx,al 
        
        mov dx,0x1F4  ; LBA mid
        shr eax,cl    ; eax为32位 ax为16位 eax的低位字节 右移8位即8~15
        out dx,al
        
        mov dx,0x1F5
        shr eax,cl
        out dx,al
        
    ;;;;;;;;;;;;;;;;;;;;;
    ;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
    ;;;;;;;;;;;;;;;;;;;;;
    
        		 
        		  ; 24 25 26 27位 尽管我们知道ax只有2 但还是需要按规矩办事 
        		  ; 把除了最后四位的其他位置设置成0
        shr eax,cl
        
        and al,0x0f 
        or al,0xe0   ;!!! 把第四-七位设置成0111 转换为LBA模式
        mov dx,0x1F6 ; 参照硬盘控制器端口表 Device 
        out dx,al
    
    ;;;;;;;;;;;;;;;;;;;;;
    ;4 向Command写操作 Status和Command一个寄存器
    ;;;;;;;;;;;;;;;;;;;;;
    
        mov dx,0x1F7 ; Status寄存器端口号
        mov ax,0x20  ; 0x20是读命令
        out dx,al
        
    ;;;;;;;;;;;;;;;;;;;;;
    ;5 向Status查看是否准备好惹 
    ;;;;;;;;;;;;;;;;;;;;;
        
    		   ;设置不断读取重复 如果不为1则一直循环
      .not_ready:     
        nop           ; !!! 空跳转指令 在循环中达到延时目的
        in al,dx      ; 把寄存器中的信息返还出来
        and al,0x88   ; !!! 0100 0100 0x88
        cmp al,0x08
        jne .not_ready ; !!! jump not equal == 0
        
        
    ;;;;;;;;;;;;;;;;;;;;;
    ;6 读取数据
    ;;;;;;;;;;;;;;;;;;;;;
    
        mov ax,di      ;把 di 储存的cx 取出来
        mov dx,256
        mul dx        ;与di 与 ax 做乘法 计算一共需要读多少次 方便作循环 低16位放ax 高16位放dx
        mov cx,ax      ;loop 与 cx相匹配 cx-- 当cx == 0即跳出循环
        mov dx,0x1F0
     .go_read_loop:
        in ax,dx      ;两字节dx 一次读两字
        mov [ebx],ax
        add ebx,2
        loop .go_read_loop
        ret ;与call 配对返回原来的位置 跳转到call下一条指令
    
    
    
  4. main.c

    //#include<stdio.h>// 不需要
    int main(void){
    	while(1);
    	return 0;
    }
    
    
  5. shell脚本

    #!/bin/bash
    #### 分功能进行shell文本的编写
    #1.删除中间文件
    rm -rf ./hd.img ./loader.bin ./kernel.bin ./main.o &&\
    
    #2.编译连接程序
    ## 将使用汇编编写的主引导记录编译成二进制文件
    nasm -I include/ -o mbr.bin mbr.s &&\
    ## 将内核加载文件编译成二进制文件
    nasm -I include/ -o loader.bin loader.s &&\
    ## 将c语言文件编译成32位汇编文件
    gcc -m32 -c -o main.o main.c &&\
    ## 将二进制文件写入硬盘镜像并指定起始虚拟地址
    ld -m elf_i386 main.o -Ttext 0xc0001500 -e main -o kernel.bin &&\
    
    #3.硬盘处理
    ## 新建硬盘镜像文件
    bin/bximage -hd -mode="flat" -size=60 -q hd.img &&\
    ## 将主引导记录的二进制文件写入硬盘镜像文件
    dd if=mbr.bin of=hd.img bs=512 count=1 conv=notrunc &&\
    ## 将内核加载文件的二进制文件写入硬盘镜像文件中
    dd if=loader.bin of=hd.img bs=512 count=4 seek=2 conv=notrunc &&\
    ## 将内核文件写入虚拟硬盘中
    dd if=kernel.bin of=hd.img bs=512 count=200 seek=9 conv=notrunc&&\
    
    #4.启动bochs
    bin/bochs -f bochsrc 
    
    
    
    
参考资料
  1. 第六章一个问题https://www.jianshu.com/p/b1f863201f4a

  2. 第五章代码https://love6.blog.csdn.net/article/details/117871478
    in.c

    //#include<stdio.h>// 不需要
    int main(void){
    	while(1);
    	return 0;
    }
    
    
  3. shell脚本

    #!/bin/bash
    #### 分功能进行shell文本的编写
    #1.删除中间文件
    rm -rf ./hd.img ./loader.bin ./kernel.bin ./main.o &&\
    
    #2.编译连接程序
    ## 将使用汇编编写的主引导记录编译成二进制文件
    nasm -I include/ -o mbr.bin mbr.s &&\
    ## 将内核加载文件编译成二进制文件
    nasm -I include/ -o loader.bin loader.s &&\
    ## 将c语言文件编译成32位汇编文件
    gcc -m32 -c -o main.o main.c &&\
    ## 将二进制文件写入硬盘镜像并指定起始虚拟地址
    ld -m elf_i386 main.o -Ttext 0xc0001500 -e main -o kernel.bin &&\
    
    #3.硬盘处理
    ## 新建硬盘镜像文件
    bin/bximage -hd -mode="flat" -size=60 -q hd.img &&\
    ## 将主引导记录的二进制文件写入硬盘镜像文件
    dd if=mbr.bin of=hd.img bs=512 count=1 conv=notrunc &&\
    ## 将内核加载文件的二进制文件写入硬盘镜像文件中
    dd if=loader.bin of=hd.img bs=512 count=4 seek=2 conv=notrunc &&\
    ## 将内核文件写入虚拟硬盘中
    dd if=kernel.bin of=hd.img bs=512 count=200 seek=9 conv=notrunc&&\
    
    #4.启动bochs
    bin/bochs -f bochsrc 
    
    
    
    
参考资料
  1. 第六章一个问题https://www.jianshu.com/p/b1f863201f4a
  2. 第五章代码https://love6.blog.csdn.net/article/details/117871478
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逆羽飘扬

如果有用,请支持一下。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值