编程环境: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 0x9020start:mov ax, INITSEGmov ds, axmov ah, 0x03xor bh, bhint 0x10mov [0], dx         ; 光标位置mov ah, 0x88int 0x15mov [2], ax         ; 扩展内存数mov ah, 0x0fint 0x10mov [4], bx         ; 显示页面mov [6], ax         ; 显示模式、字符列数mov ah, 0x12mov bl, 0x10int 0x10mov [8], ax         ; ??mov [10], bx        ; 显示内存、显示状态mov [12], cx        ; 特性参数mov ax, 0x0000mov ds, axlds si, [4 * 0x41]  ; 取中断向量0x41的值,即hd0参数表的地址->ds:simov ax, INITSEGmov es, axmov di, 0x0080      ; 传输的目的地址es:di(0x9000:0x0080)mov cx, 0x10        ; 共传输0x10字节repmovsbmov ax, 0x0000mov ds, axlds si, [4 * 0x46]  ; 取中断向量0x46的值,即hd1参数表的地址->ds:simov ax, INITSEGmov es, axmov di, 0x0090      ; 传输的目的地址es:di(0x9000:0x0090)mov cx, 0x10repmovsb; 检查是否有第二个硬盘,不存在就把第二个表清零mov ax, 0x1500mov dl, 0x81int 0x13jc  no_disk1cmp ah, 3je  is_disk1
no_disk1:mov ax, INITSEGmov es, axmov di, 0x0090mov cx, 0x10mov ax, 0x00repstosb
is_disk1:cli                 ; 保护模式下中断机制尚未建立,应禁止中断mov ax, 0x00cld
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;if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10){video_type = VIDEO_TYPE_EGAM;video_mem_end = 0xb8000;display_desc = "EGAm";}else{video_type = VIDEO_TYPE_MDA;video_mem_end = 0xb2000;display_desc = "*MDA";}}else                                  // 如果不是,就是彩色显示{video_mem_start = 0xb8000;video_port_reg = 0x3d4;video_port_val = 0x3d5;if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10){video_type = VIDEO_TYPE_EGAC;video_mem_end = 0xbc000;display_desc = "EGAc";}else{video_type = VIDEO_TYPE_CGA;video_mem_end = 0xba000;display_desc = "*CGA";}}// 打印正在使用的显示驱动程序display_ptr = ((char *)video_mem_start) + video_size_row - 8;while (*display_desc){*display_ptr++ = *display_desc++;display_ptr++;}// 初始化用于滚屏的变量origin    = video_mem_start;scr_end  = video_mem_start + video_num_lines * video_size_row;top  = 0;bottom = video_num_lines;gotoxy(ORIG_X, ORIG_Y);
}

第3-11行用将机器系统参数地址设置为宏定义,方便阅读和修改。

第19-28行定义了一些与屏幕显示相关的参数,在初始化完成后就不再修改。

第31-35行定义了一些卷屏相关的参数。虽然屏幕只显示25行的字符,但有时我们会在显存中保存超过25行的屏幕字符信息(当top!=0或bottom!=video_num_lines时就不保存这些信息)。

为了更好地解释之后的内容,以我的模拟器得到的参数为例进行讲解。

光标列号为0,行号为20(0x14),当前页面为0,列数80(0x50,一行显示80个字符),显示内存256k,彩色显示(对照上一节图表)。

第55行video_size_row = video_num_columns * 2;的意思是显存每行有160个字符,不是每行只显示80个字符吗?这160个字符有一半是要显示的字符,另一半是字符的显示模式。比如,想在第1行第1列黑底白字显示字符A,需要在0xb8000地址写数据0x41,在0xb8001地址写数据0x07,0xb8001地址的数据就是字符A的显示模式,0x07代表黑底白字,可以通过更改这个值设置白底红字、红底蓝字等。所以,虽然每行显存中每行有160个字符,但只显示80个字符。

那第58行video_erase_char = 0x0720;这个擦除字符是什么意思?0x20是要显示的字符,0x20对应的字符是空格,0x07对应黑底白字显示方式,以黑底白色的方式显示空格那就是一个黑色块,当它覆盖其他字符时看起来就像是被擦除了一样。

