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的分析,之后会补充。