欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用Rust操作开发操作系统(虚拟地址和物理地址操作)

程序员文章站 2022-07-05 18:10:33
...


在上一章中我们实现了基本的位操作,在本节中我们使用之前写好的位操作开始实现地址的操作,我们先了解一下地址的理论知识

地址空间

地址空间在一般情况下分为两类:虚拟地址空间,物理地址空间,虚拟地址空间氛围逻辑地址,有效地址,线性地址.这些地址可以相互转换

虚拟地址空间

虚拟地址空间是一个抽象的地址,大多不能独立转换为物理地址,逻辑地址,有效地址,线性地址和平坦地址都属于虚拟地址的范畴

  • 逻辑地址:指应用程序角度看到的内存单元,存储单元,一个逻辑地址由两部份组成,段标识符和段内偏移量,段标识符是由一个16位长的字段组成
  • 线性地址: 线性地址是通过逻辑地址中的段基址和段偏移组合而成,使得程序无法字节访问线性地址
  • 平坦地址:是一个种特殊的线性地址,将段基址和段长度覆盖整个线性地址空间

物理地址

物理地址是真实存在硬件设备上的地址,通过处理的引脚直接或间接与外部设备,RAM,ROM相连接,在物理地址空间中除了物理内存还有硬件设备,在处理开启分页的情况下线性地址需要经过页表映射才能转为物理地址

  • I/O地址: I/O地址空间与内存地址空间相互隔离,必须IN/OUT指令才能访问,I/O地址空间由65536个可独立寻址的I/O端口组成,寻址范围为0-0xFFFF,0xF8和0xFF保留使用
  • 内存地址: 内存地址不仅只有物理内存,还有外部的硬件设备地址空间(例如之前的VGA地址)

寻址

实模式寻址

实模式采用逻辑地址方式,通过段基地址加上段偏移地址的形式进行寻址,可以使用如下形式转换成线性地址

线性地址=段地址 * 16 + 偏移地址

在一些书上你能看到这样的公式线性地址=段地址<<4 + 偏移地址如果你知道位移知识的话就知道他们说的都是一个意思,实模式不支持分页机制,从而使线性地址直接映射到物理地址

保护模式寻址

保护模式下段寄存器无法字节加载段描述符,必须通过段选择子才能将目标段描述符加载到段寄存器,段选择子是一个16位段描述符的索引值,结构如下

15           3  2  1     0
|            |     |     |
+------------+-----+-----+
|   Index    | TI  | RPL |
+------------+-----+-----+

段选择子位功能说明

名称 功能描述
Index 用于索引目标段描述符
TI 目标段描述符所在的描述符表类型
RPL 请求特权级

处理器将Index*8作为偏移量,从描述符表中取得目标描述符,然后检查特权级,检测通过,处理器便将目标描述符加载到段寄存器的缓存区内

保护模式下段寄存器包含2个区域,可见区隐藏区,当选择子被处理器加载到段寄存器的可见区后,处理器会自动将段描述符加载到段寄存器的不可见区域,在多喝处理器系统中,当描述符表(Descriptor Table)改变时,软件需要将描述符重新加载到段寄存器

								 GDT/LDT描述符表
								+---------------+
								|				|
								| SegDescriptor	|
				|-------------->|				|
				|				+---------------|
			    |
+---------+--------+
| 隐藏部分 | 可见部分 |
+---------+--------+

保护模式段描述符

63				56 55  54 53  52 51    48 47 46 45 44 43 40 39      32 31       16 15      0
|           |  |   |  |   ||       |  ||    |  ||   ||          ||				  ||       |
+-----------+--+---+--+---+--------+--+-----+--+----+-----------+-----------+--------+
|BaseAddr(H)|G |D/B|L |AVL|Limit(H)|P | DPL |S |Type|BaseAddr(M)|BaseAddr(L)|Limit(L)|
+-----------+--+---+--+---+--------+--+-----+--+----+-----------+-----------+--------+
  • 段限长字段(Limit):段限长字段用于指定段的长度,处理器会把段描述符中两个长字段组合成一个20位的值,根据颗粒度标识指定段限长的实际含义,如果G=0则段长度的范围可从1字节到1MB字节,如果G=1则段长度可从4KB-4GB
  • 基地址字段(BaseAddr):该字段在4GB线性地址空间中一个字节长度0所处的位置,处理器会把3个分立的基地址字段组合形成32位值,段地址应该对齐16字节边界
  • 段类型字段(Type):指定段或门的类型,说明段的访问种类以及段的扩展方向,该段的解释依赖于描述符类型标识符S指明是一个应用描述符还是一个系统描述符
  • 描述符类型标志(S):指明一个段描述符时系统段描述符还是代码或数据段描述符
  • 段特权级(DPL):指明段描述符的特权级,特权级从0到3,0级最高,3级最低,DPL用于控制对段的访问
  • 段存在标志§:指出一个段时在内存中(p=1)还是不在内存中(p=0)
  • 默认操作大小/默认栈指针大小标志(D/B):根据段描述是一个可执行代码段,下扩数据段,还是一个堆栈段(对于32位代码和数据这个标志总是设置为1,对于16位代码段,这个标志被设置为0)
  • 颗粒度标识符(G):该字段用于确定限长字段的Limit值的单位,如果颗粒度标志为0则段限长值单位时字节,如果设置了颗粒度标志,则段限长使用4KB单位
  • 可用和保留比特位(AVL):段描述符第二个双字的位20可供系统软件使用,位21时保留并总是设置为0

