寒假OS学习第七天

# 寒假OS学习第七天

学不动了,后面的有点复杂

总结一下,搭建一个基于GRUB引导程序的toy OS kernel需要哪些知识

  • Linux下的存储维护相关的命令
    用于制作GRUB引导盘

  • 汇编语言,NASM或者AT&T。在x86架构下进行汇编的能力
    NASM和Intel汇编语法一样,更简单,AT&T也有学的必要

  • C语言的高级用法
    需要看一遍《C语言专家编程》,掌握一些C语言的高级用法

  • GCC的使用
    起码知道做一个内核要用GCC的哪些参数

  • 链接器ld的使用
    要做到会写ld脚本的水平

  • Makefile脚本
    会写Makefile脚本才是一个合格的程序员

  • Bochs或者QEMU的使用
    我选择使用Bochs,因为QEMU不会装
    选择Bochs,需要学会Bochs和GDB的联合调试

  • 内核调试工具的使用

  • multiboot规范
    使用GRUB引导,必须熟知Multiboot规范,并且必须熟知使用GRUB引导后机器处于什么状态

  • 熟知x86架构下的各种CPU细节、规范
    可以下一份Intel的开源文档来看

  • 熟知操作系统原理
    看那本操作系统概念

计划

接下来的时间里,我的打算是:

  1. 开始看James的教程
  2. 每天学一点基础知识

JamesM’s kernel

这个系列的教程会教你写一个基于x86架构的简单的UNIX-clone的操作系统。
使用语言:C、NASM

需要:

  1. GCC
  2. ld
  3. NASM
  4. GUN make

环境搭建

我们的kernel不会提供bootloader的教程。我们使用GRUB进行引导

floopy我在hurlex的教程里面已经做完了一次

我们的程序会在裸机上进行运行,因此调试会变得艰难,所以务必做到一次就成功

使用Bochs提供虚拟环境

Bochs配置如下:

# BOCHS配置

# 能使用的内存大小,单位为mb
megs: 32

# ROM文件,这个文件在/usr/share/bochs里面能找到
romimage: file="$BXSHARE/BIOS-bochs-latest"
vgaromimage: file="$BXSHARE/VGABIOS-lgpl-latest"

# 软盘。因为我们的目的是生成系统镜像,然后使用bochs运行,这个系统镜像就相
当于软盘。有一个软盘就用floppya,两个就是 floppyb,以此类推
floppya: 1_44=floppy.img, status=inserted

# 设置启动设备
boot: floppy

# 日志输出
log: bochs_log.txt

# 鼠标是否启用
mouse: enabled=0

# 启用键盘映射
keyboard: keymap="$BXSHARE/keymaps/x11-pc-us.map"

# CPU配置
clock: sync=realtime
cpu: ips=5000000

接下来,我们将要写一些脚本

Makefile脚本:

# 编译使用的源代码
C_SOURCES = $(shell find . -name "*.c")
S_SOURCES = $(shell find . -name "*.s")

# 编译生成的目标文件
C_OBJECTS = $(patsubst %.c, %.o, $(C_SOURCES))
S_OBJECTS = $(patsubst %.s, %.o, $(S_SOURCES))

# 输出
OUTPUT = "ymwm_kernel.img"

# 编译器
CC  = gcc
ASM = nasm

# 链接器
LD = ld

# 编译参数
C_FLAGS = 
# 生成ELF格式文件,并添加调试信息(-g),
# 使得调试信息有效(-F),格式为stabs
ASM_FALGS = -f elf -g -F stabs
# 脚本位置:scripts/link.ld, 格式:elf_i386, 不使用标准库
LD_FLAGS = -T scripts/link.ld -m elf_i386 -nostdlib



all: $(S_OBJECTS) $(C_OBJECTS) link update_image

# 对所有的C文件执行编译操作
.c.o:
	$(CC) $(C_FLAGS) $< -o $@

# 对所有的的汇编文件执行编译操作
.s.o:
	$(ASM) $(ASM_FALGS) $< -o $@

# 链接操作
.PHONY: link
link:
	$(LD) $(LD_FLAGS) $(S_OBJECTS) $(C_OBJECTS) -o $(OUTPUT)

.PHONY: clean
clean:
	$(RM) $(S_OBJECTS) $(C_OBJECTS) $(OUTPUT)

