使用Rust开发操作系统(VGA缓冲区)

释放双眼,带上耳机,听听看~!

VGA缓冲区

  • 一些无聊的理论
  • 建立库
  • 开始干活
  • 下一步是什么

在上一篇中我们使用bootimage制作bootloader并通过QEMU来引导并调入到内核中,我们的内核现在很干净,我们迫切希望能够在屏幕上显示一些字符,本节我们开始实现这个功能

一些无聊的理论

为了能在屏幕上显示一些字符,我们需要通过文本缓冲器写入VGA硬件,VGA文本缓冲区是一个二维的数字,总共25行,80列,他们直接渲染到屏幕上,在这个二维的空间中每个元素都会用一下形式描述

0-7
ASCII 码点
8-11
文字的前景色(文字的颜色)
12-14
文字的背景色
15
闪烁文字

第一项代表应以ASCII编码打印的字符,实际上它不是ASCII,它其实时ASCII字符内码列表( Code page 437)是 最初的IBM PC代码页,实现了扩展ASCII字符集

第二项定义了文字如何显示,你可以使用自己喜欢的颜色,最后一项表示字符是否会闪烁

以下颜色是可以使用的

0x0
黑色
0x8
暗灰色
0x1
蓝色
0x9
亮蓝色
0x2
绿色
0xa
亮绿色
0x3
青色
0xb
亮青色
0x4
红色
0xc
亮红色
0x5
品红色
0xd
粉色
0x6
棕色
0xe
黄色
0x7
亮灰色
0xf
白色

颜色的第4位是亮位,它例如将蓝色变成亮蓝色。对于背景色,该位被重新用作闪烁

VGA文本缓冲区可通过内存的0xB8000访问,这意味着我们直接向普通内存地址一样读写,但是它不经过RAM,因为我们直接访问硬件上的VGA文本缓冲区

注意:内存映射的硬件可能不支持所有的RAM操作,例如我们可以向一个设备写入数据,但是读取该设备时会给我们一堆垃圾数据,但是VGA支持正常的读和写(不然我们读取的就是一些奇怪的数据)

建立库

随着我们的代码逐渐增多,就不能在main.rs文件中了,我们需要份文件,做到模块化

首先我们创建一个lib.rs写添加以下内容

lib.rs


1
2
3
1#![no_std]
2
3

同main.rs一样我们也不需要std库,接着我们在main.rs中添加这一行


1
2
3
4
1#[macro_use]
2extern crate kernel;
3
4

因为我们需要用到自己定义的宏,因此加上了macro_use属性

这样我们的基本结构就弄好了

紧接着我们在main.rs同级目录创建一个vga.rs文件,同时我们需要在lib.rs文件中添加这样一行


1
2
3
1pub mod vga;
2
3

这样我们可以使用vga模块了

注意模块名和文件名要一致哦,别忘了加pub 不然访问不到

如果你不喜欢把库文件放在main文件旁边,你也可以创建一个目录

例如:创建一个目录名为buffer,之后创建一个src/buffer/mod.rs文件(这个必不可少),在同级目录下创建/src/buffer/vga.rs文件

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


1
2
3
4
1// mod.rs
2pub mod vga; // 注意模块名和文件名要一致哦
3
4

添加完成后在lib.rs中添加


1
2
3
4
1// lib.rs
2pub mod buffer; // buffer是创建的目录,如果目录中没有mod.rs文件
3
4

目录结构如下所示


1
2
3
4
5
6
7
8
9
10
11
12
13
1droll_os
2│
3├── src
4│     ├── main.rs 
5│    ├── buffer
6│       │    ├── mod.rs
7│       │    └── vga.rs
8│    └── lib.rs
9├── .gitignore
10├── Cargo.toml
11└── x86-64.json
12
13

开始干活