代码段描述符

代码段描述符Type结构如下

| 43| 42|41 |40 |
+---+---+---+---+
| - | C | R | A |
+---+---+---+---+
  • A标志位(Accessed,已访问):记录代码是否被访问过,当A=1表示被访问过否则表示未访问,处理器只负责置位不负责复位
  • R标志位(Readable,可读): 如果想读取程序段中的数据必须将该位置位
  • C标志位(Conforming,一致性): 代码段分为一致性代码段和非一致性代码段,处理器通过此标志位可以进行标识

一致性代码段和非一致性代码段,我在刚看到这个的时候也是一头雾水,这里的一致性我的理解是在低特权级程序可以执行或跳转到高特权级(相同特权级)的代码段,并在执行过程中底特权级程序的CPL不变(执行前和执行后特CPL都是一致的,不会因为跳转到高特权级代码段的时候发生改变),所有的数据段都是非一致性的,意味着不能被底特权级程序访问

这样就好比你是一个普通职员,有一天领导让你去收拾一份保密文件,在收拾保密文件的过程中你的身份还是普通职员,并不会因为你收拾了保密文件而变成高级职员

数据段的非一致性就好比你工作时使用的数据可以被你上级查看,但是上级使用的数据你不能随意查看

如果段描述符的S位与第43位同时被置1,表示这个段描述符的类型位代码段描述符,代码段的Type标志区域(第40-42位)可组合的区域如下

十进制 十六进制 位11 C R A 描述符类型 说明
8 0x8 1 0 0 0 代码 仅执行
9 0x9 1 0 0 1 代码 仅执行,已访问
10 0xA 1 0 1 0 代码 执行/可读
11 0xB 1 0 1 1 代码 执行/可读,已访问
12 0xC 1 1 0 0 代码 一致性段,仅执行
13 0xD 1 1 0 1 代码 一致性段,仅执行,已访问
14 0xE 1 1 1 0 代码 一致性段,执行/可读
15 0xF 1 1 1 1 代码 一致性段,执行/可读,已访问

数据段描述符

代码段描述符Type结构如下

| 43| 42|41 |40 |
+---+---+---+---+
| - | E | W | A |
+---+---+---+---+

如果段描述符的S标志被置1,第43位处于复位状态,那么这个段描述符的类型就是数据段描述符

十进制 十六进制 位11 位10(E) 位9(W) 位8(A) 描述符类型 说明
0 0x0 0 0 0 0 数据 只读
1 0x1 0 0 0 1 数据 只读,已访问
2 0x2 0 0 1 0 数据 执行/可读
3 0x3 0 0 1 1 数据 执行/可读,已访问
4 0x4 0 1 0 0 数据 向下扩展,只读
5 0x5 0 1 0 1 数据 向下扩展,只读,已访问
6 0x6 0 1 1 0 数据 向下扩展,可读/写
7 0x7 0 1 1 1 数据 向下扩展,可读/写,已访问
  • A标志位(Accessed,已访问):记录代码是否被访问过,当A=1表示被访问过否则表示未访问,处理器只负责置位不负责复位
  • W标志位(Write-enable,可读写): 表示数据段的读写权限,当W=1时表示可以读写,W=0时表示只读
  • E标志位(Expansion-direction,方向位): 该标志位指示数据段的扩展方向,当E=1时表示向下扩展,当E=0时表示向上扩展

系统段描述符

如果段描述符的S标志位处于复位状态,那么段描述符的类型为系统段描述符

