zoukankan      html  css  js  c++  java
  • mit6.828 记录 -- Lab 1 Part 3:The kernel

    Lab 1 Part 3:The kernel

    操作系统内核通常喜欢链接并在很高的虚拟地址(例如0xf0100000)上运行,以便为用户程序留出处理器虚拟地址空间的下部。但很多机器往往没有这么高的物理地址,这就需要进行虚拟地址和物理地址的映射。

    本实验将虚拟地址 0xf0100000 (内核代码期望在其上运行的链接地址)映射到物理地址 0x00100000 (内核被加载道到的地址)上。当 kern/entry.S 设置 CR0_PG 后,内存引用将被视为物理地址。

    exercise 7

    使用 QEMU 和 GDB 跟踪到 JOS 内核,然后在 movl %eax %cr0 停止。检查 0x001000000xf0100000 的内存。现在,使用 stepi GDB 命令单步执行该指令。同样,检查内存为 0x001000000xf0100000 。确保您了解刚刚发生的事情。

    建立新映射后的第一条指令是什么,如果没有正确的映射将无法正常工作?注释掉 kern / entry.S 中的 movl %eax %cr0 ,跟踪它,然后看您是否正确。

    在执行 movl %eax %cr0 过后,这两个地址的内容完全一样了,该指令执行过后,将虚拟地址 0xf0100000 映射到物理地址 0x00100000 上,故两者内容一致。

    注释掉该指令,make clean 过后重新执行:

    执行到该指令的下一指令 mov $relocated, %eax,虚拟地址 0xf0100000 的内容并没有变得和物理地址 0x00100000 的一致,证实了上述猜测。

    exercise 8

    在 OS 内核我们需要自己动手完成 I/O 操作,阅读 kern/printf.clib/printfmt.ckern/console.c,理解他们之间的关系。练习需要我们填充一段代码,用于实现使用 %o 打印八进制数字。

    kern/console.c 定义了很多 i/o 口以及对其的操作函数,是将字符显示到 console 上的,最接近硬件。

    lib/printfmt.c 实现了输出,printfmt 函数将输出格式化(如 %d,%s 以及 -,* 等格式),不过参数比较多。

    kern/printf.clib/printfmt.c 进行了封装,使得参数减少,是最顶层的、面向用户的函数。其 cprintf 函数的 va_list ap 代表着多个输入参数,比如 ("x = %d, y = %d", n, m) ,ap 指的是 n 和 m 。

    关联:

    kern/printf.cputch 函数调用了 kern/console.c 里的 cputchar 该函数(具体实现的细节没有仔细研读,且实现偏向硬件知识,暂时不能读得很明白,便没深入探究)实现了将字符输出到 console 中。

    kern/printf.cvcprintf 函数调用了 lib/printfmt.c 里的 printmf 函数,对该函数进行封装。

    新学到了:void 指针可以指向任意数据类型,故一般称为通用指针或者泛指针

    补充代码 printfmt.cprintfmt 函数里

    		case 'o':
    			// Replace this with your code.
    			num = getuint(&ap, lflag);
    			base = 8;
    			goto number;
    

    回答如下问题:

    1. 解释 printf.cconsole.c 之间的接口。特别是 console.c 导出什么函数? printf.c 如何使用此功能?

      printf.c 调用 cputchar 将字符输出到 console 中。console.c 中没被 static 修饰的函数都可以被其他文件调用,因为函数的定义和声明默认都是 extern 的, 但static 函数只在声明它的文件中可见。

    2. 解释 console.c 的如下内容:

      1	 // crt_pos:当前输出的位置;CRT_SIZE:每行最大显示字符数      
      2    if (crt_pos >= CRT_SIZE) {
      3              int i;
      4              
      5   		  // 将当前显示内容向上移动一行	  
      6              memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
      7              for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
      8                      crt_buf[i] = 0x0700 | ' ';
      9              crt_pos -= CRT_COLS;
      10     }
      
    3. 逐步执行如下代码:

      monitor.c 中的 mon_backtrace 中加入这两代码,make clean 之后重新 make 。在进行 gdb 调试。

      	int x = 1, y = 3, z = 4;
      	cprintf("x %d, y %x, z %d
      ", x, y, z);
      

      可以使用 b kernprintf.c:27 来设置断点

      (1) 在对 cprintf()的调用中,fmt 指向什么? ap指向什么?

      cprintf (fmt=0xf0101b4e "x %d, y %x, z %d ") at kern/printf.c:28

      (2) 列出(按执行顺序)对 cons_putcva_argvcprintf 的每个调用。对于 cons_putc,列出其参数。对于

      va_arg,请列出调用之前和之后 ap 指向的内容。对于 vcprintf,列出其两个参数的值。

      vcprintf

      vcprintf (fmt=0xf0101b4e "x %d, y %x, z %d ", ap=0xf010ff04 "01") at kern/printf.c:18

      cons_putc

      cons_putc (c=-267379889) at kern/console.c:435

      va_arg

      是我断点设置得不对吗?我怎么就没执行到这个函数?

    4. 运行如下代码

      monitor.c 中的 mon_backtrace 中加入这两代码,make clean 之后重新 make 。在进行 gdb 调试。

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

      输出是什么?解释此输出如何以上一练习的方式逐步到达。输出取决于 x86 是小端存储,如果 x86 是大端存储,那么 i 应该设置为什么才能产生相同的输出,是否需要更改 57616 为其他值。

      输出 He110 World%x, %s 分别表示 16 进制数和字符串,而十进制的 57616 转为 16 进制正好是 e110。若是采用大端存储,57616 不需要更改,0x00646c72 应改为 0x726c6400

    5. 在如下代码中,y = 之后的输出是什么?(不是固定的值。)为什么会这样?

      cprintf("x=%d y=%d", 3);

      y= 之后的输出不确定,因为他的值没给定。

    6. 假设 GCC 更改了其调用约定,以便按声明顺序将参数入栈,从而最后一个参数最后入栈。您将如何更改 cprintf或其接口,以便仍然可以向其传递可变数量的参数?

      这我不会呀?也没找到其他相关的博客。???先放着吧

      这篇 博客讲了 va_list va_start va_arg va_end 这几个函数,可能对解决这个问题有帮助。

    exercise 9

    确定内核将堆栈初始化的位置,以及堆栈在内存中的确切位置。内核如何为其堆栈保留空间?并在此保留区的哪个“末端”初始化堆栈指针指向?

    entry.S 中我们看到了栈顶指针的初始化:

    	# Clear the frame pointer register (EBP)
    	# so that once we get into debugging C code,
    	# stack backtraces will be terminated properly.
    	movl	$0x0,%ebp			# nuke frame pointer
    
    	# Set the stack pointer
    	movl	$(bootstacktop),%esp
    

    在该位置设置断点后,查看 $esp 的内容:

    栈顶的初始位置在 0xf0110000 中。一共 KSTKSIZE,其大小定义在 inc/memlayout.h 中,KSTKSIZE = 8 × PGSIZE = 8 × 4096B = 32KB

    exercise 10

    x86 堆栈指针(esp)指向栈顶,栈顶朝地址减小方向移动,即入栈时需要先减小栈顶指针,然后将内容入栈;出栈时先将内容出战,然后增加栈顶指针。在 32 位模式下,堆栈只能保存 32 位值,并且 esp 始终可被 4 整除(因为存储单元以字节为单位)。

    基址指针寄存器(ebp),当调用一个函数时,被调用的函数将其前一个函数的基址存入栈中,并将当前栈顶指针 esp 的值(地址还是内容?)复制到 ebp 中。故可以跟踪 ebp 指针并了解调用该函数的函数时哪个。

    请在 obj / kern / kernel.asm 中找到 test_backtrace 函数的地址,在其中设置一个断点,并检查内核启动后每次调用该调用会发生什么情况。 每个递归嵌套的 test_backtrace 嵌套级别将多少个32位字压入堆栈,这些字是什么?

    test_backtrace 地址为 0xf0100040

    把 ebp 的地址压入栈顶,再将栈顶内容赋给 ebp 。

    ebp 的变化如下,

    0xf010fff8: 0x00000000
    0xf010ffd8: 0xf010fff8	# 调用 test_backtrace(5)
    0xf010ffb8: 0xf010ffdb	# 调用 4
    0xf010ff98: 0xf010ffb8	# 调用 3
    0xf010ff78: 0xf010ff98	# 调用 test_backtrace(2)
    0xf010ff58: 0xf010ff78	# 调用 1
    0xf010ff38: 0xf010ff58	# 调用 0
    

    我不是很理解这是为啥 ???

    大家也可以看看这篇博客,我不是很理解,希望对大家有帮助。

    补充:

    CPU 的 ESP 寄存器存放当前线程的栈顶指针,
    EBP 寄存器中保存当前线程的栈底指针。

    C函数调用时,首先将参数 push 入栈,然后 push 返回地址,接着将原来的 EBP push入栈,然后将 ESP 的值赋给 EBP,令 ESP 指向新的栈顶。而函数返回时,会将 EBP 的值赋予ESP,然后 pop 出原来的 EBP 的值赋予 EBP 指针。 更详细准确的信息请参考《linux内核完全剖析》一书中的"3.4.1 C函数调用机制"。

    来自 :实现mon_backtrace

    补充2:

    通过 exercise 11 我理解了这部分,上面的内容左侧是 ebp 的值,而右侧是 ebp 指向的内存(即以 ebp 的值为地址的内存里的值),当进行调用时,被调用的函数将前一个函数的 ebp 值压栈,此时栈顶指针 esp 指向的地址里的内容就是刚刚入栈的 ebp 的值,该值是前一个函数的基址。这时候再将 esp 的值赋值给 ebp ,那么现在 ebp 所指向的内容就是上一个函数的 ebp ,而它的值是当前函数的基址。

    exercise 11

    实现 kern/monitor.c 里的 mon_backtrace 函数,还必须将此新函数挂接到内核监视器的命令列表中,以便用户可以交互地调用它。

    backtrace 函数应以以下格式显示函数调用帧的列表:

    Stack backtrace:
      ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031
      ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061
      ...
    

    每行包含一个 ebp,eip 和 args 。 ebp 值指示该函数使用的堆栈中的基本指针:即,刚进入该函数并将前一个函数的基址指针压栈之后的堆栈指针的位置。 列出的 eip 值是函数的返回指令指针:函数返回时控件将返回到的指令地址。 返回指令指针通常指向调用指令之后的指令(为什么?)。 最后,在 args 之后列出的五个十六进制值是所讨论函数的前五个参数,在调用该函数之前,这些参数已被压入堆栈。 当然,如果调用的函数少于五个参数,那么并非所有这五个值都有用。 (为什么回溯代码无法检测到实际有多少个参数?如何解决此限制?)

    打印的第一行反映了当前正在执行的函数,即 mon_backtrace 本身,第二行反映了称为mon_backtrace 的函数,第三行反映了调用该函数的函数,依此类推。 您应该打印所有未完成的堆栈帧。 通过研究内核/入口,您会发现有一种简单的方法可以告诉您何时停止。

    如上所述实现回溯功能。 使用与示例中相同的格式,因为否则会混淆分级脚本。 当您认为它可以正常工作时,请运行make grade来查看其输出是否符合我们的分级脚本的期望,如果没有,请对其进行修复。

    放一下这个博主的图

    img

    在函数调用时,先将上一函数的下一条指令地址入栈,接着将上一函数的 ebp(上一个函数的基址) 入栈,再将当前栈顶指针 esp(指向当前函数的基址)赋值给 ebp ,使得当前 ebp 指向上一函数的基址。

    在函数回退时,将 ebp 赋值给 esp,此时 esp 指向当前函数的栈底,栈该位置的内容是上一函数的基址,故便可 pop 出来上一函数的基址。

    int
    mon_backtrace(int argc, char **argv, struct Trapframe *tf)
    {
    	// Your code here.
    	cprintf("Stack backtrace:
    ");
    	uint32_t *ebp = (uint32_t*)read_ebp();
    	uint32_t eip;
    	
    	// 终止条件是 ebp = 0,因为 ebp 在一开始的值是 0
    	while(ebp){
    		eip = *(ebp + 1);
    		cprintf("ebp %x eip %x args", ebp, eip);
    		uint32_t *esp = ebp + 2;
    		for(int i = 0; i < 5; ++i) {
    			cprintf(" %08x", esp[i]);
    		}
    		cprintf("
    ");
    
    		// 当前 ebp 指向的地址存储上一个 ebp 的地址
    		ebp = (uint32_t*)(*ebp);
    	}
    
    	return 0;
    }
    

    make grade 结果:

    exercise 12

    此时,backtrace 函数可提供栈上导致 mon_backtrace() 被执行的函数调用程序的地址。 但是,通常还想知道与这些地址相对应的函数名称。 例如,哪些函数可能包含导致内核崩溃的错误。

    为了实现此功能,我们提供了功能 debuginfo_eip(),该函数在符号表中查找 eip 并返回该地址的调试信息。此功能在 kern / kdebug.c 中定义。

    修改堆栈回溯函数,以显示每个 eip 的函数名称,源文件名以及与该 eip 对应的行号。

    1. debuginfo_eip 中,__STAB_ * 来自哪里?

      大家看这篇文章吧,我不知很明白这部分。

      // Entries in the STABS table are formatted as follows.
      struct Stab {
      	uint32_t n_strx;	// index into string table of name		符号索引
      	uint8_t n_type;         // type of symbol		符号类型
      	uint8_t n_other;        // misc info (usually empty)		
      	uint16_t n_desc;        // description field		在文件中的行号
      	uintptr_t n_value;	// value of symbol		表示地址。特别要注意的是,这里只有FUN类型的符号的地址是绝对地址,
      								// SLINE符号的地址是偏移量,其实际地址为函数入口地址加上偏移量。
      								// 比如第3行的含义是地址f01000b8(=0xf01000a6+0x00000012)对应文件第34行。
      };
      

      来自这儿

    2. 通过将调用插入 stab_binsearch 以查找地址的行号,以完成 debuginfo_eip 的实现。向内核监视器添加 backtrace 命令,并扩展 mon_backtrace 的实现以调用 debuginfo_eip 并为表单的每个堆栈帧打印一行。

      每行给出堆栈帧 eip 的文件名和该文件内的行,然后是函数的名称以及 eip 与函数的第一条指令的偏移量(例如,monitor + 106表示返回的 eip 为106字节超过监视器的开始)。

      /*****	debuginfo_eip	*****/
      
      // 一定要先看 stab_binsearch前的文档
      // 根据 hint 写
      
      	// Your code here.
      
      	// 查看 stab.h 知道了使用 N_SLINE 
      	stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
      	if(lline > rline)
      		return -1;
      	else
      		info->eip_line = stabs[lline].n_desc;
      
      /***** mon_backtrace	*****/
      int
      mon_backtrace(int argc, char **argv, struct Trapframe *tf)
      {
      	// Your code here.
      	cprintf("Stack backtrace:
      ");
      	uint32_t *ebp = (uint32_t*)read_ebp();
      	uint32_t eip;
      	
      	// 终止条件是 ebp = 0,因为 ebp 在一开始的值是 0
      	while(ebp){
      		eip = *(ebp + 1);
      		cprintf("ebp %x eip %x args", ebp, eip);
      		uint32_t *esp = ebp + 2;
      		for(int i = 0; i < 5; ++i) {
      			cprintf(" %08x", esp[i]);
      		}
      		cprintf("
      ");
      		
      		// 当前 ebp 指向的地址存储上一个 ebp 的地址
      		ebp = (uint32_t*)(*ebp);
      
      		struct Eipdebuginfo info;
      		// addr: 传入 eip 的内容
      		debuginfo_eip((uintptr_t)eip, &info);
      		cprintf("	%s:%d: %.*s+%d
      ",
      			 info.eip_file, info.eip_line,
      			 info.eip_fn_namelen, info.eip_fn_name, (uintptr_t)eip - info.eip_fn_addr);
      	}
      
      	return 0;
      }
      

      并在 static struct Command commands[] = {} 中加入 { "backtrace", "Backtrace functions call chain", mon_backtrace }

      img

      Eipdebuginfo,来自https://blog.csdn.net/a747979985/article/details/94334901

    make grade 结果

    总结:

    1. 复习(学习?!)了 c 语言指针的用法,明白了指针取值与取地址以及指针移动等内容。
    2. 温习了大小端存储模式(exercise 8)。
    3. 明白了操作系统进行函数调用时,栈内容的变化以及 esp,eip,ebp 等内容的变化及其原因(exercise 10、11)
    4. 完成了 mon_backtrace 函数,能够实现查看当前函数及其之前(调用该函数的)函数的 ebp、eip、函数的参数以及函数的源码位置等内容。
    5. 在这之前的博客记录有些混乱,我往后尽量保持按照 lab 的 part 进行记录,也方便进行回顾。

    参考:

    https://blog.csdn.net/a747979985/article/details/94334901

    https://www.cnblogs.com/wuhualong/p/lab01_exercise12_print_more_info.html

  • 相关阅读:
    CSS3 flex 布局 图片撑大 父级元素被放大 解决办法
    CentOS 下使用 cron crond crontab 执行定时任务
    Linux 安装 pcre
    Nginx 下载编译安装
    油猴脚本编写教程
    用图形来表达你的意思
    免费绘图软件drawio.io快捷键说明
    centos/Mac 下的多线程下载工具 axel
    巧用对象,生成不重复随机数
    mac 下 Redis5 BloomFilter 安装及与 python连用
  • 原文地址:https://www.cnblogs.com/joe-w/p/12597977.html
Copyright © 2011-2022 走看看