《操作系统真象还原》第八章——实现断言函数

代码文件结构

代码实现

interrupt.h

目的:在上节开中断的实现文件里添加开关中断的函数功能

原因:由于断言函数的目的是当错误发生时打印错误信息,因而此时不应该有其他中断信号来打扰,需要关闭中断

cpp 复制代码
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;		//将intr_handler定义为void*同类型
void idt_init(void);

/*定义中断状态*/
enum intr_status
{
    INTR_OFF,//表示中断关闭
    INTR_ON  //表示中断打开
};
enum intr_status intr_get_status(void);
enum intr_status intr_set_status(enum intr_status);
enum intr_status intr_enable(void);
enum intr_status intr_disable(void);
#endif

interrupt.c

cpp 复制代码
#include "interrupt.h"      //里面定义了intr_handler类型
#include "stdint.h"         //各种uint_t类型
#include "global.h"         //里面定义了选择子
#include "io.h"             
#include "print.h"

#define PIC_M_CTRL 0x20	      // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21	      // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0	      // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1	      // 从片的数据端口是0xa1

#define IDT_DESC_CNT 0x21	   //支持的中断描述符个数33

#define EFLAGS_IF 0x00000200     //表示eflags寄存器中的if位为1
                              //将该宏与当前的if位(中断状态)进行与操作,即可获取当前的中断状态

/*
功能:获取eflags寄存器中的值
pushfl将eflags寄存器中的值压入栈顶,再使用popl将栈顶的值弹出到EFLAG_VAR所在内存中
该约束自然用表示内存的字母,但是内联汇编中没有专门表示约束内存的字母,所以只能用g
代表可以是任意寄存器,内存或立即数
*/
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl;pop %0":"=g"(EFLAG_VAR))

//定义中断描述符结构体
struct gate_desc {
   uint16_t    func_offset_low_word;        //函数地址低字
   uint16_t    selector;                    //选择子字段
   uint8_t     dcount;                      //此项为双字计数字段,是门描述符中的第4字节。这个字段无用
   uint8_t     attribute;                   //属性字段
   uint16_t    func_offset_high_word;       //函数地址高字
};

//定义中断门描述符结构体数组,形成中断描述符表idt,该数组中的元素是中断描述符
static struct gate_desc idt[IDT_DESC_CNT];
//静态函数声明,该函数用于构建中断描述符,将上述定义的中断描述符结构体按照字段说明进行填充即可
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);

//intr_handler为void*类型,intr_entry_table为中断处理程序数组,其内的元素是中断处理程序入口地址
extern intr_handler intr_entry_table[IDT_DESC_CNT];

//存储中断/异常的名字
char* intr_name[IDT_DESC_CNT];    
//定义中断处理程序数组,在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序           
intr_handler idt_table[IDT_DESC_CNT];


/*初始化可编程中断控制器8259A*/
static void pic_init(void)
{
    /* 初始化主片 */
   outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
   outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
   outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI

   /* 初始化从片 */
   outb (PIC_S_CTRL, 0x11);	// ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_S_DATA, 0x28);	// ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
   outb (PIC_S_DATA, 0x02);	// ICW3: 设置从片连接到主片的IR2引脚
   outb (PIC_S_DATA, 0x01);	// ICW4: 8086模式, 正常EOI

   /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
   outb (PIC_M_DATA, 0xfe);
   outb (PIC_S_DATA, 0xff);

   put_str("pic_init done\n");
}

/*
函数功能:构建中断描述符
函数实现:按照中断描述符结构体定义填充字段
参数:中断门描述符地址,属性,中断处理函数
*/
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) 
{ 
   p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
   p_gdesc->selector = SELECTOR_K_CODE;
   p_gdesc->dcount = 0;
   p_gdesc->attribute = attr;
   p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}

/*
函数功能:中断描述符表idt
函数实现:循环调用make_idt_desc构建中断描述符,形成中断描述符表idt
参数:中断描述符表中的某个中断描述符地址,属性字段,中断处理函数地址
*/
static void idt_desc_init(void) {
   int i;
   for (i = 0; i < IDT_DESC_CNT; i++) {
      make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); 
   }
   put_str("idt_desc_init done\n");
}

