当前位置: 代码迷 >> 综合 >> 哈工大操作系统之外设与文件系统(Lab7 终端设备的控制 深刻剖析printf函数全流程详细分析+代码实现)
  详细解决方案

哈工大操作系统之外设与文件系统(Lab7 终端设备的控制 深刻剖析printf函数全流程详细分析+代码实现)

热度:56   发布时间:2023-11-17 18:01:12.0

文章目录

    • 专栏博客链接
    • 前引闲聊
    • 相关博客查阅
    • 输出部分(正式开始实验)
      • printf函数
      • vsprintf函数
      • write函数
      • rw_char函数
      • tty_write函数
      • con_write函数
      • 输出部分修改代码分析
    • 输入部分
      • con_init函数
      • keyboard_interrupt函数
      • 输入部分修改代码分析
    • 开始编写代码
      • 新增全局变量flag console.c
      • 编写flag_change函数 console.c
      • 增加flag_change声明 修改头文件linux/tty.h
      • 新增F12函数调用myfunc keyboard.S
      • 修改F12函数调用名字
    • 实现效果


专栏博客链接

哈工大操作系统原理与实践 Lab全实验博客链接


前引闲聊


这个Lab确实很有意思哈哈哈 今天下午才发了Lab6的博客
晚上差不多花了一个多小时的分析时间(主要是找函数位置)
就成功的把这个Lab做出来了
所以现在就打算认认真真的把这个实验的全流程写一遍

因为我觉得我前面两个Lab的完成度并不是很高
或多或少都有些Bug 就导致我觉得完成的不是很好 做的并不是很到位
所以之后的几个我都会很认真的做 来弥补前面的Lab


相关博客查阅


write.c解析
Linux 0.11中write实现


输出部分(正式开始实验)


printf函数


万事不可只取片面 如果很片面的就直接把几行代码加进去了
那么这个Lab将毫无意义 所以刚开始我就打算 在看过视频的基础上
重新的跟踪整个printf到最低层的函数 究竟是如何把字符
输入进显示器的

ok 那我们就先从最基本的printf函数的定义开始找
当然 这些函数的定义和头文件 每一个其实我都找了很久 因为并不是很好找到 有好多个我都是一个一个文件点进去搜索查找才找到的

printf函数 路径init/main.c
我们只需要抓主干部分 就看到了调用了一个write函数
write函数中还调用了一个vsprintf函数

我们此时发现 一共有三个参数
先可以分析出来 printfbuf应该是printf的缓冲区

然后fmt参数是字符串的字符指针
那我们可以先去看看vsprintf的函数
看看vsprintf的返回值是什么 作用在干嘛

static char printbuf[1024];
static int printf(const char *fmt, ...)
{
    va_list args;int i;va_start(args, fmt);//printfbuf是内核区printf的公共缓冲区write(1,printbuf,i=vsprintf(printbuf, fmt, args));va_end(args);return i;
}

vsprintf函数


vsprintf函数 kernel/vsprintf.c
为printf函数提供字符串长度的功能 加上把字符串放到输出缓冲区中
这个时候发现这个时候把我们的字符串已经放到了
缓冲区中 由一个for循环结束了

然后中间那部分我们不用看了 只抓关键语句
到最后看一下返回值 不难分析出返回值是字符串的字符数

