Rust-oslab-01:向FrameBuffer中输入字符
实验完成步骤:
Rust-os-lab-01
PartA:
对比readelf -h kernel/target/x86_64-unknown-none/debug/kernel和GDB中的调试信息,找出kernel/target/x86_64-unknown-none/debug/kernel的入口地址

Readelf显示的入口地址是0x2fe0

Gdb调试的过程中,显示的地址也是0x2fe0。
ELF文件格式的信息:
以下为ELF文件的结构定义:

部分关键信息解释:
魔数:


7f 、45、4c、46分别对应ascii码的Del(删除)、字母E、字母L、字母F。这四个字节被称为ELF文件的魔数,操作系统在加载可执行文件时会确认魔数是否正确,如果不正确则拒绝加载。
第五个字节标识ELF文件是32位(01)还是64位(02)的。
第六个字节标识该ELF文件字节序是小端(01)还是大端(02)的。
第七个字节指示ELF文件的版本号,一般是01。
后九个字节ELF标准未做定义。一般为00.
文件类型:
类型: DYN

e_type成员标识文件类型,ELF文件有三种类型.
系统架构:
系统架构: Advanced Micro Devices X86-64
e_machine成员标识系统架构(机器类型),ELF定义了以下多种系统架构。可在“/usr/include/elf.h”头文件中查看.
综上,结构如下:

全部信息解释:ELF文件解析(二):ELF header详解 - JollyWing - 博客园 (cnblogs.com)
PartB:熟悉串口输出
目的:熟悉Rust语言语法,了解Rust库的调用方法
阅读示例代码中串口输出的实现(serial_print!和serial_println!)
在kernel/serial.rs中实现的serial_print与serial_println如下:

在编写这段格式化打印之前,我们需要熟悉整个过程:
Serial.rs是Rust项目中的串口驱动模块,提供了初始化串口驱动、从串口中读取一个字符、打印至串口的功能。其中,使用了core、spin、uart_16550和x86_64等Rust库。实现对串口驱动的封装,方便Rust项目中的打印和调试,代码流程实现如下:
(1) 定义了全局SERIAL变量,用于保存串口驱动实例;
(2) 实现init()初始串口驱动;
(3) 实现receive()从串口中读取一个字符;
(4) 实现Printer结构体,用于辅助打印结构,并实现了Write trait;
(5) 实现print(),将打印信息写入串口;
(6) 实现serial_print!()和serial_println!()两个宏,用于格式化打印信息到串口。
接下来我们在tests/文件夹下进行测试使用serial_println!()函数,我们先从输出特定的字符开始:
首先我们定义了一个字符(默认情况下是不可变的),接着我们采用serial_println!()将其输出,以下有几个需要注意的点:
(1)在{}前面,把"c:"去掉了( Rust 不支持在代码块上使用标签)。
(2)serial_println前面加上“kernel::” (加kernel::是说从kernel中调用)。

测试结果如下:

PartC:显卡的字符模式与图形模式
练习3:阅读参考资料中使用VGA字符模式打印字符到屏幕的方式,学习函数Writer.write_string(),思考该函数是如何将一个字符串输出到屏幕上的,思考类型Writer的各个数据成员作用。
做个记录,在文本模式下,每个元素被称作字符单元,占显存两个字节大小,每个单元中的低8位表示字符的ASCII编码,高8位定义了字符的显示方式(字体,颜色,亮度,背景色,闪烁),如下图所示:

首先是Writer中各个数据成员的作用,在该结构体中共包含usize, ColorCode, Buffer,或许我们称呼其类型更加容易理解:column_position, color_code, buffer。

Column_position变量将跟踪光标在最后一行的位置。
Color_code变量指定了当前字符的前景和背景色。
Buffer变量为存入的VGA字符缓冲区的可变借用,这里采用了显式生命周期声明了此借用在什么时候有效:’static 生命周期意味着该借用应该在整个程序的运行期间有效。(这对一个全局有效的VGA字符缓冲区来说是非常合理的。)
对于Writer.write_string()函数分析:
在打印字符串的时候,我们是将其转换为字节并依次输出的,以下是输出字节的方式:

如果这个字节是一个换行符(line feed)字节 \n,我们的 Writer 不应该打印新字符,相反,它将调用 new_line ;其它的字节应该将在 match 语句的第二个分支中被打印到屏幕上。
当打印字节时,Writer 将检查当前行是否已满。如果已满,它将首先调用 new_line 方法来将这一行字向上提升,再将一个新的 ScreenChar 写入到缓冲区,最终将当前的光标位置前进一位。
接下来我们填坑new_line方法(即换行),当换行时,我们想要把每个字符向上移动一行,删除最上方的一行,再在最后一行的起始位置继续输出。

此处的实现方法是遍历每个字符,将其位置移动到上方一行的相应位置。这里,“..” 符号是区间标号(range notation)的一种;它表示左闭右开的区间,因此不包含它的上界。在外层的枚举中,我们从第 1 行开始,省略了对第 0 行的枚举过程——因为这一行应该被移出屏幕,即它将被下一行的字符覆写。
我们又用到了clear_row(),即清空一行,向该行中打印满空格字符即可。

然后我们先在qemu的环境下测试以下write的调用是否可行。

实验PartC完成。
PartD:向FrameBuffer中输出字符
最后我们终于来到了boss:向Framebuffer中打印字符。
我们采取的思路是利用test中的字符输出,将所需要输出的像素点转换出来,然后再在framebuffer中进行输出。
先完成以下字符串的输出这个分解操作:将字符串分割成字符,通过.chars()将字符串分割为字符的集合,之后再进行输出字符。

下面是将在test/下原有的chr2pixels移植到kernel/src/fb.rs中,为了方便区分,在fb.rs中的函数我们称其为t1chr2pixels。进行传参、调用等操作处理之后,我们第一步的测试结果如下所示:

因为我们在之前已经实现了buffer的引用(这里是参考教辅老师的一个buffer切片实现):

需要提前声明的是,我们在FrameBuffer结构体中声明了如下变量:

Buffer_start表示buffer起始的位置,info是沿用的lib.rs中关于FrameBuffer的定义,stride和column_position分别表示的是要写的字符的位置在列/行中的位置。接下来我们定义的是关键内容,buffer,即为我们所在窗口输出文件时需要修改的数组。当我们对于数组中的数据进行修改时,我们所显示在窗口的界面就会相应地发生变化。
接下来我们测试一下,例如,我们将buffer数组的前0x400*0x300部分置为0xAA,那么我们会得到以下内容:

可见qemu模拟器的前置端口都是已经被赋值为灰色了。
下面我们需要了解一下buffer中储存数据的储存格式,buffer里面连续的四个字节就是一个像素的rgb值+透明度,按顺序将获得的像素值逐位赋值,通过实验我们可以知道:
[4k+0]对应的是蓝色,[4k+1]对应的是绿色,[4k+2]对应的是红色,[4k+3]对应的是透明度的选项(但是显然在实现的过程中并没有使用到透明度的设置)。
接着,我们要做的内容就是实现一个字符的打印工作。之前我们在test的过程中通过运用noto_sans_mono_bitmap库中的相关功能,实现了将字符转换为像素矩阵的方法,在make test之后,我们当时所返回的值是对应位置的灰度值,实现将灰度值转换为RGB值的操作就是将灰度值复制三遍然后R,G,B三者均为同一个值,由此我们可得到相应的结果。
实现过程就相对简单了,我们需要将输入的字符通过get_raster()操作之后转换为RasterizedChar格式,然后再仿照test中的输出方法,将相应的内容输出到FrameBuffer的屏幕上(对应修改相应的buffer位置即可)。
每一行有0x400*4个位置,即0x400个像素点,我们需要修改的就是这些内容,因为0x00对应的就是没有信息(也是初始值),所以我们直接将所在像素点的值RGB均赋为对应的灰度值即可。(对于\n,我们无法输出,采用的方法是判断为’\n’字符后采用换行操作。)

测试结果如下:

可认为我们实现了相应的功能,达到了初步的预期效果。当然,我们接下来要将此功能继续补充完善,应对更多的情况。
首先我们需要补充的是清页功能clear,即将页面上的内容均清空。我们实现的过程相对比较直观,就是将页面上的所有内容全部置为0x00,从而使得页面清空。

