操作系统实验(真象还原)
第0章
操作系统是什么
操作系统把资源获取到后交给用户进程,而不允许用户直接访问硬件资源
写一个操作系统需要了解硬件知识,这些硬件提供了软件接口,于是操作系统可以通过接口操作硬件
接口的标准
接口就是标准,访问外部硬件主要有两个方式:
1. 将外设的内存映射到地址空间中,CPU通过地址总线访问该内存区域时就落到外设的内存中。如先显卡上的显存,就被映射到主机存储的0xB8000 ~ 0xBFFFF
2. 通过访问I/O接口上的寄存器来实现对硬件的访问
应用程序对系统的访问
编译器提供了一套库函数,库函数中又封装了系统调用,这样的代码集合称之为运行库。而C语言的运行库叫C运行库,简称CRT(C Runtime Library)
用户态和内核态是对于CPU来讲的,用户进程陷入内核态是指,由于内部或者外部中断的产生,开始执行一段内核的代码。当程序陷入内核后,它的上下文已经被保存到自己的0级特权栈中了,CPU中已经运行的内核程序。
分段
分段最大的好处就是实现了重定位,加载用户程序时,只用将这整个段的内容复制到新的位置,并将段基地址改为该地址。
同时将段基地址*16 + 16位的段内偏移就可以访问20位的地址空间了。
代码中的分段,在汇编级程序员可以人为将程序分为多个段。而在高级语言中,如C,gcc会把C语言写的程序分为代码段,数据段,栈段,.bss段,堆等部分。
地址
在实模式下,基地址+偏移,就是物理地址
但是进入保护模式后,这个地址是线性地址,也叫选择字。它是个索引,通过这个索引可以在GDT中找到相应的段描述符,描述符中有段的起始,大小等信息,这样就得到了基地址,若没有开启分页功能,那么按照原来的方法这个就是物理地址。
开启分页之后,这个地址就变成了虚拟地址,要转换为物理地址。
linux程序和windows程序
linux下的可执行文件是elf格式而windows下是PE格式,不能互通。同时linux下API和windows下不同
大端字节序和小端字节序
小端字节序:高字节在高地址,低字节在低地址
大端字节序:高字节在低地址,低字节在高地址
小端优势: 低字节在低地址,在强制类型转换时就不用调节字节,低字节依旧在低字节
大端优势: 有符号数的符号就在低字节可以快速判断正负
BIOS中断,DOS中断,Linux中断
BIOS和DOS都运行在实模式下,由它们建立的中断调用都是在中断向量表中的(IVT)。它们都是通过软中断指令int来调用的。
中断向量表中的每个向量的大小是4字节。4个字节描述了一个中断程序的的段基地址和段内偏移。因为中断向量表的大小为1024字节,所以最多容纳256个中断处理程序。计算机启动之初,中断向量表中的中断程序是由BIOS建立的,它从物理地址0开启添加各种历程。
每个外设都有自己的内存,不过都是ROM。硬件自己的功能调用程序及初始化代码就存在ROM中,根据规范,第一个字节为0x55,第二个字节为0xAA,第三个字节是ROM中以512字节为单位的代码长度,第四个字节开始就是代码了
内存的物理地址0xA0000到0xFFFFF中部分用于映射,这样BIOS就检测到了硬件的存在,并执行了硬件自带的历程和初始化函数。
DOS也是运行在实模式下的,故其建立的中断调用也建立在中断向量表中,只不过中断向量号不能和BIOS的冲突。0x20~0x27是DOS中断。
linux内核是进入保护模式后才建立例程的,此时存在的是中断描述符表(IDT)
Section和Segment
不管定义了多少section,最终会把属性相同的section放到一块,合并成一个大的segment
如何控制CPU获取下一条指令
在x86中每一指令的操作码的前缀都不会是另一个操作码,于是就可检测到是什么指令,并得到长度。
库函数是用户进程和内核的桥梁
头文件中一般仅仅只有函数声明,它一般说明了至少两件事:
1. 函数返回值类型,参数的类型及其个数,用来确定分配栈空间
2.该函数是外部函数,定义在其他的文件中,现在无法为其分配地址,需要在连接阶段将该函数体所在的目标文件一同连接时再安排地址。
操作系统有自己支持,加载用户进程的规则,C运行时库就是针对该操作系统的规则,用于支持用户进程的代码库。
用户进程要与C运行时库的诸多目标文件进行合并成一个可执行文件
C运行时库,提供程序运行时所需要的库文件,而且还做了初始化的工作,所以即使不包含标准库文件,也会链接C运行时库
用户进程需要操作系统的支持必须通过系统调用
系统调用封装在库函数中
MBR,EBR,DBR,OBR
MBR是主引导记录,它位于磁盘的0盘0道1扇区。BIOS知道MBR在0盘0道1扇区,它会将0盘0道1扇区的MBR加载道物理地址0x7c00,然后跳过去执行。
MBR 引导扇区中除了引导程序,还有64字节的分区表,里面是分区信息。表中每个分区表项占16字节,因此MBR分区表有4个分区,4个分区就是次引导程序的候选人,MBR程序遍历4个分区(0x55aa结尾),找到合适的人选将系统的控制权交给它。
设置分区,如果互动标志0x80,说明这个分区中有操作系统,分区的起始扇区存放的是操作系统引导程序--内核加载器。MBR找活动分区后跳到OBR的起始处,OBR的前3字节是跳转指令,跳到引导程序,交出控制权。
第一章
环境的部署
安装CentOS 7
下载 bochs 版本 2.6.*
执行tar zxvf bochs-2.6.*.tar.gz
cd 到bochs
./configure \
--prefix=/home/porterlu/bochs \
--enable-debugger \
--enable-disasm \
--enable-iodebug \
--enable-x86-debugger\
--with-x \
--with-x11
make & make install
配置bochs
在bochs目录下建立.bochsrc
megs : 32 #设置内存
romimage: file=/home/porterlu/bochs/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/porterlu/bochs/share/bochs/VGABIOS-lgpl-latest
boot: disk
log: bochs.out
mouse: enabled=0
keyboard: keymap=/home/porterlu/bochs/share/bochs/keymaps/x11-pc-us.map
ata0:enabled=1,ioaddr1=0x1f0,ioaddr2=0x3f0,irq=14
ata0-master:type=disk,path="hd60M.img",mode=flat
创建硬盘
bin/bximage -hd -mode="flat" -size=60 -q hd60M.img
第二章
起始 结束 大小 用途 FFFF0 FFFFF 16B BISO的入口地址,jmp f000:e05b F0000 FFFEF 64KB-16B BIOS去掉入口的部分 C8000 EFFFF 160KB 用于映射硬件适配器的ROM或者内存映射式的I/O C0000 C7FFF 32KB 显示适配器 B8000 BFFFF 32KB 文本显示适配器 B0000 B7FFF 32KB 黑白显示适配器 A0000 AFFFF 64KB 彩色显示适配器 9FC00 9FFFF 1KB 扩展BIOS数据区 7E00 9FBFF 约608KB 可用区域 7C00 7DFF 512B MBR被BIOS加载到此处 500 7BFF 30KB 可用区域 400 4FF 256B BIOS数据区 000 3FF 1KB 中断向量表
;mbr.s
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
;调用int 6进行清屏
;AH = 0X06 ,AL=0 表示全部行,BH 是上卷行属性
mov ax,0x600
mov bx,0x700
mov cx,0
mov dx,0x18ef
int 0x10
;获取光标
mov ah,3
mov bh,0
int 0x10
mov cx,5
mov ax,0x1301 ;13号子功能,al=01显示字符串光标随着移动弄
mov bx,0x2 ;bh=0,0号页,bl=2 黑底绿字
int 0x10
jmp $
message db "1 MBR"
times 510-($-$$) db 0
db 0x55,0xaa
dd if=mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc
第三章
寻址方式
直接寻址
立即数寻址
内存寻址
基址寻址(BX)
变址寻址(DI,SI)
以BP作为基址的寻址:sp寄存器作为栈顶指针,相当于栈中的游标,专门给push和pop指令做为导航。既让sp不可随便移动,则用bp访问栈内的数据,bp默认的段寄存器就是ss,ss : bp 就可以把栈当成一个普通的数据段访问。
执行一个function(),先将参数压入栈,调用这个函数,之后
push ebp ;
mov ebp,esp;
之后 将esp减去一个值,相当为函数申请了局部空间,而ebp就是局部空间的分界
实模式下的ret
call 和 ret 是一对配合,用于近调用和近返回;call far 和 retf是一对配合,用于远调用和远返回
实模式下的call
16位实模式相对近调用
近的意思就是在同一个段内,不用切换段,只用给出段内的偏移
相对的意思,只用给目标地址的相对地址就可以了
call proc_name
如果proc_name被编译器分配的地址是0x1234,call 指令操作数并不是0x1234,而是目标地址减去当前的pc值 = des - (src + length_of_ins)
16位实模式间接绝对近调用
call [addr]
这里call 默认使用的是近调用
这里的操作数也可以是访问寄存器
16位实模式直接绝对远调用
call 新基址(立即数): 新偏移(立即数)
注意这里先将cs压入栈中后压ip
16位实模式间接绝对远调用
call far [bx]
这里操作数去内存中的4个字节,低字节是偏移,高字节是段基址
不支持寄存器寻找,只支持内存寻址
实模式下的jmp
16位实模式相对短转移 原理和近转移一致
jmp short 0x10
这是转移的范围缩小为 -128 ~ 127
16位实模式相对近转移,
16位实模式间接绝对近转移,
16位实模式直接绝对远转移,
16位实模式间接绝对远转移 都和调用一致
标志寄存器flags
取8086的12位
第 0 位 是CF carry flag 用于检测最高位的进位和借位 用无符号数的加减法的溢出检测
第 2 位 是PF parity flag 用于检测最低的8位中的 1 的个数是奇数还是偶数,如果是偶数个则置为1 否则 置为 0
第 4 位 是AF auxiliary carry flag 是辅助进位标记 ,用于记录低 4 位的进位情况
第 6 位 是ZF zero flag 用于表示计算结果是否为0
第 7 位 是SF sign flag 是标记符号位,用于表示结果的正负,如果结果为负数则置为1
第 8 位 是TF trap flag 是陷阱标志位,置为1 CPU就会就如单步运行模式
第 9 位 是IF interrupt flag是中断标志位,置为0 会屏蔽可屏蔽中断
第 10 位是DF direction flag是方向标志位,用于设置字符串操作中的地址增加方向,当置为1时,指令中的地址会自动减少一个单位
第 11 位是OF overflow flag 用于检测有符号数是否有溢出现象的发生
显示器输出
mbr运行在实模式下,实模式下可以用BIOS的int 0x10中断打印字符串,因为中断向量表只在实模式下存在。
显存是由显卡提供的,显卡可以读取这块内存,并显示到显示器上,显卡可以让显示器工作在图形模式也可以让显示器工作在文字模式
这里用ascii 码表示字符
显示地址分布
起始 结束 大小 用途 C0000 C7FFF 32KB 显示适配器BIOS B8000 BFFFF 32KB 用于文本模式显示适配器 B0000 B7FFF 32KB 用于黑白显示适配器 A0000 AFFFF 64KB 用于彩色显示适配器
显卡的文本显示模式也分多种模式,用“行数 * 列数" 表示,显卡加电后默认是80* 25 ,也就是一屏可以显示2000个字符
文本模式下也可以显示彩色字符,可以用连续的两个字符用于显示一个字符,高字节用于设置颜色属性,低字节用于显示字符。
格式为 背景色 K R G B ,K用于设置是否闪烁
前景色 I R G B ,I 用于设置是否高亮
ascii 字符
bochs的调试
x,xp用于查看内存,,x后面接线性地址, xp后接物理地址
u 可以反汇编程序,后面跟的是线性地址
q 可以用于关闭虚拟机
set reg=val 可以用于设置寄存器的值
show指令,show mode可以CPU每次变换模式时。做出一个提示
show int 每次有中断就有提示
show call 每次调用函数就会提示
c 会使程序一直执行下去
s 支持单步执行
p 每次也执行一条指令,但是遇到一个函数时会跳过整个函数
vb,lb,pb 都是加一个断点,分别用的是虚拟地址,线性地址,物理地址
sb delta 再执行delta条指令就中断
sba time 从 CPU开始运行算起,执行time条执行就会中断
watch命令
watch r physic_address 如果在物理地址physic_address 有读命令则产生中断
watch w physic_address 如果在物理地址physic_address 有写命令则产生中断
watch 会显示所有读写断点
unwatch 会清楚所有的读写断点
blist 显示所有断点信息
bpd/bpe n(断点号) 可以禁用一个断点或者开启一个断点
d n(断点号) 可以删除一个断点
setpmem physic_address size val 可以设置连续size个字节为val
r 可以显示寄存器信息
ptime 可以显示CPU启动后执行的指令数
print-stack n 打印栈内的信息
info指令
info pb
info CPU
info fpu
info idt
info gdt
info ldt
info tss
info ivt
info flags/eflags
sreg 显示段寄存器
dreg 显示调试寄存器
creg 显示控制寄存器
info tab 显示页表中线性地址到物理地址的映射
page line_addr 将line_addr 映射到物理地址
硬盘
磁盘上通过盘面号,扇区号,磁道号定位一个512字节的扇区
为了减少寻道时间,这里将地址的顺序记为磁道号,盘面号,扇区号。所以读取连续的空间,是优先变换磁头,即激活不同的磁头即可
磁盘接口的有的寄存器在进行读写操作时有不同的含义
这里数据寄存器是16位的,而其他的寄存器都是8位的
磁盘物理上使用 柱面,磁头,扇区定位的,即Cylinder,Head ,Sector (CHS)这种定位方式不适合给出一个地址,这里给出一种从0开始计数的方式即Logic Block Address(LBA)。
这里用的是LBA28,LBA寄存器有3个,分别是LBA low,LBA mid,LBA high 三个,存储0~23位。
而24~27放在Device寄存器的低4位,第4位用于指示使用主盘还是从盘,第六位用于设置是否开启LBA方式,其他位默认为1
0x1f7 读时是status,写时是command寄存器
主要有三个command
-----boot.inc-------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
-----mbr.s--------
%include "boot.inc"
SECTION MBR vstart = 0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0b800
mov gs,ax
mov ax,0x0600 ;表示6号功能,al=0表示全部行
mov bx,0x0700 ;bh=07 设置上卷行的属性
mov cx,0
mov dx,0x184f ;表示右下角的坐标为第18行,第80列
int 10h
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xa4
mov byte [gs:0x00],' '
mov byte [gs:0x01],0xa4
mov byte [gs:0x00],'M'
mov byte [gs:0x01],0xa4
mov byte [gs:0x00],'B'
mov byte [gs:0x01],0xa4
mov byte [gs:0x00],'R'
mov byte [gs:0x01],0xa4
mov eax,LOADER_START_SECTOR
mov bx,LOADER_BASE_ADDR
mov cx,1
call rd_disk_m_16
jmp LOADER_BASE_ADDR
rd_disk_m_16:
mov esi,eax
mov di,cx
mov dx,0x1f2; 输入sector count
mov al,cl
out dx,al
mov eax,esi
mov dx,0x1f3
out dx,al
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f
or al,0xe0 ;主盘,开启LBA
mov dx,1f6
out dx,al
mov dx,0x1f7
mov al,0x20
out dx,al;读命令
.not_ready:
nop
in al,dx
and al,0x88 ;这里0x1f7 变为status寄存器
cmp al,0x08 第7为0非忙,第3位为1表示已经就绪
jnz .not_ready
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
times 510-($-$$) db 0
db 0x55,0xaa
第4章 保护模式
这里多了一个比例因子
运行模式反转
[bits 16],[bits 32]可以告诉编译器生成16位还是32位的指令
[bits 32]中使用16操作数便会在生成的机械码前缀加上0x66,那么CPU就知道是16位的操作数
寻址模式反转
如果[bits 32]中寻址用到了2字节操作数,0x67寻址方式反转
全局描述符表
全局描述符表是保护模式下内存段的登记表
总共32位段基址,20位段界限
段的扩展方向可以向上也可以向下,数据段和代码段是向上,栈段是向下
段界限边界 = (段界限 + 1)*(段界限粒度)-1
第23位的G是0,表示粒度是1字节,G为1 表示粒度为4KB
段信息会被缓冲到段描述符缓冲寄存器中,不用每次都进行读取并整合
8~11位是type类型
第12 位是表示是系统段 还是 数据段(软件的部分,无论代码还是数据),而系统段就是各种“门”的结构。
第12位 S 加上 8~11位 描述类型
DPL表示特权级
P表示段是否在内存中
L是1 表示是64位代码,L是0表示是32位代码
对于代码段这是D位,D为0 表示有效地址和操作数是16位,D为1 表示有效地址和操作数是32位
对于栈段这是B位,B为0表示使用sp,B为1表示使用esp
全局描述表GDT,及选择子
全局体现在多个程序可以在里面定义自己的段描述符,是公共的
全局描述表需要有专门的寄存器指向它,这个寄存器便是GDTR
进入保护模式后寻址突破了1MB,可以重新加载GDT
而段寄存器cs,ds,es,fs,gs,ss中的便是段选择子,选择子中索引值加一些属性
TI表示是否全区描述符表
注意全局描述表的第0个是不可用的
打开A20
in al,0x92
or al,00000010b
out 0x92,al
保护模式的开关,CR0的PE位
mov eax,cr0
or eax,0x00000001
mov cr0,eax
进入保护模式代码
----boot.inc----
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
DESC_G_4K equ 0x800000 ;粒度为4KB
DESC_D_32 equ 0x400000 ;表示32位操作数
DESC_L equ 0x000000 ;表示是32位代码
DESC_AVL equ 0x000000 ;CPU不用此位,置为0
DESC_LIMIT_CODE2 equ 0xf0000; 段界限最高4位
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0x00000
DESC_P equ 0x8000 ;P置为1,表示在内存中
DESC_DPL_0 equ 0x0000
DESC_DPL_1 equ 0x2000
DESC_DPL_2 equ 0x4000
DESC_DPL_3 equ 0x6000
DESC_S_CODE equ 0x1000 ;数据段
DESC_S_DATA equ DESC_S_CODE ;数据段
DESC_S_sys equ 0x0000 ;系统段
DESC_TYPE_CODE equ 0x800 ;只执行代码段
DESC_CODE_DATA equ 0x200 ;可写,向下扩展数据段
DESC_CODE_HIGE4 equ 0x00<<24 + DESC_G_4K + DESC_D_32 + DESC_LIMIT_CODE2 + DESC_P + DESC+DPL_0 +DESC_S_CODE + DESC_TYPE_CODE + 0X00
;表示段基址高16位全是0,段界限粒度为4K,32位操作数,32位代码,段界限高4位为全1,同时在内存中,0级特权,非系统段,只执行代码段,
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
;段基地址高16位为0,段界限粒度为4K,32位操作数,32位代码,段界限高4位为全1,在内存中,0级特权级,非系统段,向上扩展可写数据段
DESC_VIDEO_HIGH4 equ 0x00<<24 +DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
;这里段界限高4位都是0
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
-----loader.s-----
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start
GDT_BASE: dd 0x00000000
dd 0x00000000
GODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007
dd DESC_DATA_HIGH4
GDT_SIZE equ $-GDT_BASE
GDT-LIMIT equ GDT_SIZE -1
times 60 dq 0
SELECTOR_CODE equ (0x0001<<3) + TI_GDT +RPL0
SELECTOR_DATA equ (0x0002<<3) +TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT +RPL0
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
loadermsg db '2 loader in real.'
loader_start:
mov sp,LOADER_BASE_ADDR
mov bp,loadermsg
mov cx,17
mov ax,0x1301
mov bx,0x001f
mov dx,0x1800
int 0x10
in al,0x92
or al,0x00000010b
out 0x92,al
lgdt [gtr_ptr]
mov eax,cr0
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 ax,SELECTOR_VIDEO
mov gs,ax
mov byte [gs:160],'P'
jmp $
保护模式段的保护
向段寄存器加载选择子时的保护
段基址 + 选择子索引*8 + 7 <=段描述符基址 + 段描述符界限
同时cs段只能加载具备可执行的段
具有可执行的段不允许加载到除了cs外的段寄存器
只有具有可写属性的段才能加载到DS,ES,FS,GS
代码段和数据段的保护
段界限值 = (段界限 + 1)*(4K 或 1) -1
对于粒度为1 就是描述符中的段界限
对于粒度为4K ,就是界限*0x1000 + 0xFFF
要求EIP + 指令长度 - 1 <= 实际段界限大小
栈段的保护
对于向下扩展的栈(地址减少),段界限 + 1=<栈的地址 <=栈基址
向内核迈进
获取物理内存容量
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
;此处省去GDT的定义
times 60 dq 0
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
ards_buf times 244 db 0
ards_nr dw 0
loader_start:
xor ebx,ebx
mov edx,0x534d4150
mov di,ards_buf
.e820_mem_get_loop:
mov eax,0x0000e820
mov ecx,20
int 0x15
jc .e820_failed_so_try_e801
add di,cx
inc word [ards_nr]
cmp ebx,0
jnz .e820_mem_get_loop
mov cx,[ards_nr]
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
-----------ax = e801---------
.e820_failed_so_try_e801
mov ax,e801
int 0x15
jc .e801_failed_so_try88
mov cx,0x400
mul cx ;以KB为单位
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx,0x11000000
mov esi,edx
xor eax,eax
mov ax,bx
mov cx,0x10000
mul ecx
add esi,eax
mov edx,esi
jmp .mem_get_ok
.e801_failed_so_try88
mov ah,0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF
mov cx,0x400
mul cx
shl ex,16
or edx,eax
add edx,0x100000
.mem_get_ok:
mov [total_mem_bytes],edx
开启分页
P:表示是否在内存中
RW:读写位,表示是否可写
US:用户级,特权用户还是普通用户
PWT:写透
PCD:是否禁用cache缓存
A:Access
D: Dirty
PAT:略
G:全局页将一直在TLB
AVL:不管
setup_page:
mov ecx,4096
mov esi,0
;清零页目录
.clear_page_dir:
mov byte[PAGE_DIR_TABLE_POS + esi],0
inc esi
loop .clear_page_dir
;页目录基本结构建立
.create_pde
mov eax,PAGE_DIR_TABLE_POS
add eax,0x1000
mov ebx,eax
or eax, PG_US_U| PG_RW_W|PG_P
mov [PAGE_DIR_TABLE_POS + 0x0],eax
mov [PAGE_DIR_TABLE_POS + 0xc00],eax
sub eax,0x1000
mov [PAGE_DIR_TABLE_POS + 4092],eax
;第一个页表的页表项
mov ecx,256
mov esi,0
mov edx,PG_US_U|PG_RW_W|PG_P
.create_pte:
mov [ebx+esi*4],edx
add edx,4096
inc esi
loop .create_pte
mov eax, PAGE_DIR_TABLE_POS
add eax,2000
or eax,PG_US_U|PG_RW_W|PG_P
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
页表建立的代码
call setup_page
sgdt [gtr_ptr]
mov ebx,[gdt_ptr + 2] ;获取基址
or dword [ebx + 0x18 +4],0xc0000000
add dword [gdt_ptr + 2],0xc0000000
add esp,0xc0000000
mov eax,PAGE_DIR_TABLE_POS
mov cr3,eax
mov eax,cr0
or eax,0x8000000
mov cr0,eax
lgdt [gdt_ptr]
mov byte [gs:160],'V'
jmp $
虚拟地址 物理地址 0x00000000~0x000fffff 0x00000000~0x000fffff 0xc0000000~0xc00fffff 0x00000000~0x000fffff 0xffc00000~0xffc00fff 0x00101000~0x00101fff 0xfff00000~0xfff00fff 0x00101000~0x00101fff 0xfffff000~0xffffffff 0x00100000~00100fff
ELF文件格式
e_ident[0] = x07f
e_ident[1~3]="ELF"
e_ident[4] 用于表示elf文件的类型,0表示无法识别的类型,1表示32位elf文件格式,2表示64位elf文件格式
e_ident[5] 指定大端字节序,还是小端字节序,还是非法编码格式
e_ident[6] 默认为1,表示当前版本
之后的位暂时不用
e_type 占用2字节
ET_NONE 0 表示未知的目标文件格式
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 动态共享目标文件
ET_CODE 4 core文件,即程序崩溃时其内存映像的转储格式
ET_LOPROC 0XFF00 特定处理器文件的扩展下边界
ET_HIPROC 0XFFFF 特定处理器扩展文件上边界
e_machine也占用两字节
EM_NONE 0 未指定
EM_M32 1 AT&T WE 32100
EM_SPARC 2 SPARC
EM_386 3 Intel 80386
EM_68K 4 Motorola 68000
EM_88K 5 Motorola 88000
EM_860 7 Intel 80860
EM_MIPS 8 MIPS RS3000
e_version 占用4字节,表示版本
e_entry 占用4字节,运行时告诉操作系统控制权交到的虚拟地址
e_phoff 占用4字节,表示程序头表在文件内的字节偏移
e_shoff 占用4字节,表示节头表在文件内的字节偏移
e_flags 占用4字节,指明与处理器相关的参数
e_phentsize 占用2字节,指明程序头表中的每个条目的字节大小
e_phnum 占用2字节,指明程序头表中的条目数量
e_shentsize 占用2字节,表述节头点表中每个条目字节大小
e_shnum 占用2字节,表示节头点表中条目的数量
e_shstrndx 指明string name table 在节头点表中索引的index
p_type 占用4字节
PT_NULL 0 忽略
PT_LOAD 1 可加载程序段
PT_DYNAMIC 2 动态链接信息
PT_INTERP 3 动态加载器的名称
PT_NOTE 4 一些辅助的附加信息
PT_SHLIB 5 保留
PT_PHDR 6 程序头表
PT_LOPROC 0X70000000
PT_HIPROC 0X7FFFFFFF 此范围内的类型预留给处理器专用
p_offset 占用4字节 指明此段在文件内的起始偏移地址
p_vaddr 占用4字节 用来指明本段在内存中的起始虚拟地址
p_paddr 占用4字节 只用于和物理地址相关的系统中
p_filez 占用4字节 用来指明本段在文件中的大小
p_memsz 占用4字节 指明本段在内存中的大小
p_flags 占用4字节
PF_X 1 本段具有可执行的权限
PF_W 2 本段具有可写权限
PF_R 4 本段具有可读权限
PF_MASKOS 0x0ff00000 本段与操作系统相关
PF_MASKPROC 0Xf0000000 本段与处理器相关
p_align 占用4字节 指明对齐方式
将内核载入内存
内核的文件是kernel.bin,这个文件由loader将其从硬盘读出,这里选择将kernel.bin写在磁盘的第9扇区,一共占用200个扇区
内核加载到内存中,得有个加载地址,就是缓冲区。内核被加载到内存后,loader还要分析其elf文件格式将其扩展到新的位置,内核在内存中有两份拷贝,一份是elf格式的源文件,另一份是loader解析后在内存中生成的内存映像
----加载kernel----
mov eax,KERNEL_START_SECTOR
mov ebx,KERNEL_BIN_BASE_ADDR
mov ecx,200
call rd_disk_m_32
calll setup_page
movs[bdw]指令族,重复执行指令rep,方向指令cld和std,这些指令配合实现复制大块的数据
movsb 用于从DS:ESI 搬运到 ES:EDI 一字节
cld,清楚方向标志位,用于让数据的源地址和目的地址逐渐扩大
ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
;-------------将kernel.bin的segment拷贝到elf中的地址-----------
kernel_init:
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
mov dx,[KERNEL_BIN_BASE_ADDR + 42]
mov ebx,[KERNEL_BIN_BASE_ADDR + 28]
add ebx,KERNEL_BIN_BASE_ADDR;得到程序头表的起始地址
mov cx,[KERNEL_BIN_BASE_ADDR + 44]
.each_segment:
cmp byte [ebx+0],PT_NULL
je .PTNULL
push dword[ebx+16];size
mov eax,[ebx+4]
add eax,KERNEL_BIN_BASE_ADDR
push eax ;源地址
push dword [ebx + 8];目的地址
call mem_cpy
add esp,12
.PTNULL
add ebx,edx
loop .each_segment
ret
mem_cpy:
cld
push ebp
mov ebp,esp
push ecx
mov edi,[ebp+8]
mov esi,[ebp+12]
mov ecx,[ebp+16]
rep movsb
pop ecx
pop ebp
ret
entry_kernel:
call kernel_init
mov esp,0xc009f00
jmp KERNEL_ENTRY_POINT
特权级
特权级按照权力大小分为0,1,2,3级,数字越小权力越大,操作系统位于0级,系统程序位于1,2级,用户程序运行于第3级
TSS和特权级
TSS 即 Task State Segment,即任务状态段,是系统段的一种。
它是处理器默认的多任务解决方案使用的数据结构,它是每个任务都有的结构,默认104字节。任务在特权级变换时,涉及了栈的变换问题,不同的特权级涉及了不同的栈,在TSS中有三个分别是ss0和esp0,ss1和esp1,ss2和esp2。但是没有ss4,esp4在TSS中,当处理器由低特权级转移向高特权级时,处理器才会从TSS中找到对应的特权级栈的基址和指针,特权级4是最低特权级,没有低特权级会转向它。
而高特权级向低特权级转移,只有调用返回指令,那么低特权级指令已经在栈中,并不需要TSS。
显然TSS地址,也应该存在寄存器中表示当前任务,这就是TR寄存器。
CPL 和 DPL
CS寄存器的低两位即RPL(request privilege level)位就是CPL即处理器当前的特权级
DPL 中的D是描述符的意思,也就是段描述符,段描述符中的DPL位中记录着该值。
指令访问时会检查RPL,DPL,CPL
先不考虑RPL
对于访问的为代码段的情况:只能平级转移,即CPL和DPL相等,因为对于代码,低特权级能解决的,高特权级一定能解决
对于访问的是数据段的情况:只有CPL大于等于该数据段的DPL才能访问。
非一致性代码就是上面将讲的代码段的情况,但是一致性代码要求CPL>=DPL就可以访问,且保持CPL不变,即与原来的特权级一致。
门,调用门,RPL序
除了任务们其余三种门都对应了一种例程,任务门可以在GDT,LDT,IDT中;调用门可以在GDT,LDT中;中断门和陷阱门只能在IDT中
任务们和调用门可以通过call和jmp调用。而陷阱门和中断门只存在与IDT中需要通过中断触发。
调用门可以通过call和jmp调用,call指令使用调用门可以实现向高特权级的转移,而jmp只能实现平级的代码转移
中断门,以int指令可以主动发中断的形式实现由低特权向高特权级的转移
陷阱门,一般编译器调试时使用
任务门,任务以TSS为单位,用来实现任务的切换,它可以用中断或者指令发起。当中断发生如果对应的中断向量号是任务们,则会发起任务切换
首先,门描述符与数据描述符一样,只允许比自己特权级高的或者同等特权级才能访问,但是当前CPL不能比门描述符中的目标代码段的DPL高,否则转移就转移没什么意义。
门描述符用来指向某个内核例程,是个例程就需要参数,接下来讨论如何在3特权级向0特权级传递参数,调用时要将ss,esp保存。之后更新ss,esp到0级特权栈,将原来的ss和esp压入。之后压入参数(根据描述符中的参数个数字段得知参数的个数),0级特权栈复制3级特权栈中的参数信息。最后在0级特权栈压入CS,EIP,更新cs,EIP。
注意当gs,es等寄存器中在返回时如果发现其中的特权级高于CPL,那么会自动置为0,触发异常。
同时为了防止进入0级特权后,用户程序通过参数对内核数据进行修改,引入RPL的概念,RPL时请求特权级,在选择子的低两位。是指令中的参数的低两位,当执行内核程序时即使CPL = 0 ,但是RPL仍然会保持原来的3特权级,就无法访问内核数据
对于用户伪造RPL的情况,操作系统可以通过arpl来用cs修改当前请求的选择子低两位,这里的cs在栈中,特权上升,将原本的cs,eip压入了栈。
IO特权级
在I/O读写的控制上,eflags中有IOPL位,只有特权级高于IOPL才能执行IO指令。如果特权级低于IOPL中的特权级,那么要看TSS中的IO位图是否开启了对应端口,io位图以0xff结尾
在IO位图满大小的情况,0xff可以防止访问超出界限的部分,因为1表示禁止。同时0xff可以提前结束IO位图,因为IO位图可以不满。