配合视频学习效果更佳!
https://www.bilibili.com/video/BV1cN41117cq/?vd_source=701807c4f8684b13e922d0a8b116af31
https://www.bilibili.com/video/BV12X4y1h76c/?vd_source=701807c4f8684b13e922d0a8b116af31
https://www.bilibili.com/video/BV12z4y1B7WW/?vd_source=701807c4f8684b13e922d0a8b116af31
代码仓库地址:https://github.com/xukanshan/the_truth_of_operationg_system
C语言使用cdecl调用标准,如下图:
接下来我们实现一个简单的打印函数,不过为了开发方便,我们先要定义一些数据类型 (myos/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
p266剖析print.s代码: (myos/lib/kernel/print.S)
1、代码功能
写一个实现打印功能的汇编代码编译进入内核,来实现我们常见的显示字符功能
2、实现原理(核心)
A、通过对显存段操作,我们能够在屏幕上显示字符
B、通过与显卡寄存器打交道,我们可以获得光标位置,结合原理1,能够实现我们平常见的那种在光标处显示字符,然后光标向后移动的效果。光标位置需要与VAG寄存器(显卡的寄存器)中CRT Controller Registers组中索引号为0Eh与0Fh寄存器来打交道。详见p262
3、代码逻辑
判断输入字符,对不同情况作出对应处理
4、怎么写代码?(代码完整实现的思路)
A、保存调用者的执行环境
B、加载显存段选择子(要自己定义),显示字符就是对显存进行操作
C、通过与显卡寄存器打交道,获得光标位置
D、通过栈来取出传入的参数(调用者传入的字符)
E、判断D取出的字符:
- a、回车,光标位置移置行首
- b、换行,光标位置移置下一行行首(仿照linux系统做法\r,\n,\r\n的区别 - 荷树栋 - 博客园 (cnblogs.com))
- c、退格,光标位置向前移动,并且显示一个空格(来实现我们期待的删除一个字符的功能)
- d、其他,根据当前光标位置确定显存位置,然后显示后光标后移
F、E中A,B,D都有可能造成字符超出第一页显示范围,所以我们需要实现滚屏功能(采取p270方案2)
G、恢复调用者执行环境
5、代码实现如下: myos/lib/kernel/print.S
TI_GDT equ 0 ;从这里开始三步是在定义显存段段描述符的选择子
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
[bits 32] ;采用32位编译
section .text ;表明这是个代码段
;------------------------ put_char -----------------------------
;功能描述:把栈中的1个字符写入光标所在处
;-------------------------------------------------------------------
global put_char ;将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
mov bx, ax ;现在bx中存的是光标的位置
;下面这行是在栈中获取待打印的字符
mov ecx, [esp + 36] ;pushad压入4×8=32字节,加上主调函数的返回地址4字节,故esp+36字节,现在ecx中是要打印的字符
cmp cl, 0xd ;判断是否是CR(回车)0x0d
jz .is_carriage_return
cmp cl, 0xa ;判断是否是LF(换行)0x0a
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 ;光标位置-1, 以符合我们的常识认知, 即按下退格符, 光标回退
shl bx,1 ;光标的位置就转换成了对应字符的显存位置的偏移
mov byte [gs:bx], 0x20 ;将待删除的字节补为0或空格皆可, 0x20是空格符的ascii码值
inc bx ;bx+1, 指向这个字符的属性位置, 也就是设定背景色, 字符颜色
mov byte [gs:bx], 0x07 ;0x07, 就是黑底白字
shr bx,1 ;bx虽然指向这个字符的颜色属性字节,但是除以2还是变回这个字符的光标位置
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 ;要进行16位除法,高16位置会放在dx中,要先清零
mov ax, bx ;ax是被除数的低16位.
mov si, 80 ;用si寄存器来存储除数80 由于是效仿linux,linux中\n便表示下一行的行首,所以本系统中,
div si ; 把\n和\r都处理为linux中\n的意思,也就是下一行的行首。ax/80后,ax中存商,dx中存储的是余数,汇编除法https://blog.csdn.net/loovejava/article/details/7044242
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, 0xb80a0 ; 第1行行首
mov edi, 0xb8000 ; 第0行行首
rep movsd ;rep movs word ptr es:[edi], word ptr ds:[esi] 简写为: rep movsw
;将最后一行填充为空白
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
6、其他代码详解查看书p267
为了方便其他函数调用我们写的print,所以我们为其建立一个头文件 myos/lib/kernel/print.h
#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
#endif
接下来验证我们的打印函数是否能正常工作,编写一个内核文件 myos/kernel/main.c
#include "print.h"
void main(void)
{
put_char('k');
put_char('e');
put_char('r');
put_char('n');
put_char('e');
put_char('l');
put_char('\n');
put_char('1');
put_char('2');
put_char('\b');
put_char('3');
while(1);
}
编译print.s nasm -o /home/rlk/Desktop/print.o -f elf /home/rlk/Desktop/the_truth_of_operationg_system/chapter_6/a/lib/kernel/print.S
-f参数是指定编译成为elf文件格式
编译main.c gcc-4.4 -o /home/rlk/Desktop/main.o -c -m32 -I /home/rlk/Desktop/the_truth_of_operationg_system/chapter_6/a/lib/kernel/ /home/rlk/Desktop/the_truth_of_operationg_system/chapter_6/a/kernel/main.c
-I(大写的i)参数就是用来指定程序要链接的库,-c编译但是不链接
链接main.o与print.o ld -o /home/rlk/Desktop/kernel.bin -m elf_i386 -Ttext 0xc0001500 -e main /home/rlk/Desktop/main.o /home/rlk/Desktop/print.o
写入kernel.bin dd if=/home/rlk/Desktop/kernel.bin of=/home/rlk/Desktop/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9
接下来我们编写打印字符串的函数
p276与p277剖析代码:
1、代码功能
打印字符串
2、实现原理
C语言我们定义字符串时,会为字符串最后加上ascii 为0的字符表示字符串的结尾,我们可以通过传入字符串首地址,然后通过字符串首地址取出字符,不断判断这是不是ascii码为0的字符来判断是不是结尾,如果不是就打印字符
3、代码逻辑
打印字符串中每个字符,到结尾就结束
4、怎么写代码?(1、在print.h中加上我们的函数申明,这样写包含头文件就能引用我们写的函数;2、在print.S中加入我们打印字符串的代码)
A、保存执行环境
B、从栈中取出调用者传入的字符串首字符地址
C、判断B取出的首地址,取出字符串字符,判断是不是ascii码为0的字符,如果不是就调用之前写好的打印字符的函数,如果是就结束
D、恢复执行环境
5、代码实现如下:
print.s中加入如下代码: myos/lib/kernel/print.S
[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] ;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
6、其他代码详解查看书p276
print.h中加入如下代码: myos/lib/kernel/print.h
void put_str(char* messags);
写一个新的内核来验证一下: myos/kernel/main.c
#include "print.h"
void main(void) {
put_str("I am kernel\n");
while(1);
}
分别编译main.c,pirnt.s,然后链接,最后写入,运行成功!
接下来我们来实现打印整数的功能,比如对于值0x00123,那么屏幕上就打印123(十六进制含义)
p277剖析代码:
1、代码功能
将一个值转换成字符显示出来
2、实现原理
数值的9需要转换成字符9对应的ASCII码值,才能用于显示。用待转换的数值减去各自的起始数字(如0或10)获得其对应字符相对于0字符或A字符的偏移量,再用此偏移量加上对应字符所在类别的起始字符ASCII码(如0或A的ascii码值),就是该数字对应的字符的ascii码值(实例见p279)
3、代码逻辑
将一个32位的值,从最低处开始,按照4位一组处理(因为每4位对应16进制的一位)成对应字符,倒着放入缓冲区。处理好前缀之后(出现连续的0需要跳过),一个一个打印出来
4、怎么写代码?
A、定义一个8字节的缓冲区,8字节是因为一个完整的32位值每次取四位转换成一个字符(1字节),需要8次。
B、保存执行现场
C、循环取32位值每4位,从最低4位开始取,转换成对应字符的ASCII码值,然后倒着存在缓冲区中
D、对于C的结果,不显示转换后从高位开始连续的字符0,如00123,显示123;如果是全0,则需要只显示一个0
E、恢复执行现场
5、代码实现如下:
print.s加入如下代码 myos/lib/kernel/print.S
section .data
put_int_buffer dq 0 ; 定义8字节缓冲区用于数字到字符的转换
global put_int
put_int:
pushad
mov ebp, esp
mov eax, [ebp+4*9] ; call的返回地址占4字节+pushad的8个4字节,现在eax中就是要显示的32位数值
mov edx, eax ;edx中现在是要显示的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
6、其他代码详解查看书p278
print.h中加入函数申明 myos/lib/kernel/print.h
void put_int(uint32_t num); // 以16进制打印
编写main.c代码来检验 myos/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);
}
他代码详解查看书p278**
print.h中加入函数申明 myos/lib/kernel/print.h
void put_int(uint32_t num); // 以16进制打印
编写main.c代码来检验 myos/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);
}