第60-95行初始化相关变量。已知机器采用彩色显示,则显存起始地址为0xb8000,已知0x9000B地址的值为0,则显存结束地址为0xbc000,显存大小为16k而不是256k,这是为什么?

第98-103行会在屏幕打印EGAc,简单粗暴地直接写显存,猜一猜打印的位置。

第106-109行初始化卷屏相关变量,我们会在本章第4节用到这些变量。

最后我们使用gotoxy函数更新当前光标对应的显存地址。首先要检查光标的位置是否合法,通过计算得到位置并存储在pos变量中。

接着来完善其他文件。首先是tty_io.c。

#include <linux/tty.h>void tty_init(void)
{con_init();
}

目前,这个文件用处不大,但如果把用处不大的文件删掉,将它们的内容移动到其他文件中,之后为了和原版的linux0.11保持一致,需要不断修改整理代码,这很麻烦,不如就这样了。

添加一个叫tty.h的库文件,在里面放入函数声明。

#ifndef _TTY_H_
#define _TTY_H_void con_init(void);
void tty_init(void);#endif

这个库文件之后还会添加关于串口的内容。

稍稍修改一下main.c。

#include <linux/tty.h>#define PAGE_SIZE   4096long user_stack[PAGE_SIZE >> 2];struct {long * a;short b;
} stack_start = {&user_stack[PAGE_SIZE >> 2], 0x10};void main(void)
{tty_init();while (1);
}

在main.c中包含tty.h,因为会在main函数中使用tty_init()。

还需要再kernel/chr_drv目录下创建一个Makefile文件,将这个目录下的目标文件打包成静态库。

AR   =ar
AS  =as --32
LD  =ld
LDFLAGS =-s -x
CC  =gcc
CFLAGS  =-Wall -O -m32 -fstrength-reduce -fno-stack-protector -fomit-frame-pointer -finline-functions -nostdinc -I../../include
CPP =gcc -E -nostdinc -I../../include.c.s:$(CC) $(CFLAGS) \-S -o $*.s $<
.s.o:$(AS) -c -o $*.o $<
.c.o:$(CC) $(CFLAGS) \-c -o $*.o $<OBJS  = tty_io.o console.ochr_drv.a: $(OBJS)$(AR) rcs chr_drv.a $(OBJS)syncclean:rm -f core *.o *.a tmp_make keyboard.sfor i in *.c;do rm -f `basename $$i .c`.s;doneconsole.s console.o : console.c
tty_io.s tty_io.o : tty_io.c

kernel目录下也需要一个Makefile,现在这个目录下没有文件,但需要添加clean规则,执行chr_drv内的clean规则。

AR   =ar
AS  =as --32
LD  =ld -m elf_i386
LDFLAGS =-s -x
CC  =gcc -march=i386
CFLAGS  =-Wall -O -m32 -fstrength-reduce -fno-stack-protector -fomit-frame-pointer -finline-functions -nostdinc -I../include
CPP =gcc -E -nostdinc -I../include.c.s:$(CC) $(CFLAGS) \-S -o $*.s $<
.s.o:$(AS) -o $*.o $<
.c.o:$(CC) $(CFLAGS) \-c -o $*.o $<OBJS  =clean:rm -f *.o *.afor i in *.c;do rm -f `basename $$i .c`.s;done(cd chr_drv; make clean)

最后的最后,我们还得改主目录下的Makefile,将kernel/chr_drv中的内容编译到system中。