创建完毕后我们在vga.rs文件中写入以下内容


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
1#[allow(dead_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3// 指定字节对齐方式
4#[repr(u8)]
5pub enum Color {
6    // 黑色
7    Black = 0,
8    // 蓝色
9    Blue = 1,
10    // 绿色
11    Green = 2,
12    // 青色
13    Cyan = 3,
14    // 红色
15    Red = 4,
16    // 品红色
17    Magenta = 5,
18    // 棕色
19    Brown = 6,
20    // 浅灰色
21    LightGray = 7,
22    // 深灰色
23    DarkGray = 8,
24    // 浅蓝色
25    LightBlue = 9,
26    // 浅绿色
27    LightGreen = 10,
28    // 浅青色
29    LightCyan = 11,
30    // 亮红色
31    LightRed = 12,
32    // 粉色
33    Pink = 13,
34    // 黄色
35    Yellow = 14,
36    // 白色
37    White = 15,
38}
39
40

在Color上我们添加了#[repe(u8)]这样每个枚举变量都存储为u8,实际上u4就足够了,但是rust没有u4类型,

通常情况下rust编译器会对未使用的变量提示警告,我们可以使用#[allow(dead_code)]来关闭这些警告

我们为该类型启用了Copy语义,并使其可打印并且可以比较。

为了表示指定前景色和背景色的全色代码,我们在u8类型的基础上创建一个新的类型


1
2
3
4
5
6
7
8
9
10
11
12
13
1#[derive(Debug, Copy, Clone, Eq, PartialEq)]
2#[repr(transparent)]
3pub struct ColorCode(u8);
4
5impl ColorCode {
6    pub fn new(foreground: Color, background: Color) -> ColorCode {
7       // foreground: 前景色
8       // background: 背景色
9        ColorCode((background as u8) << 4 | foreground as u8)
10    }
11}
12
13

ColorCode包含了前景色和背景色代码,像之前一样我们为ColorCode实现了Debug, Copy, Clone, Eq, PartialEq等trait,为了保证ColorCode具有与u8完全相同的数据大小,因此我们使用了#[repr(transparent)]

根据之前的理论我们知道VGA的最大行跟列为25和80,我们将其定义为常量


1
2
3
4
1pub const BUFFER_HEIGHT: usize = 25;
2pub const BUFFER_WIDTH: usize = 80;
3
4

之后我们开始定义一些VGA 字符和缓冲区结构


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1#[derive(Debug, Copy, Clone, Eq, PartialEq)]
2#[repr(C)]
3struct ScreenChar {
4    // ASCII 字符
5    ascii_char: u8,
6    // 字符颜色
7    color: ColorCode,
8}
9
10pub struct Buffer {
11    // 二维数组,使用VGA 模式显示文本
12    chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
13}
14
15

现在我们定义了ScreenChar和Buffer结构用来表示打印的字符和VGA缓冲区,但是VGA缓冲有些问题,我们的代码可能不会使用以后优化过的RUST编译器,

因为我们将会一直往Buffer中写入数据但是基本不会从Buffer中读取数据,Rust编译器并不知道我们是否真的访问VGA缓冲区并且对某些字符出现在屏幕上的副作用一无所知,因此它可能认为写操作是多余的并且会忽略写操作,为了解决这种错误的优化,我们需要将这种操作指定为volatile.这告诉编译器我们的代码不应该被优化。

我们可以在Catgo.toml文件中添加以下依赖


1
2
3
4
1[dependencies]
2volatile = "0.2.6"
3
4

接下来我们将Buffer中的数组指定为Volatile


1
2
3
4
5
6
7
8
1// vga.rs
2use volatile::Volatile;
3
4struct Buffer {
5    chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
6}
7
8

解决了该问题之后我们开始着手去写Writer


1
2
3
4
5
6
7
8
9
10
11
12
13
1// Writer总是以\n结尾
2pub struct Writer {
3    // 当前行号
4    pub row_position: usize,
5    // 当前列号
6    pub column_position: usize,
7    // 字符颜色
8    pub color_code: ColorCode,
9    // 屏幕缓冲区 生命周期位整个程序运行时间
10    pub buffer: &'static mut Buffer,
11}
12
13

其中buffer中的'static生命周期指定该引用在整个程序运行时均有效