十进制 十六进制 位43 位42 位41 位40 含义 含义
0 0x0 0 0 0 0 Reserved 保留
1 0x1 0 0 0 1 16-Bits TSS(Available) 16位TSS(可用)
2 0x2 0 0 1 0 LDT LDT
3 0x3 0 0 1 1 16-Bits TSS(Busy) 16位TSS(忙)
4 0x4 0 1 0 0 16-Bits Call Gate 16位调用门
5 0x5 0 1 0 1 Task Gate 任务门
6 0x6 0 1 1 0 16-Bits Interrupt Gate 16位中断门
7 0x7 0 1 1 1 16-Bits Call Gate 16位陷阱门
8 0x8 1 0 0 0 Reserved 保留
9 0x9 1 0 0 1 32-Bits TSS(Available) 32位TSS(可用)
10 0xA 1 0 1 0 Reserved 保留
11 0xB 1 0 1 1 32-Bits TSS(Busy) 32位TSS(忙)
12 0xC 1 1 0 0 32-Bits Call Gate 32位调用门
13 0xD 1 1 0 1 Reserved 保留
14 0xE 1 1 1 0 32-Bits Interrupt Gate 32位中断门
15 0xF 1 1 1 1 32-Bits Call Gate 32位陷阱门

LDT段描述符,TSS段描述符,调用门描述符以后介绍

段描述符关系如下

S 43 类型
1 1 代码段描述符
1 0 数据段描述符
0 - 系统段描述符

保护模式地址转换示意图

IA-32e模式寻址

IA-32e模式的线性地址位宽64位,但是线性寻址只有48位,低48位用于线性地址寻址,高16位用于符号扩展(将第47位数值扩展到64位,全为0或全为1),这种地址称为Canonical地址,在IA-32e模式下,只有Canonical地址空间是可用地址空间,而No-Canonical空间属于无效地址空间

+---------------+<- 0xFFFFFFFF_FFFFFFFF
|               |                      
|   Canonical   |                      
|               |<-0xFFFF8000_00000000 
+---------------+<-0xFFFF7FFF_FFFFFFFF 
|               |                      
| Non-Canonical |                      
|               |<-0x00008000_00000000 
+---------------+<-0x00007FFF_FFFFFFFF 
|               |                      
|   Canonical   |                      
|               |                      
+---------------+<-0x00000000_00000000 

但采用64位Canonical地址后页管理机制也改成4级,低48位参与页表索引,高16位依旧不参与页表空间索引,页管理机制支持在4KB页面基础上增加2MB和1GB物理页

Canonical地址结构

63             48 47                     0
|                ||                      |
+----------------+-----------------------+
| Sign Extension |    Liner Address      |
+----------------+-----------------------+

IA-32e段描述符

代码段描述符

结构如下


63        56 55    52 51   48 47     40 39        16 15        0 
|           ||      ||       ||        ||           ||         | 
+-----------+-------+--------+---------+------------+----------+ 
|BaseAddr(H)| Attr1 |limit(H)|  Attr2  | BaseAddr(L)| limit(L) | 
+-----------+-------+--------+---------+------------+----------+ 
                       
                          
63        56 55  54 53 52  51    48 47 46 45 44 43  42 41 40 39        16 15        0 
|           |  |   |  |   ||       |   |   |   |   |  |  |  ||           ||         | 
+-----------+--+---+--+---+--------+---+---+---+---+--+--+--+------------+----------+
|BaseAddr(H)|G |D/B|L |AVL|limit(H)|P  |DPL|S  |C/D|C |R |A | BaseAddr(L)| limit(L) | 
+-----------+--+---+--+---+--------+---+---+---+---+--+--+--+------------+----------+  
   															(详细版)

数据段描述符

63        56 55  54 53 52  51    48 47 46 45 44 43  42 41 40 39        16 15        0 
|           |  |   |  |   ||       |   |   |   |   |  |  |  ||           ||         | 
+-----------+--+---+--+---+--------+---+---+---+---+--+--+--+------------+----------+
|BaseAddr(H)|G |D/B|L |AVL|limit(H)|P  |DPL|S  |C/D|E |W |A | BaseAddr(L)| limit(L) | 
+-----------+--+---+--+---+--------+---+---+---+---+--+--+--+------------+----------+ 

IA-32e模式数据段不仅忽略了段基址和段长度而且L标志位,D/B标志位,G标志位也均不起作用

系统段描述符

IA-32e模式的系统段描述符从保护模式的8B扩展到16B,IA-32e还对Type区域进行了精简

43 42 41 40 功能
0 0 0 0 16B描述符的高8B
0 0 0 1 保留
0 0 1 0 LDT段描述符
0 0 1 1 保留
0 1 0 0 保留
0 1 0 1 保留
0 1 1 0 保留
0 1 1 1 保留
1 0 0 0 保留
1 0 0 1 64位TSS段描述符
1 0 1 0 保留
1 0 1 1 64位TSS段描述符
1 1 0 0 64位调用门描述符
1 1 0 1 保留
1 1 1 0 64位中断门描述符
1 1 1 1 64位陷进门描述符