int vsprintf(char *buf, const char *fmt, va_list args)
{
    int len;int i;char * str;//字符指针char *s;int *ip;int flags;		/* flags to number() */int field_width;	/* width of output field */int precision;		/* min. # of digits for integers; maxnumber of chars for from string */int qualifier;		/* 'h', 'l', or 'L' for integer fields *//*字符串放入缓冲区*/for (str=buf ; *fmt ; ++fmt) {
     //当fmt == '\0'的时候终止)if (*fmt != '%') {
    *str++ = *fmt;continue;}/* process flags */flags = 0;repeat:++fmt;		/* this also skips first '%' */switch (*fmt) {
    case '-': flags |= LEFT; goto repeat;case '+': flags |= PLUS; goto repeat;case ' ': flags |= SPACE; goto repeat;case '#': flags |= SPECIAL; goto repeat;case '0': flags |= ZEROPAD; goto repeat;}/* get field width */field_width = -1;if (is_digit(*fmt))field_width = skip_atoi(&fmt);else if (*fmt == '*') {
    /* it's the next argument */field_width = va_arg(args, int);if (field_width < 0) {
    field_width = -field_width;flags |= LEFT;}}/* get the precision */precision = -1;if (*fmt == '.') {
    ++fmt;	if (is_digit(*fmt))precision = skip_atoi(&fmt);else if (*fmt == '*') {
    /* it's the next argument */precision = va_arg(args, int);}if (precision < 0)precision = 0;}/* get the conversion qualifier */qualifier = -1;if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L') {
    qualifier = *fmt;++fmt;}switch (*fmt) {
    case 'c':if (!(flags & LEFT))while (--field_width > 0)*str++ = ' ';*str++ = (unsigned char) va_arg(args, int);while (--field_width > 0)*str++ = ' ';break;case 's':s = va_arg(args, char *);len = strlen(s);if (precision < 0)precision = len;else if (len > precision)len = precision;if (!(flags & LEFT))while (len < field_width--)*str++ = ' ';for (i = 0; i < len; ++i)*str++ = *s++;while (len < field_width--)*str++ = ' ';break;case 'o':str = number(str, va_arg(args, unsigned long), 8,field_width, precision, flags);break;case 'p':if (field_width == -1) {
    field_width = 8;flags |= ZEROPAD;}str = number(str,(unsigned long) va_arg(args, void *), 16,field_width, precision, flags);break;case 'x':flags |= SMALL;case 'X':str = number(str, va_arg(args, unsigned long), 16,field_width, precision, flags);break;case 'd':case 'i':flags |= SIGN;case 'u':str = number(str, va_arg(args, unsigned long), 10,field_width, precision, flags);break;case 'n':ip = va_arg(args, int *);*ip = (str - buf);break;default:if (*fmt != '%')*str++ = '%';if (*fmt)*str++ = *fmt;else--fmt;break;}}*str = '\0';return str-buf;//字符串的字符数
}

到这里我们大概能分析出 sprintf做了什么了
第一个 返回了字符串的字符数 第二个把我们的字符串放入了缓冲区位置
那我们再重新返回到write函数
此时write函数的1我们可以先不管
后面的参数printfbuf是缓冲区
第三个参数i是字符串的字符数
ok 我们那就转到write函数


write函数


write函数 fs/read_write.c

这个时候我们就可以来分析参数1是什么了
这里取的参数名字是fd
然后最后又用file->f_inode 就看出来fd应该是注册的设备号
然后放到了current->file中 此时不难分析出1应该是我们的显示器中的设备号

一张图说明linux 设备 节点 驱动 主设备号 和次设备号之间的关系 博主:遥同学

我们继续来看 这个时候

int sys_write(unsigned int fd,char * buf,int count)
{
    struct file * file; struct m_inode * inode;if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd])) //file取出来return -EINVAL;if (!count)return 0;//把设备信息取出来 inode相关设备信息//下面根据inode里面存储了为 什么设备信息//ISCHR ISBLK ISREG 字符设备 块设备//进入到相关设备的的处理inode=file->f_inode;if (inode->i_pipe)return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;if (S_ISCHR(inode->i_mode))//WRITE inode->i_zone[0] buf count file->fpos//我们这个时候可以先不用管参数 关键参数在buf count//这个是储存了我们的字符串的地方//我们可以转到rw_char 字符设备处理函数return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);if (S_ISBLK(inode->i_mode))return block_write(inode->i_zone[0],&file->f_pos,buf,count);if (S_ISREG(inode->i_mode))return file_write(inode,file,buf,count);printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);return -EINVAL;
}

rw_char函数


rw_char函数 fs/char_dev.c
这个时候的pos 我们可以稍微推断一下
应该是pos是相关的光标位置 显示位置
这里的call_addr看名称不难推断出 这里应该是具体的块设备的处理
而这里的

int rw_char(int rw,int dev, char * buf, int count, off_t * pos)
{
    crw_ptr call_addr;if (MAJOR(dev)>=NRDEVS)return -ENODEV;//call_addr = crw_table[MAJOR(dev)]//应该是从调用表中把相关函数调用出来//不妨我们先去看看crw_tableif (!(call_addr=crw_table[MAJOR(dev)]))return -ENODEV;return call_addr(rw,MINOR(dev),buf,count,pos);
}

