第5章 保护模式进阶,向内核迈进
-
BIOS中断利用0x15子功能0xe802获取内存
-
汇编语言子功能的调用
- 填写调用前相关寄存器
- 进行int中断调用
- 获取返回结果输出到对应寄存器的值
-
80286 拥有24 位地址线,其寻址空间是16MB 。有一些ISA 只使用15MB,剩下的1MB作为缓冲区,为了兼容保留了下来,但是现在很少ISA设备,操作系统不可以用此段内存空间。所以成为了内存空洞memory hole。
-
BIOS的0x15中断获取内存空间的三个子功能
- eax=0xe820:遍历主机上全部内存
- ax = 0xe801:分别检测出低15MB和16~4GB的内存
- ah = 0x88:最多检测出64MB内存
-
查询内存信息的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 $
-
成功截图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hQ65SRj-1689072108287)(C:\Users\waterstop\AppData\Roaming\Typora\typora-user-images\image-20220226103917712.png)] -
运行脚本改进
- 增加&&,使得指令只能顺序执行
- / 用于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
-
操作系统和硬件是相互依赖、相互推动、相互促进而发展起来的
-
内存和硬盘中的数据都是以二进制存储的
-
内存分页机制的基础原理:通过映射将连续的线性地址也任意的物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续
-
通过
段基址+偏移地址
获得的线性地址,再检查线性地址- 分页机制打开:使用线性地址在页表中查询
- 分页机制关闭:将线性地址作为物理地址直接使用
-
分页机制的作用
- 将线性地址转换成物理地址
- 用大小相等的页代替大小不等的段
-
4G的线性地址空间属于所有进程的共享资源,其中标注为已分配页的内存块被分配给了其他进程,当前进程只能使用未分配页。
-
逻辑地址与物理地址的对应关系称为映射,而页表存储了这种映射关系
-
页表的地址转换:用线性地址的高20 位在页表中索引页表项,用线性地址的低12 位与页表项
中的物理地址相加,所求的和便是最终线性地址对应的物理地址。 -
页表是用于管理内存的数据结构,也要占用内存
-
二级页表机制
- 容量:页目录表中有1024个页表,每个页表中有1024个物理页,即总容量为
1024*1024*4KB = 4GB
- 作用:32位的虚拟地址中,高10 位在页目录表中索引一个页目录项PDE,页目录项中有页表物理地址。中间10 位在页表中索引到某个页表项PTE,页表项中有分配的物理页地址。用获得物理页地址和低12 位作为页内偏移量用于在已经定位到的物理页内寻址
- 访问页表内的任何数据都要使用物理地址,页目录表项和页表项都是4字节,所以真正的表内物理地址需要*4
- 容量:页目录表中有1024个页表,每个页表中有1024个物理页,即总容量为
-
页目录项及页表项
- 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),可用位
-
启用分页机制的步骤
- 准备好页目录表及页表
- 将页表地址写入控制寄存器cr3
- 寄存器cr0的PG位置1(分页机制的开关)
-
控制寄存器cr3 用于存储页表物理地址,所以cr3 寄存器又称为页目录基址寄存器
-
控制寄存器和通用寄存器可以相互间进行数据的传递,所以可以使用mov进行处理
-
操作系统安全机制:用户进程必须运行在低特权级,当用户进程需要访问硬件相关的资源时,需要向操作系统申请,由操作系统去做,之后将结果返回给用户进程。
-
分页模式
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
-
成功截图
-
汇编代码编译成机器码后,加载到内存中,在不进行编译优化的情况下,源码和对应地址的二进制编码相对应,即紧跟jmp指令后面的内存空间的定义(dd、dw···)不是jmp直接跳过的,可以认为无优化编译是一种静态的翻译,而CPU真正执行时,对应的内存空间已经存入对应的值
-
PDE是页目录项,PTE是页表项
-
当物理内存不足时,操作系统的虚拟内存管理机制有可能会将该PDE 或PTE 指向的物理页框换出到磁盘上,此时PDE 或PTE 的P 位便被操作系统置为0,处理器访问该PDE 或PTE时会触发page_fault 缺页异常,操作系统为该异
常注册了中断处理程序,该程序会将所缺的页从磁盘上重新加载到内存中,并将P 位置为1 -
为什么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中没有空描述符一说
-
进入分页机制运行模式:数据在内存中最终以物理地址来访问,但访问任何物理地址都需要通过虚拟地址
-
二级页表可以通过增加或删除页表项或页目录项进行动态增减。
-
页表是将虚拟地址转换成物理地址的映射表,在分页机制下,如何用虚拟地址访问到页表自身呢?
-
虚拟地址直接与物理地址一一对应(不用)
-
虚拟地址与物理地址乱序映射
先要从CR3 寄存器中获取页目录表物理地址,然后用虚拟地址的高10 位乘以4 的积作为在页目录表中的偏移量去寻址目录项pde ,从pde 中读出页表物理地址,然后再用虚拟地址的中间10 位乘以4 的积作为在该页表中的偏移量去寻址页表项pte,从该pte 中读出页框物理地址,用虚拟地址的低12 位作为该物理页框的偏移量(物理地址为基址,逻辑地址乘以4作为偏移量)
-
-
在虚拟机中使用
info tab
命令可以获取逻辑和物理地址的映射关系 -
用虚拟地址获取页表
- 获取页目录表物理地址:让虚拟地址的高20 位为0xfffff,低12 位为0x000 ,即0xfffff000,这也是页目录表中第0 个页目录项自身的物理地址。
- 访问页目录中的页目录项,即获取页表物理地址:要使虚拟地址为Oxffiffxxx ,其中xxx 是页目录项的索引乘以4 的积。
- 访问页表中的页表项z 要使虚拟地址高10 位为0x3ff,目的是获取页目录表物理地址。中间10 位为页表的索引,因为是10 位的索引值,所以这里不用乘以4 低12 位为页表内的偏移地址,用来定位页表项,它必须是己经乘以4 后的值。
-
虚拟地址和物理地址的转换需要频繁进行内存访问,处理器中断等待资源被浪费。快表TLB(Translation Lookaside Buffer)用来专门存储虚拟地址页框和物理地址页框的映射关系,根据程序的局部性,减少到内存的访问,匹配高速的处理器速率和低速的内存访问速度。处理器在寻址之前优先访问TLB,会用虚拟地址的高20 位作为索引来查找TLB 中的相关条目,如果命中则返回虚拟地址所映射的物理页框地址,否则会查询内存中的页表,获得页框物理地址后再更新TLB。而且只有P 位为1 的页表项才有资格在TLB 中,如果TLB 被装满了,需要将很少使用的条目换出。
-
TLB对方程序员透明,但是可以间接进行更新TLB
- 重新加载CR3
- 使用
invlpg 虚拟地址
,可以刷新TLB中的该虚拟地址表项
-
标准库程序是对于系统调用的效率和规范的平衡
-
gcc编译后生成的
.o
文件知识一个目标文件,还需要进行重定位(给文件中的所有符号安排地址) -
操作系统是给用户提供功能支持的平台
-
BIOS 调用mbr, mbr 的地址是0x7c00, mbr 调用loader,loader 的地址是0x900。这
两个地址是固定的 -
不同平台的c 编译器也会根据系统平台自动添加文件头,文件头用来描述程序的内存布局信息,通常有8个字节,前四个是程序的长度,后四个是程序的入口地址(控制信息)
-
Windows下可执行文件的格式是PE(exe只是扩展名),Linux下可执行文件格式是ELF,是经过编译链接后可以直接运行的文件
-
执行的程序由段(segment)和节(section)组成,多个节经过链接后合并成了一个段
-
硬盘中的不同程序尽量不要完全相邻,隔开点不容易出现问题
-
gcc常用参数
- -c 的作用是编译、汇编到目标代码,不进行链接,也就是直接生成目标文件。
- -o 的作用是将输出的文件以指定文件名来存储,有同名文件存在时直接覆盖。
-
.o
文件是一个待重定位文件,即文件中的符号地址需要其他目标文件进行地址编排,才能链接成为一个可执行文件 -
程序的开头常常有函数或数据的定义,所以入口地址通常不是函数的开始处,main 函数通常在运行库代码初始化完环境后才被调用。
-
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"的下标 };
-
内存空间的规划最好隔开一点,对于不再使用的进行无情的覆盖
内核运行完整代码
选的课太多了,两个月没搞,有点忘记了,把所有的程序代码整理注释了一遍,下面是运行程序和过程
-
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
-
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结尾的标识
-
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下一条指令
-
main.c
//#include<stdio.h>// 不需要 int main(void){ while(1); return 0; }
-
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
参考资料
-
第六章一个问题https://www.jianshu.com/p/b1f863201f4a
-
第五章代码https://love6.blog.csdn.net/article/details/117871478
in.c//#include<stdio.h>// 不需要 int main(void){ while(1); return 0; }
-
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
参考资料
- 第六章一个问题https://www.jianshu.com/p/b1f863201f4a
- 第五章代码https://love6.blog.csdn.net/article/details/117871478