第四节 组件 axdtb - 操作设备树

在第二章的第一节中,我们已经学习了如何通过 qemu 命令行,导出平台的配置信息。

这一节来到了内核在启动早期的一个重要环节,让内核自己解析 fdt 设备树来获得硬件平台的配置情况,作为后面启动过程的基础。

回顾一下,内核最初启动时从 SBI 得到两个参数分别在 a0 和 a1 寄存器中。其中 a1 寄存器保存的是 dtb 的开始地址,而 dtb 就是 fdt 的二进制形式,它的全称 device tree blob。由于它已经在内存中放置好,内核启动时就可以直接解析它的内容获取信息。

首先来了解一下标准 dtb 文件的布局规定:

dtb布局

我们内核要解析的 dtb 内存块实际就是 dtb 文件在内存中的映射,主要包含四个部分:

  1. 头结构 header:固定长度和格式,保存着全局信息和后面各个部分的相对偏移,所以解析 header 是解析整个 dtb 的第一步。
  2. 保留的内存区域信息:这部分我们暂时用不到。跳过。
  3. 主体结构:dtb 主体是由 Node 构成的树型结构,每个 Node 可以有自己的 properties。这个是我们要解析的主体,后面重点说明。
  4. 字符串表:一系列将被 Node 引用的字符串信息,由于每个字符串都是不定长的,所以把它们集中归置成一个块,并放到最后。

下面开始创建 axdtb 组件,首先给出解析设备树所需的常量和错误定义:

// axdtb/src/lib.rs
#![no_std]

use core::str;
use axconfig::align_up;

mod util;
pub use crate::util::SliceRead;

extern crate alloc;
use alloc::{borrow::ToOwned, string::String, vec::Vec};

const MAGIC_NUMBER     : u32 = 0xd00dfeed;
const SUPPORTED_VERSION: u32 = 17;
const OF_DT_BEGIN_NODE : u32 = 0x00000001;
const OF_DT_END_NODE   : u32 = 0x00000002;
const OF_DT_PROP       : u32 = 0x00000003;

#[derive(Debug)]
pub enum DeviceTreeError {
    BadMagicNumber,
    SliceReadError,
    VersionNotSupported,
    ParseError(usize),
    Utf8Error,
}

pub type DeviceTreeResult<T> = Result<T, DeviceTreeError>;

第 13~17 行:规范对设备树文件中各个常量的定义。

第 19~28 行:解析设备树过程中需要的错误类型定义。

定义 DeviceTree 作为主类型:

// axdtb/src/lib.rs
pub struct DeviceTree {
    ptr: usize,
    totalsize: usize,
    pub off_struct: usize,
    off_strings: usize,
}

impl DeviceTree {
    pub fn init(ptr: usize) -> DeviceTreeResult<Self> {
        let buf = unsafe {
            core::slice::from_raw_parts(ptr as *const u8, 24)
        };

        if buf.read_be_u32(0)? != MAGIC_NUMBER {
            return Err(DeviceTreeError::BadMagicNumber)
        }
        if buf.read_be_u32(20)? != SUPPORTED_VERSION {
            return Err(DeviceTreeError::VersionNotSupported);
        }

        let totalsize = buf.read_be_u32(4)? as usize;
        let off_struct = buf.read_be_u32(8)? as usize;
        let off_strings = buf.read_be_u32(12)? as usize;

        Ok(
            Self {ptr, totalsize, off_struct, off_strings}
        )
    }
}

第 2~7 行:定义设备树的数据结构 DeviceTree,其中 ptr 和 totalsize 保持 dtb 内存块开始地址和总长度,另外两个成员分别是主体结构和字符串表的偏移。整个 DeviceTree 结构实际就对应于 dtb header。

第 10~29 行:初始化方法 init(),解析 dtb header,首先校验 magic 和版本,然后取出 header 的基本信息填充 DeviceTree 实例。

完成 header 信息解析后,后面主要的任务就是通过 off_struct 标记的偏移,解析 dtb 主体结构。前面提到,dtb 主体结构是一个由 Node 构成的树型结构,来看一下这棵树的示意图:

dtb_tree

每一个节点 Node 由两部分组成,属性列表 properties 和子节点 Node 列表。每个属性 property 是 key - value 的形式。每个子节点重复上述的构成。从实现的角度,我们当然可以定义 Node 的数据结构,并在解析 dtb 时申请内存构建 Node 实例,如此逐渐构成一棵树,这样以后就可以随时遍历它获取目标信息。但是这种方式比较耗费内存资源,而我们内核目前只能使用 1M 的动态内存堆,所以考虑了另外一种方案:从 dtb 根节点开始,进行一次性的深度优先递归遍历,每遍历到一个节点 Node,就把它的信息传递给一个回调函数(闭包)处理。这种方案比较节省内存,并且目前不需要定义 Node 数据结构,我们在调用回调函数时,可以直接把 Node 的各个属性直接传入。