struct crw_table fs/char_dev.c


//这里应该就是到相关的处理函数了
//根据推断 应该是rw_ttyx 或者 rw_tty
//继续向下跳转 看看
static crw_ptr crw_table[]={
    NULL,		/* nodev */rw_memory,	/* /dev/mem etc */NULL,		/* /dev/fd */NULL,		/* /dev/hd */rw_ttyx,	/* /dev/ttyx */rw_tty,		/* /dev/tty */NULL,		/* /dev/lp */NULL};		/* unnamed pipes */

rw_tty函数 fs/char_dev.c
rw_ttyx函数 fs/char_dev.c

这里rw_tty函数应该是进行一个错误判断 然后仍然跳转到rw_ttyx
然后这里我们可以看出来

static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos)
{
    //rw 决定此处处理是读取还是写入//下分到下面的函数处理 究竟是read还是write//既然这里是printf 向显示屏 显存写那么我们下看到tty_writereturn ((rw==READ)?tty_read(minor,buf,count):tty_write(minor,buf,count));
}static int rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos)
{
    if (current->tty<0)return -EPERM;return rw_ttyx(rw,current->tty,buf,count,pos);
}

tty_write函数


tty_write函数 kernel/chr_dev/tty_io.c
我们前面的可以先不管 先看参数
nr就是 字符数 buf还是我们的公共缓冲区

这个时候我们就抓关键的地方即可 注释部分把我觉得比较重要的地方注释了出来

int tty_write(unsigned channel, char * buf, int nr)
{
    static int cr_flag=0;struct tty_struct * tty;char c, *b=buf; //b指针指向的就是 缓冲区if (channel>2 || nr<0) return -1;//tty指针赋值tty = channel + tty_table;while (nr>0) {
    //如果写入队列满了 先进入阻塞sleep_if_full(&tty->write_q);if (current->signal)break;while (nr>0 && !FULL(tty->write_q)) {
    c=get_fs_byte(b); //在缓冲区开始取字符了if (O_POST(tty)) {
    if (c=='\r' && O_CRNL(tty))c='\n';else if (c=='\n' && O_NLRET(tty))c='\r';if (c=='\n' && !cr_flag && O_NLCR(tty)) {
    cr_flag = 1;PUTCH(13,tty->write_q);continue;}if (O_LCUC(tty))c=toupper(c);}b++; nr--; // 挨个挨个在缓冲区中取出来cr_flag = 0;PUTCH(c,tty->write_q); //把我们的缓冲区的字符放到//tty 写入队列 write_queue}tty->write(tty); //调用函数 在结构体中的write函数//这个时候再进入tty结构体看看if (nr>0)schedule();}return (b-buf); //返回字符数
}

tty_struct函数 include/linux/tty.h

再根据初始化定义 发现了调用了函数 con_write
我们可以根据上面说的tty指针tty = channel + tty_table;

初始化表格的时候就应该指向的是con_write函数

struct tty_struct tty_table[] = {
    {
    {
    ICRNL,		/* change incoming CR to NL */OPOST|ONLCR,	/* change outgoing NL to CRNL */0,ISIG | ICANON | ECHO | ECHOCTL | ECHOKE,0,		/* console termio */INIT_C_CC},0,			/* initial pgrp */0,			/* initial stopped */con_write,{
    0,0,0,0,""},		/* console read-queue */{
    0,0,0,0,""},		/* console write-queue */{
    0,0,0,0,""}		/* console secondary queue */},{
    {
    0, /* no translation */0,  /* no translation */B2400 | CS8,0,0,INIT_C_CC},0,0,rs_write,{
    0x3f8,0,0,0,""},		/* rs 1 */{
    0x3f8,0,0,0,""},{
    0,0,0,0,""}},{
    {
    0, /* no translation */0,  /* no translation */B2400 | CS8,0,0,INIT_C_CC},0,0,rs_write,{
    0x2f8,0,0,0,""},		/* rs 2 */{
    0x2f8,0,0,0,""},{
    0,0,0,0,""}}
};struct tty_struct {
    struct termios termios;int pgrp;int stopped;void (*write)(struct tty_struct * tty);struct tty_queue read_q;struct tty_queue write_q;struct tty_queue secondary;
};

