《操作系统真象还原》第八章(1)——内存管理系统

发布于:2025-04-16 ⋅ 阅读:(26) ⋅ 点赞:(0)

前言

前两天北方大风,不太好去实验室,《真象还原》的学习有所搁置,这两天继续。

第八章标题内存管理系统,下面分5节。分别是makefile相关、实现assert断言、实现字符串操作函数、位图相关、内存管理系统。我们的博客也分成这几部分。

贴一下我的第七章博客链接和本章参考的love6博客链接:

《操作系统真象还原》第七章(1)——中断-CSDN博客

《操作系统真象还原》第七章(2)——中断-CSDN博客

《操作系统真象还原》第八章 ---- 初入内存管理系统 涉足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语言代码的效率++,代码补全还是很好用的,建议大家都装一个。