测试结果如下:

此外我们实现了字符放大的功能,即记录相对应的blocksz,将原本1x1的像素扩展到3x3的像素,这是正方形的扩展,实现代码如下(须注意,当我们扩展成了3x3之后,原有的偏移offset的计算方式也会有所改变,这里为了方便我们就把这个过程提取出来了):

测试结果如下:

接着是呈现在窗口中输出两行:

这个的实现是通过修改行的位置来决定的,同时我们编写的代码支持字体大小的变化,字体变大的情况下依然适用:

代码实现为:

下面我们介绍新建一行的函数new_line():
对于new_line来说我们可以把行号的调整放入new_line()中,不过感觉不如直接写在输出字符部分中直观,所以我就直接写在输出字符的部分了。未来在改进代码的过程中可能会选择将行号调整移动到这里。
建立new_line有两种情况:对于还没写满/窗口已经写满了:对于窗口还没有写满的情况,我们直接清空下面的内容,列号移到下面一行即可(其实不清空也可以);对于已经写满的情况,将现在的内容向上移动一格,之后再向下写入。具体的实现如下:

最后整个活(bushi):

实验报告:
一、实验完成情况
已完成所有实验任务,并完成一部分拓展(字体放大,自适应换行等),同时还整了个活(阶梯形)。
先放实验效果截图:
1. 向FrameBuffer中输入字符

2. 实现clear()方法

3. 实现换行,new_line()

4. 字体放大+自适应换行(调整blocksz大小,其实用fontsz更好)

5. 阶梯形输出

6. 输出特定颜色的字符(buffer[4k+0]对应的是蓝色,[4k+1]对应的是绿色,[4k+2]对应的是红色,[4k+3]对应的是透明度)

7.另外地,我们在实现了VGA的字符输出buffer