con_write函数


con_write函数 kernel/chr_dev/console.c
可以根据下面我注释的代码
抓住核心关键部分

就能很轻松的发现 对于这个Lab
核心关键发现了 字符到最后的一步 每一个都是放在变量c中
而应证了这个事情的是
c>31 && c<127 32-126刚好是ascii码值的范围
GETCH函数也说明了这个事情
那对于printf函数这部分就先结束了

void con_write(struct tty_struct * tty)
{
    int nr;char c;nr = CHARS(tty->write_q);//得到字符数//一个字符一个字符循环处理while (nr--) {
    GETCH(tty->write_q,c);//不难推断出 字符取出来放到变量c中switch(state) {
    case 0:if (c>31 && c<127) {
     //ascii码值范围if (x>=video_num_columns) {
    x -= video_num_columns;pos -= video_size_row;lf();}//al存储字符值 ah存储字符属性//真正的向设备输出字符了out .... mov ..__asm__("movb attr,%%ah\n\t""movw %%ax,%1\n\t"::"a" (c),"m" (*(short *)pos));pos += 2;x++;} else if (c==27)state=1;else if (c==10 || c==11 || c==12)lf();else if (c==13)cr();else if (c==ERASE_CHAR(tty))del();else if (c==8) {
    if (x) {
    x--;pos -= 2;}} else if (c==9) {
    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;} else if (c==7)sysbeep();break;case 1:state=0;if (c=='[')state=2;else if (c=='E')gotoxy(0,y+1);else if (c=='M')ri();else if (c=='D')lf();else if (c=='Z')respond(tty);else if (x=='7')save_cur();else if (x=='8')restore_cur();break;case 2:for(npar=0;npar<NPAR;npar++)par[npar]=0;npar=0;state=3;if ((ques=(c=='?')))break;case 3:if (c==';' && npar<NPAR-1) {
    npar++;break;} else if (c>='0' && c<='9') {
    par[npar]=10*par[npar]+c-'0';break;} else state=4;case 4:state=0;switch(c) {
    case 'G': case '`':if (par[0]) par[0]--;gotoxy(par[0],y);break;case 'A':if (!par[0]) par[0]++;gotoxy(x,y-par[0]);break;case 'B': case 'e':if (!par[0]) par[0]++;gotoxy(x,y+par[0]);break;case 'C': case 'a':if (!par[0]) par[0]++;gotoxy(x+par[0],y);break;case 'D':if (!par[0]) par[0]++;gotoxy(x-par[0],y);break;case 'E':if (!par[0]) par[0]++;gotoxy(0,y+par[0]);break;case 'F':if (!par[0]) par[0]++;gotoxy(0,y-par[0]);break;case 'd':if (par[0]) par[0]--;gotoxy(x,par[0]);break;case 'H': case 'f':if (par[0]) par[0]--;if (par[1]) par[1]--;gotoxy(par[1],par[0]);break;case 'J':csi_J(par[0]);break;case 'K':csi_K(par[0]);break;case 'L':csi_L(par[0]);break;case 'M':csi_M(par[0]);break;case 'P':csi_P(par[0]);break;case '@':csi_at(par[0]);break;case 'm':csi_m();break;case 'r':if (par[0]) par[0]--;if (!par[1]) par[1] = video_num_lines;if (par[0] < par[1] &&par[1] <= video_num_lines) {
    top=par[0];bottom=par[1];}break;case 's':save_cur();break;case 'u':restore_cur();break;}}}//设置光标位置set_cursor();
}

输出部分修改代码分析


我们这个Lab要求的是 用F12按键控制输出为星号*
其实到这里我们就已经完成一半了
因为这里修改为星号已经分析出来
核心我们只要控制最后的c*号即可
我们不妨这里测试一下 如果全部的输出字符都改成*
我们只需要加一句

if (c>31 && c<127) 
{
     	//此处是加的 如果想要只把字母变成*的话 就加个if即可if((c>='a' && c<='z') || (c>='A' && c<='Z'))c = '*';if (x>=video_num_columns) {
    x -= video_num_columns;pos -= video_size_row;lf();}
}