TSS,调用门,中断门等在后续文章中讲述

开始干活

好了,我们已经知道了关于在实模式,保护模式,IA-32e模式下的地址变化,我们现在开始着手编写IA-32e模式下的地址操作,我们可以构造虚拟地址和物理地址的结构VirtAddrPhyisAddr,VirtAddr用于Canonical地址,对u64类型进行包装,在使用时会检查一段地址是否属于Canonical地址,并且我们要提供一些指针算术的操作(加,减)等操作,PhyisAddrVirtualAddr操作基本一致

首先我们在system项目中创建新的模块称为ia_32e并创建子模块addr.rs,目录结构如下


​```
system
|
|__ src
|		|
|		|__ lib.rs
|		|
|		|__ bits
|		|		|
|		|		|__ mod.rs
|		|
|		|__ ia_32e
|				|
|				|__mod.rs
|				|
|				|__addr.rs
|
|__ Cargo.toml
|
|__ .gitignore
​```

然后在src/lib.rs文件中添加以下内容

pub mod ia_32e;

之后在src/ia_32e/mod.rs文件中添加以下内容

pub mod addr;

虚拟地址

紧接着我们在src/ia-32e/addr.rs文件中创建VirtAddrNoCanonicalAddr结构体,NoCanonicalAddr表示不属于Canonical地址,

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct VirtAddr(u64);

#[derive(Debug)]
pub struct NoCanonicalAddr(u64);

这几个宏的用法大家应该都知道了,就不再赘述

然后我们创建一个new_unchecked方法

use crate::bits::BitOpt;
impl VirtAddr{
  pub fn new_unchecked(mut addr: u64) -> VirtAddr {
    if addr.get_bit(47) {
      addr.set_bits(48..64, 0xFFFF);
    } else {
      addr.set_bits(48..64, 0);
    }
    VirtAddr(addr)
  }
}

根据之前的Canonical地址结构我们知道第47位为P表示已存在标示,如果47位被置1表示该地址在内存中存在,我们使用直线写的bits::BitOpttrait来完成位的填充操作,位填充完毕后,我们将它用VirtAddr封装以下,我们可以发现new_unchecked方法对传入的地址不加以判断便直接修改,我还要提供一个会判断的方法

 pub fn try_new(addr: u64) -> Result<VirtAddr, NoCanonicalAddr> {
   // 获取[47,64)
   match addr.get_bits(47..64) {
     // 这里47位标示内存已存在
     0 | 0x1FFFF => Ok(VirtAddr(addr)),
     1 => Ok(VirtAddr::new_unchecked(addr)),
     other => Err(NoCanonicalAddr(other)),
   }
}

在这里我们对[47,64)位进行的判断,如果传入的地址第47-63位均为0表示属于Canonical地址,相应的,第47位和第48-63位全是1也属于Canonical地址,如果传入的地址47位为1(48-63位均为0)我们需要对符号扩展做一些修改(把48-63位全部置1),其他地址均不属于Canonical地址

最后我们创建一个new方法

pub fn new(addr: u64) -> VirtAddr {
  // 给定的地址48-64位必须是不包含任何数据的(全0或全1)
  Self::try_new(addr).expect("given address can not contain any data in bits 48 to 64")
}

new方法在创建的地址的时候会检测地址是否属于Canonical地址,如果不属于将会Panic

我们再提供一个全0地址

pub fn zero() -> VirtAddr {
  VirtAddr(0)
}

我们的地址最终要专为u64类型的,所以我们提供这样的方法

pub fn as_u64(&self) -> u64 {
  self.0
}

现在我们创建的VirtAddr支持u64类型转换,但是我们以后需要将指针转换成VirtAddr并且可以将VirtAddr转为指针

在转换的时候我们也需要转换过程是安全的,在这我们使用cast,在Cargo.toml文件中添加以下内容

[dependencies.cast]
version = "0.2.2"
default-features = false

然后我们开始实现

pub fn from_pointer<T>(pointer: *const T) -> Self {
  // 将裸指针转为usize,利用cast::u64转为u64类型
  Self::new(cast::u64(pointer as usize))
}

#[cfg(target_pointer_width = "64")]
pub fn as_ptr<T>(self) -> *const T {
  cast::usize(self.as_u64()) as *const T
}