AS   =as --32
LD  =ld
LDFLAGS =-m elf_i386
CC  =gcc -march=i386
CFLAGS  =-Wall -O2 -m32 -fomit-frame-pointer -fno-stack-protectordefault: allDRIVERS =kernel/chr_drv/chr_drv.a.c.s:$(CC) $(CFLAGS) \-nostdinc -Iinclude -S -o $*.s $<
.s.o:$(AS) -c -o $*.o $<
.c.o:$(CC) $(CFLAGS) \-nostdinc -Iinclude -c -o $*.o $<all: ImageImage: clean mkimg boot/bootsect.bin boot/setup.bin systemobjcopy -O binary -R .note -R .comment system kernel.bindd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notruncdd if=boot/setup.bin of=kernel.img bs=512 count=4 seek=1 conv=notruncdd if=kernel.bin of=kernel.img bs=512 count=384 seek=5 conv=notruncrm kernel.bin -fbochs -qf bochsrcboot/head.o: boot/head.sgcc -m32 -traditional -c boot/head.s -o boot/head.osystem: boot/head.o init/main.o $(DRIVERS)$(LD) $(LDFLAGS) boot/head.o init/main.o \$(DRIVERS)  \-o system -T kernel.ldsnm system | grep -v '\(compiled\)\|\(\.o$$\)\|\( [aU] \)\|\(\.\.ng$$\)\|\(LASH[RL]DI\)'| sort > System.map kernel/chr_drv/chr_drv.a:(cd kernel/chr_drv; make)boot/bootsect.bin: boot/bootsect.snasm boot/bootsect.s -o boot/bootsect.binboot/setup.bin: boot/setup.snasm boot/setup.s -o boot/setup.binmkimg:./mkimg.shclean:rm -rf boot/*.bin boot/*.o init/*.o System.map system kernel.img(cd kernel;make clean)init/main.o: init/main.c

好了,来运行一下程序吧。

可以看到EGAc被打印在右上角。

3.简单的printk函数1

想要通过显存在屏幕上打印字符串,我们需要知道几个信息:打印的位置,显示模式,字符串地址和长度。当然,这些信息并不难获取。上一节中,我们通过gotoxy函数将光标当前位置保存在pos变量中,显示模式就直接指定为黑底白字,字符串地址可以当作参数传入函数,长度也很好计算。

以下是添加到console.c的代码,con_write的参数buf是字符串地址,nr是字符串长度。

void con_write(char *buf, int nr)
{int i;char c;for (i = 0; i < nr; i++) {c = buf[i];if (c > 31 && c < 127) {__asm__("movb %1, %%ah\n\t""movw %%ax, %2\n\t"::"a"(c), "m"(attr), "m"(*(short *)pos));pos += 2;x++;}}
}

这段代码会将可打印字符(数字、字母和符号)打印在屏幕上,对于不可打印字符(换行、退格等)之后再进行处理。这里使用内联汇编将字符和显示模式写入显存中(内联汇编相关的知识可以看这篇博客:https://www.cnblogs.com/taek/archive/2012/02/05/2338838.html),这段代码把字符写入al寄存器中,把显示模式attr写入ah寄存器中,再把ax写入pos指向的地址中。最后更新pos和x(光标列号)的值。

现在,你就可以用con_write("Hello World!", 12);这么一行代码打印字符串。但是,你肯定不想每一次打印都要自己数一次字符串长度吧。另外,这个函数也不能格式化输出,比如printf就可以通过%d打印数字,%s打印字符串。那就让我们再添加一些代码吧。

首先是在tty_io.c添加一个函数。

int tty_write(char *buf, int nr)
{char c;if (nr < 0)return -1;con_write(buf, nr);
}

这个函数目前没有多大的意义,只是为了和原版的linux0.11保持一致。

接着是printk.c。

#include <stdarg.h>#include <linux/kernel.h>static char buf[1024];extern int vsprintf(char * buf, const char * fmt, va_list args);int printk(const char *fmt, ...)
{va_list args;int i;va_start(args, fmt);i = vsprintf(buf, fmt, args);va_end(args);return tty_write(buf, i);
}

函数参数使用了可变参数,即参数的数量和类型不是固定的,更多的内容还请自行百度。重中之重是vsprintf函数,举个例子理解vsprintf的用法,我有printk("%s World!", "Hello");这么一段代码,此时,fmt内的字符串是"%s World!",args内的参数是"Hello",经过vsprintf处理后,buf内的字符串会变成"Hello World!",并返回字符串的长度。printk的返回值是打印字符串的长度。

vsprintf函数只是单纯的字符串处理函数,与操作系统没有多大的关系,这里就不展开说明,具体的代码放在vsprintf.c中,有兴趣的话可以自行阅读代码。

printk.c中要使用stdarg.h,vsprintf中要使用string.h,需要在include目录下添加这些文件(具体内容请看我的码云仓库)。本来是可以直接复制linux0.11的string.h的,但是可能是因为gcc版本不同,我的代码使用linux0.11的strlen函数会出现一些错误,所以就自己写了一个简单的strlen函数。

还要在tty.h中添加函数声明。

#ifndef _TTY_H_
#define _TTY_H_void con_init(void);
void tty_init(void);int tty_write(char *buf, int nr);void con_write(char *buf, int nr);#endif

另外再创建一个新文件kernel.h,这个文件只是存放一些函数声明,也是为了与linux0.11保持一致而添加的文件。

int printk(const char * fmt, ...);
int tty_write(char *buf, int nr);

虽然我也想快点改main.c,然后运行程序,但我们还是得改改Makefile,将添加的文件编译到内核中。

先修改kernel目录下的Makefile。

AR   =ar
AS  =as --32
LD  =ld -m elf_i386
LDFLAGS =-s -x
CC  =gcc -march=i386
CFLAGS  =-Wall -O -m32 -fstrength-reduce -fno-stack-protector -fomit-frame-pointer -finline-functions -nostdinc -I../include
CPP =gcc -E -nostdinc -I../include.c.s:$(CC) $(CFLAGS) \-S -o $*.s $<
.s.o:$(AS) -o $*.o $<
.c.o:$(CC) $(CFLAGS) \-c -o $*.o $<OBJS  =printk.o vsprintf.okernel.o: $(OBJS)$(LD) -r -o kernel.o $(OBJS)syncclean:rm -f *.o *.afor i in *.c;do rm -f `basename $$i .c`.s;done(cd chr_drv; make clean)printk.s printk.o : printk.c
vsprintf.s vsprintf.o : vsprintf.c

再修改主目录下的Makefile。

ARCHIVES=kernel/kernel.osystem: boot/head.o init/main.o $(ARCHIVES) $(DRIVERS)$(LD) $(LDFLAGS) boot/head.o init/main.o \$(ARCHIVES) \$(DRIVERS) \-o system -T kernel.ldsnm system | grep -v '\(compiled\)\|\(\.o$$\)\|\( [aU] \)\|\(\.\.ng$$\)\|\(LASH[RL]DI\)'| sort > System.map kernel/kernel.o:(cd kernel; make)

主目录的Makefile内容过多,这里只给出与kernel目录相关的部分。

最后,修改main.c,然后终于可以编译了。

#include <linux/tty.h>extern int printk(const char *fmt, ...);#define PAGE_SIZE   4096long user_stack[PAGE_SIZE >> 2];struct {long * a;short b;
} stack_start = {&user_stack[PAGE_SIZE >> 2], 0x10};void main(void)
{tty_init();printk("%s World!", "Hello");while (1);
}

printk要放在tty_init函数之后。预期的结果就是打印出Hello World。事不宜迟,马上编译让程序跑起来。

运行成功!可是,从图片中就可以看出,光标并没有随字符打印而移动,另外,如果字符数量过多也会出现错误,比如,我要打印下面这段文字。

For hardware functions such as input and output and memory allocation, the operating system acts as an intermediary between programs and the computer hardware,[1][2] although the application code is usually executed directly by the hardware and frequently makes system calls to an OS function or is interrupted by it. Operating systems are found on many devices that contain a computer – from cellular phones and video game consoles to web servers and supercomputers.

运行结果如下:

上面的字符串共470个字符,屏幕每行显示80个字符,所以应该会显示6行,而这里只显示了5行,有一部分内容并没有显示出来,这也需要改进。

不过,改进的内容都放在下一节进行吧。

4.简单的printk函数2

这一节要改进上一节的printk函数,为其添加光标移动和滚屏的功能。

先修改console.c的con_write函数,给它添加换行和光标随字符串移动的功能。

void con_write(char *buf, int nr)
{int i;char c;for (i = 0; i < nr; i++) {c = buf[i];if (c > 31 && c < 127) {if (x >= video_num_columns) {   // 打印完一行后换行x -= video_num_columns;pos -= video_size_row;lf();}__asm__("movb %1,%%ah\n\t""movw %%ax,%2\n\t"::"a" (c),"m"(attr),"m" (*(short *)pos));pos += 2;x++;}}set_cursor(); // 打印完字符串后,重新设置光标
}

之前打印字符串时,我们都是让x不断自增,这显然是不合理的,现在打印完一行后,让x归零,让pos回到行首位置,然后调用lf函数进行换行,当打印完屏幕的最后一行后还需要进行滚屏操作,腾出空行。

打印完字符串之后,需要重新设置光标,调用set_cursor函数。

static inline void set_cursor(void)
{cli();outb_p(14, video_port_reg);outb_p(0xff&((pos-video_mem_start)>>9), video_port_val);outb_p(15, video_port_reg);outb_p(0xff&((pos-video_mem_start)>>1), video_port_val);sti();
}

set_cursor函数较为简单,就先讲它吧。首先关中断,避免下面的操作被中断打断(虽然我们还没初始化中断),然后下面4行是干什么的?

在我的机器上,经过con_init,video_port_reg的值为0x3d4,video_port_val的值为0x3d5。让我们看看这两个端口有什么用。

如图所示,我们可以通过向0x3d4端口输入不同的值,访问0x3d5下不同的寄存器。向0x3d4写入0xE和0xF分别可以读写光标地址的高低字节。如,想将光标移动到第2行第2列,此时的光标位置为1*80+1=81,第一个1是行号,第二个1是列号,80是每行可显示的字符数。将81的高8位写入MSB中,低八位写入LSB中,光标就移动到指定位置。

其中outb_p函数定义在io.h中,用于向端口输出数据。

#define outb_p(value,port) \
__asm__ ("outb %%al,%%dx\n" \"\tjmp 1f\n" \"1:\tjmp 1f\n" \"1:"::"a" (value),"d" (port))

第一个参数是输入端口的值,第二个参数是端口号,把数据放入eax中,端口号放入edx中,输出完成后,jmp 1f并无实际意义,只是用来延时。

设置完光标位置后,开启中断。开关中断的函数定义在system.h中,具体定义如下所示:

#define sti() __asm__ ("sti"::)
#define cli() __asm__ ("cli"::)
#define nop() __asm__ ("nop"::)#define iret() __asm__ ("iret"::)

这里顺便也定义了nop和iret的函数。

接着讲讲lf函数。

static void lf(void)
{if (y + 1 < bottom) {y++;pos += video_size_row;return;}scrup();
}

bottom代表底行行号。如果不是在最后一行换行,让光标行号加1,将pos设置为下一行行首位置即可。如果是在最后一行换行,将显示下一个字符的位置改为最后一行的第一列,然后调用scrup函数滚动屏幕。

static void scrup(void)
{if (video_type == VIDEO_TYPE_EGAC || video_type == VIDEO_TYPE_EGAM){if (!top && bottom == video_num_lines) {origin += video_size_row;pos += video_size_row;scr_end += video_size_row;if (scr_end <= video_mem_end) {__asm__("cld\n\t""rep\n\t""stosw"::"a"(video_erase_char),"c"(video_num_columns),"D"(scr_end - video_size_row));}else {__asm__("cld\n\t""rep\n\t""movsd\n\t""movl %2, %%ecx\n\t""rep\n\t""stosw"::"a"(video_erase_char),"c"((video_num_lines - 1) * video_num_columns >> 1),"m"(video_num_columns),"D"(video_mem_start),"S"(origin));scr_end -= origin - video_mem_start;pos -= origin - video_mem_start;origin = video_mem_start;}set_origin();}else {__asm__("cld\n\t""rep\n\t""movsd\n\t""movl %2, %%ecx\n\t""rep\n\t""stosw"::"a"(video_erase_char),"c"((bottom - top - 1) * video_num_columns >> 1),"m"(video_num_columns),"D"(origin + video_size_row * top),"S"(origin + video_size_row * (top + 1)));}}else       /* Not EGA/VGA */{__asm__("cld\n\t""rep\n\t""movsd\n\t""movl %2, %%ecx\n\t""rep\n\t""stosw"::"a"(video_erase_char),"c"((bottom - top - 1) * video_num_columns >> 1),"m"(video_num_columns),"D"(video_mem_start + video_size_row * top),"S"(video_mem_start + video_size_row * (top + 1)));}
}

