第四节 组件 axdtb - 操作设备树
在第二章的第一节中,我们已经学习了如何通过 qemu 命令行,导出平台的配置信息。
这一节来到了内核在启动早期的一个重要环节,让内核自己解析 fdt 设备树来获得硬件平台的配置情况,作为后面启动过程的基础。
回顾一下,内核最初启动时从 SBI 得到两个参数分别在 a0 和 a1 寄存器中。其中 a1 寄存器保存的是 dtb 的开始地址,而 dtb 就是 fdt 的二进制形式,它的全称 device tree blob。由于它已经在内存中放置好,内核启动时就可以直接解析它的内容获取信息。
首先来了解一下标准 dtb 文件的布局规定:
我们内核要解析的 dtb 内存块实际就是 dtb 文件在内存中的映射,主要包含四个部分:
- 头结构 header:固定长度和格式,保存着全局信息和后面各个部分的相对偏移,所以解析 header 是解析整个 dtb 的第一步。
- 保留的内存区域信息:这部分我们暂时用不到。跳过。
- 主体结构:dtb 主体是由 Node 构成的树型结构,每个 Node 可以有自己的 properties。这个是我们要解析的主体,后面重点说明。
- 字符串表:一系列将被 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 构成的树型结构,来看一下这棵树的示意图:
每一个节点 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 行:参数 pos
从 off_struct
开始,跟踪当前解析到达的位置;
第 5~6 行:参数 addr_cells
和 size_cells
分别是当前节点 Node 中,地址&长度属性包含的 cells 的个数,每个 cell 是一个大端表示的 32 位数值,是拼接地址和长度的基本元素;
第 7 行:参数 cb 传入处理节点的回调闭包。在递归过程中,它保持不变一直传递下去。
第 13~15 行:确保当前正在解析的是一个 Node 节点。
第 18 行:读出当前节点的名称,并在第 41 行把它从字节串转换成字符串。
第 22~39 行:解析节点所有的属性,把他们组成属性列表。
第 42 行:调用 cb 回调函数,传入当前节点的名称、addr_cells
和 size_cells
和属性列表。这个 cb 回调函数就是 axdtb 框架与具体处理逻辑之间的接口,下一节我们通过实现它来获取我们期望的物理内存长度和 virtio 信息。
第 44~46 行:对每个子节点递归调用 parse 方法。注意发生变化的除了 pos 这个扫描位置之外,addr_cells
和 size_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
进行测试,成功!