-
直接执行 initlab.sh, 下载实验所需代码
-
初始化
QEMU:配置 qemu
cd qemu ./configure --disable-kvm --disable-werror --target-list="i386-softmmu x86_64-softmmu"
如果配置 qemu 时缺少包(在 initlab.sh 中应已执行):
apt install pkg-config libglib2.0-dev zlib1g-dev libpixman-1-dev
编译安装 qemu
make && make install
-
编译内核
cd lab make生成了文件
obj/kern/kernel.img, 就是我们的虚拟硬盘. 这个"硬盘"镜像中包含了 boot loader (obj/boot/boot) 和内核 obj/kernel .生成的目录结构:
root@MyServer:~/6828/lab# tree obj obj |-- boot | |-- boot | |-- boot.asm | |-- boot.o | |-- boot.out | `-- main.o `-- kern |-- console.o |-- entry.o |-- entrypgdir.o |-- init.o |-- kdebug.o |-- kernel |-- kernel.asm |-- kernel.img |-- kernel.sym |-- monitor.o |-- printf.o |-- printfmt.o |-- readline.o `-- string.o下一步,启动 QEMU, 加载硬盘。
-
启动 QEMU
cd lab make qemu-nox此命令启动 QEMU, 将输出
root@MyServer:~/6828/lab# make qemu-nox sed "s/localhost:1234/localhost:25000/" < .gdbinit.tmpl > .gdbinit *** *** Use Ctrl-a x to exit qemu *** qemu-system-i386 -nographic -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::25000 -D qemu.log 6828 decimal is XXX octal! entering test_backtrace 5 entering test_backtrace 4 entering test_backtrace 3 entering test_backtrace 2 entering test_backtrace 1 entering test_backtrace 0 leaving test_backtrace 0 leaving test_backtrace 1 leaving test_backtrace 2 leaving test_backtrace 3 leaving test_backtrace 4 leaving test_backtrace 5 Welcome to the JOS kernel monitor! Type 'help' for a list of commands. K>
退出方式:
Ctrl+a(松手)x.此时只有两个有效命令:
help和kerninfo. 后者的输出是:K> kerninfo Special kernel symbols: _start 0010000c (phys) entry f010000c (virt) 0010000c (phys) etext f0101871 (virt) 00101871 (phys) edata f0112300 (virt) 00112300 (phys) end f0112940 (virt) 00112940 (phys) Kernel executable memory footprint: 75KB -
调试内核
首先退出 qemu 执行环境,执行
make qemu-gdb进入 qemu 调试环境;root@MyServer:~/6828/lab# make qemu-gdb *** *** Now run 'make gdb'. *** qemu-system-i386 -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::25000 -D qemu.log -S VNC server running on `::1:5900'
然后在另一 session 用 gdb 链接此环境,只需执行调试内核命令
make gdb:root@MyServer:~/6828/lab# make gdb gdb -n -x .gdbinit ...(省略一些信息)... + target remote localhost:25000 warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration of GDB. Attempting to continue with the default i8086 settings. The target architecture is assumed to be i8086 [f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b 0x0000fff0 in ?? () + symbol-file obj/kern/kernel (gdb)
我们可以获取的信息有:
- qemu 从物理地址
0xffff0开始执行。f000:fff0表示0x f000*16+0x fff0=0xf fff0. 这对 qemu 来说是写死了的,第一条指令必须从这里开始执行。 - PC 启动时,位于
CS = 0xf000,IP = 0xfff0. 的段地址位置。段地址=CS*16+IP.16 * 0xf000 + 0xfff0 # in hex multiplication by 16 is = 0xf0000 + 0xfff0 # easy--just append a 0. = 0xffff0
- 执行的第一条指令是
jmp, 跳转到CS = 0xf000,IP = 0xe05b的地址。
结合物理内存分布来看:
+------------------+ <- 0xFFFFFFFF (4GB) | 32-bit | | memory mapped | | devices | | | /\/\/\/\/\/\/\/\/\/\ /\/\/\/\/\/\/\/\/\/\ | | | Unused | | | +------------------+ <- depends on amount of RAM | | | Extended Memory | | | +------------------+ <- 0x00100000 (1MB) | BIOS ROM | +------------------+ <- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ <- 0x000C0000 (768KB) | VGA Display | +------------------+ <- 0x000A0000 (640KB) | | | Low Memory | | | +------------------+ <- 0x00000000第一条指令所在的地址
0xffff0距离 BIOS 结束 (0x100000) 只剩 16 个 byte , 啥都做不了,所以第一条指令是跳转到了 BIOS 中更早一些的位置。在 BIOS 初始化了重要的设备 (device) 之后,BIOS 开始读取硬盘上的 boot loader, 并将控制权转移给它。
- qemu 从物理地址
引导硬盘/软盘的最小读写单位是扇区 (sector), 每块 512 个 byte. 每次读写操作必须是整数个 sector 单位。
- 读取引导扇区的 512 个 byte 至物理地址从
0x7c00始到0x7dff正好 512 个 byte 的位置。 - 执行
jmp指令设置 CS:IP 为0000:7c00, 也就是引导扇区内容所在的物理内存,至此将控制权移交给 boot loader. 这几个地址都是神器的魔数,无需深究。但是这是业界通用的。
引导光驱的步骤更复杂,也更强大。扇区单位变成了 2018 个 byte, 且 BIOS 会读取多个扇区。
对于本实验而言,使用的是传统的 512 字节读取的方式。boot loader 包含一个汇编源文件:boot/boot.S和一个 C 源文件:boot/main.c. 它们主要执行两个任务:
- 首先,boot loader 将处理器从 16 位的 real mode 转换到 32 位的保护模式。只有在保护模式,软件才能访问物理内存中 1MB 以上的部分。
- 然后,boot loader 从通过 x86 专用 IO 指令的方式硬盘读取内核代码。
理解源代码
boot.S的工作:
- 被 BIOS 从硬盘上的第一个扇区读取到
0x7c00, 并在实模式下执行,执行开始时 cs=0 ,ip=7c00. - 通过
lgdt gdtdesc,orl $CR0_PE_ON, %eax等指令切换至实模式; - 跳转到 C 代码并执行,
movl $start, %esp call bootmain
boot/main.c的工作:
加载内核镜像。如何加载?内核镜像是 ELF 二进制文件格式。我们对三个**段 (section) **感兴趣:
.text: 程序的可执行指令.rodata: 只读数据,如 C 编译器生成的字符串常量.data: 程序的初始化数据,例如int x = 5声明的全局变量。
当连接器计算程序内存布局时,会在紧邻
.data段的bss段为 未初始化的全局变量 保留空间,如int x;.C 语言要求未初始化的全局变量值为 0. 因此不需要在 ELF 二进制文件中保存bss的内容。链接器只记录.bss段�的地址和大小。程序加载器或者程序本身必须将。bss段归零。
练习 3:
-
处理器何时开始执行 32 位代码?什么最终导致了 16 位模式到 32 位模式的切换?
答:boot.S 48~51 行。将
cr0寄存器的最后一位置打开,即开启了保护模式。lgdt gdtdesc movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0lgdt : 加载全局中断描述符 orl : 逻辑或
-
boot loader 执行的最后一个指令是什么,它读取的内核的第一行代码是什么?
答:最后一行代码是
((void (*)(void)) (ELFHDR->e_entry))();, 位于main.c的第 60 行。内核的入口可以通过以下命令查看:
root@MyServer:~/6828/lab# objdump -f obj/kern/kernel obj/kern/kernel: file format elf32-i386 architecture: i386, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x0010000c <---------- entry point如
entry.S所说,_start指定了 ELF 文件的入口。故内核的入口位于entry.S的第 44 行movw $0x1234,0x472 # warm boot. -
内核的第一个指令在哪?
内核是一个 elf 文件,它本身描述了 loader 的进程从哪里开始执行。通过
readelf -h可以看到 elf 文件头描述的入口点:root@MyServer:~/6828/lab/obj/kern# readelf -h kernel ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x10000c <=========== 进程执行入口 Start of program headers: 52 (bytes into file) Start of section headers: 82728 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 3 Size of section headers: 40 (bytes) Number of section headers: 11 Section header string table index: 8 -
为了从磁盘中读取整个 kernel,boot loader 是如何决定读取多少个扇区的?它是怎么找到这个信息的?
ELF 头文件信息指明了最后的位置。
如何查看内核 ELF 文件所有段的信息?
cd obj/
objdump -h kern/kernel输出:
root@MyServer:~/6828/lab/obj# objdump -h kern/kernel
kern/kernel: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00001871 00100000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000714 f0101880 00101880 00002880 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 000038d1 f0101f94 00101f94 00002f94 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 000018bb f0105865 00105865 00006865 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data 0000a300 f0108000 00108000 00009000 2**12
CONTENTS, ALLOC, LOAD, DATA
5 .bss 00000648 f0112300 00112300 00013300 2**5
CONTENTS, ALLOC, LOAD, DATA
6 .comment 00000035 00000000 00000000 00013948 2**0
CONTENTS, READONLYVMA: Virtual Memory Address, 链接地址,或者说执行地址,也就是程序运行时 PC 应当等于的值 LMA: Load Memory Address, 加载地址,是存储的位置.
(关于 VMA 和 LMA, 详细可参考这篇 博客。)
-
段被加载到内存,然后执行。
-
加载地址是段希望被加载到的内存的地址,链接地址是段期望从哪里开始执行的地址。
-
这些地址与内容相关,只有在特定的地址执行才会有意义(个人理解). 现代的个共享库也可以生成与地址无关的代码,但本实验不采用。
-
通常链接地址与加载地址相同。如:
root@MyServer:~/6828/lab# objdump -h obj/boot/boot.out obj/boot/boot.out: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000186 00007c00 00007c00 00000074 2**2 CONTENTS, ALLOC, LOAD, CODE 1 .eh_frame 000000a8 00007d88 00007d88 000001fc 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .stab 00000720 00000000 00000000 000002a4 2**2 CONTENTS, READONLY, DEBUGGING 3 .stabstr 0000088f 00000000 00000000 000009c4 2**0 CONTENTS, READONLY, DEBUGGING 4 .comment 00000035 00000000 00000000 00001253 2**0 CONTENTS, READONLY -
boot loader 通过 ELF 的 header 来决定如何加载段。如何查看 header?
objdump -x obj/kern/kernel输出:
... 省略。.. Program Header: LOAD off 0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 2**12 filesz 0x00007120 memsz 0x00007120 flags r-x LOAD off 0x00009000 vaddr 0xf0108000 paddr 0x00108000 align 2**12 filesz 0x0000a948 memsz 0x0000a948 flags rw- STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4 filesz 0x00000000 memsz 0x00000000 flags rwx ... 省略。..被标记为"load"的对象需要被加载到内存中。输出也给出了虚拟地址 (addr), 物理地址 (paddr), 加载区域大小 (memsz, filesz).
在boot/main.c中,ph->p_pa字段包含段的目标物理地址。
// elf.h
struct Proghdr {
uint32_t p_type;
uint32_t p_offset;
uint32_t p_va;
uint32_t p_pa;
uint32_t p_filesz;
uint32_t p_memsz;
uint32_t p_flags;
uint32_t p_align;
};
//main.c
void
bootmain(void)
{
struct Proghdr *ph, *eph;
// read 1st page off disk
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
...
// 加载每个段到目标地址
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
// p_pa 就是这个段的目标加载地址,也就是物理地址
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
// 从 ELF hdader 中调用 entry point, 且永不返回
((void (*)(void)) (ELFHDR->e_entry))();
....
}entry point 在哪?
objdump -f obj/kern/kernel
输出:
root@MyServer:~/6828/lab# objdump -f obj/kern/kernel
obj/kern/kernel: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c <---------- entry point
总结 boot/main.c包含一个极简的 ELF 加载器,它把硬盘上 kernel 的每个段都加载到内存中(的目标加载地址), 然后跳转到内核的 entry point。
和 boot loader 类似,kernel 部分也是从几句汇编开始(用于初始化 C 语言环境)。
虽然 boot loader 的链接地址与加载地址相同,但 kernel 的链接地址和加载地址相差很大:
root@MyServer:~/6828/lab/obj# objdump -h kern/kernel
kern/kernel: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00001871 f0100000 00100000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000714 f0101880 00101880 00002880 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 000038d1 f0101f94 00101f94 00002f94 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 000018bb f0105865 00105865 00006865 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data 0000a300 f0108000 00108000 00009000 2**12
CONTENTS, ALLOC, LOAD, DATA
5 .bss 00000648 f0112300 00112300 00013300 2**5
CONTENTS, ALLOC, LOAD, DATA
6 .comment 00000035 00000000 00000000 00013948 2**0
CONTENTS, READONLY链接内核与链接 boot loader 更加复杂。
操作系统内核通常在非常高的虚拟地址执行(如0xf0100000), 以便把处理器的虚拟地址空间的下半部分留给用户程序使用。
但是很多机器在这么高的地址处是没有物理内存的,那里不能存储内核。我们将使用处理器的内存管理硬件来将虚拟地址 0xf0100000(内核代码期待被运行的位置) 映射到物理地址 0x00100000(boot loader 把内核代码加载到的位置)。 这样内核的虚拟地址足够高,但是被加载到物理内存 1 MB 的位置,就在 BIOS 的上方。
现在,我们只需映射物理内存的前 4MB 就足够启动/运行了。使用kern/entrypgdir.c中手写的,静态初始化的页面目录 (page directory) 和页表 (page table) 来达到这个目的。
设备之间通过端口来进行通信。
控制台输出
VGA:Video Graphics Array, 视频图形阵列。
执行
make qemu时,将在执行此命令的终端和 qemu 自己的终端输出。为什么?
答:JOS 内核被设置为把它自己的 console 输出不仅输出到虚拟 VGA 显示器(就是调用此命令的控制台), 还输出到模拟 PC 的虚拟串口,也就是 QEMU 自己的标准输出。类似地,内核不仅接受键盘输入,还接受串口输入。也就是两个窗口都可以输入。
-
解释
printf.c和console.c之间的接口。console.c导出了那些函数?printf.c是怎样使用它们的?答: 如
console.h所示,导出了下列函数:void cons_init(void); int cons_getc(void); void kbd_intr(void); // irq 1 void serial_intr(void); // irq 4
printf.c中最终暴露出来的是cprintf函数。调用链是printf.c:cprintf->vcprintf->putch->cputchar->
console.c:cputchar->cons_putc -
解释
console.c中的以下代码:1 if (crt_pos >= CRT_SIZE) { 2 int i; 3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t)); 4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++) 5 crt_buf[i] = 0x0700 | ' '; 6 crt_pos -= CRT_COLS; 7 }
答:
/* * 当前行光标达到了预定的显示阵列的最大容量,就腾出一行空间。 * 注意这里没有显式进行动态内存调整,还是利用原来的数组, * 可以看做是显示窗口向下滑了一行。 * */ if (crt_pos >= CRT_SIZE) { int i; // 三个参数 1 缓冲区起始位置 // 2 缓冲区起始位置 + 一行宽度 // 3 (最大容量 - 一行宽度) * 无符号 short 型字节数 // 把从第二行开始到最后一行的内容复制到第一行开始的位置,并把最后一行清空。 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; // 腾出一行的空间 }
-
跟踪以下代码:
int x = 1, y = 3, z = 4; cprintf("x %d, y %x, z %d\n", x, y, z);
- 在
cprintf的调用过程中,fmt指向了什么?ap指向了什么? - 以执行顺序列出与
cons_putc, va_arg, vcprintf的所有调用。对于cons_putc, 把参数也列出来。对于va_arg, 把ap在调用函数之前之后的所指列出来。对于vcprintf, 把它两个参数值列出来。
- 在
// vprintfmt
// (unsigned) octal
case 'o':
// Replace this with your code.
num = getuint(&ap,lflag);
base = 8;
goto number;内核监控函数:打印一个栈的backtrace: 当前执行点调用call指令时保存起来的指令指针寄存器 (IP).
练习 9 确定内核是如何初始化栈的,栈在内存位置是哪里?内核是如何为栈保留空间的?栈指针寄存器初始化的时候指向了这个保存区域的哪一端?
寄存器信息
esp, stack pointer, 栈指针寄存器 指向当前栈的最低位置。在其位置以下的的内存都是空闲的。把一个值 push 进栈中,需要减少栈指针,然后把数据写入栈指针指向的位置。esp 的值永远可以被 4 整除。一些指令,如 call, 与栈指针寄存器牢牢地绑定在一起。也就是说,esp 可以看做是"硬件相关"的。
ebp, base pointer, 基址指针 与 esp 相反,是"软件相关"的。在进入 C 函数时,函数的序言 (prologue) 代码把之前的函数的 ebp 的值压入栈中保存起来,然后把之前的 esp 的值复制到当前的 ebp 中,用于当前函数。如果程序中的所有函数都遵守这一约定,那么在程序执行的任意时刻,就可以顺着已保存的 ebp 的值,确定哪些函数调用导致程序执行至此函数。 这个功能非常有用,比如当一个函数由于传入错误的参数导致执行参数校验的assert失败了,
而你又不知道是哪个函数传进来的,那么就可以通过 ebp 来找到目标。stack backtrace可以帮你找到罪魁祸首。
分析
以一个最简单的函数调用为例:
#include <stdio.h>
test3(){}
test2(){
test3();
}
test1(){
test2();
}
int main(){test1();}用命令 gcc -S -O0 -m32 -o test.s test.c 将其编译为 test.s:
test3:
pushl %ebp
movl %esp, %ebp
popl %ebp
ret
test2:
pushl %ebp
movl %esp, %ebp
call test3
popl %ebp
ret // 怎样知道返回到哪里?通常是 call 指令执行时会把下一条指令的地址保存到 eax. https://zhuanlan.zhihu.com/p/24265088
test1:
pushl %ebp // 存基址
movl %esp, %ebp // 用栈指针更新基址
call test2 // 函数调用,返回。esp 在调用和返回时自动更新。
popl %ebp // 弹出 ebp, 用于当前函数的剩余执行
ret
main:
pushl %ebp // 把当前栈的最低位置保存
movl %esp, %ebp // 把
call test1
movl $0, %eax
popl %ebp
ret
可以看出,考虑到每次调用其他函数,返回值后需要继续执行代码,所以需要将 ebp 临时保存起来 (esp 跟随 cpu, 不用操心). 而所调用函数的基址指针值就是当前函数的栈指针,所以要把栈指针复制给基址指针。进入子函数,栈指针自动从 0 开始,基址指针从刚刚传入的父函数的栈指针开始。调用结束,从哪里继续执行?从之前保存的 ebp 处继续执行,所以 pop 即可。
练习 10 再熟悉下 x86 的调用约定,找到 obj/kern/kernel.asm 中的 test_backtrace 的地址,打上断点,看看每次内核启动之后执行到此发生了什么。每次test_backtrace的递归压入栈中多少个 32 位的 word ? 这些 word 是什么?
void
test_backtrace(int x)
{
f0100040: 55 push %ebp // 保存基址
f0100041: 89 e5 mov %esp,%ebp // 把基址刷新为栈指针
f0100043: 53 push %ebx // 上个函数返回地址入栈---> 这里可以感受到,事实上,函数返回地址 也可以看作为参数---每个函数调用,都有一个隐含的参数!
f0100044: 83 ec 0c sub $0xc,%esp // 申请 12byte/4int = 3 个整数的栈空间。
f0100047: 8b 5d 08 mov 0x8(%ebp),%ebx // ebp+2int 处的值赋给 ebx.
// 从上个函数调用本函数,bp 附近的栈,向增长方向,内容是 :
// 传给本函数的参数->上个函数返回地址->本函数内保存的 bp 基址。
// 所以当前 ebp 指向的值是刚刚保存的 ebp 的值
// ebp+1int 处的值是上个函数返回地址
// ebp+2int 处的值是传给本函数的(最后一个)参数
cprintf("entering test_backtrace %d\n", x);
f010004a: 53 push %ebx // 整数参数 x 入栈
f010004b: 68 a0 18 10 f0 push $0xf01018a0 // 字符串参数入栈
代码:
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
cprintf("Stack backtrace:\n");
uint32_t *ebp =(uint32_t *) read_ebp();
while(0!=ebp)
{
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",ebp,ebp[1],ebp[2],ebp[3],ebp[4],ebp[5],ebp[6],ebp[7]);
ebp=(uint32_t *)*ebp;
}
return 0;
}练习 11
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
cprintf("Stack backtrace:\n");
uint32_t *ebp =(uint32_t *) read_ebp();
while(0!=ebp)
{
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",ebp,ebp[1],ebp[2],ebp[3],ebp[4],ebp[5],ebp[6],ebp[7]);
ebp=(uint32_t *)*ebp;
}
return 0;
}练习 12
参考代码及其注释.
// monitor.c
static struct Command commands[] = {
{ "help", "Display this list of commands", mon_help },
{ "kerninfo", "Display information about the kernel", mon_kerninfo },
{"backtrace", "print information for each stack frame", mon_backtrace},
};
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
cprintf("Stack backtrace:\n");
uint32_t *ebp =(uint32_t *) read_ebp();
while(0!=ebp)
{
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",ebp,ebp[1],ebp[2],ebp[3],ebp[4],ebp[5],ebp[6],ebp[7]);
struct Eipdebuginfo info;
debuginfo_eip(ebp[1],&info);
cprintf(" %s:%d: %.*s+%d\n",info.eip_file, info.eip_line, info.eip_fn_namelen,info.eip_fn_name,(ebp[1]-info.eip_fn_addr)/8);
ebp=(uint32_t *)*ebp;
}
return 0;
}
// kdebug.c
// debuginfo_eip:
...
// Your code here.
stab_binsearch(stabs,&lline,&rline,N_SLINE,addr);
if (lline <= rline) {
info->eip_line = stabs[lline].n_desc;
}else{
info->eip_line = -1;
}
...输出:
Type 'help' for a list of commands.
K> backtrace
Stack backtrace:
ebp f010ff68 eip f0100908 args 00000001 f010ff80 00000000 f010ffc8 f0112540
kern/monitor.c:133: monitor+32
ebp f010ffd8 eip f01000e1 args 00000000 00001aac 00000640 00000000 00000000
kern/init.c:43: i386_init+9
ebp f010fff8 eip f010003e args 00111021 00000000 00000000 00000000 00000000
kern/entry.S:83: <unknown>+0
K>
最终测试:
...
make[1]: Leaving directory '/root/6828/lab1'
running JOS: (1.5s)
printf: OK
backtrace count: OK
backtrace arguments: OK
backtrace symbols: OK
backtrace lines: OK
Score: 50/50