这个函数存在多个if分支,不好讲解,我们就从最简单的地方开始着手吧。

首先讲解第52-66行,如果显示模式不是VIDEO_TYPE_EGAC或VIDEO_TYPE_EGAM,就执行这段代码。我们之前的代码已经将显示模式打印在屏幕右上角,如果你是一步步按照我之前的操作做的话,你的显示模式应该是VIDEO_TYPE_EGAC,但是还是请看看下面的内容,因为各种滚屏操作有相似之处。

这个模式的滚屏操作很简单,将后24行的内容移动到前24行中,然后把第25行的内容擦掉。

cld rep movsd中的movsd可以扩展为movsd ds:[esi], es:[edi],将ds:[esi]处4个字节移动到es:[edi]处,cld会让esi、edi在每次移动后递增,rep会让移动操作进行ecx次。ecx中的值为(bottom - top - 1) * video_num_columns >> 1,即前24行显存总字节数除以4(因为每次移动4字节,所以要除以4),edi的值为video_mem_start + video_size_row * top(屏幕第零行在显存中的位置),esi的值为video_mem_start + video_size_row * (top + 1)(屏幕第一行在显存中的位置)。这里就完成了把后24行的内容移动到前24行的功能。

rep stosw中的stosw是将ax的内容放入es:[edi]中,ax的内容为video_erase_char(0x0720),此时edi的值为屏幕最后一行的显存的位置,第57行代码将video_num_columns的值放入ecx中,rep会让移动操作执行ecx次。也就是说,会将0x0720放入显存最后一行,0x07代表黑底白字,0x20代表空格,所以看起来就是黑色块,相当于清空了最后一行的内容。