.PHONY: update_image
update_image:
	sudo mount floppy.img /mnt/kernel
	sudo cp ymwm_kernel.img /mnt/kernel/ymwm_kernel.img
	sleep 1
	sudo umount /mnt/kernel

.PHONY: run
run:
	bochs

.PHONY: remake
remake:
	@make clean
	@make all

已经很熟悉了

LD脚本

/* 程序入口 */
ENTRY(start)

/* 程序section */
SECTIONS
{
	/* 程序的起始位置 */
	. = 0x100000;
	.text:
	{
		/* 将所有输入文件的.text section合并 */
		*(.text)
		/* 进行对齐 */
		. = ALIGN(4096);
	}

	.data:
	{
		*(.data)
		/* 为了方便起见,将只读段也加入.data */
		*(.rodata)
		. = ALIGN(4096);
	}

	.bss:
	{
		*(.bss)
		. = ALIGN(4096);
	}
}

ld脚本告诉链接器要怎么去链接。
首先告诉链接器内核的入口为start,然后告诉链接器.text段应当被放在最开始的地方,且内核的起始地址为0x100000(1MB)。

开始

首先要写boot代码
由于我们使用了GRUB帮助引导,因此这一段的代码比较简单

# boot/boot.s

MBOOT_HEADER_MAGIC equ 0x1BADB002  ; 魔数
MBOOT_PAGE_ALIGN   equ 1 << 0  ; 进行页对齐
MBOOT_MEM_INFO     equ 1 << 1  ; 将内存信息放入结构体

MBOOT_HEADER_FLAGS equ MBOOT_MEM_INFO | MBOOT_PAGE_ALIGN
MBOOT_CHECKSUM     equ -(MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS)

[BITS 32]  ; 32位

section .text  ; 代码段

dd MBOOT_HEADER_MAGIC
dd MBOOT_HEADER_FLAGS
dd MBOOT_CHECKSUM

[GLOBAL start]  ; 程序入口
[GLOBAL glb_mboot]  ; mboot struct
[EXTERN kernel_entry]  ; 入口

start:  ; 入口
	cli  ; 关闭中断。此时尚未建立IDT, 发生中断会导致启动失败-
	mov [glb_mboot], ebx  ; GRUB将mboot_t结构体指针放在ebx处
	mov ebp, 0
	mov esp, STACK_TOP  ; 函数运行时栈
	call kernel_entry  ;
stop:
	hlt
	jmp stop

section .bss  ; bss段
glb_mboot:
	resb 4  ;分配大小为4的空间
STACK_TOP equ 0x8000

我们的内核运行时栈的大小就只有0x8000这么大
为什么要把大小设置成这么大?
因为1MB下有很多的其他外设的接口,例如显卡就在0xB8000
但是0~0x8000这个区域就绝对什么都没有,一干二净

至于函数入口函数,写一个hello world

/*
* @Author: yingmanwumen
* @Date:   2021-02-04 21:49:12
* @Last Modified by:   yingmanwumen
* @Last Modified time: 2021-02-04 23:40:15
*/

int kernel_entry()
{
    
    
	char *input = (char *)0xB8000;
	char color = (0 << 4) | (15 & 0x0F);
	*input ++ = 'H'; *input ++ = color;
	*input ++ = 'e'; *input ++ = color;
	*input ++ = 'l'; *input ++ = color;
	*input ++ = 'l'; *input ++ = color;
	*input ++ = 'o'; *input ++ = color;
	*input ++ = ','; *input ++ = color;
	*input ++ = ' '; *input ++ = color;
	*input ++ = 'W'; *input ++ = color;
	*input ++ = 'o'; *input ++ = color;
	*input ++ = 'r'; *input ++ = color;
	*input ++ = 'l'; *input ++ = color;
	*input ++ = 'd'; *input ++ = color;
	*input ++ = '!'; *input ++ = color;
	return 0;
}

屏幕输出

我们通过GRUB将显卡初始化为文本模式
文本模式一般的规格是80*25

本教程不会教你要怎么做图像模式的

显卡的帧缓冲通过0xB8000进行访问
帧缓冲是一个16位的数组,每一个字节的0-7个位是字符,8-11位是前景色,12-15位是背景色

首先定义一些以后会经常使用的类型与接口操作函数

#ifndef INCLUDE_COMMON_H_
#define INCLUDE_COMMON_H_

typedef unsigned int   u32_t;
typedef          int   s32_t;
typedef unsigned short u16_t;
typedef          short s16_t;
typedef unsigned char  u8_t;
typedef          char  s8_t;

