首页 > 其他分享 >第六章 完善内核

第六章 完善内核

时间:2025-01-12 20:23:42浏览次数:1  
标签:字符 完善 打印 kernel int edi put 内核 第六章

第六章 完善内核

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

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

知识部分

调用约束

首先了解下两种常见的调用约束,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

标签:字符,完善,打印,kernel,int,edi,put,内核,第六章
From: https://www.cnblogs.com/fdxsec/p/18667223

相关文章

  • 常见的浏览器内核都有哪些?并介绍下你对内核的理解
    常见的浏览器内核主要包括以下几种:Trident(IE内核):由微软开发,也被称作IE内核。它是最早的浏览器内核之一,具有较好的兼容性,能够支持各种网站和网页标准。然而,其渲染速度相对较慢,页面渲染效果可能不如其他内核。尽管如此,由于其广泛的用户基础和成熟的接口设计,仍有许多浏览器选择使用......
  • Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
    引言在嵌入式Linux开发中,网络加载内核、设备树和根文件系统是一种常见的方法。这种方法通常用于开发和调试阶段,允许开发者快速更新和测试内核及文件系统。本文将详细介绍如何在Ubuntu20.04上搭建环境,以支持嵌入式Linux的网络加载。前提条件Ubuntu20.04系统。已安装的TFTP服......
  • 《Linux设备驱动开发详解(第3版)》 第9章 Linux内核定时器与工作队列
    9.1内核定时器内核定时器用于在指定的延迟时间后执行特定的函数。它在内核中常用于实现周期性任务或延迟执行的任务。#include<linux/module.h>#include<linux/kernel.h>#include<linux/timer.h>//定义一个内核定时器structtimer_listmy_timer;//定时器到......
  • 第五章 保护模式进阶,向内核迈进
    第五章保护模式进阶,向内核迈进本文是对《操作系统真象还原》第五章学习的笔记,欢迎大家一起交流。a获取物理内存知识部分为了在后期做好内存管理工作,咱们先得知道自己有多少物理内存才行。所以现在的工作是为了获取物理内存,一共介绍三种方法,都是利用的BIOS0x15中断,三种方......
  • 【Windows内核】Ntdll解除挂钩学习
    简介我们之前都是直接通过使用直接系统调用的方式来绕过用户态钩子,通过在项目文件中创建并调用系统调用来实现此目标。还有另外一种方法也可以绕过用户态的钩子,那么这种方法是将已经加载到进程中的钩子DLL替换为一个未经修改且未被钩主的版本来达到相同的目标。将勾住的DLL替换......
  • CC430F5137IRGZR数据手册 CC430F613x、CC430F612x、CC430F513一款低功耗射频无线收发
    TICC430系列超低功耗片上系统(SoC)器微控制搭载有集成射频收发器内核,并包含数个采用各种不同外设集的器件,可广泛应用于各种应用。此架构经过优化,与五种低功耗模式相配合使用,可延长便携式测量应用中的电池寿命。该器件具有功能强大的MSP43016位RISCCPU、16位寄存器和有......
  • 征程 6X release版本内核模块安全加载
    1.概述征程6X系统在release编译时支持内核模块签名验证,仅加载使用正确密钥进行数字签名的内核模块。禁止加载未签名的内核模块或使用错误密钥签名的内核模块,客户需要替换成自己的key进行签名。模块签名启用后,Linux内核将仅加载使用正确密钥进行数字签名的内核模块。禁止......
  • 编译Ubuntu 24.04 LTS 内核(BuildYourOwnKernel)
    1、配置环境修改apt源修改/etc/apt/sources.list.d/ubuntu.sources,添加"deb-src"到Types:,修改后的文件内容如下:Types:debdeb-srcURIs:http://cn.archive.ubuntu.com/ubuntu/Suites:noblenoble-updatesnoble-backportsComponents:mainrestricteduniversemult......
  • IPoIB模块初始化:深入解析Linux内核模块的初始化过程
    在Linux内核中,模块初始化是确保模块能够正确加载并运行的关键步骤。IPoverInfiniBand(IPoIB)模块作为一种网络技术模块,允许通过InfiniBand网络高效传输IP数据包。本文将深入解析IPoIB模块的初始化函数,展示其如何通过一系列配置和注册步骤为模块的运行做好准备。IPoIB模块的......
  • Linux内核的固定映射:提升性能的秘密武器
    在当今数字化时代,高效稳定的Linux内核是众多技术应用的基石。你是否好奇,如何让Linux内核在复杂任务中实现卓越性能?今天,我们要揭开其提升性能的秘密武器——固定映射。它就像一位默默发力的幕后英雄,通过独特的机制,优化内核内存访问,让系统运行如丝般顺滑。下面,让我们一同走......