现在来看看第3-51行的代码。如果显示模式是VIDEO_TYPE_EGAC或VIDEO_TYPE_EGAM,就执行这段代码。在这种显示模式下,滚屏时会保存之前的屏幕内容,要比上面的显示模式更高级,但代码也更多。

先看看当top=0而且bottom=25的if分支,即第5-36行代码。首先更新origin、pos和scr_end的值,origin代表屏幕在显存的起始地址,在我的机器上一开始是0xb8000,在滚屏后变成0xb80a0。scr_end代表屏幕在显存的结束地址,在我的机器上一开始是0xb8fa0,在滚屏后变成0xb9040。origin到scr_end是屏幕在显存上的地址范围。pos代表光标在显存中的位置。如果scr_end还在显存的范围内(第9-17行代码),我们只需将屏幕的最后一行清空,然后重新设置屏幕在显存的地址即可。如果scr_end超出显存范围(第18-34行),会将屏幕内容拷贝到显存起始地址,并将最后一行清空,将scr_end、pos和origin重新设置为显存起始地址的相应位置。再看看set_origin函数。

static inline void set_origin(void)
{cli();outb_p(12, video_port_reg);outb_p(0xff&((origin-video_mem_start)>>9), video_port_val);outb_p(13, video_port_reg);outb_p(0xff&((origin-video_mem_start)>>1), video_port_val);sti();
}