void outb(u16_t port, u8_t value);
u8_t inb(u16_t port);
u16_t inw(u16_t port);

#endif
/*
* @Author: yingmanwumen
* @Date:   2021-02-05 20:31:34
* @Last Modified by:   yingmanwumen
* @Last Modified time: 2021-02-05 20:34:25
*/

void outb(u16_t port, u8_t value)
{
    
    
	__asm__ volatile(
		"outb %1, %0"
		: : "dN"(port), "a"(value)
	);
}

u8_t inb(u16_t port)
{
    
    
	u8_t res;
	__asm__ volatile(
		"inb %1, %0"
		:"=a"(res)
		:"dN"(port)
	);
	return res;
}

u16_t inw(u16_t port)
{
    
    
	u16_t res;
	__asm__ volatile(
		"inw %1, %0"
		:"=a"(res)
		:"dN"(port)
	);
	return res;
}

接下来需要写一些屏幕操作函数

首先要定义一些常量、全局变量

// include/console.h
typedef enum
{
    
    
	rc_black         = 0,
	rc_blue          = 1,
	rc_green         = 2,
	rc_cyan          = 3,
	rc_red           = 4,
	rc_magenta       = 5,
	rc_brown         = 6,
	rc_light_grey    = 7,
	rc_dark_grey     = 8,
	rc_light_blue    = 9,
	rc_light_green   = 10,
	rc_light_cyan    = 11,
	rc_light_red     = 12,
	rc_light_magneta = 13,
	rc_light_brown   = 14,
	rc_white         = 15
}realcolor_t;

// driver/console.c
static const u16_t commond_port = 0x3D4;
static const u16_t set_port     = 0x3D5;
static const u16_t screen_width = 80;
static const u16_t screen_hight = 25;
static u16_t *const video_mem   = (u16_t *const)0xB8000;
static const u16_t tab_width    = 8;

// x y坐标
static u16_t cur_x = 0;
static u16_t cur_y = 0;

接下来进行光标的操作

static inline void
mv_cur()
{
    
    
	u16_t pos = cur_y * screen_width + cur_x;
	outb(commond_port, 14);  // 设置高位
	outb(set_port, pos >> 8);
	outb(commond_port, 15);  // 设置低位
	outb(set_port, pos);
}

static inline void
scroll()
{
    
    
	u16_t blank = ' ' | attr_byte(rc_black, rc_white);
	// 向上滚动屏幕
	if (cur_y >= screen_hight)
	{
    
    
		int i = 0;
		while (i < 1920)
		{
    
    
			video_mem[i] = video_mem[i + screen_width];
			i ++;
		}
		while (i < 2000)
		{
    
    
			video_mem[i ++] = blank;
		}
		cur_y = screen_hight-1;
	}
}

操作完光标后,进行输出操作

void inline
con_putc_color(char c, realcolor_t back, realcolor_t fore)
{
    
    
	switch(c)
	{
    
    
		case 0x08:  // Backspace
			cur_x --; break;
		case '\t':  // tab
			cur_x = (cur_x + tab_width) & ~(tab_width - 1);
			// & ~(tab_width - 1) == % tab_width
			break;
		case '\n':
			cur_y ++;
		case '\r':
			cur_x = 0;
			break;
		default:
			if (c >= ' ')
			{
    
    
				u16_t ch = c | attr_byte(back, fore);
				video_mem[cur_pos] = ch;
				cur_x ++;
			}
	}
	if (cur_x >= screen_width)
	{
    
    
		cur_x = 0;
		cur_y ++;
	}
	scroll();
	mv_cur();
}

最后,补充一些函数

void inline
con_putc(char c)
{
    
    
	con_putc_color(c, rc_black, rc_white);
}

void inline
con_puts(char *c)
{
    
    
	while (*c)
		con_putc_color(*c ++, rc_black, rc_white);
}

void inline
con_puts_color(char *c, realcolor_t back, realcolor_t fore)
{
    
    
	while (*c)
		con_putc_color(*c ++, back, fore);
}

void inline
con_clear()
{
    
    
	u16_t blank = ' ' | attr_byte(rc_black, rc_white);
	int i = 0;
	while (i < 2000)
		video_mem[i ++] = blank;

	cur_x = cur_y = 0;
	mv_cur();
}