我们开始写几个方法


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1impl Writer {
2    // 清空整个屏幕
3    fn clear_screen(&mut self) {
4        let blank = ScreenChar {
5            ascii_char: b' ',
6            color: self.color_code,
7        };
8        for row in 0..BUFFER_HEIGHT {
9            for col in 0..BUFFER_WIDTH {
10                self.buffer.chars[row][col].write(blank);
11            }
12        }
13    }
14    
15    fn new_line(&mut self) {
16        // 如果超过最大行数
17        if self.row_position >= BUFFER_HEIGHT {
18        // 清空屏幕
19            self.clear_screen();
20            self.row_position = 0;
21        }
22        self.row_position += 1;
23        self.column_position = 0;
24      }
25}
26
27

我们现在写了2个函数,clear_screen函数用来清空整个屏幕,实现很简单,使用空格填充整个屏幕,new_line 函数用于换行,当换行超过屏幕最大行数的时候会清空屏幕,换行比较简单,只需要将行号+1,把列号置0即可

随后我们就可以写入字节了


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1impl Writer {
2    .......
3   pub fn write_bytes(&mut self, byte: u8) {
4        match byte {
5           // 如果遇到\n字符就换行
6            b'\n' => self.new_line(),
7            byte => {
8                // 检查是否超过每行最大字符数
9                if self.column_position >= BUFFER_WIDTH {
10                    // 超过需要换行
11                    self.new_line()
12                }
13
14                let row = self.row_position;
15                let col = self.column_position;
16                let color = self.color_code;
17                // 使用write方法来代替= 保证编译器将永远不会优化写操作。
18                self.buffer.chars[row][col].write(ScreenChar {
19                    ascii_char: byte,
20                    color,
21                });
22              // 写完后列号+1
23                self.column_position += 1;
24            }
25        }
26    }
27}
28
29

write_bytes函数用于写入一个字符,每次写入时都会判断是否超过最大列数,我们使用了Volatile保证编译器不会优化写操作

我们每次一个一个字符的写太慢了,所以我们写一个一次性写入一个字符串的函数


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1impl Writer {
2    .......
3   pub fn write_string(&mut self, s: &str) {
4        for byte in s.bytes() {
5            match byte {
6                //32(空格) - 126(~)
7                0x20..=0x7e | b'\n' => self.write_bytes(byte),
8                // 不是ASCII可打印字符 会打印■
9                _ => self.write_bytes(0xfe),
10            }
11        }
12    }
13}
14
15

这里需要注意的是,有些字符不可打印,对于这些不可打印的字符使用■代替,该函数只会打印ASCII表中32-126的字符(从空格字符到~字符)

接下来我们可以使用我们的函数去打印一些字符了


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1// main.rs
2#[macro_use]
3extern crate kernel;
4
5use core::panic::PanicInfo;
6use kernel::vga::{Writer, ColorCode, Color, Buffer};
7
8#[no_mangle]
9pub extern "C" fn _start() -> ! {
10    let mut write = Writer {
11        row_position: 0,
12        column_position: 0,
13        color_code: ColorCode::new(Color::White, Color::Black),
14        buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
15    };
16    write.write_string("Hello World");
17    
18    loop {}
19}
20
21

然后执行一下命令


1
2
3
4
5
1cargo bootimage
2
3qemu-system-x86_64 -drive format=raw,file=target/x86-64/debug/bootimage-kernel.bin
4
5

或者你可以选择写一个Makefile或shell


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1GO= cargo
2TEST_FLAGS=xtest
3RUST_BUILD=$(GO) xbuild
4RUST_FLAGS=--target x86-64.json
5IMAGE=target/x86-64/debug/bootimage-kernel.bin
6
7build: src/main.rs
8   $(RUST_BUILD) $(RUST_FLAGS)
9
10boot:
11  $(GO) bootimage
12
13run: boot
14  qemu-system-x86_64 -drive format=raw,file=$(IMAGE)
15
16test:
17  $(GO) $(TEST_FLAGS)
18
19

然后敲make run就可以了,这样我们可以我们可以通过VGA缓冲在屏幕上打印字符串了!是不是有些激动!

下一步是什么

本节我们通过直接向VGA缓冲区写入字符串虽然我们可以显示了,但是使用起来比较麻烦,在下篇中我们将实现2个宏print和println宏

给TA打赏
共{{data.count}}人
人已打赏
安全技术

C++迭代器

2022-1-11 12:36:11

安全经验

ThinkPHP 发布 5.1.33 版本——包含安全更新

2019-1-21 11:12:22

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索