#if 1
/*用c语言定义中断处理函数的具体实现,不再用汇编语言在kernel.S中定义中断处理函数的实现*/
//参数:中断向量号
static void general_intr_handler(uint8_t vec_nr)
{
   //伪中断向量,无需处理
   if(vec_nr==0x27 || vec_nr==0x2f)
   {
      return;
   }
   put_str("int vector:0x");
   put_int(vec_nr);
   put_char('\n');
}
#endif

/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void) {			   // 完成一般中断处理函数注册及异常名称注册
   int i;
   for (i = 0; i < IDT_DESC_CNT; i++) {

/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
 * 见kernel/kernel.S的call [idt_table + %1*4] */
      idt_table[i] = general_intr_handler;   // 默认为general_intr_handler。
							                        // 以后会由register_handler来注册具体处理函数。
      intr_name[i] = "unknown";				   // 先统一赋值为unknown 
   }
   intr_name[0] = "#DE Divide Error";
   intr_name[1] = "#DB Debug Exception";
   intr_name[2] = "NMI Interrupt";
   intr_name[3] = "#BP Breakpoint Exception";
   intr_name[4] = "#OF Overflow Exception";
   intr_name[5] = "#BR BOUND Range Exceeded Exception";
   intr_name[6] = "#UD Invalid Opcode Exception";
   intr_name[7] = "#NM Device Not Available Exception";
   intr_name[8] = "#DF Double Fault Exception";
   intr_name[9] = "Coprocessor Segment Overrun";
   intr_name[10] = "#TS Invalid TSS Exception";
   intr_name[11] = "#NP Segment Not Present";
   intr_name[12] = "#SS Stack Fault Exception";
   intr_name[13] = "#GP General Protection Exception";
   intr_name[14] = "#PF Page-Fault Exception";
   // intr_name[15] 第15项是intel保留项,未使用
   intr_name[16] = "#MF x87 FPU Floating-Point Error";
   intr_name[17] = "#AC Alignment Check Exception";
   intr_name[18] = "#MC Machine-Check Exception";
   intr_name[19] = "#XF SIMD Floating-Point Exception";

}

/*获取当前中断状态*/
enum intr_status intr_get_status(void)
{
   uint32_t eflags=0;
   //获取eflags寄存器中的值,存到eflags
   GET_EFLAGS(eflags);
   return (eflags & EFLAGS_IF)?INTR_ON:INTR_OFF;
}

/*开中断并返回中断前的状态*/
enum intr_status intr_enable(void)
{
   enum intr_status old_status;
   if(intr_get_status()==INTR_ON)
   {
      old_status=INTR_ON;
      return old_status;
   }else{
      old_status=INTR_OFF;
      asm volatile("sti");//开中断,sti命令将if位置为1
      return old_status;
   }
}

/*关中断,并返回关中断前的状态*/
enum intr_status intr_disable(void)
{
   enum intr_status old_status;
   if(intr_get_status()==INTR_ON)
   {
      old_status=INTR_ON;
      asm volatile("cli":::"memory");//关中断,cli命令将if位置为0
      return old_status;
   }else{
      old_status=INTR_OFF;
      return old_status;
   }
}

/*设置中断状态*/
enum intr_status intr_set_status(enum intr_status status)
{
   return (status & INTR_ON)?intr_enable():intr_disable();
}


/*完成有关中断的所有初始化工作*/
void idt_init(void) {
   put_str("idt_init start\n");
   idt_desc_init();	                        //完成中段描述符表的构建
   pic_init();		                           //设定化中断控制器,只接受来自时钟中断的信号
   exception_init();

   /* 加载idt */
   uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));    //定义要加载到IDTR寄存器中的值
   asm volatile("lidt %0" : : "m" (idt_operand));
   put_str("idt_init done\n");
}

debug.h

以下是断言函数的实现代码

cpp 复制代码
#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H

