第六章 完善内核
本文是对《操作系统真象还原》第六章学习的笔记,欢迎大家一起交流。
本章的主要任务是实现字符打印、字符串打印以及整数打印。
知识部分
调用约束
首先了解下两种常见的调用约束,cdecl 和 stdcall:
- cdecl 参数从右至左入栈,调用者负责清理堆栈
- stdcall 参数也是从右至左入栈,但是是被调用者清理堆栈
显卡的端口控制
对这类分组的寄存器操作方法是先在 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
结果
标签:字符,完善,打印,kernel,int,edi,put,内核,第六章 From: https://www.cnblogs.com/fdxsec/p/18667223