LAB1-3 Kernel

格式化打印控制台

exercise1.8

八进制格式

  • 在操作系统内核中,我们需要实现自己的IO。系统IO主要包括3个文件实现,kern/printf.c, lib/printfmt.c, 以及 kern/console.c。这三个文件属于递进关系,依次对后面的函数进行封装。
    八进制格式代码:

    		num = getuint(&ap,lflag);
    		base = 8;
    		goto number;
    		break;
    

printf.c与console.c接口

  • console.c导出putchar函数,然后printf.c将其封装为putch函数,目的是向控制台输出单个字符。

  • // What is the purpose of this?
    if (crt_pos >= CRT_SIZE) {//如果字符位置超过屏幕显示的范围
    	int i;
    	//就把第二行的挪到缓冲区开头,空出一行的位置
    	memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
    	//空格擦去最后一行
    	for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
    		crt_buf[i] = 0x0700 | ' ';
    	//光标位置变为最后一行开始
    	crt_pos -= CRT_COLS;
    }
    
  • 单步调试下面的代码

    //exercise1.8.3,init.c中while(1)循环补全
    int x=1,y=3,z=4;
    cprintf("x %d,y %x,z %d\n",x,y,z);
    

    从kernel.asm文件中找到这段汇编的入口地址为0xf01000f9,然后通过设置断点调试到达这一步。
    因此fmt=0xf0101ad2,ap=0xf010ffc4在这里插入图片描述
    在这里插入图片描述
    查看fmt与ap地址中的值,可以得出,fmt中存放的是格式化的字符串,而ap指向可变参数的栈顶。根据note,x86的堆栈向下增长,也就是从高地址向低地址增长,而C函数调用参数压栈顺序从右向左,因此栈顶存放的内容为1,也就三ap指向x的存放地址。
    在这里插入图片描述vcprintf参数:
    在这里插入图片描述
    调用前ap:
    在这里插入图片描述
    consputc参数:执行中从控制台读取输入char,120对应x的ASCII码,后面的值也同理。
    在这里插入图片描述

  • 单步调试下面的代码

    unsigned int i=0x00646c72;
    cprintf("H%x Wo%s",57616,&i);
    

    输出字符串“he110 world”,57616对应16进制为“e110”,由于x86属于小端PC,因此后面字符对应0x72,0x6c,0x46,0x00,换算为10进制分别为rld\0的ASCII码。
    若为大端,则i为0x00726c64,57616不需要改变,因为这只是影响将其变为16进制后字节的存储顺序,相应的读取顺序也会发生改变。

      大端小端: 指的是在多字节数据类型中哪些字节最重要,并描述了字节序列在计算机内存中的存储顺序。在大端系统中,序列中最重要的值存储在最低的存储地址(即第一个)。在 小端 系统中,首先存储序列中最不重要的值。
    
  • y=-267321380,C函数压栈顺序从右往左,且堆栈生长向下,从高地址到低地址,因此在读取x的地址后,又向上增加四个字节读取该内存的内容。

  • 若C语言压栈顺序变为从左向右,则将第一个参数设置为函数参数的数目。

堆栈

exercise1.9 ESP

  • Kernel.asm:

    # Set the stack pointer
    movl	$(bootstacktop),%esp
    f0100034:	bc 00 00 11 f0       	mov    $0xf0110000,%esp
    

    ESP为栈指针寄存器,因此内核初始化堆栈的位置为清除帧指针寄存器EBP之后,为了确保堆栈回溯正确中止。
    SP栈顶位置为0xf0110000,预留空间为8页,32K,因此空间为0xf0108000-0xf0110000。初始堆栈指针指向0xf0110000。

    #define KSTKSIZE	(8*PGSIZE)   		// size of a 		kernel 	stack
    

    (1) x86 堆栈指针(esp 寄存器)指向当前正在使用的堆栈的最低位置。为堆栈保留的区域中该位置以下的所有内容都是空闲的。将值压入堆栈涉及减少堆栈指针,然后将值写入堆栈指针指向的位置。从堆栈中弹出一个值涉及读取堆栈指针指向的值,然后增加堆栈指针。在 32 位模式下,堆栈只能容纳 32 位值,并且 esp 始终可以被 4 整除。各种 x86 指令,例如 call,都是“硬连线”以使用堆栈指针寄存器。
    (2) C函数调用涉及EBP寄存器。在进入 C 函数时,函数的序言代码通常通过将前一个函数的基指针压入堆栈来保存它,然后在函数运行期间将当前 esp 值复制到 ebp 中。如果程序中的所有函数都遵守这个约定,那么在程序执行期间的任何给定点,都可以通过跟踪保存的 ebp 指针链并准确确定是什么嵌套的函数调用序列导致了这种特殊情况,来追溯堆栈要到达的程序中的点。堆栈回溯可让找到有问题的函数。

exercise1.10 test_backtrace()