void panic_spin(char* filename,int line,const char* func,const char* condition);
#define PANIC(...) panic_spin(__FILE__,__LINE__,__func__,__VA_ARGS__)

/*
如果定义了NDEBUG,那么下面定义的ASSERT就是个空。
这样我们可以便捷的让所有ASSERT宏失效。
因为有时候断言太多,程序会运行很慢。
我们如果不想要ASSERT起作用,编译时用gcc-DNDEBUG就行了
*/
#ifdef NDEBUG
    #define ASSERT(CONDITION) ((void)0)
#else
#define ASSERT(CONDITION) \
    if(CONDITION){} \
    else {PANIC(#CONDITION);}//加#后,传入的参数变成字符串
#endif//#ifdef NDEBUG

#endif//#define __KERNEL_DEBUG_H

debug,c

cpp 复制代码
#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!!!!!error!!!!!\n");
    put_str("filename:");put_str(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);
}

print.h

这里在原来实现打印单个字符和打印字符串函数的基础上增加了打印整数的函数

cpp 复制代码
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"                 //我们的stdint.h中定义了数据类型,包含进来
void put_char(uint8_t char_asci);   //在stdint.h中uint8_t得到了定义,就是unsigned char
void put_str(char* message);
void put_int(uint32_t num);	        // 以16进制打印
#endif

print.S

cpp 复制代码
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
global put_str

put_str:
    push ebx
    push ecx
    xor ecx,ecx
    mov ebx,[esp+12]                ;ebx存放字符串首地址
.goon:
    mov cl,[ebx]                    ;取出字符串的第一个字符,存放到cx寄存器中
    cmp cl,0                        ;判断是否到了字符串结尾,字符串结尾标志符'\0'即0
    jz .str_over
    push ecx                        ;压入put_char参数,调用put_char函数
    call put_char
    add esp,4                       ;回收栈空间
    inc ebx                         ;指向字符串的下一个字符
    jmp .goon
.str_over:
    pop ecx                         ;回收栈空间
    pop ebx                         ;回收栈空间
    ret


;--------------------   单字符打印函数   -----------------------
[bits 32]
section .text
global put_char                     ;通过global关键字将put_char函数定义为全局符号
                                    ;使其对外部文件可见
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
put_char:
    pushad                          ;push all double,压入所有双字长的寄存器
                                    ;入栈顺序为eax->ecx->edx->ebx->esp->ebp->esi->edi

    mov ax,SELECTOR_VIDEO
    mov gs,ax                       ;为gs寄存器赋予显存段的选择子

;以下代码用于获取光标的坐标位置,光标的坐标位置存放在光标坐标寄存器中
;其中索引为0eh的寄存器和索引为0fh的寄存器分别存放光标高8位和低8位
;访问CRT controller寄存器组的寄存器,需要先往端口地址为0x03d4的寄存器写入索引
;从端口地址为0x03d5的数据寄存器中读写数据
    mov dx,0x03d4                   ;将0x03d4的端口写入dx寄存器中
    mov al,0x0e                     ;将需要的索引值写入al寄存器中
    out dx,al                       ;向0x03d4端口写入0x0e索引
    mov dx,0x03d5                   
    in al,dx                        ;从0x03d5端口处获取光标高8位
    mov ah,al                       ;ax寄存器用于存放光标坐标,
                                    ;因此将光标坐标的高8位数据存放到ah中

;同上,以下代码获取光标坐标的低8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    in al,dx                        ;此时ax中就存放着读取到的光标坐标值
    mov bx,ax                       ;bx寄存器不仅是光标坐标值,同时也是下一个可打印字符的位置
                                    ;而我们习惯于bx作为基址寄存器,以后的处理都要基于bx寄存器
                                    ;因此才将获取到的光标坐标值赋值为bx
                                    

    mov ecx,[esp+36]                ;前边已经压入了8个双字(2个字节)的寄存器,
                                    ;加上put_char函数的4字节返回地址
                                    ;所以待打印的字符在栈顶偏移36字节的位置

    cmp cl,0xd                      ;回车符处理
    jz .is_carriage_return

    cmp cl,0xa                      ;换行符处理
    jz .is_line_feed

    cmp cl,0x8                      ;退格键处理
    jz .is_backspace                
    jmp .put_other                  ;正常字符处理

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;退格键处理
    ;处理思路:
        ;1.将光标位置减一
        ;2.将待删除的字符使用空格字符(ASCII:0x20)代替
.is_backspace:
    dec bx                          ;bx中存储光标的坐标位置,将光标坐标位置减去一,即模拟退格
    shl bx,1                        ;由于文本模式下一个字符占用两个字节(第一个字节表示字符的ASCII码,第二个字节表示字符的属性),
                                    ;故光标位置乘以2就是光标处字符的第一个字节的偏移量
    mov byte[gs:bx],0x20            ;将空格键存入待删除字符处
    inc bx                          ;此时bx中存储的是字待删除字符的第一个字节位置,
                                    ;使用inc指令将bx加1后就是该字符的第二个字节的位置
    mov byte[gs:bx],0x07            ;将黑底白字(0x07)属性加入到该字符处
    shr bx,1                        ;bx除以2,恢复光标坐标位置
    jmp .set_cursor                 ;去设置光标位置, 这样光标位置才能真正在视觉上更新

;将cx指向的字符放入到光标处
.put_other:
    shl bx,1                        ;将光标坐标转换为内存偏移量
    mov byte[gs:bx],cl              ;将cx指向的字符放入到光标处
    inc bx                          ;bx指向当前字符的下一个字节处,存放当前字符属性
    mov byte[gs:bx],0x07            ;存放字符属性
    shr bx,1                        ;将内存偏移量恢复为光标坐标值
    inc bx                          ;bx指向下一个待写入字符位置
    cmp bx,2000                     ;80*25=2000,判断是否字符已经写满屏了
    jl .set_cursor                  ;更新光标坐标值


;换行处理
    ;思路:首先将光标移动到本行行首,之后再将光标移动到下一行行首
.is_line_feed:
.is_carriage_return:
    xor dx,dx
    ;将光标移动到本行行首
    mov ax,bx
    mov si,80
    div si                          ;除法操作,ax/si,结果ax存储商,dx存储余数
    sub bx,dx

    .is_carriage_return_end:
        add bx,80                   ;将光标移动到下一行行首
        cmp bx,2000
    .is_line_feed_end:
        jl .set_cursor

;滚屏处理
    ;思路:屏幕行范围是0~24,滚屏的原理是将屏幕的1~24行搬运到0~23行,再将第24行用空格填充
.roll_screen:
    cld                             ;清除方向标志位,字符串的内存移动地址从低地址向高地址移动
                                    ;若方向标志位被设置,则字符串的内存移动地址从高地址向低地址移动
    mov ecx,960                     ;共移动2000-80=192个字符,每个字符占2个字节,故共需移动1920*2=3840个字节
                                    ;movsd指令每次移动4个字节,故共需执行该指令3840/4=960次数
    mov esi,0xb80a0                 ;第一行行首地址,要复制的起始地址
    mov edi,0xb8000                 ;第0行行首地址,要复制的目的地址
    rep movsd                       ;rep(repeat)指令,重复执行movsd指令,执行的次数在ecx寄存器中

    ;将最后一行填充为空白
    mov ebx,3840
    mov ecx,80
    .cls:
        mov word[gs:ebx],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


;--------------------   将小端字节序的数字变成对应的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字节,现在eax中就是要显示的32位数值
   mov edx, eax                                             ;ebx中现在是要显示的32位数值
   mov edi, 7                                               ; 指定在put_int_buffer中初始的偏移量,也就是把栈中第一个字节取出放入buffer最后一个位置,第二个字节放入buff倒数第二个位置
   mov ecx, 8			                                    ; 32位数字中,16进制数字的位数是8个
   mov ebx, put_int_buffer                                  ;ebx现在存储的是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:
   mov [ebx+edi], dl		                                ; 此时dl中是数字对应的字符的ascii码
   dec edi                                                  ;edi是表示在buffer中存储的偏移,现在向前移动
   shr eax, 4                                               ;eax中是完整存储了这个32位数值,现在右移4位,处理下一个4位二进制表示的16进制数字
   mov edx, eax                                             ;把eax中的值送入edx,让ebx去处理
   loop .16based_4bits

                                                            ;现在put_int_buffer中已全是字符,打印之前,
                                                            ;把高位连续的字符去掉,比如把字符00000123变成123
.ready_to_print:
   inc edi			                                        ; 此时edi退减为-1(0xffffffff),加1使其为0
.skip_prefix_0:                                             ;跳过前缀的连续多个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                                                ;当edi=8时,虽然不会去打印,但是实际上已经越界访问缓冲区了
   jl .put_each_num
   popad
   ret

main.c

为了测试断言函数是否能正确输出错误信息,我们在主函数中添加以下代码

bash 复制代码
ASSERT(1==2);

很显然,1是不等于2的,因此如果断言函数能够正确执行,它应该报出这个错误

cpp 复制代码
#include "print.h"
#include "init.h"
#include "debug.h"
int main(void)
{
    put_str("I am kernel\n");
    init_all();
    // asm volatile("sti"); // 为演示中断处理,在此临时开中断
    ASSERT(1==2);
    while(1);
    return 0;
}

编译运行

makefile

cpp 复制代码
BUILD_DIR=./build
# 程序的入口地址
ENTRY_POINT=0xc0001500
HD60M_PATH=/home/minios/bochs/hd60M.img
AS=nasm
CC=gcc-4.4
LD=ld
LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/
ASFLAGS= -f elf

# -Wall:warning all的意思,产生尽可能多警告信息
# -fno-builtin:不要采用内部函数
# -W:会显示警告,但是只显示编译器认为会出现错误的警告
# -Wstrict-prototypes 要求函数声明必须有参数类型,否则发出警告
# -Wmissing-prototypes 必须要有函数声明,否则发出警告
CFLAGS= -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32
#-Map,生成map文件,就是通过编译器编译之后,生成的程序、数据及IO空间信息的一种映射文件
#里面包含函数大小,入口地址等一些重要信息
LDFLAGS= -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map -m elf_i386

OBJS=$(BUILD_DIR)/main.o $(BUILD_DIR)/init.o \
	$(BUILD_DIR)/interrupt.o $(BUILD_DIR)/kernel.o \
	$(BUILD_DIR)/print.o $(BUILD_DIR)/debug.o

######################编译两个启动文件的代码#####################################
boot:$(BUILD_DIR)/mbr.o $(BUILD_DIR)/loader.o
$(BUILD_DIR)/mbr.o:boot/mbr.S
	$(AS) boot/mbr.S -o build/mbr.o -I boot/include/
$(BUILD_DIR)/loader.o:boot/loader.S
	$(AS) boot/loader.S -o build/loader.o -I boot/include/

######################编译C内核代码###################################################

# $@表示规则中目标文件名的集合这里就是$(BUILD_DIR)/main.o
# $<表示规则中依赖文件的第一个,这里就是kernle/main.c 
$(BUILD_DIR)/main.o:kernel/main.c
	$(CC) $(CFLAGS) -o $@ $<

$(BUILD_DIR)/init.o:kernel/init.c
	$(CC) $(CFLAGS) -o $@ $<

$(BUILD_DIR)/interrupt.o:kernel/interrupt.c
	$(CC) $(CFLAGS) -o $@ $<

$(BUILD_DIR)/debug.o:kernel/debug.c
	$(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 build all boot	#定义了6个伪目标
mk_dir:
#判断build文件夹是否存在,如果不存在,则创建
	if [ ! -d $(BUILD_DIR) ];then mkdir $(BUILD_DIR);fi 
hd:
	dd if=build/mbr.o of=$(HD60M_PATH) count=1 bs=512 conv=notrunc && \
	dd if=build/loader.o of=$(HD60M_PATH) count=4 bs=512 seek=2 conv=notrunc && \
	dd if=$(BUILD_DIR)/kernel.bin of=$(HD60M_PATH) bs=512 count=200 seek=9 conv=notrunc

#-f, --force忽略不存在的文件,从不给出提示,执行make clean就会删除build下所有文件
clean:
	@cd $(BUILD_DIR) && rm -f ./* && echo "remove ./build all done"

#执行build需要依赖kernel.bin,但是一开始没有,就会递归执行之前写好的语句编译kernel.bin
build:$(BUILD_DIR)/kernel.bin

#make all 就是依次执行mk_dir build hd
all:mk_dir boot build hd

在makefile文件所在的位置处执行

cpp 复制代码
make all

屏幕会打印编译信息

cpp 复制代码
if [ ! -d ./build ];then mkdir ./build;fi
nasm boot/mbr.S -o build/mbr.o -I boot/include/
nasm boot/loader.S -o build/loader.o -I boot/include/
gcc-4.4 -Wall -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32 -o build/main.o kernel/main.c
gcc-4.4 -Wall -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32 -o build/init.o kernel/init.c
gcc-4.4 -Wall -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32 -o build/interrupt.o kernel/interrupt.c
nasm -f elf -obuild/kernel.o kernel/kernel.S
nasm -f elf -obuild/print.o lib/kernel/print.S
gcc-4.4 -Wall -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32 -o build/debug.o kernel/debug.c
ld -Ttext 0xc0001500 -e main -Map ./build/kernel.map -m elf_i386 -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/kernel.o build/print.o build/debug.o
dd if=build/mbr.o of=/home/minios/bochs/hd60M.img count=1 bs=512 conv=notrunc && \
dd if=build/loader.o of=/home/minios/bochs/hd60M.img count=4 bs=512 seek=2 conv=notrunc && \
dd if=./build/kernel.bin of=/home/minios/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
1+0 records in
1+0 records out
512 bytes copied, 0.000430623 s, 1.2 MB/s
2+1 records in
2+1 records out
1290 bytes (1.3 kB, 1.3 KiB) copied, 0.00081601 s, 1.6 MB/s
15+1 records in
15+1 records out
7848 bytes (7.8 kB, 7.7 KiB) copied, 0.00122429 s, 6.4 MB/s

如果有警告,则进行修改即可

如笔者曾遇到下述警告,原因是函数声明没有参数列表的类型信息

bash 复制代码
 warning: function declaration isn't a prototype

假设你有一个函数声明如下面这样:

cpp 复制代码
int myFunction();

这种声明方式可能会导致编译器发出警告,因为它没有指定参数类型。正确的做法是明确指出参数类型,即使它没有参数,也要使用 void

cpp 复制代码
int myFunction(void);

运行

bash 复制代码
 ./bin/bochs -f boot.disk
相关推荐
OpenAnolis小助手1 天前
正式邀测! OS Copilot——一款基于大模型构建的 Linux 智能操作系统助手
运维·操作系统·龙蜥社区·智能操作系统助手·os copilot
openEuler社区2 天前
openEuler 社区 2024 年 5 月运作报告
开源·操作系统·openeuler
葟雪儿3 天前
Linux 常用命令汇总
linux·操作系统·终端命令
岁岁岁平安3 天前
os实训课程模拟考试(大题复习)
linux·笔记·操作系统·头歌
Adward.xi5 天前
操作系统真象还原:进一步完善内核
操作系统·系统调用·内存分配·内存释放·arean
Lorin洛林5 天前
什么是协程?协程和线程的区别
后端·操作系统
我要成为C++领域大神6 天前
【Linux】线程Thread
linux·服务器·c语言·开发语言·ubuntu·操作系统·多进程
我要成为C++领域大神7 天前
【高性能服务器】服务器概述
linux·服务器·c语言·ubuntu·操作系统·epoll·多进程
算法小白(真小白)9 天前
操作系统——考研笔记(一)操作系统概述
笔记·学习·考研·操作系统
爱桥代码的程序媛10 天前
鸿蒙开发系统基础能力:【@ohos.screenLock (锁屏管理)】
操作系统·移动开发·harmonyos·鸿蒙·鸿蒙系统·openharmony·鸿蒙开发