第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
- 页目录项及页表项
- 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
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