学不动了,后面的有点复杂

总结一下,搭建一个基于GRUB引导程序的toy OS kernel需要哪些知识

  • Linux下的存储维护相关的命令
    用于制作GRUB引导盘

  • 汇编语言,NASM或者AT&T。在x86架构下进行汇编的能力
    NASM和Intel汇编语法一样,更简单,AT&T也有学的必要

  • C语言的高级用法
    需要看一遍《C语言专家编程》,掌握一些C语言的高级用法

  • GCC的使用
    起码知道做一个内核要用GCC的哪些参数

  • 链接器ld的使用
    要做到会写ld脚本的水平

  • Makefile脚本
    会写Makefile脚本才是一个合格的程序员

  • Bochs或者QEMU的使用
    我选择使用Bochs,因为QEMU不会装
    选择Bochs,需要学会Bochs和GDB的联合调试

  • 内核调试工具的使用

  • multiboot规范
    使用GRUB引导,必须熟知Multiboot规范,并且必须熟知使用GRUB引导后机器处于什么状态

  • 熟知x86架构下的各种CPU细节、规范
    可以下一份Intel的开源文档来看

  • 熟知操作系统原理
    看那本操作系统概念

计划

接下来的时间里,我的打算是:

  1. 开始看James的教程
  2. 每天学一点基础知识

JamesM’s kernel

这个系列的教程会教你写一个基于x86架构的简单的UNIX-clone的操作系统。
使用语言:C、NASM

需要:

  1. GCC
  2. ld
  3. NASM
  4. GUN make

环境搭建

我们的kernel不会提供bootloader的教程。我们使用GRUB进行引导

floopy我在hurlex的教程里面已经做完了一次

我们的程序会在裸机上进行运行,因此调试会变得艰难,所以务必做到一次就成功

使用Bochs提供虚拟环境

Bochs配置如下:

# BOCHS配置

# 能使用的内存大小,单位为mb
megs: 32

# ROM文件,这个文件在/usr/share/bochs里面能找到
romimage: file="$BXSHARE/BIOS-bochs-latest"
vgaromimage: file="$BXSHARE/VGABIOS-lgpl-latest"

# 软盘。因为我们的目的是生成系统镜像,然后使用bochs运行,这个系统镜像就相
当于软盘。有一个软盘就用floppya,两个就是 floppyb,以此类推
floppya: 1_44=floppy.img, status=inserted

# 设置启动设备
boot: floppy

# 日志输出
log: bochs_log.txt

# 鼠标是否启用
mouse: enabled=0

# 启用键盘映射
keyboard: keymap="$BXSHARE/keymaps/x11-pc-us.map"

# CPU配置
clock: sync=realtime
cpu: ips=5000000

接下来,我们将要写一些脚本

Makefile脚本:

# 编译使用的源代码
C_SOURCES = $(shell find . -name "*.c")
S_SOURCES = $(shell find . -name "*.s")

# 编译生成的目标文件
C_OBJECTS = $(patsubst %.c, %.o, $(C_SOURCES))
S_OBJECTS = $(patsubst %.s, %.o, $(S_SOURCES))

# 输出
OUTPUT = "ymwm_kernel.img"

# 编译器
CC  = gcc
ASM = nasm

# 链接器
LD = ld

# 编译参数
C_FLAGS = 
# 生成ELF格式文件,并添加调试信息(-g),
# 使得调试信息有效(-F),格式为stabs
ASM_FALGS = -f elf -g -F stabs
# 脚本位置:scripts/link.ld, 格式:elf_i386, 不使用标准库
LD_FLAGS = -T scripts/link.ld -m elf_i386 -nostdlib



all: $(S_OBJECTS) $(C_OBJECTS) link update_image

# 对所有的C文件执行编译操作
.c.o:
	$(CC) $(C_FLAGS) $< -o $@

# 对所有的的汇编文件执行编译操作
.s.o:
	$(ASM) $(ASM_FALGS) $< -o $@

# 链接操作
.PHONY: link
link:
	$(LD) $(LD_FLAGS) $(S_OBJECTS) $(C_OBJECTS) -o $(OUTPUT)

.PHONY: clean
clean:
	$(RM) $(S_OBJECTS) $(C_OBJECTS) $(OUTPUT)

.PHONY: update_image
update_image:
	sudo mount floppy.img /mnt/kernel
	sudo cp ymwm_kernel.img /mnt/kernel/ymwm_kernel.img
	sleep 1
	sudo umount /mnt/kernel