#[cfg(target_pointer_width = "64")]
pub fn as_mut_ptr<T>(self) -> *mut T {
  self.as_ptr::<T>() as *mut T
}

我们的系统是x86-64位的,我们的指针宽度应该也是64位的,因此我们使用了target_pointer_width属性,来保证我们从u64类型转换后指针的宽度为64位

我们也提供了只读指针和可读写指针as_ptras_mut_ptr

最后我们为VirtAddr实现Add,AddAssign,Sub,SubAssign,Debug这几个trait

Add trait

Add trait类似于C++中的操作符重载,实现了Add trait可以完成对+的操作,例如

use std::ops::Add;

#[derive(Debug, PartialEq)] 
struct Point {              
    x: i32,                 
    y: i32,                 
}  

impl Add for Point {                    
    type Output = Self;                 
                                        
    fn add(self, other: Self) -> Self { 
        Self {                          
            x: self.x + other.x,        
            y: self.y + other.y,        
        }                               
    }                                   
} 

assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },Point { x: 3, y: 3 });                      

AddAssign trait

实现了AddAssign trait我们可以完成+=操作,例如

use std::ops::AddAssign;

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl AddAssign for Point {
    fn add_assign(&mut self, other: Self) {
        *self = Self {
            x: self.x + other.x,
            y: self.y + other.y,
        };
    }
}

let mut point = Point { x: 1, y: 0 };
point += Point { x: 2, y: 3 };
assert_eq!(point, Point { x: 3, y: 3 });

Sub trait

use std::ops::Sub;                                                  
                                                                    
#[derive(Debug, PartialEq)]                                         
struct Point<T> {                                                   
    x: T,                                                           
    y: T,                                                           
}                                                                   
                                                                    
// Notice that the implementation uses the associated type `Output`.
impl<T: Sub<Output = T>> Sub for Point<T> {                         
    type Output = Self;                                             
                                                                    
    fn sub(self, other: Self) -> Self::Output {                     
        Point {                                                     
            x: self.x - other.x,                                    
            y: self.y - other.y,                                    
        }                                                           
    }                                                               
}                                                                   
                                                                    
assert_eq!(Point { x: 2, y: 3 } - Point { x: 1, y: 0 },Point { x: 1, y: 3 });                                   

SubAssign参考AddAssign

知道了这几个trait的用途之后我们开始实现这几个trait

impl fmt::Debug for VirtAddr {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Virtual Address: {:#x}", self.0)
    }
}

impl Add<u64> for VirtAddr {
    type Output = Self;

    fn add(self, rhs: u64) -> Self::Output {
        VirtAddr::new(self.0 + rhs)
    }
}

impl AddAssign<u64> for VirtAddr {
    fn add_assign(&mut self, rhs: u64) {
        *self = *self + rhs;
    }
}

#[cfg(target_pointer_width = "64")]
impl Add<usize> for VirtAddr {
    type Output = Self;

    fn add(self, rhs: usize) -> Self::Output {
        self + cast::u64(rhs)
    }
}

#[cfg(target_pointer_width = "64")]
impl AddAssign<usize> for VirtAddr {
    fn add_assign(&mut self, rhs: usize) {
        self.add_assign(cast::u64(rhs));
    }
}

impl Sub<u64> for VirtAddr {
    type Output = Self;

    fn sub(self, rhs: u64) -> Self::Output {
        VirtAddr::new(self.0.checked_sub(rhs).unwrap())
    }
}


impl SubAssign<u64> for VirtAddr {
    fn sub_assign(&mut self, rhs: u64) {
        *self = *self - rhs;
    }
}

#[cfg(target_pointer_width = "64")]
impl Sub<usize> for VirtAddr {
    type Output = Self;

    fn sub(self, rhs: usize) -> Self::Output {
        self - cast::u64(rhs)
    }
}

#[cfg(target_pointer_width = "64")]
impl SubAssign<usize> for VirtAddr {
    fn sub_assign(&mut self, rhs: usize) {
        self.sub(cast::u64(rhs));
    }
}

impl Sub<VirtAddr> for VirtAddr{
    type Output = u64;

    fn sub(self, rhs: VirtAddr) -> Self::Output {
        self.as_u64().checked_sub(rhs.as_u64()).unwrap()
    }
}

我们除了实现对u64类型的指针运算以外还实现了了对usize类型的指针运算,这样我们可以使用对裸指针进行运算了,最后对VirtAddr自身实现了Sub方法这样我们可计算指针的偏移量

我们src/ia-32e/addr.rs文件内容就是这样的

use crate::bits::BitOpt;
use core::result::Result;
use core::fmt;
use core::ops::{Add, AddAssign, Sub, SubAssign};
use core::convert::TryInto;

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct VirtAddr(u64);

