文章目录
前言
前两天北方大风,不太好去实验室,《真象还原》的学习有所搁置,这两天继续。
第八章标题内存管理系统,下面分5节。分别是makefile相关、实现assert断言、实现字符串操作函数、位图相关、内存管理系统。我们的博客也分成这几部分。
贴一下我的第七章博客链接和本章参考的love6博客链接:
《操作系统真象还原》第八章 ---- 初入内存管理系统 涉足MakeFile 了解摸谈一二_makefile内存管理-CSDN博客
makefile相关
简单来说makefile是一种脚本,由make指令执行。通过检验依赖文件的mtime是否比目标文件mtime新,决定是否执行makefile里的指令。
这部分内容在P358页,博客不再详细说明,使用时多查阅即可。
基本语法
目标文件:依赖文件
[Tab]命令
跳转到目标处执行
执行完一个目标后就退出,不再执行后面的指令,格式如下
make 目标名称
伪目标
.PHONY:伪目标名
常见伪目标名称截图
自定义变量与系统变量
自定义变量定义的格式:变量名=值(字符串)
自定义变量引用的格式:$(变量名)
系统变量表截图
隐含规则
反斜杠\是多行之间的连续符。井号#用来注释。
在缺少依赖文件时,make指令按照一些默认的习惯,用隐含的规则完善依赖文件。简单的说就是自动编译,根据源码文件.c、.cc、.C、.p生成目标文件.o。
自动化变量
一种能代表一类文件文件名的符号。
- @ ,表示规则中的目标文件名集合,如果存在多个目标文件, @,表示规则中的目标文件名集合,如果存在多个目标文件, @,表示规则中的目标文件名集合,如果存在多个目标文件,@则表示其中每一个文件名。助记,’@’ 很像是at,aim at,表示瞄准目标。
- $<,表示规则中依赖文件中的第1个文件。助记,‘<’很像是集合的最左边,也就是第1个。
- , 表示规则中所有依赖文件的集合,如果集合中有重复的文件, ^,表示规则中所有依赖文件的集合,如果集合中有重复的文件, ,表示规则中所有依赖文件的集合,如果集合中有重复的文件,会自动去重。助记,’’很像从上往下 罩的动作,能罩住很大的范围,所以称为集合。
- $?,表示规则中,所有比目标文件 mtime 更新的依赖文件集合。助记,’?’表示疑问,make 最大的疑 问就是依赖文件的mtime是否比目标文件的mtime要新。
模式规则
就一句话,%用来匹配任意多个非空字符。比如%.o代表所有以.o为结尾的文件,g%s.o是以字符g开头的所有以.o 为结尾的文件,make会拿这个字符串模式去文件系统上查找文件,默认为当前路径下。
实现assert断言
简单来说就是写一个debug程序,如果内核运行过程中出错,这个程序告诉我们哪个文件哪个函数哪行出了问题,把相关信息打印到屏幕上,帮助我们解决问题。下面直接给出代码。
interrupt.c
新增部分是两行宏定义和四个函数。关于这个枚举体还有四个函数的声明,看下面的interrupt.h。
#define EFLAGS_IF 0x00000200 // 中断标志位IF,在EFLAGS寄存器中
#define GET_EFLAGS_IF(EFLAGS_VAR) asm volatile ("pushfl ; popl %0" : "=g" (EFLAGS_VAR)) // 获取中断标志位IF
/* 开启中断并返回开启中断之前的状态 */
enum intr_status intr_enable(void) {
enum intr_status old_status;
if(INTR_ON == intr_get_status()) {
old_status= INTR_ON; // 如果当前中断已经打开,则直接返回
return old_status;
}
else {
old_status = INTR_OFF; // 如果当前中断关闭,则打开中断
asm volatile ("sti"); // 开中断
return old_status;
}
}
/* 关闭中断并返回在关闭中断之前的状态 */
enum intr_status intr_disable(void) {
enum intr_status old_status;
if(INTR_OFF == intr_get_status()) {
old_status = INTR_OFF; // 如果当前中断已经关闭,则直接返回
return old_status;
}
else {
old_status = INTR_ON; // 如果当前中断打开,则关闭中断
asm volatile ("cli" : : : "memory"); // 关中断
return old_status;
}
}
/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status status) {
return (status & INTR_ON) ? intr_enable() : intr_disable();
}
/* 获取当前中断状态 */
enum intr_status intr_get_status(void) {
uint32_t eflags = 0;
GET_EFLAGS_IF(eflags);
return (eflags & EFLAGS_IF) ? INTR_ON : INTR_OFF;
}
interrupt.h
新增部分是一个枚举声明,四个函数声明,直接贴出完整代码
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
/* 定义中断的两种状态 */
/*关于枚举体enum,第一项默认值0,后面每项默认+1*/
enum intr_status {
INTR_OFF, // 关中断
INTR_ON // 开中断
};
enum intr_status intr_get_status(void); // 获取中断状态
enum intr_status intr_set_status(enum intr_status status); // 设置中断状态
enum intr_status intr_enable(void); // 打开中断
enum intr_status intr_disable(void); // 关闭中断
#endif
debug.h
放在kernel文件夹下。主要内容是assert的声明和打印函数的声明,代码如下。
#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic_spin(char* filename, int line, const char* func, const char* condition);
/* __VA_ARGS__代表若干个参数,对应前面的... */
#define PANIC(...) panic_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)
#ifdef NDEBUG
#define ASSERT(condition) ((void)0)
#else
#define ASSERT(condition) \
if (condition){} \
else { \
PANIC(#condition); \ /*关于#,学名字符串化宏,相当于宏定义了""字符串标号*/
}
#endif /*结束__NDEBUG*/
#endif /*结束__KERNEL_DEBUG_H*/
debug.c
同样放在kernrl文件夹下,实现报错打印函数
#include "debug.h"
#include "print.h"
#include "interrupt.h"
/* 打印相关信息并悬停程序 */
void panic_spin(char* filename, int line, const char* func, const char* condition){
intr_disable(); // 关闭中断
put_str("\n\n\n!!!kernel panic!!!\n");
put_str("filename:");put_str((char*)filename);put_str("\n");
put_str("line:0x"); put_int(line);put_str("\n");
put_str("function:");put_str((char*)func);put_str("\n");
put_str("condition:");put_str((char*)condition);put_str("\n");
while (1);
}
main.c
简单修改一下内核,用来测试assert
#include "print.h"
#include "init.h"
#include "debug.h"
int main(void){
put_str("HongBai's OS\n");
init_all();
asm volatile("sti");
ASSERT(1 == 2); // 断言失败,会调用panic_spin函数
while(1);
}
makefile
相当于是写脚本,目的是编译、连接、写入之前的文件,整体cv的love6的博客,修改了路径。如果要cv下面的代码,记得修改路径。
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
$(BUILD_DIR)/debug.o
############## c代码编译 ###############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/kernel/stdint.h kernel/init.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
lib/kernel/stdint.h kernel/interrupt.h device/timer.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
lib/kernel/stdint.h kernel/global.h kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/stdint.h\
kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
lib/kernel/print.h lib/kernel/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o: lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@
############## 链接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
.PHONY : mk_dir hd clean all
mk_dir:
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
hd:
dd if=$(BUILD_DIR)/kernel.bin \
of=/home/hongbai/bochs/bin/c.img \
bs=512 count=200 seek=10 conv=notrunc
clean:
cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build hd
测试
我们make all
,结果如下
运行一下bochs
实现了99%功能,就是打印的有点歪,bug应该出现在print.S里,留待后续修改。
debug(无果)
先去研究了一下print.S,重写了一下换行回车部分代码
;关于换行符:我们按照习惯,把换行回车结合起来,实现日常敲下enter的效果,所以换行=换行+回车
.is_line_feed: ;换行操作:光标挪到下一行行首
;内联的回车逻辑
xor dx,dx
mov ax,bx
mov si,80
div si
sub bx,dx
;单独的换行逻辑
add bx,80 ;目前是本行行首,再加80就是下一行行首
cmp bx,2000 ;是否清屏
jl .set_cursor ;bx<2000,即还在这一屏,就执行
.is_backspace: ;退格操作:光标bx前移一个显存位置,待删除位置补空格字符
dec bx ;自减,前移到上一个坐标
shl bx,1 ;左移1位,等价于*2,坐标位置*2=实际字节偏移量
mov byte [gs:bx],0x20
inc bx
mov byte [gs:bx],0x07 ;上面三行代码等价于mov word [gs:bx],0x0720,即打印一个空格字符,属性是黑底白字。
shr bx,1 ;右移1位,将地址重新转化为坐标
jmp .set_cursor ;将光标转移到新位置
然而仍然没解决问题😡
后面尝试了ds给出的几种解决方案,包括强制重置光标控制器,暂时禁用中断等,都没解决这个问题,调试2小时无果,遂暂时放弃,继续后面的章节。
目前先cv了love6第七章博客的print.S,据说他也是cv的别人的,我测试是可以无bug正常用的
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
section .data
put_int_buffer dq 0 ; 定义8字节缓冲区用于数字到字符的转换
[bits 32]
section .text
;--------------------------------------------
;put_str 通过put_char来打印以0字符结尾的字符串
;--------------------------------------------
global put_str
put_str:
;由于本函数中只用到了ebx和ecx,只备份这两个寄存器
push ebx
push ecx
xor ecx, ecx ; 准备用ecx存储参数,清空
mov ebx, [esp + 12] ; 从栈中得到待打印的字符串地址
.goon:
mov cl, [ebx]
cmp cl, 0 ; 如果处理到了字符串尾,跳到结束处返回
jz .str_over
push ecx ; 为put_char函数传递参数
call put_char
add esp, 4 ; 回收参数所占的栈空间
inc ebx ; 使ebx指向下一个字符
jmp .goon
.str_over:
pop ecx
pop ebx
ret
;-------------------- 将小端字节序的数字变成对应的ascii后,倒置 -----------------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x,如打印10进制15时,只会直接打印f,不会是0xf
;------------------------------------------------------------------------------------------
global put_int
put_int:
pushad
mov ebp, esp
mov eax, [ebp+4*9] ; call的返回地址占4字节+pushad的8个4字节
mov edx, eax
mov edi, 7 ; 指定在put_int_buffer中初始的偏移量
mov ecx, 8 ; 32位数字中,16进制数字的位数是8个
mov ebx, put_int_buffer
;将32位数字按照16进制的形式从低位到高位逐个处理,共处理8个16进制数字
.16based_4bits: ; 每4位二进制是16进制数字的1位,遍历每一位16进制数字
and edx, 0x0000000F ; 解析16进制数字的每一位。and与操作后,edx只有低4位有效
cmp edx, 9 ; 数字0~9和a~f需要分别处理成对应的字符
jg .is_A2F
add edx, '0' ; ascii码是8位大小。add求和操作后,edx低8位有效。
jmp .store
.is_A2F:
sub edx, 10 ; A~F 减去10 所得到的差,再加上字符A的ascii码,便是A~F对应的ascii码
add edx, 'A'
;将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区put_int_buffer
;高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序.
.store:
; 此时dl中应该是数字对应的字符的ascii码
mov [ebx+edi], dl
dec edi
shr eax, 4
mov edx, eax
loop .16based_4bits
;现在put_int_buffer中已全是字符,打印之前,
;把高位连续的字符去掉,比如把字符000123变成123
.ready_to_print:
inc edi ; 此时edi退减为-1(0xffffffff),加1使其为0
.skip_prefix_0:
cmp edi,8 ; 若已经比较第9个字符了,表示待打印的字符串为全0
je .full0
;找出连续的0字符, edi做为非0的最高位字符的偏移
.go_on_skip:
mov cl, [put_int_buffer+edi]
inc edi
cmp cl, '0'
je .skip_prefix_0 ; 继续判断下一位字符是否为字符0(不是数字0)
dec edi ;edi在上面的inc操作中指向了下一个字符,若当前字符不为'0',要恢复edi指向当前字符
jmp .put_each_num
.full0:
mov cl,'0' ; 输入的数字为全0时,则只打印0
.put_each_num:
push ecx ; 此时cl中为可打印的字符
call put_char
add esp, 4
inc edi ; 使edi指向下一个字符
mov cl, [put_int_buffer+edi] ; 获取下一个字符到cl寄存器
cmp edi,8
jl .put_each_num
popad
ret
;------------------------ put_char -----------------------------
;功能描述:把栈中的1个字符写入光标所在处
;-------------------------------------------------------------------
global put_char
put_char:
pushad ;备份32位寄存器环境
;需要保证gs中为正确的视频段选择子,为保险起见,每次打印时都为gs赋值
mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器
mov gs, ax
;;;;;;;;; 获取当前光标位置 ;;;;;;;;;
;先获得高8位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
in al, dx ;得到了光标位置的高8位
mov ah, al
;再获取低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
;将光标存入bx
mov bx, ax
;下面这行是在栈中获取待打印的字符
mov ecx, [esp + 36] ;pushad压入4×8=32字节,加上主调函数的返回地址4字节,故esp+36字节
cmp cl, 0xd ;CR是0x0d,LF是0x0a
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed
cmp cl, 0x8 ;BS(backspace)的asc码是8
jz .is_backspace
jmp .put_other
;;;;;;;;;;;;;;;;;;
.is_backspace:
;;;;;;;;;;;; backspace的一点说明 ;;;;;;;;;;
; 当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
; 但有可能在键入backspace后并不再键入新的字符,这时在光标已经向前移动到待删除的字符位置,但字符还在原处,
; 这就显得好怪异,所以此处添加了空格或空字符0
dec bx
shl bx,1
mov byte [gs:bx], 0x20 ;将待删除的字节补为0或空格皆可
inc bx
mov byte [gs:bx], 0x07
shr bx,1
jmp .set_cursor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.put_other:
shl bx, 1 ; 光标位置是用2字节表示,将光标值乘2,表示对应显存中的偏移字节
mov [gs:bx], cl ; ascii字符本身
inc bx
mov byte [gs:bx],0x07 ; 字符属性
shr bx, 1 ; 恢复老的光标值
inc bx ; 下一个光标值
cmp bx, 2000
jl .set_cursor ; 若光标值小于2000,表示未写到显存的最后,则去设置新的光标值
; 若超出屏幕字符数大小(2000)则换行处理
.is_line_feed: ; 是换行符LF(\n)
.is_carriage_return: ; 是回车符CR(\r)
; 如果是CR(\r),只要把光标移到行首就行了。
xor dx, dx ; dx是被除数的高16位,清0.
mov ax, bx ; ax是被除数的低16位.
mov si, 80 ; 由于是效仿linux,linux中\n便表示下一行的行首,所以本系统中,
div si ; 把\n和\r都处理为linux中\n的意思,也就是下一行的行首。
sub bx, dx ; 光标值减去除80的余数便是取整
; 以上4行处理\r的代码
.is_carriage_return_end: ; 回车符CR处理结束
add bx, 80
cmp bx, 2000
.is_line_feed_end: ; 若是LF(\n),将光标移+80便可。
jl .set_cursor
;屏幕行范围是0~24,滚屏的原理是将屏幕的1~24行搬运到0~23行,再将第24行用空格填充
.roll_screen: ; 若超出屏幕大小,开始滚屏
cld
mov ecx, 960 ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次
mov esi, 0xc00b80a0 ; 第1行行首
mov edi, 0xc00b8000 ; 第0行行首
rep movsd
;;;;;;;将最后一行填充为空白
mov ebx, 3840 ; 最后一行首字符的第一个字节偏移= 1920 * 2
mov ecx, 80 ;一行是80字符(160字节),每次清理1字符(2字节),一行需要移动80次
.cls:
mov word [gs:ebx], 0x0720 ;0x0720是黑底白字的空格键
add ebx, 2
loop .cls
mov bx,1920 ;将光标值重置为1920,最后一行的首字符.
.set_cursor:
;将光标设为bx值
;;;;;;; 1 先设置高8位 ;;;;;;;;
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al
;;;;;;; 2 再设置低8位 ;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret
实现字符串操作函数
文件名string.c,放在lib目录下(?),实现c语言中常见的字符串操作的函数,相当于写自己系统的c标准库,每个函数的说明请看注释
#include "string.h"
#include "global.h"
#include "debug.h"
#include "stdint.h"
/* 将dst_起始的size个字节设置为value */
void memset(void* dst_,uint8_t value,uint32_t size) {
ASSERT(dst_ != NULL);
uint8_t* dst = (uint8_t*)dst_;
while (size-->0) {
*dst = value;
dst++;
}
}
/* 将src_起始的size个字节拷贝到dst_ */
void memcpy(void* dst_,const void* src_,uint32_t size) {
ASSERT(dst_ != NULL && src_ != NULL);
uint8_t* dst = (uint8_t*)dst_;
const uint8_t* src = (const uint8_t*)src_;
while (size-->0) {
*dst = *src;
dst++;
src++;
}
}
/* 连续比较a_和b_两个地址开头的size个字节,若相等返回0,如果a_大于b_返回1,否则返回-1 */
int32_t memcmp(const void* a_,const void* b_,uint32_t size) {
ASSERT(a_ != NULL && b_ != NULL);
const uint8_t* a = (const uint8_t*)a_;
const uint8_t* b = (const uint8_t*)b_;
while (size-->0) {
if (*a != *b) {
return (*a > *b) ? 1 : -1;
}
a++;
b++;
}
return 0;
}
/* 将字符串从src_复制到dst_ */
char* strcpy(char* dst_,const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* ret = dst_;
while ((*dst_++ = *src_++));
return ret;
}
/* 返回字符串长度 */
uint32_t strlen(const char* str_) {
ASSERT(str_ != NULL);
const char* s = str_;
while (*s++);
return (uint32_t)(s - str_ - 1);
}
/* 比较两个字符串a和b_,如果a_大于b_返回1,相等返回0,否则返回-1 */
int32_t strcmp(const char* a_,const char* b_) {
ASSERT(a_ != NULL && b_ != NULL);
while (*a_ && (*a_ == *b_)) {
a_++;
b_++;
}
return (*a_ > *b_) ? 1 : (*a_ < *b_) ? -1 : 0;
}
/* 从左到右,查找str字符串中首次出现ch字符的地址 */
char* strchr(const char* str_,char ch) {
ASSERT(str_ != NULL);
while (*str_) {
if (*str_ == ch) {
return (char*)str_;
}
str_++;
}
return NULL;
}
结语
那么今天就先完成第八章前三部分,剩下位图相关和内存管理(free-malloc)留到明天完成。
在ubuntu上安装vscode后,写c语言代码的效率++,代码补全还是很好用的,建议大家都装一个。