.PHONY: run
run:
	bochs

.PHONY: remake
remake:
	@make clean
	@make all

已经很熟悉了

LD脚本

/* 程序入口 */
ENTRY(start)

/* 程序section */
SECTIONS
{
	/* 程序的起始位置 */
	. = 0x100000;
	.text:
	{
		/* 将所有输入文件的.text section合并 */
		*(.text)
		/* 进行对齐 */
		. = ALIGN(4096);
	}

	.data:
	{
		*(.data)
		/* 为了方便起见,将只读段也加入.data */
		*(.rodata)
		. = ALIGN(4096);
	}

	.bss:
	{
		*(.bss)
		. = ALIGN(4096);
	}
}

ld脚本告诉链接器要怎么去链接。
首先告诉链接器内核的入口为start,然后告诉链接器.text段应当被放在最开始的地方,且内核的起始地址为0x100000(1MB)。

开始

首先要写boot代码
由于我们使用了GRUB帮助引导,因此这一段的代码比较简单

# boot/boot.s

MBOOT_HEADER_MAGIC equ 0x1BADB002  ; 魔数
MBOOT_PAGE_ALIGN   equ 1 << 0  ; 进行页对齐
MBOOT_MEM_INFO     equ 1 << 1  ; 将内存信息放入结构体

MBOOT_HEADER_FLAGS equ MBOOT_MEM_INFO | MBOOT_PAGE_ALIGN
MBOOT_CHECKSUM     equ -(MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS)

[BITS 32]  ; 32位

section .text  ; 代码段

dd MBOOT_HEADER_MAGIC
dd MBOOT_HEADER_FLAGS
dd MBOOT_CHECKSUM

[GLOBAL start]  ; 程序入口
[GLOBAL glb_mboot]  ; mboot struct
[EXTERN kernel_entry]  ; 入口

start:  ; 入口
	cli  ; 关闭中断。此时尚未建立IDT, 发生中断会导致启动失败-
	mov [glb_mboot], ebx  ; GRUB将mboot_t结构体指针放在ebx处
	mov ebp, 0
	mov esp, STACK_TOP  ; 函数运行时栈
	call kernel_entry  ;
stop:
	hlt
	jmp stop

section .bss  ; bss段
glb_mboot:
	resb 4  ;分配大小为4的空间
STACK_TOP equ 0x8000

我们的内核运行时栈的大小就只有0x8000这么大
为什么要把大小设置成这么大?
因为1MB下有很多的其他外设的接口,例如显卡就在0xB8000
但是0~0x8000这个区域就绝对什么都没有,一干二净

至于函数入口函数,写一个hello world

/*
* @Author: yingmanwumen
* @Date:   2021-02-04 21:49:12
* @Last Modified by:   yingmanwumen
* @Last Modified time: 2021-02-04 23:40:15
*/

int kernel_entry()
{
    
    
	char *input = (char *)0xB8000;
	char color = (0 << 4) | (15 & 0x0F);
	*input ++ = 'H'; *input ++ = color;
	*input ++ = 'e'; *input ++ = color;
	*input ++ = 'l'; *input ++ = color;
	*input ++ = 'l'; *input ++ = color;
	*input ++ = 'o'; *input ++ = color;
	*input ++ = ','; *input ++ = color;
	*input ++ = ' '; *input ++ = color;
	*input ++ = 'W'; *input ++ = color;
	*input ++ = 'o'; *input ++ = color;
	*input ++ = 'r'; *input ++ = color;
	*input ++ = 'l'; *input ++ = color;
	*input ++ = 'd'; *input ++ = color;
	*input ++ = '!'; *input ++ = color;
	return 0;
}

屏幕输出

我们通过GRUB将显卡初始化为文本模式
文本模式一般的规格是80*25

本教程不会教你要怎么做图像模式的

显卡的帧缓冲通过0xB8000进行访问
帧缓冲是一个16位的数组,每一个字节的0-7个位是字符,8-11位是前景色,12-15位是背景色

首先定义一些以后会经常使用的类型与接口操作函数

#ifndef INCLUDE_COMMON_H_
#define INCLUDE_COMMON_H_

typedef unsigned int   u32_t;
typedef          int   s32_t;
typedef unsigned short u16_t;
typedef          short s16_t;
typedef unsigned char  u8_t;
typedef          char  s8_t;