#[derive(Debug)]
pub struct NoCanonicalAddr(u64);


impl VirtAddr {
    /// 创建一个Canonical地址,传入的地址不会进行检查
    /// 该方法会检查第47位(P:已存在标示),48-63位将会被重写
    pub fn new_unchecked(mut addr: u64) -> VirtAddr {
        if addr.get_bit(47) {
            addr.set_bits(48..64, 0xFFFF);
        } else {
            addr.set_bits(48..64, 0);
        }
        VirtAddr(addr)
    }

    /// 该函数尝试创建一个Canonical地址,
    /// 如果48位到64位是正确的符号扩展名(即47位的副本)或全部为空,将成功返回
    pub fn try_new(addr: u64) -> Result<VirtAddr, NoCanonicalAddr> {
        // 获取[47,64)
        match addr.get_bits(47..64) {
            // 这里47位标示内存已存在
            0 | 0x1FFFF => Ok(VirtAddr(addr)),
            1 => Ok(VirtAddr::new_unchecked(addr)),
            other => Err(NoCanonicalAddr(other)),
        }
    }

    pub fn new(addr: u64) -> VirtAddr {
        // 给定的地址48-64位必须是不包含任何数据的
        Self::try_new(addr).expect("given address can not contain any data in bits 48 to 64")
    }


    /// 创建全0地址
    pub fn zero() -> VirtAddr {
        VirtAddr(0)
    }

    pub fn as_u64(&self) -> u64 {
        self.0
    }
    /// 从给定的指针中创建虚拟地址
    pub fn from_pointer<T>(pointer: *const T) -> Self {
        Self::new(cast::u64(pointer as usize))
    }

    #[cfg(target_pointer_width = "64")]
    pub fn as_ptr<T>(self) -> *const T {
        cast::usize(self.as_u64()) as *const T
    }

    #[cfg(target_pointer_width = "64")]
    pub fn as_mut_ptr<T>(self) -> *mut T {
        self.as_ptr::<T>() as *mut T
    }
}

impl fmt::Debug for VirtAddr {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Virtual Address: {:#x}", self.0)
    }
}

/// 地址添加计算
impl Add<u64> for VirtAddr {
    type Output = Self;

    fn add(self, rhs: u64) -> Self::Output {
        VirtAddr::new(self.0 + rhs)
    }
}

impl AddAssign<u64> for VirtAddr {
    fn add_assign(&mut self, rhs: u64) {
        *self = *self + rhs;
    }
}

#[cfg(target_pointer_width = "64")]
impl Add<usize> for VirtAddr {
    type Output = Self;

    fn add(self, rhs: usize) -> Self::Output {
        self + cast::u64(rhs)
    }
}

#[cfg(target_pointer_width = "64")]
impl AddAssign<usize> for VirtAddr {
    fn add_assign(&mut self, rhs: usize) {
        self.add_assign(cast::u64(rhs));
    }
}

impl Sub<u64> for VirtAddr {
    type Output = Self;

    fn sub(self, rhs: u64) -> Self::Output {
        VirtAddr::new(self.0.checked_sub(rhs).unwrap())
    }
}


impl SubAssign<u64> for VirtAddr {
    fn sub_assign(&mut self, rhs: u64) {
        *self = *self - rhs;
    }
}

#[cfg(target_pointer_width = "64")]
impl Sub<usize> for VirtAddr {
    type Output = Self;

    fn sub(self, rhs: usize) -> Self::Output {
        self - cast::u64(rhs)
    }
}

#[cfg(target_pointer_width = "64")]
impl SubAssign<usize> for VirtAddr {
    fn sub_assign(&mut self, rhs: usize) {
        self.sub(cast::u64(rhs));
    }
}

impl Sub<VirtAddr> for VirtAddr{
    type Output = u64;

    fn sub(self, rhs: VirtAddr) -> Self::Output {
        self.as_u64().checked_sub(rhs.as_u64()).unwrap()
    }
}

地址对齐

地址对齐是内存中排列和访问数据的一种方式,

在编程语言中,一个对象通常包含2个属性,自身的值和存储的地址,地址对齐表示数据的地址可以被1,2,4,8整除,换句话说
数据大小都会按照1字节,2字节,4字节,8字节进行对齐(2的次方即可)
CPU每次读取内存的时候不会一字节一字节读取,相反,CPU每次会读取2,4,8,16或32字节,这样可以减少内存访问频率,提供更高的性能(每次读取4字节肯定比每次读取1字节快)

