《操作系统真象还原》第六章 完善内核

第六章 完善内核

本文是对《操作系统真象还原》第六章学习的笔记,欢迎大家一起交流。

本章的主要任务是实现字符打印、字符串打印以及整数打印。

知识部分

调用约束

首先了解下两种常见的调用约束,cdecl 和 stdcall:

  • cdecl 参数从右至左入栈,调用者负责清理堆栈
  • stdcall 参数也是从右至左入栈,但是是被调用者清理堆栈

显卡的端口控制

image

对这类分组的寄存器操作方法是先在 Address Register 中指定寄存器的索引值,用来确定所操作的寄存器是哪个,然后在 Data Register 寄存器中对所索引的寄存器进行读写操作。

我们主要用的是 Controller Data Registers 中索引为 0Eh 的 Cursor Location High Register 寄存器和索引为 0Fh 的 Cursor Location Low Register 寄存器,这两个寄存器都是 8 位长度,分别用来存储光标坐标的低 8 位和高 8 位地址。访问 CRT controller 寄存器组的寄存器,需要先往端口地址为 0x3D4 的 Address Register 寄存器中写入寄存器的索引,再从端口地址为 0x3D5 的 Data Register 寄存器读、写数据。

代码部分

首先看 /lib/kernel/print.s ​的新代码,代码有点多,但主要是三个函数,分别用来打印字符、字符串和整数,其中打印字符是其他两个的前提。

put_char

TI_GDT equ  0
RPL0  equ   0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

[bits 32]
section .text
;------------------------   put_char   -----------------------------
;功能描述:把栈中的1个字符写入光标所在处
;-------------------------------------------------------------------   
global put_char ;导出函数
put_char:
    pushad	   ;备份32位寄存器环境,8个寄存器全部备份
    ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印时都为gs赋值
    mov ax, SELECTOR_VIDEO	       ; 不能直接把立即数送入段寄存器
    mov gs, ax

;;;;;;;;;  获取当前光标位置 ;;;;;;;;;
   ;先获得高8位
    mov dx, 0x3d4   ;索引寄存器
    mov al, 0x0e     ;用于提供光标位置的高8位
    out dx, al  

    mov dx, 0x3d5   ;通过读写数据端口0x3d5来获得或设置光标位置 
    in al, dx       ;得到了光标位置的高8位
    mov ah, al      ;高八位放到ah寄存器

    ;再获得低8位
    mov dx, 0x3d4   ;索引寄存器
    mov al, 0x0f     ;用于提供光标位置的高8位
    out dx, al  

    mov dx, 0x3d5   ;通过读写数据端口0x3d5来获得或设置光标位置 
    in al, dx       ;得到了光标位置的高8位

    ;将光标存入bx
    mov bx, ax
    ;下面这行是在栈中获取待打印的字符
    mov ecx, [esp + 36]   ;八个32位寄存器+1个32位返回地址 4*9=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                   ; 光标位置是用2字节表示,将光标值乘2,表示对应显存中的偏移字节
    mov byte [gs:bx], 0x20      ; 将待删除的字节补为0或空格皆可
    inc bx
    mov byte [gs:bx], 0x07      ; 高字节属性
    shr bx,1                    ; 光标位置
    jmp .set_cursor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

 .put_other:
    shl bx, 1
    mov [gs:bx], cl     ; 字符存在ecx, 但是只需cl就可以表示
    inc bx
    mov byte [gs:bx], 0x07      ; 高字节属性
    shr bx, 1			; 恢复老的光标值
    inc bx				; 下一个光标值
    cmp bx, 2000		   
    jl .set_cursor		; 显示是80*25的 
                        ; 若光标值小于2000,表示未写到显存的最后,则去设置新的光标值
                        ; 若超出屏幕字符数大小(2000)则换行处理

.is_line_feed:		    ; 是换行符LF(\n)
.is_carriage_return:	; 是回车符CR(\r)
    					; 这两种都是把光标移到下一行行首就行了。
    xor dx, dx
    mov ax, bx				  ; ax是被除数的低16位.
    mov si, 80				  ; 一行80个字节
    div si
    sub bx, dx				  ; 光标值减去除80的余数便是取整

    add bx, 80                ; 换行
    cmp bx, 2000              ; 判断是否滚屏
    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, 0xb80a0			  ; 第1行行首
    mov edi, 0xb8000			  ; 第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, 0x3d4   ;索引寄存器
    mov al, 0x0e     ;用于提供光标位置的高8位
    out dx, al  

    mov dx, 0x3d5   ;通过读写数据端口0x3d5来获得或设置光标位置 
    mov al, bh      ;高八位给到al
    out dx, al      ;写过去


;;;;;;;再设置低8位 ;;;;;;;;
    mov dx, 0x3d4   ;索引寄存器
    mov al, 0x0f    ;用于提供光标位置的高8位
    out dx, al  

    mov dx, 0x3d5   ;通过读写数据端口0x3d5来获得或设置光标位置 
    mov al, bl      ;低八位给到al
    out dx, al      ;写过去
