编程环境:Ubuntu Kylin 16.04、gcc-5.4.0
代码仓库:https://gitee.com/AprilSloan/linux0.11-project
linux0.11源码下载(不能直接编译,需进行修改)
本章目标
编写printk函数,实现它的功能。
printk函数的功能与printf相同,区别在于printk在内核态被使用,printf在用户态被使用。首先printk要把格式化字符串转换成普通字符串,如"%s"转换成它指向的字符串,把"%d"转换成数字字符串等等。接着要把字符串显示在屏幕上,这里采用写显存的方式完成。显存是内存中的一块地址空间,在这里写入可打印字符会在屏幕上显示相应的字符。
1.获取机器系统参数
在写显存之前,我们要获取一些相关参数,如显存起始地址,显存大小,显存状态(彩色/单色),特性参数等等,在setup.s中完成这个操作。一些其他的参数以后可能会用到,为了避免反复修改setup.s,也就一起获取吧。
下面是setup.s的部分代码:
INITSEG equ 0x9000
SYSSEG equ 0x1000
SETUPSEG equ 0x9020
start:
mov ax, INITSEG
mov ds, ax
mov ah, 0x03
xor bh, bh
int 0x10
mov [0], dx ; 光标位置
mov ah, 0x88
int 0x15
mov [2], ax ; 扩展内存数
mov ah, 0x0f
int 0x10
mov [4], bx ; 显示页面
mov [6], ax ; 显示模式、字符列数
mov ah, 0x12
mov bl, 0x10
int 0x10
mov [8], ax ; ??
mov [10], bx ; 显示内存、显示状态
mov [12], cx ; 特性参数
mov ax, 0x0000
mov ds, ax
lds si, [4 * 0x41] ; 取中断向量0x41的值,即hd0参数表的地址->ds:si
mov ax, INITSEG
mov es, ax
mov di, 0x0080 ; 传输的目的地址es:di(0x9000:0x0080)
mov cx, 0x10 ; 共传输0x10字节
rep
movsb
mov ax, 0x0000
mov ds, ax
lds si, [4 * 0x46] ; 取中断向量0x46的值,即hd1参数表的地址->ds:si
mov ax, INITSEG
mov es, ax
mov di, 0x0090 ; 传输的目的地址es:di(0x9000:0x0090)
mov cx, 0x10
rep
movsb
; 检查是否有第二个硬盘,不存在就把第二个表清零
mov ax, 0x1500
mov dl, 0x81
int 0x13
jc no_disk1
cmp ah, 3
je is_disk1
no_disk1:
mov ax, INITSEG
mov es, ax
mov di, 0x0090
mov cx, 0x10
mov ax, 0x00
rep
stosb
is_disk1:
cli ; 保护模式下中断机制尚未建立,应禁止中断
mov ax, 0x00
cld
do_move: ; 将内核从0x10000移动到0x00
获得光标位置等BIOS中断可以看BIOS接口技术参考手册,这段程序首先改变了数据段寄存器的值,获取的参数保存在0x90000开始的地址。这里着重讲一下检查硬盘的操作。

BIOS中断后,如果CF=1(读取出错)或AH!=3(不是硬盘),则清零第二张硬盘表。BIOS接口技术参考手册中有两个INT 0x13的章节,一个是软盘的,一个是硬盘的,上图是硬盘的内容,请不要搞错了。
读取的参数和保留的内存位置如下图。

之后我们会用C语言指针访问这些地址,使用这些参数。
2.初始化屏幕相关变量
获得了显存起始地址其实就可以开始写字符了,但这样处理还是太粗糙了,无法完成字符打印时光标的移动处理,屏幕滚动,字符显示模式变更等操作。为了实现上述功能,这节就来使用上节的参数完成屏幕的初始化。
以下是console.c的内容(这里面定义的全局变量确实有点多啊)。
#include <linux/tty.h>
#define ORIG_X (*(unsigned char *)0x90000) // 光标列号
#define ORIG_Y (*(unsigned char *)0x90001) // 光标行号
#define ORIG_VIDEO_PAGE (*(unsigned short *)0x90004) // 当前页号
#define ORIG_VIDEO_MODE (*(unsigned char *)0x90006) // 显示模式
#define ORIG_VIDEO_COLS (*(unsigned char *)0x90007) // 显示列数
#define ORIG_VIDEO_LINES (25) // 显示行数
#define ORIG_VIDEO_EGA_AX (*(unsigned short *)0x90008) // [??]
#define ORIG_VIDEO_EGA_BX (*(unsigned short *)0x9000a) // 显示内存和色彩模式
#define ORIG_VIDEO_EGA_CX (*(unsigned short *)0x9000c) // 显示卡特性参数
// 定义显示器单色/彩色显示模式类型符号常数
#define VIDEO_TYPE_MDA 0x10 // 单色文本显示
#define VIDEO_TYPE_CGA 0x11 // CGA显示
#define VIDEO_TYPE_EGAM 0x20 // EGA/VGA单色模式
#define VIDEO_TYPE_EGAC 0x21 // EGA/VGA彩色模式
static unsigned char video_type; // 显示模式
static unsigned long video_num_columns; // 显示列数
static unsigned long video_size_row; // 每行字节数
static unsigned long video_num_lines; // 显示行数
static unsigned char video_page; // 初始页面
static unsigned long video_mem_start; // 显存起始地址
static unsigned long video_mem_end; // 显存结束地址
static unsigned short video_port_reg; // 显示控制器索引寄存器端口
static unsigned short video_port_val; // 显示控制器数据寄存器端口
static unsigned short video_erase_char; // 擦除字符属性与字符(0x0720)
// 以下这些变量用于屏幕卷屏操作
static unsigned long origin; // 用于EGA/VGA快速滚屏
static unsigned long scr_end; // 用于EGA/VGA快速滚屏
static unsigned long pos; // 当前光标对应的显存地址
static unsigned long x, y; // 当前光标位置
static unsigned long top, bottom;// 滚动时顶行行号,底行行号
static unsigned char attr = 0x07;// 字符属性(黑底白字)
static inline void gotoxy(unsigned int new_x, unsigned int new_y)
{
if (new_x > video_num_columns || new_y >= video_num_lines)
return;
x = new_x;
y = new_y;
pos = origin + y * video_size_row + (x << 1);
}
void con_init(void)
{
register unsigned char a;
char *display_desc = "????";
char *display_ptr;
video_num_columns = ORIG_VIDEO_COLS;
video_size_row = video_num_columns * 2;
video_num_lines = ORIG_VIDEO_LINES;
video_page = ORIG_VIDEO_PAGE;
video_erase_char = 0x0720; // 擦除字符(0x20显示字符,0x07是属性)
if (ORIG_VIDEO_MODE == 7) // 如果为单色显示
{
video_mem_start = 0xb0000;
video_port_reg = 0x3b4;
video_port_val = 0x3b5;

本文详细介绍如何在 Linux0.11 中实现 printk 函数,包括获取机器系统参数、初始化屏幕相关变量、实现基本打印功能及光标移动、处理特殊字符等功能。
990

被折叠的 条评论
为什么被折叠?