回调闭包的形式是 FnMut(String, usize, usize, Vec<(String, Vec<u8>)>,四个参数按顺序分别是节点 Node 的名称、#address-cells、#size-cells 和属性列表,每个属性都是名值对形式。关于 #address-cells 和 #size-cells,它们用于指明如何解释当前 Node 及下级 Node 的 reg 属性,在第二章第一节已经说明了它们的作用。

下面来实现解析 dtb 主体部分的方法 parse

// axdtb/src/lib.rs
impl DeviceTree {
    pub fn parse(
        &self, mut pos: usize,
        mut addr_cells: usize,
        mut size_cells: usize,
        cb: &mut dyn FnMut(String, usize, usize, Vec<(String, Vec<u8>)>)
    ) -> DeviceTreeResult<usize> {
        let buf = unsafe {
            core::slice::from_raw_parts(self.ptr as *const u8, self.totalsize)
        };
        // check for DT_BEGIN_NODE
        if buf.read_be_u32(pos)? != OF_DT_BEGIN_NODE {
            return Err(DeviceTreeError::ParseError(pos))
        }
        pos += 4;

        let raw_name = buf.read_bstring0(pos)?;
        pos = align_up(pos + raw_name.len() + 1, 4);
        // First, read all the props.
        let mut props = Vec::new();
        while buf.read_be_u32(pos)? == OF_DT_PROP {
            let val_size = buf.read_be_u32(pos+4)? as usize;
            let name_offset = buf.read_be_u32(pos+8)? as usize;
            // get value slice
            let val_start = pos + 12;
            let val_end = val_start + val_size;
            let val = buf.subslice(val_start, val_end)?;
            // lookup name in strings table
            let prop_name = buf.read_bstring0(self.off_strings + name_offset)?;
            let prop_name = str::from_utf8(prop_name)?.to_owned();
            if prop_name == "#address-cells" {
                addr_cells = val.read_be_u32(0)? as usize;
            } else if prop_name == "#size-cells" {
                size_cells = val.read_be_u32(0)? as usize;
            }
            props.push((prop_name, val.to_owned()));
            pos = align_up(val_end, 4);
        }
        // Callback for parsing dtb
        let name = str::from_utf8(raw_name)?.to_owned();
        cb(name, addr_cells, size_cells, props);
        // Then, parse all its children.
        while buf.read_be_u32(pos)? == OF_DT_BEGIN_NODE {
            pos = self.parse(pos, addr_cells, size_cells, cb)?;
        }
        if buf.read_be_u32(pos)? != OF_DT_END_NODE {
            return Err(DeviceTreeError::ParseError(pos))
        }
        pos += 4;
        Ok(pos)
    }
}

impl From<str::Utf8Error> for DeviceTreeError {
    fn from(_: str::Utf8Error) -> DeviceTreeError {
        DeviceTreeError::Utf8Error
    }
}

第 4 行:参数 posoff_struct 开始,跟踪当前解析到达的位置;

第 5~6 行:参数 addr_cellssize_cells 分别是当前节点 Node 中,地址&长度属性包含的 cells 的个数,每个 cell 是一个大端表示的 32 位数值,是拼接地址和长度的基本元素;

第 7 行:参数 cb 传入处理节点的回调闭包。在递归过程中,它保持不变一直传递下去。

第 13~15 行:确保当前正在解析的是一个 Node 节点。

第 18 行:读出当前节点的名称,并在第 41 行把它从字节串转换成字符串。

第 22~39 行:解析节点所有的属性,把他们组成属性列表。

第 42 行:调用 cb 回调函数,传入当前节点的名称、addr_cellssize_cells 和属性列表。这个 cb 回调函数就是 axdtb 框架与具体处理逻辑之间的接口,下一节我们通过实现它来获取我们期望的物理内存长度和 virtio 信息。

第 44~46 行:对每个子节点递归调用 parse 方法。注意发生变化的除了 pos 这个扫描位置之外,addr_cellssize_cells也 有可能变化,子节点如果有这两个属性,可以覆盖父节点的属性值,即局部可以在自己的域范围内覆盖上级域的配置。

解析内容时,会对 slice 调用 read_be_[u32|u64] 方法,把大端的数值转换为小端序。这个不是 slice 的标准功能,需要我们自己实现一个 trait,其中包含 read_be_u32 在内的四个附加处理方法。

// axdtb/src/util.rs
use crate::{DeviceTreeResult, DeviceTreeError};

pub trait SliceRead {
    fn read_be_u32(&self, pos: usize) -> DeviceTreeResult<u32>;
    fn read_be_u64(&self, pos: usize) -> DeviceTreeResult<u64>;
    fn read_bstring0(&self, pos: usize) -> DeviceTreeResult<&[u8]>;
    fn subslice(&self, start: usize, len: usize) -> DeviceTreeResult<&[u8]>;
}

impl<'a> SliceRead for &'a [u8] {
    fn read_be_u32(&self, pos: usize) -> DeviceTreeResult<u32> {
        // check size is valid
        if ! (pos+4 <= self.len()) {
            return Err(DeviceTreeError::SliceReadError)
        }
        Ok(
            (self[pos] as u32) << 24
            | (self[pos+1] as u32) << 16
            | (self[pos+2] as u32) << 8
            | (self[pos+3] as u32)
        )
    }
    fn read_be_u64(&self, pos: usize) -> DeviceTreeResult<u64> {
        let hi: u64 = self.read_be_u32(pos)?.into();
        let lo: u64 = self.read_be_u32(pos+4)?.into();
        Ok((hi << 32) | lo)
    }
    fn read_bstring0(&self, pos: usize) -> DeviceTreeResult<&[u8]> {
        let mut cur = pos;
        while cur < self.len() {
            if self[cur] == 0 {
                return Ok(&self[pos..cur])
            }
            cur += 1;
        }
        Err(DeviceTreeError::SliceReadError)
    }
    fn subslice(&self, start: usize, end: usize) -> DeviceTreeResult<&[u8]> {
        if ! (end < self.len()) {
            return Err(DeviceTreeError::SliceReadError)
        }
        Ok(&self[start..end])
    }
}

第 4~9 行:定义一个针对 slice 的新的 trait,用来处理大小端的转换。

第 11~45 行:为 slice 实现上述 trait。重点是 read_be_u32 和 read_be_u64,逆转字节序以适应我们内核的需要。

至此,axdtb 组件本身已经实现。下面来测试一下:

先建立一个用于测试的设备树文件 sample.dts,

/dts-v1/;

/ {
    address-cells = <0x2>;
    size-cells = <0x2>;
    compatible = "riscv-virtio";

    soc {
        address-cells = <0x2>;
        size-cells = <0x2>;
        compatible = "simple-bus";

        virtio_mmio@10001000 {
            reg = <0x0 0x10001000 0x0 0x1000>;
            compatible = "virtio,mmio";
        };
    };
};

执行如下命令把它转化为二进制格式,我们在第二章第一节使用过该命令,只是转化方向与这里是相反的。

dtc ./sample.dts -o ./sample.dtb

编写测试用例:

// axdtb/tests/test_dtb.rs
use std::str;
use std::io::Read;
use axdtb::SliceRead;

#[test]
fn test_dtb() {
    let mut input = std::fs::File::open("tests/sample.dtb").unwrap();
    let mut buf = Vec::new();
    input.read_to_end(&mut buf).unwrap();

    let mut cb = |name: String, addr_cells: usize, size_cells: usize, props: Vec<(String, Vec<u8>)>| {
        match name.as_str() {
            "" => {
                assert_eq!(addr_cells, 2);
                assert_eq!(size_cells, 2);
                for prop in &props {
                    if prop.0.as_str() == "compatible" {
                        assert_eq!(str::from_utf8(&(prop.1)), Ok("riscv-virtio\0"));
                    }
                }
            },
            "soc" => {
                assert_eq!(addr_cells, 2);
                assert_eq!(size_cells, 2);
                for prop in &props {
                    if prop.0.as_str() == "compatible" {
                        assert_eq!(str::from_utf8(&(prop.1)), Ok("simple-bus\0"));
                    }
                }
            },
            "virtio_mmio@10001000" => {
                assert_eq!(addr_cells, 2);
                assert_eq!(size_cells, 2);
                for prop in &props {
                    if prop.0.as_str() == "compatible" {
                        assert_eq!(str::from_utf8(&(prop.1)), Ok("virtio,mmio\0"));
                    } else if prop.0.as_str() == "reg" {
                        let reg = &(prop.1);
                        assert_eq!(reg.as_slice().read_be_u64(0).unwrap(), 0x10001000);
                        assert_eq!(reg.as_slice().read_be_u64(8).unwrap(), 0x1000);
                    }
                }
            },
            _ => {}
        }
    };

    let dt = axdtb::DeviceTree::init(buf.as_slice().as_ptr() as usize).unwrap();
    assert_eq!(dt.parse(dt.off_struct, 0, 0, &mut cb).unwrap(), 396);
}

第 12~47 行:测试用例主要是编写这个回调闭包,过滤测试文件的目标节点进行解析并验证结果。

第 49~50 行:初始化设备树对象,然后启动 parse 过程,每遇到一个节点 Node,都会自动调用上面的 cb 回调函数进行处理。

执行 make test 进行测试,成功!