二、主要代码介绍
代码修改部分:
1. FrameBufferDriver的结构体定义:
pub struct FrameBufferDriver {
// TODO:设计结构体需要的成员
buffer_start: usize,
info: FrameBufferInfo,
stride: usize,
column_position: usize,
buffer: &'static mut [u8],
}
结构体解释:
Buffer_start表示buffer起始的位置,
info是沿用的lib.rs中关于FrameBuffer的定义,
stride和column_position分别表示的是要写的字符的位置在列/行中的位置,
关键内容,buffer,即为我们所在窗口输出文件时需要修改的数组。当我们对于数组中的数据进行修改时,我们所显示在窗口的界面就会相应地发生变化。
这里我们选用对于buffer的定义方式是buffer: &'static mut [u8],其中&表示是取得一个数组的地址,我们是对数组进行操作;'static表示其声明周期是贯穿全局的,'mut'表明其为可变变量,这也是我们在之后的部分对其进行修改的必要声明。
2. FrameBuffer中的new方法
pub fn new<'a>(_framebuffer: &'a mut FrameBuffer) -> Self {
// TODO:新建FrameBuffer驱动
let buffer_start = _framebuffer.buffer().as_ptr() as usize;
let info = _framebuffer.info().clone();
let stride = info.stride;
let buffer_len = info.byte_len;
let column_position = 0;
let buffer =
unsafe { core::slice::from_raw_parts_mut(buffer_start as *mut u8, buffer_len) };
Self {
buffer_start,
info,
stride,
column_position,
buffer,
}
}
即新建一个FrameBuffer的驱动,我们需要的修改量应该在这里提前声明,对于buffer而言,我们要确定其声明周期在后面调用的稳健性,要拿到里面buffer的可变引用,我们采用的方案是拿到起始地址和长度,自己创建一个buffer:let buffer = unsafe { core::slice::from_raw_parts_mut(buffer_start as *mut u8, buffer_len) }。
3. write_str方法调用
/// 写字符串
pub fn write_str(&mut self, _s: &str) {
// Self::clear()清屏 -> Self::t1chr2pixels()输出单个字符 -> Self::new_line()创建新行
let mut num = 0;
if self.column_position == 0 {
Self::clear(self);
}
for c in _s.chars() {
Self::t1chr2pixels(self, c, num, self.column_position);
num += 1;
}
}
在打印字符串的时候,我们是将其转换为字节并依次输出的,以下是输出字节的方式。
在输出第一个字符串的时候,Self::clear()将屏幕上清空;
之后我们用chars()转换成字符串,之后再取得这个字符串中的字符去输出。在输出的时候,我们需要记录当前输出的位置,即当前的行号和列号,这个我们是存在self中的,在输出字符的时候修改;
在输出字符的时候我们进行判断,是否需要采用new_line()新建一行。
4. 输出字符
/// 写字符
fn t1chr2pixels(&mut self, c: char, num: usize, lie: usize) {
// 待测字符
// 0x400 0x300
{
let blocksz = 1;
//读入到换行符或者字符超了一行的限制之后进行换行操作
if c == '\n' || (9 * blocksz + 9 * blocksz * num) > 0x400 {
//将行的位置偏移加上blocksz
if blocksz > 2 {
self.column_position += (blocksz - 2) * 16;
} else {
self.column_position += 16;
}
//建立一个新行
Self::new_line(self, self.column_position);
return;
}
//serial_println!("print char: {:?}", c);
let rendered_char =
get_raster(c, FONT_WEIGHT, LETTER_HEIGHT).expect("Failed to render a char");
// 打印像素点
// 行是0x400 列是0x300
//serial_println!("");
for (_y, row) in rendered_char.raster().iter().enumerate() {
for (_x, byte) in row.iter().enumerate() {
//serial_print!("{:02x} ", *byte);
//serial_print!("({},{}) ", _y, _x);
for k1 in 0..blocksz + 1 {
for k2 in 0..blocksz + 1 {
//self.buffer[(_y * 0x400 + k1) * blocksz + _x * blocksz + k2] = *byte;
let offset = ((lie + _y) * 0x400 + k1) * blocksz
+ _x * blocksz
+ k2
+ 9 * blocksz * num;
self.buffer[offset * 4 + 0] = *byte;
self.buffer[offset * 4 + 1] = *byte;
self.buffer[offset * 4 + 2] = *byte;
}
}
}
}
};
这个是我们方法的主要实现部分,大体上我们可以划分成两个部分:判断是否换行,并相应调整行号;对于相应的字符,我们输出一定大小的字符。
(1) 判断换行部分
判断换行的条件有两个:建立新行new_line有两种情况:该行已经写满了/读入到换行符,当我们读入相应的符号之后,进行换行的操作。
(2)输出一定大小的字符
输出字符: 我们需要了解一下buffer中储存数据的储存格式,其中连续的四个字节就是一个像素的rgb值+透明度,按顺序将获得的像素值逐位赋值,通过实验我们可以知道:
[4k+0]对应的是蓝色,[4k+1]对应的是绿色,[4k+2]对应的是红色,[4k+3]对应的是透明度的选项(但是显然在实现的过程中并没有使用到透明度的设置)。
接着,我们要做的内容就是实现一个字符的打印工作。之前我们在test的过程中通过运用noto_sans_mono_bitmap库中的相关功能,实现了将字符转换为像素矩阵的方法,在make test之后,我们当时所返回的值是对应位置的灰度值,实现将灰度值转换为RGB值的操作就是将灰度值复制三遍然后R,G,B三者均为同一个值,由此我们可得到相应的结果。

实现过程就相对简单了,我们需要将输入的字符通过get_raster()操作之后转换为RasterizedChar格式,遍历相应的像素点在buffer上的位置,将相应的内容输出到FrameBuffer的屏幕上(对应修改相应的buffer位置即可)。需要注意的是,我们将引用noto_sans_mono_bitmap中的方法的过程中需要的相应变量定义放在了整个方法实现的外面(并非方法私有)。
经测试,每一行有0x4004个位置,即0x400个像素点,我们需要修改的就是这些内容,因为0x00对应的就是没有信息(也是初始值),所以我们直接将所在像素点的值RGB均赋为对应的灰度值即可。(对于\n,我们无法输出,采用的方法是判断为’\n’字符后采用换行操作。)
修改字符的大小:我们在输出的时候选择的是对于blockszblocksz的像素统一赋值为一个值,这种方法可以比较方便地修改字体的大小。同时,为了方便操作,我们的新建行、换行判断对于放大字体的情况依然是适用的。

5.创建新行new_line()
/// 创建新行
fn new_line(&mut self, lie: usize) {
let blocksz = 1;
//对于窗口还没有写满的情况,我们直接清空下面的内容,列号移到下面一行即可(其实不清空也可以)
if lie < 0x300 {
let limit = 0x400 * 0x300 * 4;
for i in (lie * 0x400 * 4 * 16)..limit {
self.buffer[i] = 0x00;
}
}
//对于已经写满的情况,将现在的内容向上移动一格,之后再向下写入
else {
for _y in 0..(0x300 - 1) {
for _x in 0..0x400 * 4 {
let offset0 = ((_y) * 0x400 + _x) * 4;
let offset1 = ((_y + blocksz) * 0x400 + _x) * 4;
self.buffer[offset0 + 0] = self.buffer[offset1 + 0];
self.buffer[offset0 + 1] = self.buffer[offset1 + 1];
self.buffer[offset0 + 2] = self.buffer[offset1 + 2];
self.buffer[offset0 + 3] = self.buffer[offset1 + 3];
}
}
}
}
对于new_line来说我们可以把行号的调整放入new_line()中,不过感觉不如直接写在输出字符部分中直观,所以我就直接写在输出字符的部分了。未来在改进代码的过程中可能会选择将行号调整移动到这里。
建立new_line有两种情况:对于还没写满/窗口已经写满了:对于窗口还没有写满的情况,我们直接清空下面的内容,列号移到下面一行即可(其实不清空也可以);对于已经写满的情况,将现在的内容向上移动一格,之后再向下写入。在实现过程中,我们采用了offset的方式来方便编写。
6. 调整字体颜色

这个部分就是将之前的灰度值只赋给三个变量的一个值就可以了,[4k+0]对应的是蓝色,[4k+1]对应的是绿色,[4k+2]对应的是红色,我们选定一个就可以了。
7. 刷新页面

就是循环输出,检测new_line()刷新效果。
附录(源码等)
我们主要修改的是kernel/fb.rs文件。
//! 格式化输出至framebuffer,部分参考bootloader实现
use bootloader_api::info::{FrameBuffer, FrameBufferInfo};
use core::fmt;
use core::fmt::Write;
use spin::{Mutex, Once};
use x86_64::instructions::interrupts;
/// FrameBuffer驱动全局变量
pub static FRAME_BUFFER: Once<Mutex<FrameBufferDriver>> = Once::new();
/// 初始化FrameBuffer驱动
pub fn init<'a>(frame_buffer: &'a mut FrameBuffer) {
serial_println!("[Kernel] {:#x?}", frame_buffer);
// 根据BootInfo传入信息创建FrameBuffer驱动
FRAME_BUFFER.call_once(|| Mutex::new(FrameBufferDriver::new(frame_buffer)));
}
/// 辅助打印结构,主要实现Write trait
struct Printer;
impl Write for Printer {
fn write_str(&mut self, s: &str) -> fmt::Result {
interrupts::without_interrupts(|| {
FRAME_BUFFER
.get()
.and_then(|fb| Some(fb.lock().write_str(s)))
.expect("Uninit frame buffer");
});
Ok(())
}
}
/// 打印至FrameBuffer
pub fn print(args: fmt::Arguments) {
// write_fmt函数在Write trait中定义,因此需要实现Write trait
Printer.write_fmt(args).unwrap();
}
/// 格式化打印至FrameBuffer(无换行)
#[macro_export]
macro_rules! fb_print {
($($arg:tt)*) => {
$crate::driver::fb::print(format_args!($($arg)*))
};
}
/// 格式化打印至FrameBuffer(有换行)
#[macro_export]
macro_rules! fb_println {
() => ($crate::fb_print!("\n"));
($fmt:expr) => ($crate::fb_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::fb_print!(
concat!($fmt, "\n"), $($arg)*));
}
/// FrameBuffer驱动
pub struct FrameBufferDriver {
// TODO:设计结构体需要的成员
column_position: usize,
buffer: &'static mut [u8],
}
/// 引入bitmap
use noto_sans_mono_bitmap::{get_raster, get_raster_width, FontWeight, RasterHeight};
/// 字高
const LETTER_HEIGHT: RasterHeight = RasterHeight::Size16;
/// 字宽
const _LETTER_WIDTH: usize = get_raster_width(FontWeight::Regular, LETTER_HEIGHT);
/// 字体
const FONT_WEIGHT: FontWeight = FontWeight::Regular;
// TODO:设计结构体需要的接口,至少应包括new和write_str(前面的代码调用过)
impl FrameBufferDriver {
/// 创建FrameBuffer驱动
pub fn new<'a>(_framebuffer: &'a mut FrameBuffer) -> Self {
// TODO:新建FrameBuffer驱动
let buffer_start = _framebuffer.buffer().as_ptr() as usize;
let info = _framebuffer.info().clone();
let stride = info.stride;
let buffer_len = info.byte_len;
let column_position = 0;
let buffer =
unsafe { core::slice::from_raw_parts_mut(buffer_start as *mut u8, buffer_len) };
Self {
column_position,
buffer,
}
}
/// 写字符串
pub fn write_str(&mut self, _s: &str) {
// TODO
let mut num = 0;
if self.column_position == 0 {
Self::clear(self);
}
for c in _s.chars() {
Self::t1chr2pixels(self, c, num, self.column_position);
num += 1;
}
}
/// 写字符
fn t1chr2pixels(&mut self, c: char, num: usize, lie: usize) {
// 待测字符
// 0x400 0x300
{
let blocksz = 3;
//读入到换行符或者字符超了一行的限制之后进行换行操作
if c == '\n' || (9 * blocksz + 9 * blocksz * num) > 0x400 {
//将行的位置偏移加上blocksz
if blocksz > 2 {
self.column_position += (blocksz - 2) * 16;
} else {
self.column_position += 16;
}
//建立一个新行
Self::new_line(self, self.column_position);
return;
}
//serial_println!("print char: {:?}", c);
let rendered_char =
get_raster(c, FONT_WEIGHT, LETTER_HEIGHT).expect("Failed to render a char");
// 打印像素点
// 行是0x400 列是0x300
//serial_println!("");
for (_y, row) in rendered_char.raster().iter().enumerate() {
for (_x, byte) in row.iter().enumerate() {
//serial_print!("{:02x} ", *byte);
//serial_print!("({},{}) ", _y, _x);
for k1 in 0..blocksz + 1 {
for k2 in 0..blocksz + 1 {
//self.buffer[(_y * 0x400 + k1) * blocksz + _x * blocksz + k2] = *byte;
let offset = ((lie + _y) * 0x400 + k1) * blocksz
+ _x * blocksz
+ k2
+ 9 * blocksz * num;
self.buffer[offset * 4 + 0] = *byte;
self.buffer[offset * 4 + 1] = *byte;
self.buffer[offset * 4 + 2] = *byte;
}
}
}
}
};
}
/// 创建新行
fn new_line(&mut self, lie: usize) {
let blocksz = 1;
//对于窗口还没有写满的情况,我们直接清空下面的内容,列号移到下面一行即可(其实不清空也可以)
if lie < 0x300 {
let limit = 0x400 * 0x300 * 4;
for i in (lie * 0x400 * 4 * 16)..limit {
self.buffer[i] = 0x00;
}
}
//对于已经写满的情况,将现在的内容向上移动一格,之后再向下写入
else {
for _y in 0..(0x300 - 1) {
for _x in 0..0x400 * 4 {
let offset0 = ((_y) * 0x400 + _x) * 4;
let offset1 = ((_y + blocksz) * 0x400 + _x) * 4;
self.buffer[offset0 + 0] = self.buffer[offset1 + 0];
self.buffer[offset0 + 1] = self.buffer[offset1 + 1];
self.buffer[offset0 + 2] = self.buffer[offset1 + 2];
self.buffer[offset0 + 3] = self.buffer[offset1 + 3];
}
}
}
}
/// 清页
fn clear(&mut self) {
for i in 0..0x400 * 0x300 * 4 {
self.buffer[i] = 0x00;
}
}
}


浙公网安备 33010602011771号