我们假设CPU每次读取是4字节,我们的数据data也是4字节的
在字节对齐的情况下是这样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OQfAy9zN-1575549305763)(4byte.png)]

我们可以看到CPU只需要读取一次便可读取完整个data的内容,假如我们的data没有按照4字节对齐,那么将会是这样子的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8zkCcN57-1575549305764)(misaligned.png)]

当发生这样的情况,CPU需要读取2次,并且还要移除掉不需要的字节然后合并在一起,这样的读取速度会很低,有些CPU甚至拒绝读取没有对齐的数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Q3PmdLE-1575549305765)(CPURead.png)]

在32位x86系统中,对齐方式与其数据类型的大小基本相同。编译器会使用变量自然的长度进行对齐。 CPU将正确处理未对齐的数据,因此无需显式对齐地址

例如C语言中数据类型如下

数据类型 大小
char 1
short 2
int 4
float 4
double 4或8

但是,对于struct,union或class对象中的成员数据而言,情况略有不同

struct,union或class的成员变量必须与任何成员变量大小的最高字节对齐,以防止性能下降

如果结构中有1个char变量(1字节)和1个int变量(4字节),则编译器将在这两个变量之间填充3个字节

此struct变量的总大小为8个字节,而不是5个字节
这样,该结构数据的地址就可以被4整除。这称为结构成员对齐。当然,结构的大小将因此而增长

例如:

// 大小 = 2 字节, 对齐方式 = 1字节, 地址可以被1整除
struct S1 {
    char m1;    // 1字节
    char m2;    // 1字节
};

// 大小 = 4 字节, 对齐方式 = 2字节, 地址可以被2整除
struct S2 {
    char m1;    // 1字节
                // 填充 1字节
    short m2;   // 2字节
};

// 大小 = 8 字节, 对齐方式 = 4字节, 地址可以被4整除
struct S3 {
    char m1;    // 1字节
                // 填充 3字节
    int m2;     // 4字节
};

// 大小 = 16 字节, 对齐方式 = 8字节, 地址可以被8整除
struct S4 {
    char m1;    // 1字节
                // 填充 7字节
    double m2;  // 8字节
};

// 大小 = 16 字节, 对齐方式 = 8字节, 地址可以被8整除
struct S5 {
    char m1;    // 1字节
                // 填充 3字节
    int m2;     // 4字节
    double m2;  // 8字节
};

现在有这样的内存结构

   +-----+<- 0x7200 0007 (H)
   |     |
   +-----+<- 0x7200 0006
   |     |
   +-----+<- 0x7200 0005 
   |     |
   +-----+<- 0x7200 0004
   |     |
   +-----+<- 0x7200 0003
   |     |
   +-----+<- 0x7200 0002
   |     |
   +-----+<- 0x7200 0001 

我们需要做的就是获取某个地址的对齐地址,例如我们按照4字节对齐,0x7200 0002的对齐地址(低)为0x7200 0000

0x7200 0002的对齐地址(高)为0x7200 0004

如果我们按照2字节对齐,0x7200 0002的对齐地址(低)为0x7200 0002,对齐地址(高)为0x7200 0003

7200 0002 转为2进制数为,采取的是4字节对齐

0111 0010 0000 0000 0000 0000 0000 0010

    0111 0010 0000 0000 0000 0000 0000 0010
and 0000 0000 0000 0000 0000 0000 0000 0011
--------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 0010

如果and以后为0 表示刚好是2的次方的结果,如果不是则不为0,我们再以0x7200 0004举例

    0111 0010 0000 0000 0000 0000 0000 0100
and 0000 0000 0000 0000 0000 0000 0000 0011
--------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 0000

这样我们就可以判断当前的地址是否是对齐后的地址,如果不是对齐后的地址我们可以通过以下方式算出对齐地址(高)和对齐地址(低)

高对齐地址计算方式如下

mask = 对齐字节 - 1

对齐地址(高) = (原地址 | 对齐字节) + 1

对齐地址(低) = 原地址 & !(对齐字节 - 1)

其中对齐字节必须是2次幂的结果

我们接下来开始编写VirtAddr的对齐字节计算方法的函数,VirtAddr和PhysAddr都将会使用该函数,因此我们做成单独的函数

pub fn align_down(addr: u64, align: u64) -> u64 {
    assert!(align.is_power_of_two(), "`align` must be a power of two");
    addr & !(align - 1)
}

pub fn align_up(addr: u64, align: u64) -> u64 {
    assert!(align.is_power_of_two(), "`align` must be a power of two");
    let mask = align - 1;
    if addr & mask == 0 {
        addr
    } else {
        (addr | mask) + 1
    }
}