;结束
.put_char_done: 
   popad
   ret
  • 1-3 行先定义显存段选择子
  • 10 行用 global 关键词导出函数
  • 12 行 pushad ​会一次性将 8 个寄存器备份起来
  • 19-36 行通过上面说的通过端口获取光标的方式,拿到光标位置,并且把位置存到 ebx 中
  • 38-46 行先拿到待打印字符,然后判断字符类型分类处理
  • 49-61 行是打印退格符,就是先退格,然后再在对应的位置输出一个空白符,不要忘记一个字符的显示需要两字节的配合
  • 63-71 行是输出正常字符,直接打印即可,最后做了一个是否换行的判断,我们的屏幕是 80*25 ​的,所以和 2000 判断,然后进行换行/滚屏操作
  • 75-86 行是处理换行/回车符,这两种符号我们的操作都是先取整光标,在到下一行,在最后判断是否需要滚屏
  • 89-105 是处理滚屏,我们这里的操作时第 1-24 行搬运到 0-23 行,然后最后一行填充空白即可,还记得之前的 memcpy 函数吗,这里的处理与其类似。
  • 107-126 行设置新的光标值,这里和获取光标值是反过来的,也很好理解

put_str

global put_str
put_str:
    push ebx
    push ecx
    xor ecx, ecx
    mov ebx, [esp + 12]     ;3*4=12,获取参数地址
.goon:
    mov cl, [ebx]       ;获取字符
    cmp cl, 0           ;与结束符对比
    jz .str_over
    push ecx
    call put_char
    add esp, 4          ;平栈
    inc ebx             ;下一个字符
    jmp .goon
.str_over:
    pop ecx
    pop ebx
    ret

  • put_str 函数更简单,简单分析一下即可
  • 3-6 行先备份寄存器,获取字符串地址
  • 7-15 行先去内存中获取字符然后与结束符对比,如果是结束符则跳转至 str_over,否则调用 put_char 进行打印,然后循环
  • 16-19 行恢复寄存器,返回

put_int

section .data
put_int_buffer    dq    0     ; 定义8字节缓冲区用于数字到字符的转换

;--------------------   将小端字节序的数字变成对应的ascii后,倒置   -----------------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x,如打印10进制15时,只会直接打印f,不会是0xf
;------------------------------------------------------------------------------------------
global put_int
put_int:
    pushad
    mov ebp, esp
    mov eax, [ebp + 9*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
    dec edi			       ;edi在上面的inc操作中指向了下一个字符,若当前字符不为'0',要恢复edi指向当前字符		   
    jmp .put_each_num

.full0:
    mov cl,'0'	    ; 输入的数字为全0时,则只打印0,并且此时edi为8
.put_each_num:
    push ecx        ; 此时cl中为打印字符
    call put_char
    add esp, 4
    inc edi         ; 指向下一个
    mov cl, [put_int_buffer+edi]
    cmp edi, 8
    jl .put_each_num

    popad
    ret

大体思路如下:由于在内存中按小端存放,但是要按照大端展示,所以在原来的中从前往后处理,展示时要从后往前写,处理完所有的字符之后,还要再看高位的 0,不予展示,如果全 0 则输出 1 个'0'即可

  • 9-16行做一些准备工作,edi给7,展示时是从后往前的处理的,ecx给8,代表一共8个待处理的十六进制数

  • 19-37行进行处理数字

    • 20行取出来edx的低四位,这也是我们这次处理的目标
    • 21-27行则判断edx的范围,是在0-9还是A-F,转为ASCII码时要进行不同的处理
    • 33行将ASCII码存储
    • 34行将edi的值减1,指向下一个要处理的十六进制数
    • 35行将eax右移四位,得到下一个要处理的
    • 36-37行将eax的值再次给到edx,然后循环处理
  • 41-56行是对高位0的处理,当遇到0时就将edi++,这样就跳过了高位的0,如果edi等于8了,证明全都是0,则将‘0’送到cl中,此时只会打印一次0

  • 57-64行就是循环打印数字

  • 66-67行返回即可

其他改动以及结果

lib/stdint.h

#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;
#endif

lib/kernel/print.h

#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "../stdint.h"
void put_char(uint8_t char_asci);
void put_str(char* message);
void put_int(uint32_t num);	 // 以16进制打印
#endif

kernel/main.c

#include "print.h"
void main(void) {
   put_str("I am kernel\n");
   put_int(0);
   put_char('\n');
   put_int(9);
   put_char('\n');
   put_int(0x00021a3f);
   put_char('\n');
   put_int(0x12345678);
   put_char('\n');
   put_int(0x00000000);
   while(1);
}

编译脚本

#!/bin/sh
nasm -I ./boot/include/ -o ./boot/mbr.bin ./boot/mbr.s
dd if=./boot/mbr.bin of=../../hd60M.img bs=512 count=1 conv=notrunc

nasm -I ./boot/include/ -o ./boot/loader.bin ./boot/loader.s
dd if=./boot/loader.bin of=../../hd60M.img bs=512 count=4 seek=2  conv=notrunc


nasm -f elf -o lib/kernel/print.o lib/kernel/print.s  
gcc-4.4 -I ./lib/kernel -c -o ./kernel/main.o ./kernel/main.c -m32 
ld ./kernel/main.o ./lib/kernel/print.o -Ttext 0xc0001500 -e main -o ./kernel/kernel.bin -m elf_i386
dd if=./kernel/kernel.bin of=../../hd60M.img bs=512 count=200 seek=9  conv=notrunc

结果

image

posted @ 2025-01-12 20:17  fdx_xdf  阅读(59)  评论(0)    收藏  举报