# 寒假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的开源文档来看 -
熟知操作系统原理
看那本操作系统概念
计划
接下来的时间里,我的打算是:
- 开始看James的教程
- 每天学一点基础知识
JamesM’s kernel
这个系列的教程会教你写一个基于x86架构的简单的UNIX-clone的操作系统。
使用语言:C、NASM
需要:
- GCC
- ld
- NASM
- 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的开源文档来看 -
熟知操作系统原理
看那本操作系统概念
计划
接下来的时间里,我的打算是:
- 开始看James的教程
- 每天学一点基础知识
JamesM’s kernel
这个系列的教程会教你写一个基于x86架构的简单的UNIX-clone的操作系统。
使用语言:C、NASM
需要:
- GCC
- ld
- NASM
- 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();
}