实现回溯函数

  • 在0xf0100040设置断点,分析前两个esp寄存器的值,推入堆栈的32位字有8个,包括局部变量参数等,以及上一个调用函数的调用入口ebp。

    test_backtrace(5);
    f01000ea:	c7 04 24 05 00 00 00 	movl   $0x5,(%esp)
    f01000f1:	e8 4a ff ff ff       	call   f0100040 <test_backtrace>
    f01000f6:	83 c4 10             	add    $0x10,%esp
    

    在这里插入图片描述在这里插入图片描述

  • X86讲义NOTE中,关于函数调用栈帧的描述:
    每个函数都有一个由 %ebp, %esp 标记的堆栈帧,以及它保存的内容如下图:在esp中保存当前函数的栈帧,esp前面的字节保存了当前被调用函数的参数,ebp中保存了上一个函数的栈基址ebp,紧接着保存了上一个函数的入口地址eip,以及上一个函数的参数。
    因此,回溯函数的输出ebp由read_ebp()返回的ebp给出,eip则为ebp[1],然后ebp[2]–ebp[6]为参数的地址,然后ebp=saved ebp,得到上一个调用函数的入口。
    在这里插入图片描述

    查找调用函数的函数名,文件名,行号,通过debuginfo_eip()实现,而我们需要的信息来源于struct Eipdebuginfo *info。

    // Debug information about a particular instruction pointer
    struct Eipdebuginfo {
    	const char *eip_file;		// Source code filename for EIP文件名
    	int eip_line;			// Source code linenumber for EIP行数
    	const char *eip_fn_name;	// Name of function containing EIP函数名字,没有\0结尾符号
    					//  - Note: not null terminated!
    	int eip_fn_namelen;		// Length of function name函数名字长度
    	uintptr_t eip_fn_addr;		// Address of start of function函数起始地址
    	int eip_fn_narg;		// Number of function arguments参数数量
    };
    int debuginfo_eip(uintptr_t eip, struct Eipdebuginfo *info);
    

    实现的mon_backtrace():

    int
    mon_backtrace(int argc, char **argv, struct Trapframe *tf)
    {	cprintf("Stack backtrace:\n");
    	unsigned int ebpad=read_ebp();//adress
    	unsigned int* ebp= (unsigned int*)ebpad;
    	struct Eipdebuginfo info1;
    	while(ebpad!=0){
    		ebp=(unsigned int*)ebpad;//
    		cprintf("ebp %x ",ebp);
    		cprintf("eip %08x ",ebp[1]);
    		uintptr_t addr=(uintptr_t) ebp[1];
    		struct Eipdebuginfo *info=&info1;
    		debuginfo_eip(addr, info);
    		cprintf("args %08x %08x %08x %08x %08x\n",ebp[2],ebp[3],ebp[4],ebp[5],ebp[6]);
    		int line=(unsigned int*)ebp[1]-(unsigned int*)(info->eip_fn_addr);
    		cprintf("%s:%d: ",info->eip_file,line);
    		cprintf("%.*s", info->eip_fn_namelen, info->eip_fn_name);
    		cprintf("+%d\n",info->eip_line);
    		ebpad=ebp[0];
    	}
    	return 0;
    }
    

    向窗口增加交互命令:

    static struct Command commands[] = {
    	{ "help", "Display this list of commands", mon_help },
    	{ "kerninfo", "Display information about the kernel", mon_kerninfo },
    	{"backtrace","backtrace",mon_backtrace}
    };
    

    效果:
    在这里插入图片描述


  • 一些关于EBP,ESP,EIP的补充:来自博客:https://www.cnblogs.com/milantgh/p/3881278.html。
    在做了LAB3中elf文件加载后的补充:

    EIP,EBP,ESP都是系统的寄存器,里面存的都是些地址,系统中栈的实现上离不开他们三个。
    对于函数fun():

    void fun(void)
    {
       printf("hello world")}
    void main(void)
    {
      fun()
      printf("函数调用结束");
    }
    
  • 当调用fun函数开始时,三者的作用。
    1.EIP寄存器里存储的是CPU下次要执行的指令的地址。
    也就是调用完fun函数后,让CPU知道应该执行main函数中的printf(“函数调用结束”)语句了。
    2.EBP寄存器里存储的是是栈的栈底指针,通常叫栈基址,这个是一开始进行fun()函数调用之前,由ESP传递给EBP的。(在函数调用前你可以这么理解:ESP存储的是栈顶地址,也是栈底地址。)
    3.ESP寄存器里存储的是在调用函数fun()之后,栈的栈顶。并且始终指向栈顶。

  • 当调用fun函数结束后,三者的作用:
    1.系统根据EIP寄存器里存储的地址,CPU就能够知道函数调用完,下一步应该做什么,也就是应该执行main函数中的printf(“函数调用结束”)。
    2.EBP寄存器存储的是栈底地址,而这个地址是由ESP在函数调用前传递给EBP的。等到调用结束,EBP会把其地址再次传回给ESP。所以ESP又一次指向了函数调用结束后,栈顶的地址。
    其实我们对这个只需要知道三个指针是什么就可以,可能对我们以后学习栈溢出的问题以及看栈这方面的书籍有些帮助。


参考资料
Part1:PC Bootstrap
MIT–JOS2014
MIT 6.828 JOS 操作系统学习笔记
第一个LAB的笔记就到此结束啦,虽然做了好几天,从一开始的一窍不通到现在学到了很多,算是小有收获吧:),中间省略了STAB的分析,之后会补充。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值