我们添加了1个断言,利用align & (align - 1) == 0来判断该数是否属于2的次方

之后我们为VirtAddr实现对应的方法

pub fn align_up<U>(self, align: U) -> Self where U: Into<u64> {
  VirtAddr(align_down(self.0, align.into()))
}

pub fn align_down<U>(self, align: U) -> Self where U: Into<u64> {
  VirtAddr(align_up(self.0, align.into()))
}

pub fn is_aligned<U>(self, align: U) -> bool where U: Into<u64> {
  self.align_down(align) == self
}

我们为这几个方法增加了范型参数,并对范型参数做了约束,要求传入的类型实现Into<u64>trait

这样我们的虚拟地址的包装就做好了

物理地址

跟虚拟地址类似有些函数可以根据做好的拷贝过来

结构体和初始化函数如下

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct PhysAddr(u64);

pub struct NoInvalidPhysAddr(u64);

impl PhysAddr{
  pub fn new(addr: u64) -> PhysAddr {
  assert_eq!(addr.get_bits(52..64), 0, "physical addresses must not have any bits in the range 52 to 64 set");
  PhysAddr(addr)
}

  pub fn try_new(addr: u64) -> Result<PhysAddr, NoInvalidPhysAddr> {
    match addr.get_bits(52..64) {
      0 => Ok(PhysAddr(addr)),
      other => Err(NoInvalidPhysAddr(other)),
    }
  }
}

在创建PhysAddr的时候我们需要检查52-63位全部为0new函数在检测到52-63位不为0是将会抛出断言异常

跟VirtAddr一样我们也需要实现,Debug,Add,AddAssgin,Sub,SubAddsign,和地址对齐,如果你懒得写的话可以把VirtAddr的实现拷贝过来,改下名字就行了(果然CV大法好)

impl PhysAddr{
  	pub fn as_u64(self) -> u64 {
        self.0
    }

    pub fn is_null(&self) -> bool {
        self.0 == 0
    }
  
    pub fn align_up<U>(self, align: U) -> Self where U: Into<u64>,
    {
        PhysAddr(align_up(self.0, align.into()))
    }

    pub fn align_down<U>(self, align: U) -> Self where U: Into<u64>,
    {
        PhysAddr(align_down(self.0, align.into()))
    }

    pub fn is_aligned<U>(self, align: U) -> bool where U: Into<u64>,
    {
        self.align_down(align) == self
    }
}

impl fmt::Debug for PhysAddr {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Physical Address({:#x})", self.0)
    }
}

impl Add<u64> for PhysAddr {
    type Output = Self;
    fn add(self, rhs: u64) -> Self::Output {
        PhysAddr::new(self.0 + rhs)
    }
}

impl AddAssign<u64> for PhysAddr {
    fn add_assign(&mut self, rhs: u64) {
        *self = *self + rhs;
    }
}

#[cfg(target_pointer_width = "64")]
impl Add<usize> for PhysAddr {
    type Output = Self;
    fn add(self, rhs: usize) -> Self::Output {
        self + cast::u64(rhs)
    }
}

#[cfg(target_pointer_width = "64")]
impl AddAssign<usize> for PhysAddr {
    fn add_assign(&mut self, rhs: usize) {
        self.add_assign(cast::u64(rhs))
    }
}

impl Sub<u64> for PhysAddr {
    type Output = Self;
    fn sub(self, rhs: u64) -> Self::Output {
        PhysAddr::new(self.0.checked_sub(rhs).unwrap())
    }
}

impl SubAssign<u64> for PhysAddr {
    fn sub_assign(&mut self, rhs: u64) {
        *self = *self - rhs;
    }
}

#[cfg(target_pointer_width = "64")]
impl Sub<usize> for PhysAddr {
    type Output = Self;
    fn sub(self, rhs: usize) -> Self::Output {
        self - cast::u64(rhs)
    }
}

#[cfg(target_pointer_width = "64")]
impl SubAssign<usize> for PhysAddr {
    fn sub_assign(&mut self, rhs: usize) {
        self.sub_assign(cast::u64(rhs))
    }
}

impl Sub<PhysAddr> for PhysAddr {
    type Output = u64;
    fn sub(self, rhs: PhysAddr) -> Self::Output {
        self.as_u64().checked_sub(rhs.as_u64()).unwrap()
    }
}

这样我们就完成了针对内存地址的抽象

下一步要做什么

在下一篇文件中,我们开始着手编写基本的描述符以及GDT功能,我们在编写的过程中会涉及到汇编,因此还会讲述Rust内嵌汇编的使用(大家加油!)