操作系统真象还原——第5章 保护模式进阶,向内核迈进
第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 0x00000000dd 0x00000000CODE_DESC: dd 0x0000FFFFdd DESC_CODE_HIGH4STACK_DATA_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4VIDIO_DESC: dd 0x80000007dd DESC_VIDEO_HIGH4GDT_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_LIMITdd GDT_START ; int 15h eax = 0000E820h,edx=534D4150h,则为获取内存布局 start:xor ebx,ebx ; 第一次调用时,ebx清0mov 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 0x15add 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_bufxor edx,edx ; edx存放最大内存容量,在此先清零 ; 冒泡排序找最大的内存容量,并存放到edx中 find_max_mem_loop:mov eax,[ebx]add eax,[ebx+8]add ebx,20cmp edx,eaxjge nextmov 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_DATAmov ds,axmov es,axmov ss,axmov esp,LOADER_STACK_TOPmov ax,SELECT_VIDIOmov gs,axmov 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 0x00000000dd 0x00000000 ; 根据段选择子位功能进行的拼凑 CODE_DESC: dd 0x0000FFFFdd DESC_CODE_HIGH4STACK_DATA_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4VIDIO_DESC: dd 0x80000007dd DESC_VIDEO_HIGH4GDT_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_LIMITdd GDT_START ; int 15h eax = 0000E820h,edx=534D4150h,则为获取内存布局 start:xor ebx,ebx ; 第一次调用时,ebx清0mov 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 0x15add 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_bufxor edx,edx ; edx存放最大内存容量,在此先清零 ; 冒泡排序找最大的内存容量,并存放到edx中 find_max_mem_loop:mov eax,[ebx]add eax,[ebx+8]add ebx,20cmp edx,eaxjge nextmov 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_DATAmov ds,axmov es,axmov ss,axmov esp,LOADER_STACK_TOPmov ax,SELECT_VIDIOmov gs,ax ; 创建页目录及页表并初始化页内存位图call setup_page;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载sgdt [gdt_ptr] ; 存储到原来gdt所有的位置;将gdt描述符中视频段描述符中的段基址+0xc0000000mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xc0000000 ;视频段是第3个段描述符,每个描述符是8字节,故0x18。;段描述符的高4字节的最高位是段基址的31~24位;将gdt的基址加上0xc0000000使其成为内核所在的高地址add dword [gdt_ptr + 2], 0xc0000000add esp, 0xc0000000 ; 将栈指针同样映射到内核地址; 把页目录地址赋给cr3mov eax, PAGE_DIR_TABLE_POSmov cr3, eax; 打开cr0的pg位(第31位)mov eax, cr0or eax, 0x80000000mov 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 ; 目录页表为4KBmov esi, 0 .clear_page_dir:mov byte [PAGE_DIR_TABLE_POS + esi], 0 ; 大写的是页目录表物理地址的宏,值为0x100000inc esiloop .clear_page_dir;开始创建页目录项(PDE) .create_pde: ; 创建Page Directory Entrymov eax, PAGE_DIR_TABLE_POSadd 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, 0x1000mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址;下面创建页表项(PTE)mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256mov esi, 0mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1 .create_pte: ; 创建Page Table Entrymov [ebx+esi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 add edx,4096inc esiloop .create_pte;创建内核其它页表的PDEmov eax, PAGE_DIR_TABLE_POSadd eax, 0x2000 ; 此时eax为第二个页表的位置or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为0mov ebx, PAGE_DIR_TABLE_POSmov ecx, 254 ; 范围为第769~1022的所有目录项数量mov esi, 769 .create_kernel_pde:mov [ebx+esi*4], eaxinc esiadd eax, 0x1000loop .create_kernel_pderet
-
成功截图
-
汇编代码编译成机器码后,加载到内存中,在不进行编译优化的情况下,源码和对应地址的二进制编码相对应,即紧跟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; //本结构体的sizeElf32_Half e_phentsize; //单个Program头的sizeElf32_Half e_phnum; //Segment头表中Segment头的个数Elf32_Half e_shentsize; //单个Section头的szieElf32_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_0000000000000bDESC_S_sys equ 0_000000000000b ;第12位为0 则表示系统段 为1则表示数据段 DESC_S_CODE equ 1_000000000000b ;第12位与type字段结合 判断是否为系统段还是数据段 DESC_S_DATA equ DESC_S_CODEDESC_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 0xc0001500PT_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 ; 段寄存器不能使用立即数进行赋值,可以使用通用寄存器axmov es,ax ; 将其他段寄存器初始化为0mov ss,ax mov fs,axmov sp,0x7c00 ; SP是堆栈指针寄存器,存放着当前堆栈栈顶地址,程序都要使用堆栈而0x7c00以下是安全的(栈向低地址生长,0x7c00以上是MBR程序)mov ax,0xb800 ; OxB8000后32KB的内存区域是用于文本显示,所以该处输出的宇符即可通过显存打印在显示器屏幕上mov gs,ax; 4.向显存输出字符 ; 使用10号中断的0x06功能号,进行窗口上卷的清屏操作,避免BIOS检测信息影响显示mov ax,0600h ; ah存放将要调用的中断子功能号mov bx,0700hmov 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],0xA4mov byte [gs:0x04],'M'mov byte [gs:0x05],0xA4mov byte [gs:0x06],'B'mov byte [gs:0x07],0xA4mov 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 ; 备份eaxmov di,cx ; 备份cx; 读写硬盘; 第一步:设置要读取的扇区数mov dx,0x1f2 ; 存储端口号mov al,clout dx,al ; 读取的扇区数(out指令用于向端口写数据)mov eax,esi ; 恢复ax; 第二步:将LBA地址存入端口0x1f3 ~ 0x1f6; LBA地址的0~7位写入端口0x1f3mov dx,0x1f3out dx,al; LBA地址15~8位写入端口0x1f4mov cl,8shr eax,clmov dx,0x1f4out dx,al; LBA地址23~16位写入端口0x1f5shr eax,clmov dx,0x1f5out dx,alshr eax,cland al,0x0f ; lab第24~27位or al,0xe0 ; 设置7-4位为1110,表示lba模式mov dx,0x1f6out dx,al; 第三步:向0x1f7端口写入读命令0x20mov dx,0x1f7mov al,0x20out dx,al; 第四步:检测硬盘状态.not_ready: ; 同一端口写时表示写入命令字,读时表示读入硬盘状态nopin al,dx and al,0x88 ; 第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙cmp al,0x08jnz .not_ready ; 若未准备好,继续等待; 第5步:从0x1f0端口读数据; di为要读取的扇区数,一个扇区有512字节,每读取一个字,共需di*512/2次mov ax,dimov dx,256mul dxmov cx,axmov dx,0x1f0.go_on_read:in ax,dxmov [bx],axadd bx,2loop .go_on_readret ; $表示本行指令所在的地址,$$表示本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_BASEards_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 ;异或自己 即等于0mov ax,0 mov es,ax ;心有不安 还是把es给初始化一下mov di,ards_buf ;di指向缓冲区位置 .e820_mem_get_loop:mov eax,0x0000E820 ;每次都需要初始化mov ecx,0x14mov edx,0x534d4150int 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,0jne .e820_mem_get_loop ;直至读取完全结束 则进入下面的处理时间mov cx,[ards_nr] ;反正也就是5 cx足以mov ebx,ards_bufxor edx,edx .find_max_mem_area:mov eax,[ebx] ;我也不是很清楚为什么用内存上限来表示操作系统可用部分add eax,[ebx+8] ;既然作者这样用了 我们就这样用add ebx,20 ;简单的排序cmp edx,eaxjge .next_ardsmov edx,eax.next_ards:loop .find_max_mem_areajmp .mem_get_ok.e820_failed_so_try_e801: ;地址段名字取的真的简单易懂 哈哈哈哈 mov ax,0xe801int 0x15jc .e801_failed_so_try_88;1 先算出来低15MB的内存 mov cx,0x400mul cx ;低位放在ax 高位放在了dxshl 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,eaxmov ax,bxmov ecx,0x10000 ;0x10000为64KB 64*1024 mul ecx ;高32位为0 因为低32位即有4GB 故只用加eaxmov edx,esiadd edx,eaxjmp .mem_get_ok.e801_failed_so_try_88:mov ah,0x88int 0x15jc .error_hltand eax,0x0000FFFFmov cx,0x400 ;1024mul cxshl edx,16or edx,eax add edx,0x100000.error_hlt:jmp $ .mem_get_ok:mov [total_mem_bytes],edx ; --------------------------------- 设置进入保护模式 ----------------------------- ; 1 打开A20 gate ; 2 加载gdt ; 3 将cr0 的 pe位置1in al,0x92 ;端口号0x92 中 第1位变成1即可or al,0000_0010bout 0x92,allgdt [gdt_ptr]mov eax,cr0 ;cr0寄存器第0位设置位1or eax,0x00000001 mov cr0,eax;-------------------------------- 已经打开保护模式 ---------------------------------------jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线[bits 32]p_mode_start: mov ax,SELECTOR_DATAmov ds,axmov es,axmov ss,axmov esp,LOADER_STACK_TOP;------------------------------- 加载内核到缓冲区 -------------------------------------------------mov eax, KERNEL_BIN_SECTORmov ebx, KERNEL_BIN_BASE_ADDRmov ecx,200call 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],0xc0000000add dword [gdt_ptr+2],0xc0000000 ;gdt起始地址增加 分页机制开启的前奏add esp,0xc0000000 ;栈指针也进入高1GB虚拟内存区mov eax,PAGE_DIR_TABLE_POSmov cr3,eaxmov eax,cr0or eax,0x80000000mov cr0,eaxlgdt [gdt_ptr]mov eax,SELECTOR_VIDEOmov gs,eaxmov byte [gs:160],'V'jmp SELECTOR_CODE:enter_kernel;------------------------------ 跳转到内核区 enter_kernel:call kernel_init ;根据我们的1M以下的内存分布区 综合考虑出的数据mov esp,0xc009f000jmp KERNEL_ENTER_ADDR;------------------------------- 创建页表 ------------------------------------------------ setup_page:mov ecx,0x1000 ;循环4096次 将页目录项清空 内存清0mov esi,0 .clear_page_dir_mem: ;dir directory 把页目录项清空mov byte [PAGE_DIR_TABLE_POS+esi],0inc esiloop .clear_page_dir_mem.create_pde: mov eax,PAGE_DIR_TABLE_POS ;页目录项 起始位置add eax,0x1000 ;页目录项刚好4k字节 add eax即得第一个页表项的地址;接下来我们要做的是 把虚拟地址1M下和3G+1M 两部分的1M内存在页目录项中都映射到物理地址0-0XFFFFFor 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_POSadd eax,0x1000mov ecx,256mov esi,0mov ebx,PG_P | PG_RW_W | PG_US_U .create_kernel_pte: mov [eax+esi*4],ebxinc esiadd ebx,0x1000loop .create_kernel_pte ;这里对于我们这里填写的目录项所对应的页表 页表中我们还没填写的值 ;为了实现 真正意义上的 内核空间被用户进程完全共享 ;只是把页目录与页表的映射做出来了 mov eax,PAGE_DIR_TABLE_POSadd eax,0x2000 ;eax此时处于第二个页表or eax,PG_P | PG_RW_W | PG_US_U ;这里循环254次可以来分析一下 我们这里做的是 0xc0 以上部分的映射 0xc0 对应的是第768个页表项 页表项中一共有 2^10=1024项 ;第1023项我们已经设置成 映射到页目录项本身位置了 即1022 - 769 +1 = 254mov ebx,PAGE_DIR_TABLE_POSmov ecx,254 mov esi,769.create_kernel_pde:mov [ebx+esi*4],eaxinc esiadd eax,0x1000loop .create_kernel_pde ret ;----------------------- 初始化内核 把缓冲区的内核代码放到0x1500区域 ------------------------------------------ ;这个地方主要对elf文件头部分用的很多 ;可以参照着书上给的格式 来比较对比 kernel_init:xor eax,eax ;全部清零xor ebx,ebxxor ecx,ecxxor 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_NULLje .PTNULL ;空即跳转即可 不进行mem_cpymov eax,[ebx+8]cmp eax,0xc0001500jb .PTNULLpush dword [ebx+16] ;ebx+16在存储的数是filesz 可以翻到Loader刚开始mov eax,[ebx+4] add eax,KERNEL_BIN_BASE_ADDRpush eax ;p_offset 在文件中的偏移位置 源位置 push dword [ebx+8] ;目标位置call mem_cpyadd esp,12 ;把三个参数把栈扔出去 等于恢复栈指针.PTNULL:add ebx,edx ;edx是一个描述符字节大小loop .get_each_segment ;继续进行外层循环 retmem_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+4mov esi,[ebp+12] ;源指针 源位置 8+4mov ecx,[ebp+16] ;与Movsb好兄弟 互相搭配 12+4rep movsb ;一个一个字节复制pop ecx pop ebpret;------------------------ 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 ; !!! 备份eaxmov di,cx ; !!! 备份cxmov dx,0x1F2 ; 0x1F2为Sector Count 端口号 送到dx寄存器中mov al,cl ; !!! 忘了只能由ax al传递数据out dx,al ; !!! 这里修改了 原out dx,clmov eax,esi ; !!!袄无! 原来备份是这个用 前面需要ax来传递数据 麻了;;;;;;;;;;;;;;;;;;;;; ;2 写入LBA 24位寄存器 确认扇区 ;;;;;;;;;;;;;;;;;;;;;mov cl,0x8 ; shr 右移8位 把24位给送到 LBA low mid high 寄存器中mov dx,0x1F3 ; LBA lowout dx,al mov dx,0x1F4 ; LBA midshr eax,cl ; eax为32位 ax为16位 eax的低位字节 右移8位即8~15out dx,almov dx,0x1F5shr eax,clout dx,al;;;;;;;;;;;;;;;;;;;;; ;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1 ;;;;;;;;;;;;;;;;;;;;;; 24 25 26 27位 尽管我们知道ax只有2 但还是需要按规矩办事 ; 把除了最后四位的其他位置设置成0shr eax,cland 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 0x88cmp al,0x08jne .not_ready ; !!! jump not equal == 0;;;;;;;;;;;;;;;;;;;;; ;6 读取数据 ;;;;;;;;;;;;;;;;;;;;;mov ax,di ;把 di 储存的cx 取出来mov dx,256mul dx ;与di 与 ax 做乘法 计算一共需要读多少次 方便作循环 低16位放ax 高16位放dxmov cx,ax ;loop 与 cx相匹配 cx-- 当cx == 0即跳出循环mov dx,0x1F0.go_read_loop:in ax,dx ;两字节dx 一次读两字mov [ebx],axadd ebx,2loop .go_read_loopret ;与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
相关文章:

操作系统真象还原——第5章 保护模式进阶,向内核迈进
第5章 保护模式进阶,向内核迈进 BIOS中断利用0x15子功能0xe802获取内存 汇编语言子功能的调用 填写调用前相关寄存器进行int中断调用获取返回结果输出到对应寄存器的值 80286 拥有24 位地址线,其寻址空间是16MB 。有一些ISA 只使用15MB,剩下…...
设计一款助听器可能需要用到以下音频算法
设计一款助听器可能需要用到以下音频算法: 1 响度补偿算法:助听器可能需要根据用户的听力损失情况调整不同频率范围内的增益,以提供个性化的听力补偿。这可以通过基于用户配置或自适应算法的频率响应调整来实现。 2 噪声抑制:用于…...
【端午节】用Vue3写粽子——从零开始
前言 在端午节即将到来之际,我们来一起写一个粽子组件来庆祝这个传统节日。 准备工作 首先,我们需要安装Vue3及其相关依赖,这里使用Vue CLI来创建项目。 # 安装Vue CLI npm install -g vue/cli # 创建Vue3项目 vue create zongzi接下来&a…...

大象机器人人工智能套装2023版深度学习协作机器人、先进机器视觉与应用场景
引言: 介绍当前的版本 今天我们要介绍的是aikit2023,aikit2023是aikit的全新升级版。 AIkit 2023 是一套集视觉,定位抓取、自动分拣模块为一体的入门级人工智能套装。 该套装基于python平台,可通过开发软件实现机械臂的控制&am…...

Cesium Token申请
一、什么是Cesium ion? Cesium ion是一个提供瓦片图和3D地理空间数据的平台,支持把数据添加到用户自己的应用程序中。 二、为什么需要access token? 使用Cesium ion需要申请access token,当用户将数据添加到自己的账户后,便可以…...

ubuntu系统自带的Text Editor编辑器不高亮解决办法
平时在写launch文件时,我喜欢用ubuntu系统自带的text编辑器,但发现使用text打开launch 文件时,没有高亮功能了,如下图所示: 解决办法非常简单,因为launch和xml文件语法规则类似,只需将text编辑…...

Docker NGINX 加载Geoip模板
前提环境: Docker 环境 涉及参考文档: ngx_http_geoip_module 模块Loki NGINX Service MeshGeoIP IP库 一、下载GeoIP IP库 二、配置Nginx主配置文件 vim /data/nginx/MangoMoh/dos/nginx.confuser nginx; worker_processes auto;error_log /var…...

springboot基于协同过滤算法商品推荐系统
开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7 数据库工具:Navicat11 开发软件:eclipse/myeclipse/idea Maven包:Maven3.3.9 浏览器&…...

基于机器学习算法:朴素贝叶斯和SVM 分类-垃圾邮件识别分类系统(含Python工程全源码)
目录 前言总体设计系统整体结构图系统流程图 运行环境Python 环境安装pytesseract注册百度云账号 模块实现1. 数据模块2. 模型构建3. 附加功能 系统测试1. 文字邮件测试准确率2. 网页测试结果 工程源代码下载其它资料下载 前言 本项目采用朴素贝叶斯和支持向量机(S…...
在Linux下将PNG和JPG批量互转的四种方法
计算机术语中,批处理指的是用一个非交互式的程序来执行一序列的任务的方法。这篇教程里,我们会使用 Linux 命令行工具,并提供 4 种简单的处理方式来把一些 .PNG 格式的图像批量转换成 .JPG 格式的,以及转换回来。 计算机术语中&a…...

Scala中使用 break 和 continue
Scala中没有 break 和 continue 关键字,但是我们可以用 Breaks 类提供的相应方法来实现对应功能。 在Java中,break continue return的区别 1、break:break不仅可以结束其所在的循环,还可结束其外层循环,但一次只能结束…...
【全栈开发指南】打包sentinel-dashboard镜像推送到Docker Hub镜像仓库
在使用sentinel-dashboard的时候,发现官方并没有把jar包发布到Docker Hub镜像仓库,所以,我们需要自己手动将需要版本的sentinel-dashboard.jar发布到Docker Hub镜像仓库。首先需要在Docker Hub镜像仓库网站 https://hub.docker.com/ 上注册账…...

【数据可视化】SVG(一)
一、邂逅SVG和初体验 什么是SVG SVG全称为(Scalable Vector Graphics),即可缩放矢量图形。(矢量定义:既有大小又有方向的量。在物理学中称作矢量,如一个带箭头线段:长度表示大小࿰…...

linux 系统errno 对应参考及代码
结论 linux下系统errno都有对应的说明描述,发生错误时获取errno即可知道具体问题描述 如下图 代码如下 golang版 package main import ("syscall""strings""fmt" ) func main() {for i : 0; i < 200; i {if !strings.HasPrefi…...

PowerShell快速ssh
文件 ~/.ssh/config 内容 Host masterHostName 192.168.10.154User root访问 $ ssh master 效果 进阶 免密的方式ssh 本地生成秘钥 ssh-keygen输入文件名称然后两次回车,完成后,在~/.ssh目录下会生成my_rsa和 my_rsa.pub两个文件 linux服务器上…...

从php5.6到golang1.19-文库App性能跃迁之路
作者 | 百度文库App 导读 本文深入浅出地分享了百度文库App服务端技术栈从PHP迁移至Go的实战经验,包含了技术选型、基础建设、流量迁移的具体方案,以及核心项目案例的重构实践。 全文6209字,预计阅读时间16分钟。 01 动机 长期以来ÿ…...
成功解决 AttributeError: ‘Field‘ object has no attribute ‘vocab‘
最近复现代码过程中,需要用到 torchtext.data 中的 Field 类。本篇博客记录使用过程中的问题及解决方式。 注意 torchtext 版本不宜过新 在较新版本的 torchtext.data 里面并没有 Field 方法,这一点需要注意。 启示:在复现别人代码时&#…...

ikbc键盘2.4G接收器丢失,重新对码
我的键盘:ikbc W200 1.键盘关掉重开; 2.新接收器插在电脑上; 3.电脑上打开软件,点开始对码,一会就连接上了。 对码软件放在这里: 我用夸克网盘分享了「IKBC 对码.rar」,点击链接即可保存。打开…...

STM32 Proteus仿真医用仓库环境控制系统紫外线消毒RS232上传CO2 -0066
STM32 Proteus仿真医用仓库环境控制系统紫外线消毒RS232上传CO2 -0066 Proteus仿真小实验: STM32 Proteus仿真医用仓库环境控制系统紫外线消毒RS232上传CO2 -0066 功能: 硬件组成:STM32F103R6单片机 LCD1602显示器DHT11温度湿度电位器模拟…...

Docker(二)之容器技术所涉及Linux内核关键技术
容器技术所涉及Linux内核关键技术 一、容器技术前世今生 1.1 1979年 — chroot 容器技术的概念可以追溯到1979年的UNIX chroot。它是一套“UNIX操作系统”系统,旨在将其root目录及其它子目录变更至文件系统内的新位置,且只接受特定进程的访问。这项功…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...

跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...

《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...
LeetCode - 199. 二叉树的右视图
题目 199. 二叉树的右视图 - 力扣(LeetCode) 思路 右视图是指从树的右侧看,对于每一层,只能看到该层最右边的节点。实现思路是: 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
PAN/FPN
import torch import torch.nn as nn import torch.nn.functional as F import mathclass LowResQueryHighResKVAttention(nn.Module):"""方案 1: 低分辨率特征 (Query) 查询高分辨率特征 (Key, Value).输出分辨率与低分辨率输入相同。"""def __…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...