此时我们可以不妨测试一下
我这里是键入的./hello 果然发现所有的英文字母变成了*
那我们需要继续进入按键F12怎么操控部分分析
分析这个 不如我们先看看实验册给我们的提示

在这里插入图片描述


输入部分


con_init函数


这里再来考虑一下键盘输入 同理 我们再来看看键盘是怎么键入的
根据实验册说的

键盘 I/O 是典型的中断驱动,在 kernel/chr_drv/console.c 文件中:void con_init(void)  //控制台的初始化
{// 键盘中断响应函数设为 keyboard_interruptset_trap_gate(0x21, &keyboard_interrupt);
}所以每次按键有动作,keyboard_interrupt 函数就会被调用,
它在文件 kernel/chr_drv/keyboard.S(注意,扩展名是大写的 S)中实现。
所有与键盘输入相关的功能都是在此文件中实现的,
所以本实验的部分功能也可以在此文件中实现。简单说,keyboard_interrupt 被调用后,
会将键盘扫描码做为下标,
调用数组 key_table 保存的与该按键对应的响应函数

我们先找到 con_init() 因为整个函数重点部分其实并不是很多
我就贴出来核心部分

con_init函数 kernel/chr_dev/console.c

set_trap_gate(0x21, &keyboard_interrupt);

对 其实就是这一句话
每次我们按键时都会触发中断 而这个中断会触发中断响应函数keyboard_interrupt 所以我们再去这个函数看看



keyboard_interrupt函数


keyboard_interrupt函数 kernel/chr_dev/keyboard.S(大写S)
这里也是 我也只贴出来核心需要分析的点 其他地方忽略点

老套路 中断后储存寄存器 记录当前时刻状态 此时eax中保存的是我们键入的值 然后通过key_table来看看调用什么函数 调用位置key_table+eax*4 那我们就转到key_table 看看他要怎么弄

keyboard_interrupt:pushl %eaxpushl %ebxpushl %ecxpushl %edxpush %dspush %esmovl $0x10,%eaxmov %ax,%dsmov %ax,%esxor %al,%al		/* %eax is scan code */inb $0x60,%alcmpb $0xe0,%alje set_e0cmpb $0xe1,%alje set_e1call key_table(,%eax,4)movb $0,e0

key_table结构体 kernel/chr_dev/keyboard.S(大写S)
我们find即找到定义位置 并通过旁边的注释一下子就能看懂在嘛
long 四个字节 每个字符都对应着一个字符处理函数
而且旁边也给出了注释 例如 1 2 3 4 5 6 都对应着处理函数do_self
那我们先不如定位到F12 因为我们要处理这个

在这里插入图片描述


定位成功 发现F12会调用func函数 此时我们不妨先寻找一下func函数
在这里插入图片描述


通过搜索 发现还是定义存在于相同的文件中
并发现 do_self等函数也是在其中
这个时候我们可以断定 对于我们按键F12
每个按键对应着一个函数 而我们只需要写一个F12我们需要功能的函数即可对于键入中断我们就先分析到这里了 下面就到了我们对修改部分的分析了

在这里插入图片描述


输入部分修改代码分析


那个时候我就有一个想法
我们不妨在 con_write那个c文件中定义一个全局变量flag
比如按一下F12 flag就由0->1 再按一下就再切换一下
c = '*'这个判断条件就需要改一下 多一条判断flag是否==1
再对c进行修改


开始编写代码


下面的工作就很简单了
我们先按照我们的思路做就行了

下面我就直接给出代码吧


新增全局变量flag console.c


在这里插入图片描述


编写flag_change函数 console.c


在这里插入图片描述


增加flag_change声明 修改头文件linux/tty.h


在这里插入图片描述


新增F12函数调用myfunc keyboard.S


在这里插入图片描述


修改F12函数调用名字


在这里插入图片描述


实现效果


上面的修改部分就已经全部结束了
我们只需要重新编译 完了之后 我们进入虚拟机
效果怎么样我们试试就知道了

测试结果说明 我们修改成功了
这个实验也就完美结束了 撒花★,°:.☆( ̄▽ ̄)/$:.°★

在这里插入图片描述

  相关解决方案