void outb(u16_t port, u8_t value);
u8_t inb(u16_t port);
u16_t inw(u16_t port);

#endif
/*
* @Author: yingmanwumen
* @Date:   2021-02-05 20:31:34
* @Last Modified by:   yingmanwumen
* @Last Modified time: 2021-02-05 20:34:25
*/

void outb(u16_t port, u8_t value)
{
    
    
	__asm__ volatile(
		"outb %1, %0"
		: : "dN"(port), "a"(value)
	);
}

u8_t inb(u16_t port)
{
    
    
	u8_t res;
	__asm__ volatile(
		"inb %1, %0"
		:"=a"(res)
		:"dN"(port)
	);
	return res;
}

u16_t inw(u16_t port)
{
    
    
	u16_t res;
	__asm__ volatile(
		"inw %1, %0"
		:"=a"(res)
		:"dN"(port)
	);
	return res;
}

接下来需要写一些屏幕操作函数

首先要定义一些常量、全局变量

// include/console.h
typedef enum
{
    
    
	rc_black         = 0,
	rc_blue          = 1,
	rc_green         = 2,
	rc_cyan          = 3,
	rc_red           = 4,
	rc_magenta       = 5,
	rc_brown         = 6,
	rc_light_grey    = 7,
	rc_dark_grey     = 8,
	rc_light_blue    = 9,
	rc_light_green   = 10,
	rc_light_cyan    = 11,
	rc_light_red     = 12,
	rc_light_magneta = 13,
	rc_light_brown   = 14,
	rc_white         = 15
}realcolor_t;

// driver/console.c
static const u16_t commond_port = 0x3D4;
static const u16_t set_port     = 0x3D5;
static const u16_t screen_width = 80;
static const u16_t screen_hight = 25;
static u16_t *const video_mem   = (u16_t *const)0xB8000;
static const u16_t tab_width    = 8;

// x y坐标
static u16_t cur_x = 0;
static u16_t cur_y = 0;

接下来进行光标的操作

static inline void
mv_cur()
{
    
    
	u16_t pos = cur_y * screen_width + cur_x;
	outb(commond_port, 14);  // 设置高位
	outb(set_port, pos >> 8);
	outb(commond_port, 15);  // 设置低位
	outb(set_port, pos);
}

static inline void
scroll()
{
    
    
	u16_t blank = ' ' | attr_byte(rc_black, rc_white);
	// 向上滚动屏幕
	if (cur_y >= screen_hight)
	{
    
    
		int i = 0;
		while (i < 1920)
		{
    
    
			video_mem[i] = video_mem[i + screen_width];
			i ++;
		}
		while (i < 2000)
		{
    
    
			video_mem[i ++] = blank;
		}
		cur_y = screen_hight-1;
	}
}

操作完光标后,进行输出操作

void inline
con_putc_color(char c, realcolor_t back, realcolor_t fore)
{
    
    
	switch(c)
	{
    
    
		case 0x08:  // Backspace
			cur_x --; break;
		case '\t':  // tab
			cur_x = (cur_x + tab_width) & ~(tab_width - 1);
			// & ~(tab_width - 1) == % tab_width
			break;
		case '\n':
			cur_y ++;
		case '\r':
			cur_x = 0;
			break;
		default:
			if (c >= ' ')
			{
    
    
				u16_t ch = c | attr_byte(back, fore);
				video_mem[cur_pos] = ch;
				cur_x ++;
			}
	}
	if (cur_x >= screen_width)
	{
    
    
		cur_x = 0;
		cur_y ++;
	}
	scroll();
	mv_cur();
}

最后,补充一些函数

void inline
con_putc(char c)
{
    
    
	con_putc_color(c, rc_black, rc_white);
}

void inline
con_puts(char *c)
{
    
    
	while (*c)
		con_putc_color(*c ++, rc_black, rc_white);
}

void inline
con_puts_color(char *c, realcolor_t back, realcolor_t fore)
{
    
    
	while (*c)
		con_putc_color(*c ++, back, fore);
}

void inline
con_clear()
{
    
    
	u16_t blank = ' ' | attr_byte(rc_black, rc_white);
	int i = 0;
	while (i < 2000)
		video_mem[i ++] = blank;

	cur_x = cur_y = 0;
	mv_cur();
}

猜你喜欢

转载自blog.csdn.net/weixin_45206746/article/details/113704488