set_origin函数与set_cursor函数相似,都是对0x3d4和0x3d5寄存器进行操作。

如果要将第2行第1列设置为屏幕的起始地址,将Start address设置为1*80+0=80,1代表行号,0代表列号,80代表一行的字符数,将高8位写入0xC寄存器,将低8位写入0xD寄存器。

最后看看37-50行的代码,当top!=0或bottom!=25时,执行此段代码。这段代码的操作与第52-66行的代码类似,就不多做解释。这段代码与第5-36行的代码相比,它不保存之前的屏幕显示内容。

终于讲完了显示的流程,最后修改一下main.c就开始运行吧。

void main(void)
{char buf[] = "For hardware functions such as input and output and memory allocation, the operating system acts as an intermediary between programs and the computer hardware,[1][2] although the application code is usually executed directly by the hardware and frequently makes system calls to an OS function or is interrupted by it. Operating systems are found on many devices that contain a computer – from cellular phones and video game consoles to web servers and supercomputers.";tty_init();printk("%s", buf);cli();while (1);
}

请注意,我在调用printk函数之后使用cli关闭了中断。这节的代码在printk之后的死循环中会触发中断,但我们还没有初始化中断,导致双重错误(Double Fault)。当初我调试的时候,模拟器总是莫名其妙地挂了,在虚拟机上一直不断关机重启,真的是一把辛酸一把泪。至于到底触发了什么中断我也不太清楚,在之后章节的代码中又会莫名其妙好了。。。

