Exercise-Coding

剩下的一些coding练习

练习五:

实现函数调用堆栈跟踪函数

这个任务原则上并不太需要阅读很多代码,但出于学习还是尝试读完了,感觉收获还是蛮大的。

首先,代码定义了名为stabs的数据结构,它以数组的方式进行存储,该数据结构在GDB之中得到了广泛的应用,可以用于读取符号表内的行、函数、文件信息,是一个很强大的跟踪用数据结构。该数据结构如下方代码块所示,其中n_strx是存放名字信息很重要的组件,而n_type定义了符号的类型,主流类型包括源代码类型、函数类型、行类型等等,具体可以查看源码文件进行学习。

此外,为了更好完成信息的存储和传递,该文件内设计了eipdebuginfo函数,其中会存放某地址addr或者ip对应的文件信息、行信息、函数信息等等,他们在之后会发挥很好的功效。

  7 #define STACKFRAME_DEPTH 20
  8
  9 extern const struct stab __STAB_BEGIN__[];  // beginning of stabs table
 10 extern const struct stab __STAB_END__[];    // end of stabs table
 11 extern const char __STABSTR_BEGIN__[];      // beginning of string table
 12 extern const char __STABSTR_END__[];        // end of string table
 13
 14 /* debug information about a particular instruction pointer */
 15 struct eipdebuginfo {
 16     const char *eip_file;                   // source code filename for eip
 17     int eip_line;                           // source code line number for eip
 18     const char *eip_fn_name;                // name of function containing eip
 19     int eip_fn_namelen;                     // length of function's name
 20     uintptr_t eip_fn_addr;                  // start address of function
 21     int eip_fn_narg;                        // number of function arguments
 22 };
 
   9 /* Entries in the STABS table are formatted as follows. */
  8 struct stab {
  7     uint32_t n_strx;        // index into string table of name
  6     uint8_t n_type;         // type of symbol
  5     uint8_t n_other;        // misc info (usually empty)
  4     uint16_t n_desc;        // description field
  3     uintptr_t n_value;      // value of symbol
  2 };

下面是本文件的核心函数,我在原本代码的基础上添加了更多的注释以方便读者理解。简言之,stab_binsearch是一个根据addr信息在stabs数组中全部块内,根据type类型,利用二分查找策略寻找能够包住addr的左stab项值和右stab项值。这边的工作逻辑是,首先找到确实能够包住type类型addr块的左边界和右边界,找到边界之后再尽可能缩小这个区间。因为二分查找确实会略过一些数据,所以有必要在最后进行一次缩小区间的小型遍历,以锁定最小范围。

在这里我对stabs机制也有一些疑惑,其实只要简单利用objdump -G来看一看。对照这个表格,函数debuginfo_eip就差不多能够明白了。简单来说,这个函数实现了文件、函数、代码行的信息追踪和解析。

n_value说白了就是地址偏移量,且stabs是一个地址增长的排列,这就给二分查找带来了可能。特别的,如果n_typeSLINE,其n_value会减去其所对应的Function偏移量。如果n_typeFUNString中会利用:来帮助分割其函数名。利用这些信息,可以较为清楚地解读debuginfo_eip函数。由于冗长且细节繁多,笔者直接做的代码间注释,其余部分姑且不表。

print_debuginfo函数则是一个换皮打印函数,能够打印存在info内的基本信息。下面看到函数read_eip,这个函数特地设置成了__noinline,这是个很关键的细节,我们暂时不谈。

下面是本人对print_stackframe函数的一个简单实现:这个原理其实很简单,通过函数栈帧不断向更高的地址内迁移,可以查看Ucore官方文档进行学习。

为什么一开始对于eip一定要调用函数read_eip来呢?注意到函数read_eipnon_inline的,这说明一定会通过间接跳转进入该函数,而此时再去看return address,就能恰好找到print_stackframe函数当前的eip值。

之后就正常流程跑跑就好了,非常轻松。

练习六:

  1. 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

  2. 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。

  3. 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

中断描述符表的一个表项占据8个字节,如下所示:

中断描述符表

其中Selector可以帮助在LDT或者GDT表中找到对应的基地址,而Offset可以作为偏移量。二者进行相加后,就能得到最终的中断处理代码的入口。

后面的两个练习,第一个要比第二个麻烦的多。第二个我开摆了,就按照他说的这样做吧,暂时不细究细节了。

首先,我们会发现idt_init这个函数会在init.c函数中出现,init.c则是在链接中第一个装上的目标文件,这和一开始利用linker.ld链接的顺序是一致的。

于是idt_init函数的目的就昭然若揭:需要利用vectors对中断表做一个简单的初始化,我们看向这个vectors的结构,在vectors.S之中。

可以看到所有中断向量存放在__vectors这一数组之中,他们存放的位置是在数据段上,并且可以简单地通过4字节偏移地址去访问这些向量。那么,这些向量到底存放了什么东西?查看该汇编文件的代码段信息,我们不难发现这样一个事实:

在简单地进行压栈等特质化操作后,控制流将会跳转到__alltraps处。__alltraps最早被定义在文件kern/trap/trapentry.S之中,以此开始针对各自情况进行异常处理,你会看到压栈和CALL的操作。

不过这里并不是我们的重点,暂时忽略。

为了实现初始化,题目建议采用宏的方式进行逐项处理(具体请看原题的注释):

本质上这是对IDT表格中的项进行处理的一个流程,根据我们在前置资料中的笔记,理解他们是不难的。特别的,istrap在我们题目的情况下是可以置零,不过还有一个问题,我们需要找到选择子以及偏移量,但现在我们拥有的条件只有vector本身的地址信息,这是不足够的,因为它其实只不过就是个偏移量罢了。

于是我们需要寻找任何和选择子有关的东东,找了一下发现这玩意在memlayout.h里头。我们看到下面这些规定:

考虑到现在电脑才刚启动,分权机制也没有添加,所以答案就很简单了。我们的选择子应该是SEG_KTEXT,用以跳转到vector所指向的中断处理代码。

解答:

Last updated