可以看到,所有地字符都完整地显示了,光标也确实是随着字符移动了。OHHHHHHHHH!!!

我们的printk函数还是有点小问题,它没法打印’\n’、’\t’这种特殊字符,当然,我们会在下一节解决这个问题。

5.简单的printk函数3

这一节主要是让printk能够处理特殊字符。主要修改的文件还是console.c。

为了能够识别不可打印字符,让我们在con_write中添加一些判断。

static void cr(void)
{pos -= x << 1;x = 0;
}static void del(void)
{if (x) {pos -= 2;x--;*(unsigned short *)pos = video_erase_char;}
}void con_write(char *buf, int nr)
{int i;char c;for (i = 0; i < nr; i++) {c = buf[i];if (c > 31 && c < 127) {if (x >= video_num_columns) {   // 打印完一行后换行x -= video_num_columns;lf();}__asm__("movb %1, %%ah\n\t""movw %%ax, %2\n\t"::"a" (c), "m"(attr), "m" (*(short *)pos));pos += 2;x++;}else if (c == 10 || c == 11 || c == 12) // '\n',换行,使光标下移一格lf();else if (c == 13)  // '\r',回车,使光标移至行首cr();else if (c == '\177')    // 八进制数,等于十进制127,删除del();else if (c == 8) { // '\b',退格if (x) {x--;pos -= 2;}}else if (c == 9) { // '\t',水平制表符c = 8 - (x & 7);x += c;pos += c << 1;if (x > video_num_columns) {x -= video_num_columns;pos -= video_size_row;lf();}c = 9;}}set_cursor();    // 打印完字符串后,重新设置光标
}

换行操作直接调用lf函数,这个函数我们在上一节就已经讲过了。

回车操作就是把x和pos移动到行首的位置,之后set_cursor会将光标也移动到行首位置。

删除操作会删掉一个字符,并将光标左移一位。

退格操作只会把光标左移一位,不会删除字符。

水平制表符会将光标移动到8的倍数的位置上。

这一节的内容很简单,或许我应该把c=7(响铃)的操作也加进来。算了,修改一下main.c运行吧。

void main(void)
{char buf[] = "For hardware functions such as input and output and memory allocation, the operating system acts as an intermediary between programs and the computer hardware,[1][2] although the application code is usually executed directly by the hardware and frequently makes system calls to an OS function or is interrupted by it. \n\rOperating systems are found on many devices that contain a computer – from cellular phones and video game consoles to web servers and supercomputers.";tty_init();printk("%s\n\r", buf);cli();while (1);
}

可以正确换行,另外的特殊字符就让大家自己测试吧。

你可能会疑问,为什么换行是用"\n\r"而不是printf常用的"\n"呢?这个涉及到一些状态的处理。虽然看起来printk函数已经足够完整了,但是我们还是有不少功能没有添加,比如ANSI转义字符序列的处理,当然这些功能在之后的章节中会慢慢补全。

printk的内容就告一段落,下一章会讲讲部分系统的初始化操作。

从零编写linux0.11 - 第三章 printk函数相关推荐

  1. 《Python数据分析基础教程:NumPy学习指南(第2版)》笔记6:第三章 常用函数2——中位数、方差、日期、展平

    本章将介绍NumPy的常用函数.具体来说,我们将以分析历史股价为例,介绍怎样从文件中载入数据,以及怎样使用NumPy的基本数学和统计分析函数.这里还将学习读写文件的方法,并尝试函数式编程和NumPy线 ...

  2. 【C语言笔记初级篇】第三章:函数与递归

    第三章:函数 (1)函数是什么 在计算机科学中,子程序是一个大型程序中的某部分代码, 由一个或多个语句块组成.它负责完成某项特定任务,而且相较于其他代码,具备独立性.一般会有输入参数并有返回值,提供对 ...

  3. javascript进阶课程--第三章--匿名函数和闭包

    javascript进阶课程--第三章--匿名函数和闭包 一.总结 二.学习要点 掌握匿名函数和闭包的应用 三.匿名函数和闭包 匿名函数 没有函数名字的函数 单独的匿名函数是无法运行和调用的 可以把匿 ...

  4. 《Python数据分析基础教程:NumPy学习指南(第2版)》笔记5:第三章 常用函数1——文件读写、算术平均值、最大值最小值、极值

    本章将介绍NumPy的常用函数.具体来说,我们将以分析历史股价为例,介绍怎样从文件中载入数据,以及怎样使用NumPy的基本数学和统计分析函数.这里还将学习读写文件的方法,并尝试函数式编程和NumPy线 ...

  5. 《Python数据分析基础教程:NumPy学习指南(第2版)》笔记8:第三章 常用函数4——线性模型、数组修剪与压缩、阶乘

    本章将介绍NumPy的常用函数.具体来说,我们将以分析历史股价为例,介绍怎样从文件中载入数据,以及怎样使用NumPy的基本数学和统计分析函数.这里还将学习读写文件的方法,并尝试函数式编程和NumPy线 ...

  6. 计量经济学及stata应用思维导图_人教版A版高中数学必修1第三章《函数的应用》思维导图...

    用思维导图复习,一天顶一个月.高中数学必修和选修课本共计13本,通常两年内学完,平均一年6本,每学期3本.每本平均三到四章,每学期5个月,大约半月学完一章.而高考总复习的时间则更为宝贵,如果高考一轮复 ...

  7. 如何用计算机算分数指数幂,第三章冪函数指数函数及其图像3.1指数和幂概念的推广.doc...

    第三章冪函数指数函数及其图像3.1指数和幂概念的推广 第三章 幂函数 指数函数及其图像 在第一章我们学习了用计算器求诸如an,的数值,也就是说,至今我们所接触的数的运算,还仅限于+,-,?,?四则运算 ...

  8. 第三章 Python函数基础及进阶

    第三章 函数基础及进阶 3.1 上章补充内容 3.1.1 Bytes类型 计算机的数据需要存到硬盘上,但是硬盘只能存储二进制的数据. 我们知道将计算机里的数据转换成我们能看懂的数据是将二进制 -> ...

  9. matlab贝塞尔函数重积分,第三章 贝塞尔函数 柱函数.pdf

    第十四章 贝塞尔函数 柱函数 贝塞 尔函数(也称 为圆柱 函数)是现代科 学技术领 域 中经常遇 到的一类特殊 函数 .1732 年伯努利研究 直悬链的摆动 问题,以及 1764 年欧拉研 究拉 紧圆 ...

最新文章

  1. 通过Navicat for MySQL远程连接的时候报错mysql 1130的解决方法
  2. linux:uabntu日常操作
  3. Python基础day02 作业解析【6道 if 判断题、9道 循环题】
  4. C++案例-员工分组
  5. Ui5 tool debug - ctrl+alt+shift+s实现原理
  6. Windows下Visual studio 2013 编译 Audacity
  7. Angular之jwt令牌身份验证
  8. MySQL count(1) , count(*), count(列名) 的异同
  9. 【Pytorch】MNIST数据集的训练和测试
  10. 八大妙招:改善企业网络安全
  11. WebMvc中MultipartFile文件上传
  12. Java 混淆那些事(五):ProGuard 其他的选项
  13. 编译mcu media server
  14. LitePal使用详解
  15. 神经网络与机器学习笔记
  16. SpringBoot+Mybatis实现三级分类联动
  17. call方法 java_漫谈JS中的call和apply方法
  18. 北方直播卖货搞钱“第一城“,竟然是临沂,200万人发家快手电商
  19. h5计时器(requestAnimationFrame)
  20. 阿里云教你掌握API的使用方法

热门文章

  1. vue 记住密码下次自动登录
  2. 图形学笔记(三)画一个彩色的三角形
  3. C#入门-Person类
  4. pandas 第十二期组队-pandas基础
  5. 距阵乘以一个未知距阵得单位矩阵 怎么算_贷款利息怎么算,房贷车贷消费贷,利息有什么区别...
  6. SEO具体是怎么优化的?
  7. java JDBC连接MySQL数据库调用存储过程进行查询
  8. 为什么我选择并且推崇用ROS开发机器人?
  9. Idea自带http测试功能真香
  10. SQLyog的免费使用方式