MIT 6.S081 xv6调试不完全指北
前言
今晚在实验室摸鱼做6.S081的Lab3 Allocator,并立下flag,改掉一个bug就拍死一只在身边飞的蚊子。在击杀8只蚊子拿到Legendary后仍然没能通过usertest,人已原地裂解开来。遂早退实验室滚回宿舍,捡起自己已经两年没写的blog,码点自己用vscode调试xv6的心得和小tips,如果对同样在码xv6但无法忍受gdb调试界面的小伙伴们有帮助那就太好了,积点功德,但愿明天能通过test,少打几只蚊子(
还是从直接用gdb调试说起
刚开始码lab时,我想很多人第一反应和我是一样的:我的程序是在程序上跑的,那我该如何调试我的程序?
google之可以找到答案:https://stackoverflow.com/questions/10534798/debugging-user-code-on-xv6-with-gdb
但实际执行过程有点不同,拿我个人写的sleep.c来说吧,代码如下:
#include "kernel/types.h"
#include "user.h" int parse_int(const char* arg) {
const char* p = arg;
for ( ; *p ; p++ ) {
if ( *p < '0' || *p > '9' ) {
return -1;
}
}
return atoi(arg);
} int main(int argc,char** argv) {
int time;
if (argc != 2) {
printf("you must input one argument only\n");
exit(0);
} time = parse_int(argv[1]);
if (time < 0) {
printf("error argument : %s\n",argv[1]);
exit(0);
}
sleep(time);
exit(0);
}
函数parse_int的作用是检查我们输入的参数(睡眠的时间)是否包括除了数字以外的东西。编写好之后,在makefile中把我们写好的sleep.c加进去:
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
........
$U/_kalloctest\
$U/_bcachetest\
$U/_alloctest\
$U/_bigfile\
$U/_sleep\
执行 make fs.img,sleep.c就会被编译成elf文件_sleep,并保存在xv6的文件系统中。
接下来我们打开一个窗口,输入 make qemu-gdb,qemu会卡住,等待gdb与他连接。
注意,MIT 6.S081 2019提供的xv6采用的指令集是riscv,因此我们虚拟机上针对x86指令集的gdb可能无法较好的调试。我们需要用交叉编译工具来编译xv6,并用交叉编译工具提供的gdb来调试。交叉编译工具在课程主页上有提供(但我找不到链接到哪儿去了)。我的虚拟机已经下载了完整的交叉编译链,并且环境变量也已经设置完毕。因此我只需要在makefile中添加下面一行:
gdb:
riscv64-unknown-elf-gdb kernel/kernel
在另一个窗口执行make gdb,即可调用专用于riscv的gdb(riscv64-unknown-elf-gdb),调试内核文件kernel/kernel。
接下来的操作其实与stackoverflow上面的高赞回答几乎一致了:
ms@ubuntu:~/public/MIT 6.S081/Lab4/xv6-riscv-fall19$ make gdb
riscv64-unknown-elf-gdb kernel/kernel
GNU gdb (GDB) 9.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-unknown-elf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>. For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from kernel/kernel...
The target architecture is assumed to be riscv:rv64
0x0000000000001000 in ?? ()
(gdb) file user/_sleep
Reading symbols from user/_sleep...
(gdb) b parse_int
Breakpoint 1 at 0x0: file user/sleep.c, line 6.
(gdb) c
我们已经在sleep.c上打了断点。按c执行到断点处:
(gdb) file user/_sleep
Reading symbols from user/_sleep...
(gdb) b parse_int
Breakpoint 1 at 0x0: file user/sleep.c, line 6.
(gdb) c
Continuing. Breakpoint 1, parse_int (
arg=0x505050505050505 <error: Cannot access memory at address 0x505050505050505>)
at user/sleep.c:6
6 for ( ; *p ; p++ ) {
(gdb)
这程序输出?wtf ?我们xv6的界面还没有提示shell启动,为什么就跳转到了这个函数上了?
不急,我们先看看pc指针的值:
Breakpoint 1, parse_int (
arg=0x505050505050505 <error: Cannot access memory at address 0x505050505050505>)
at user/sleep.c:6
6 for ( ; *p ; p++ ) {
(gdb) info reg pc
pc 0x0 0x0 <parse_int>
(gdb)
pc指向0x0,也就是NULL,这个地址很明显是一个虚地址。而我们在parse_int上打下的断点,地址也是在0x0处。其实看到这里应该你应该已经猜到,gdb很可能就是在监视pc值,当pc值等于断点值时断点就会被触发。其实这个断点触发是因为内核加载完成后启动的第一个用户程序,具体代码在kernel/proc.c中的userinit.c中:
// Set up first user process.
void
userinit(void)
{
struct proc *p;
p = allocproc(); // xv6的第一个进程,其pid = 1
initproc = p;
uvminit(p->pagetable, initcode, sizeof(initcode)); // 第一个进程的代码段就是proc.c下的initcode,将这段代码的虚实映射关系添加到用户进程页表中
p->sz = PGSIZE;
p->tf->epc = 0; // 设定用户进程的pc指针初始值为0,这就是sleep.c中断点被触发的原因
p->tf->sp = PGSIZE;
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
p->state = RUNNABLE; // 该进程等待调度
release(&p->lock);
}
你只需要知道,xv6会在内核加载完毕后创建第一个进程,第一个进程的代码段是proc.c下的initcode数组,程序入口地址为0x0。当这个进程被调度时,pc指针被设为0,触发了我们打在sleep.c中的断点。这个时候断点虽然被触发,但程序并没有执行到我们想要的地方,仅仅是pc值正好与断点值相同而已。
进一步讨论
下面我们提出一个问题:
1) 从上面的讨论来看,gdb只是在监测pc指针。以及一些其他寄存器(例如说堆栈指针sp、其他的用户可访问寄存器)。那么为什么我们设断点b parse_int, gdb就可以知道断点打在0x0处?为什么gdb可以告诉我们我们的变量值?
为了搞懂这个问题,我们需要对elf文件有一个简单的了解。我们知道,代码的虚拟地址是在编译(链接)期生成的,而代码编译后的结果一般是一个ELF(Executable Linkable Format)文件。ELF文件记录了我们代码中每个函数的虚拟地址,此外还会有一些其他有助于我们的信息。我们可以使用指令查看一下user/_sleep这个ELF文件的格式。新开一个终端,输入命令readelf -a user/_sleep
1 ms@ubuntu:~/public/MIT 6.S081/Lab4/xv6-riscv-fall19$ readelf -a user/_sleep
2 ELF 头:
3 Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
4 类别: ELF64
5 数据: 2 补码,小端序 (little endian)
6 版本: 1 (current)
7 OS/ABI: UNIX - System V
8 ABI 版本: 0
9 类型: EXEC (可执行文件)
10 系统架构: RISC-V
11 版本: 0x1
12 入口点地址: 0x3a
13 程序头起点: 64 (bytes into file)
14 Start of section headers: 22520 (bytes into file)
15 标志: 0x5, RVC, double-float ABI
16 本头的大小: 64 (字节)
17 程序头大小: 56 (字节)
18 Number of program headers: 1
19 节头大小: 64 (字节)
20 节头数量: 18
21 字符串表索引节头: 17
22
23 节头:
24 [号] 名称 类型 地址 偏移量
25 大小 全体大小 旗标 链接 信息 对齐
26 [ 0] NULL 0000000000000000 00000000
27 0000000000000000 0000000000000000 0 0 0
28 [ 1] .text PROGBITS 0000000000000000 00000078
29 0000000000000834 0000000000000000 WAX 0 0 2
30 [ 2] .rodata PROGBITS 0000000000000838 000008b0
31 0000000000000059 0000000000000000 A 0 0 8
32 [ 3] .sbss NOBITS 0000000000000898 00000909
33 0000000000000008 0000000000000000 WA 0 0 8
34 [ 4] .bss NOBITS 00000000000008a0 00000909
35 0000000000000010 0000000000000000 WA 0 0 8
36 [ 5] .comment PROGBITS 0000000000000000 00000909
37 0000000000000012 0000000000000001 MS 0 0 1
38 [ 6] .riscv.attributes LOPROC+0x3 0000000000000000 0000091b
39 0000000000000035 0000000000000000 0 0 1
40 [ 7] .debug_aranges PROGBITS 0000000000000000 00000950
41 00000000000000f0 0000000000000000 0 0 16
42 [ 8] .debug_info PROGBITS 0000000000000000 00000a40
43 0000000000000ea7 0000000000000000 0 0 1
44 [ 9] .debug_abbrev PROGBITS 0000000000000000 000018e7
45 00000000000005ab 0000000000000000 0 0 1
46 [10] .debug_line PROGBITS 0000000000000000 00001e92
47 000000000000133c 0000000000000000 0 0 1
48 [11] .debug_frame PROGBITS 0000000000000000 000031d0
49 0000000000000488 0000000000000000 0 0 8
50 [12] .debug_str PROGBITS 0000000000000000 00003658
51 00000000000002d0 0000000000000001 MS 0 0 1
52 [13] .debug_loc PROGBITS 0000000000000000 00003928
53 0000000000001578 0000000000000000 0 0 1
54 [14] .debug_ranges PROGBITS 0000000000000000 00004ea0
55 0000000000000080 0000000000000000 0 0 1
56 [15] .symtab SYMTAB 0000000000000000 00004f20
57 00000000000006a8 0000000000000018 16 24 8
58 [16] .strtab STRTAB 0000000000000000 000055c8
59 000000000000017b 0000000000000000 0 0 1
60 [17] .shstrtab STRTAB 0000000000000000 00005743
61 00000000000000b5 0000000000000000 0 0 1
62 Key to Flags:
63 W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
64 L (link order), O (extra OS processing required), G (group), T (TLS),
65 C (compressed), x (unknown), o (OS specific), E (exclude),
66 p (processor specific)
67
68 There are no section groups in this file.
69
70 程序头:
71 Type Offset VirtAddr PhysAddr
72 FileSiz MemSiz Flags Align
73 LOAD 0x0000000000000078 0x0000000000000000 0x0000000000000000
74 0x0000000000000891 0x00000000000008b0 RWE 0x8
75
76 Section to Segment mapping:
77 段节...
78 00 .text .rodata .sbss .bss
79
80 There is no dynamic section in this file.
81
82 该文件中没有重定位信息。
83
84 The decoding of unwind sections for machine type RISC-V is not currently supported.
85
86 Symbol table '.symtab' contains 71 entries:
87 Num: Value Size Type Bind Vis Ndx Name
88 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
89 1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
90 2: 0000000000000838 0 SECTION LOCAL DEFAULT 2
91 3: 0000000000000898 0 SECTION LOCAL DEFAULT 3
92 4: 00000000000008a0 0 SECTION LOCAL DEFAULT 4
93 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
94 6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
95 7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
96 8: 0000000000000000 0 SECTION LOCAL DEFAULT 8
97 9: 0000000000000000 0 SECTION LOCAL DEFAULT 9
98 10: 0000000000000000 0 SECTION LOCAL DEFAULT 10
99 11: 0000000000000000 0 SECTION LOCAL DEFAULT 11
100 12: 0000000000000000 0 SECTION LOCAL DEFAULT 12
101 13: 0000000000000000 0 SECTION LOCAL DEFAULT 13
102 14: 0000000000000000 0 SECTION LOCAL DEFAULT 14
103 15: 0000000000000000 0 FILE LOCAL DEFAULT ABS sleep.c
104 16: 0000000000000000 0 FILE LOCAL DEFAULT ABS ulib.c
105 17: 0000000000000000 0 FILE LOCAL DEFAULT ABS printf.c
106 18: 00000000000003b8 34 FUNC LOCAL DEFAULT 1 putc
107 19: 00000000000003da 170 FUNC LOCAL DEFAULT 1 printint
108 20: 0000000000000880 17 OBJECT LOCAL DEFAULT 2 digits
109 21: 0000000000000000 0 FILE LOCAL DEFAULT ABS umalloc.c
110 22: 0000000000000898 8 OBJECT LOCAL DEFAULT 3 freep
111 23: 00000000000008a0 16 OBJECT LOCAL DEFAULT 4 base
112 24: 00000000000000a2 28 FUNC GLOBAL DEFAULT 1 strcpy
113 25: 0000000000000690 54 FUNC GLOBAL DEFAULT 1 printf
114 26: 0000000000001091 0 NOTYPE GLOBAL DEFAULT ABS __global_pointer$
115 27: 000000000000025e 88 FUNC GLOBAL DEFAULT 1 memmove
116 28: 0000000000000358 0 NOTYPE GLOBAL DEFAULT 1 mknod
117 29: 000000000000015a 116 FUNC GLOBAL DEFAULT 1 gets
118 30: 0000000000000891 0 NOTYPE GLOBAL DEFAULT 2 __SDATA_BEGIN__
119 31: 0000000000000390 0 NOTYPE GLOBAL DEFAULT 1 getpid
120 32: 00000000000002f0 24 FUNC GLOBAL DEFAULT 1 memcpy
121 33: 000000000000074e 230 FUNC GLOBAL DEFAULT 1 malloc
122 34: 00000000000003a0 0 NOTYPE GLOBAL DEFAULT 1 sleep
123 35: 0000000000000320 0 NOTYPE GLOBAL DEFAULT 1 pipe
124 36: 0000000000000330 0 NOTYPE GLOBAL DEFAULT 1 write
125 37: 0000000000000368 0 NOTYPE GLOBAL DEFAULT 1 fstat
126 38: 0000000000000662 46 FUNC GLOBAL DEFAULT 1 fprintf
127 39: 0000000000000340 0 NOTYPE GLOBAL DEFAULT 1 kill
128 40: 0000000000000484 478 FUNC GLOBAL DEFAULT 1 vprintf
129 41: 0000000000000380 0 NOTYPE GLOBAL DEFAULT 1 chdir
130 42: 0000000000000348 0 NOTYPE GLOBAL DEFAULT 1 exec
131 43: 0000000000000318 0 NOTYPE GLOBAL DEFAULT 1 wait
132 44: 0000000000000000 58 FUNC GLOBAL DEFAULT 1 parse_int
133 45: 0000000000000328 0 NOTYPE GLOBAL DEFAULT 1 read
134 46: 0000000000000360 0 NOTYPE GLOBAL DEFAULT 1 unlink
135 47: 00000000000002b6 58 FUNC GLOBAL DEFAULT 1 memcmp
136 48: 0000000000000308 0 NOTYPE GLOBAL DEFAULT 1 fork
137 49: 00000000000008b0 0 NOTYPE GLOBAL DEFAULT 4 __BSS_END__
138 50: 0000000000000398 0 NOTYPE GLOBAL DEFAULT 1 sbrk
139 51: 00000000000003a8 0 NOTYPE GLOBAL DEFAULT 1 uptime
140 52: 0000000000000891 0 NOTYPE GLOBAL DEFAULT 3 __bss_start
141 53: 0000000000000114 34 FUNC GLOBAL DEFAULT 1 memset
142 54: 000000000000003a 104 FUNC GLOBAL DEFAULT 1 main
143 55: 00000000000003b0 0 NOTYPE GLOBAL DEFAULT 1 ntas
144 56: 00000000000000be 44 FUNC GLOBAL DEFAULT 1 strcmp
145 57: 0000000000000388 0 NOTYPE GLOBAL DEFAULT 1 dup
146 58: 0000000000000891 0 NOTYPE GLOBAL DEFAULT 2 __DATA_BEGIN__
147 59: 00000000000001ce 70 FUNC GLOBAL DEFAULT 1 stat
148 60: 0000000000000891 0 NOTYPE GLOBAL DEFAULT 2 _edata
149 61: 00000000000008b0 0 NOTYPE GLOBAL DEFAULT 4 _end
150 62: 0000000000000370 0 NOTYPE GLOBAL DEFAULT 1 link
151 63: 0000000000000310 0 NOTYPE GLOBAL DEFAULT 1 exit
152 64: 0000000000000214 74 FUNC GLOBAL DEFAULT 1 atoi
153 65: 00000000000000ea 42 FUNC GLOBAL DEFAULT 1 strlen
154 66: 0000000000000350 0 NOTYPE GLOBAL DEFAULT 1 open
155 67: 0000000000000136 36 FUNC GLOBAL DEFAULT 1 strchr
156 68: 0000000000000378 0 NOTYPE GLOBAL DEFAULT 1 mkdir
157 69: 0000000000000338 0 NOTYPE GLOBAL DEFAULT 1 close
158 70: 00000000000006c6 136 FUNC GLOBAL DEFAULT 1 free
159
160 No version information found in this file.
readelf
可以看到编译后的结果中有不少.debug段。这些程序段为我们debug提供辅助。在编译时如果提供了调试选项 -g,那么编译后就会给我们提供这些辅助信息。这些辅助信息是我们程序中的符号。gdb可以监控pc、sp、各类寄存器的值,配合这些符号,就可以将这些信息“翻译”为我们想要看的变量。
举个不恰当的例子。某个函数f(int a,int b)那么函数调用时,将会执行两次 sp -= sizeof(int)的操作,将两个int压到栈上。当我们用gdb调试时,gdb根据sp、pc值,结合符号表可知此时有两个int类型变量a和b正在被调用,于是将sp + sizeof(int)处的地址解释为int b,将sp + 2 * sizeof(int)解释为int a,并展示在gdb前端界面上。执行bt查看堆栈时,gdb也是根据sp,通过查阅符号表,将堆栈中的函数地址解释为我们的函数名,并展示在gdb前端上。
我们曾经输入过命令 file user/_sleep,其目的就是告诉gdb,加载_sleep的符号表,用它的符号表去解释你看到的东西!
你可以尝试一下在其他地方打下断点:
(gdb) b sleep
Breakpoint 2 at 0x3a0: file user/usys.S, line 100.
(gdb) b sys_close
Function "sys_close" not defined.
在_sleep的符号表中可以看到sleep的段,即ELF文件_sleep包含了sleep函数的符号信息,因此这个断点可以被准确打下。
sys_close的断点是无法打下来的,有时它还会提示你“Cannot access address at XXXX”。原因也很明显,_sleep的符号表中没有sys_close函数的记录。实际上这个函数的符号存放在kernel/kernel的符号表中。除非让gdb加载kernel/kernel的符号表,否则gdb就根本不知道这个函数到底在哪里。
这个时候你也可以理解,为什么parse_int的函数参数这么奇怪了。因为这个时候执行的根本不是_sleep,拿_sleep的符号表去解释这些信息,肯定是错误的。
(gdb) c
Continuing. Breakpoint 1, parse_int (
arg=0x1 <parse_int+1> "\021\006\354\"\350&\344", <incomplete sequence \340>)
at user/sleep.c:6
6 for ( ; *p ; p++ ) {
(gdb) c
Continuing. Breakpoint 1, parse_int (arg=0x1460 "") at user/sleep.c:6
6 for ( ; *p ; p++ ) {
(gdb)
Continuing.
按了几次c后,终于出现了我们的shell界面。
随后,在xv6的shell输入命令 sleep 10。可能还需要按几次c,才能到达真正的parse_int函数的断点。
这个时候我们已经可以调试parse_int了,enjoy it!
调试xv6的第一个进程
虽然我们已经很好的解释了为什么parse_int的断点被触发了,但上述内容并不是我们的重点,下面来我们的重头戏之一:让我们看看initcode那堆东西到底做了什么,即xv6的第一个用户进程到底做了什么!
我们直接将断点打在0x0上,查看汇编代码,si调试:
......
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from kernel/kernel...
The target architecture is assumed to be riscv:rv64
0x0000000000001000 in ?? ()
(gdb) b *0x0
Breakpoint 1 at 0x0
(gdb) c
Continuing. Breakpoint 1, 0x0000000000000000 in ?? ()
=> 0x0000000000000000: 17 05 00 00 auipc a0,0x0
(gdb) si
0x0000000000000004 in ?? ()
=> 0x0000000000000004: 13 05 05 02 addi a0,a0,32
(gdb)
0x0000000000000008 in ?? ()
=> 0x0000000000000008: 97 05 00 00 auipc a1,0x0
(gdb)
0x000000000000000c in ?? ()
=> 0x000000000000000c: 93 85 05 02 addi a1,a1,32
(gdb)
0x0000000000000010 in ?? ()
=> 0x0000000000000010: 9d 48 li a7,7
(gdb)
0x0000000000000012 in ?? ()
=> 0x0000000000000012: 73 00 00 00 ecall
(gdb)
系统调用detected,编号为7,查看kerne/syscall.h可知,编号为7的系统调用是SYS_EXEC。我们先把断点1删掉避免gdb因为断点崩溃掉,然后再exec上打断点:
0x0000000000000010 in ?? ()
=> 0x0000000000000010: 9d 48 li a7,7
(gdb)
0x0000000000000012 in ?? ()
=> 0x0000000000000012: 73 00 00 00 ecall
(gdb) delete 1
(gdb) b exec
Cannot access memory at address 0x80004da8
(gdb)
嗯,失败了...不过可以理解,因为这个时候进程在执行用户程序,而exec的代码在内核区,用户区自然不能去访问内核区的代码了。我们老老实实si单步调试过ecall,直到CPU进入内核态后再看看能不能打下这个断点:
=> 0x0000000000000010: 9d 48 li a7,7
(gdb)
0x0000000000000012 in ?? ()
=> 0x0000000000000012: 73 00 00 00 ecall
(gdb) delete 1
(gdb) b exec
Cannot access memory at address 0x80004da8
(gdb) si
0x0000003ffffff004 in ?? ()
=> 0x0000003ffffff004: 23 34 15 02 sd ra,40(a0)
(gdb)
0x0000003ffffff008 in ?? ()
=> 0x0000003ffffff008: 23 38 25 02 sd sp,48(a0)
(gdb) ..... 0x0000003ffffff07e in ?? ()
=> 0x0000003ffffff07e: 83 32 05 01 ld t0,16(a0)
(gdb)
0x0000003ffffff082 in ?? ()
=> 0x0000003ffffff082: 03 33 05 00 ld t1,0(a0)
(gdb)
0x0000003ffffff086 in ?? ()
=> 0x0000003ffffff086: 73 10 03 18 csrw satp,t1
(gdb) b exec
Cannot access memory at address 0x80004da8
(gdb) si
0x0000003ffffff08a in ?? ()
=> 0x0000003ffffff08a: 73 00 00 12 sfence.vma
(gdb) b exec
Breakpoint 2 at 0x80004da8: file kernel/exec.c, line 14.
(gdb) c
在执行完csrw satp, t1后,我们终于能在exec上打下断点了!不过不要打下这个断点,我们继续一步一步调试,代码会进入到kernel/trap.c中:
0x0000003ffffff086 in ?? ()
=> 0x0000003ffffff086: 73 10 03 18 csrw satp,t1
(gdb)
0x0000003ffffff08a in ?? ()
=> 0x0000003ffffff08a: 73 00 00 12 sfence.vma
(gdb)
0x0000003ffffff08e in ?? ()
=> 0x0000003ffffff08e: 82 82 jr t0
(gdb)
usertrap () at kernel/trap.c:41
41 {
(gdb) n
44 if((r_sstatus() & SSTATUS_SPP) != 0)
(gdb)
54 return x;
(gdb)
继续调试,终于我们看到了系统调用总入口,按下s进入系统调用总入口syscall,然后进入我们想要看的系统调用sys_exec中。
(gdb)
56 if(r_scause() == 8){
(gdb)
224 return x;
(gdb)
59 if(p->killed)
(gdb)
64 p->tf->epc += 4;
(gdb)
68 intr_on();
(gdb)
70 syscall();
(gdb) s
syscall () at kernel/syscall.c:138
138 struct proc *p = myproc();
(gdb) n
140 num = p->tf->a7;
(gdb)
141 if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
(gdb)
142 p->tf->a0 = syscalls[num]();
(gdb) s
sys_exec () at kernel/sysfile.c:419
419 if(argstr(0, path, MAXPATH) < 0 || argaddr(1, &uargv) < 0){
(gdb) n
422 memset(argv, 0, sizeof(argv));
(gdb)
424 if(i >= NELEM(argv)){
(gdb) n
427 if(fetchaddr(uargv+sizeof(uint64)*i, (uint64*)&uarg) < 0){
(gdb)
430 if(uarg == 0){
(gdb)
434 argv[i] = kalloc();
(gdb)
435 if(argv[i] == 0)
(gdb)
437 if(fetchstr(uarg, argv[i], PGSIZE) < 0){
(gdb)
424 if(i >= NELEM(argv)){
(gdb)
427 if(fetchaddr(uargv+sizeof(uint64)*i, (uint64*)&uarg) < 0){
(gdb)
430 if(uarg == 0){
(gdb)
431 argv[i] = 0;
(gdb)
442 int ret = exec(path, argv);
(gdb) p path
$1 = "/init\000\000\000 \337\377\377?\000\000\000\340\061\001\200\000\000\000\000 \337\377\377?\000\000\000@\337\377\377?\000\000\000\246\n\000\200\000\000\000\000\330\061\001\200\000\000\000\000\310\061\001\200\000\000\000\000`\337\377\377?\000\000\000\034\061\000\200\000\000\000\000\310\061\001\200", '\000' <repeats 12 times>, "\220\337\377\377?\000\000\000\256?\000\200\000\000\000\000\220\337\377\377?\0
OK,exec的东西我们已经可以知道了,它要将init这个程序“装入”到内核中。这个程序对应的C代码在user/init.c下,对应的ELF文件为user/_init。我们不再仔细的看exec了,后面我可能会单独写一篇blog细讲ELF文件和exec(不过大概率无限咕咕咕),直接单行跳过,从sys_exec中跳出,回到了trap.c的usertrap()函数中,下一步就会从用户trap里返回用户态:
442 int ret = exec(path, argv);
(gdb) p path
$1 = "/init\000\000\000 \337\377\377?\000\000\000\340\061\001\200\000\000\000\000 \337\377\377?\000\000\000@\337\377\377?\000\000\000\246\n\000\200\000\000\000\000\330\061\001\200\000\000\000\000\310\061\001\200\000\000\000\000`\337\377\377?\000\000\000\034\061\000\200\000\000\000\000\310\061\001\200", '\000' <repeats 12 times>, "\220\337\377\377?\000\000\000\256?\000\200\000\000\000\000\220\337\377\377?\000\000\000\220\337\377\377?\000\000"
(gdb) n
444 for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
(gdb) n
445 kfree(argv[i]);
(gdb)
444 for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
(gdb)
447 return ret;
(gdb) n
usertrap () at kernel/trap.c:79
79 if(p->killed)
(gdb) n
86 usertrapret();
usertrap () at kernel/trap.c:79
79 if(p->killed)
(gdb)
86 usertrapret();
(gdb) s
usertrapret () at kernel/trap.c:95
95 struct proc *p = myproc();
(gdb) n
99 intr_off();
(gdb)
166 asm volatile("csrw stvec, %0" : : "r" (x));
(gdb)
106 p->tf->kernel_satp = r_satp(); // kernel page table
(gdb)
202 return x;
(gdb)
107 p->tf->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
(gdb)
108 p->tf->kernel_trap = (uint64)usertrap;
(gdb)
109 p->tf->kernel_hartid = r_tp(); // hartid for cpuid()
(gdb)
297 return x;
(gdb)
115 unsigned long x = r_sstatus();
(gdb)
116 x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
(gdb)
60 asm volatile("csrw sstatus, %0" : : "r" (x));
(gdb)
120 asm volatile("csrw sepc, %0" : : "r" (x));
(gdb)
130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
这个诡异的函数指针和函数调用,我们不能用n,因为很可能找不到对应的C代码,我们用si苟过去:
130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb) si
0x0000000080002814 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x0000000080002816 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x000000008000281a 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x000000008000281e 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x0000000080002820 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x0000000080002822 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x0000000080002824 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x0000000080002826 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x0000000080002828 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x000000008000282c 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x000000008000282e 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x0000000080002830 130 ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
(gdb)
0x0000003ffffff090 in ?? ()
=> 0x0000003ffffff090: 73 90 05 18 csrw satp,a1
(gdb)
0x0000003ffffff094 in ?? ()
=> 0x0000003ffffff094: 73 00 00 12 sfence.vma
(gdb)
0x0000003ffffff098 in ?? ()
=> 0x0000003ffffff098: 83 32 05 07 ld t0,112(a0)
(gdb)
0x0000003ffffff09c in ?? ()
=> 0x0000003ffffff09c: 73 90 02 14 csrw sscratch,t0
后面的汇编代码其实就是trampoline.S下的userret函数。它完成从内核态到用户态的返回。至此系统调用sys_exec的其实在系统调用之前执行的外壳函数(就是ecall那一块的代码),就是其下的uservec函数。userret函数完成从内核态到用户态的返回。至此系统调用sys_exec的流程已经结束。如果你希望看到这段代码回到用户态,还需要重新加载用户态相应的符号表。但用户态代码是initcode,所以你无法观看。不过没关系,掌握了内核符号表与用户程序符号表的切换,你可以随心所欲的调试系统调用,套路都是一样的。
最后我们来看一下init.c的代码:
// init: The initial user-level program #include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fcntl.h" char *argv[] = { "sh", 0 }; int
main(void)
{
int pid, wpid; if(open("console", O_RDWR) < 0){
mknod("console", 1, 1);
open("console", O_RDWR);
}
dup(0); // stdout
dup(0); // stderr for(;;){
printf("init: starting sh\n");
pid = fork();
if(pid < 0){
printf("init: fork failed\n");
exit(1);
}
if(pid == 0){
exec("sh", argv);
printf("init: exec sh failed\n");
exit(1);
}
while((wpid=wait(0)) >= 0 && wpid != pid){
//printf("zombie!\n");
}
}
}
大致意思是打开标准输入(0)、标准输出(1)、标准错误输出(2)对应的终端。由于所有的进程的祖先进程都是这个pid = 1的进程,因此它们都会继承标准输入和标准输出。随后初代进程(怎么这么中二?)fork,自己循环调用wait回收僵尸进程,子进程(即pid = 2的进程)执行sh,即加载我们的shell,这样我们就可以利用shell操作我们的xv6了。
最后我们总结一下xv6的第一个用户程序总流程:
1) xv6成功boot,启动第一个用户程序,初始化代码为initcode,这段initcode写死在了kernel/proc.c中
2) 第一个用户程序(即initcode代码)开始执行,初始指针为0x0。initcode代码仅仅是一行 exec("init"),即将init"装入"到当前进程中。
3) init程序装入后执行fork,父进程pid=1,无限循环调用wait回收僵尸进程,子进程pid=2,调用exec("sh"),即启动shell,打开交互界面
OK,如果你能把这节内容掌握,你就可以自由的在xv6中往返于内核和用户空间了。
用vscode调试xv6
下面进入本blog的第二个重头戏:告别gdb的界面,使用vscode来调试内核!
其实vscode本身仅仅是个编辑器,并不具有调试能力,它所做的不过是和gdb交互,将gdb输出的调试信息重新渲染到界面上而已。
调试xv6,需要用到gdb的remote debug模式,由qemu提供一个GDBstub,gdb需要连接到这个GDBstub上,建议阅读以下文档:http://davis.lbl.gov/Manuals/GDB/gdb_17.html
我们需要给在vscode中为xv6配置相应的launch.json文件:
{
"version": "0.2.0",
"configurations": [
{
"name": "debug xv6",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/kernel/kernel",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"miDebuggerServerAddress": "localhost:26000",
"miDebuggerPath": "/usr/local/bin/riscv64-unknown-elf-gdb",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "pretty printing",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"logging": {
// "engineLogging": true,
// "programOutput": true,
}
}
]
}
program就是在kernel/下的kernel
miDebuggerServerAddress设定为gdbstub的地址(我的机器上一般是localhost:26000,可以查看makefile的输出确定)
miDebuggerPath是我们调试riscv所用的gdb地址
stopAtEntry设定为true时,程序将在入口处触发一次断点,方便我们打新的断点
logging选项控制vscode端调试过程的输出,engineLogging和programOutput是两个比较重要的调试日志,如果调试出现错误,可以将这两个选项设为true,查看日志输出确认问题所在。
配置好上述文件好,先不要启动调试,先打开一个终端,输入make qemu-gdb。在项目根目录下会有一个.gdbinit文件,打开文件可以看到下面的内容:
set confirm off
set architecture riscv:rv64
target remote 127.0.0.1:26000
symbol-file kernel/kernel
set disassemble-next-line auto
.gdbinit文件gdb初始化时的配置文件。当启动gdb时,gdb会自动在根目录下搜索.gdbinit文件,如果有则一定会执行一次其中的配置。这个.gdbinit告诉我们,qemu提供了一个GDBstub(127.0.0.1:26000)。另一台机器启动时可以连接到这个GDBstub上,即可远程调试。
由于我们在vscode中已经设置了target-remote模式,因此在执行vscode中的debug时,(127.0.0.1:26000)的连接会被建立两次,一次由vscode触发,另一次由.gdbinit触发,第二次连接会强行中断第一次连接。因此执行make qemu-gdb后,要将target remote 127.0.0.1:26000这行删去,否则会爆GDBstub错误。
set confirm off
set architecture riscv:rv64 symbol-file kernel/kernel
set disassemble-next-line auto
在vscode中点击调试按钮,程序即可到达内核main的入口:
那么在vscode中怎么切换符号表文件呢?底侧栏有一个“调试控制台”,在其中可以直接输入gdb命令。我们只需要输入 -exec file /user/_sleep,即可切换到_sleep的符号表,现在我们的user/sleep.c下已经可以打断点了!但是如果打断点,一定要在代码侧栏打,不要再调试控制台中用 -exec b func来打,否则vscode会出现异常。
注意我们的断点是红的,说明断点有效。
如果vscode调试提示GDBstub出现问题,基本可以确定时因为gdb的设置出现了问题,可以将launch.json中logging的几个选项置true,然后在底端的“输出”栏看输出日志,定位问题在哪里。
ok,和gdb说f**k off吧!
小Tips
1、后面的Lab会对内核进行魔改,经常会出现page fault。如果没有实现lazy allocator,基本可以确定是代码越界问题。但如果打断点在panic上,只能看到内核态的堆栈,而看不到用户态的堆栈,不利于定位用户代码的问题。这种情况下可以看日志stepc的输出,并在用这个地址打下断点。根据gdb的调试信息,可以确认问题所在的文件和行数。仅当epc真正指向一个有效的函数块内时可以使用。如果出现instruction page fault,那么epc指向的就是一个非法地址,在这个地址上是无法打断点的。
2、使用*(array)@10,可以将指针array解释为数组,并打印后面的10个元素。具体可见这篇blog:https://github.com/Microsoft/vscode-cpptools/issues/172#issuecomment-460063503
后记
熬夜写完后突然想起来,今天晚上离开实验是的时候忘记打卡了.....一个晚上白干.....
后面会慢慢开始更新blog,主要更新自己上的一些公开课(已经完成了的6.824,正在肝的6.828和15-445)的一些笔记。后面会有开题和小论文,下半年留着刷leetcode和背面试八股,能发育的事件已经所剩不多了,加油吧。
个人是半途转行的非科班生,对于技术的见解也会有很多错误,如果有dalao发现错误,还请从评论区指出,万分感谢。
MIT 6.S081 xv6调试不完全指北的更多相关文章
- MIT 6.S081 Lab5 Copy-On-Write Fork
前言 最近绝大多数的空闲时间都拿来锤15-445了,很久没动6.S081.前几天回头看了一下一个月前锤完的Lazy Allocation,自己写的代码几乎都不认识了.......看来总结之类的东西最好 ...
- Python 简单入门指北(一)
Python 简单入门指北(一) Python 是一门非常容易上手的语言,通过查阅资料和教程,也许一晚上就能写出一个简单的爬虫.但 Python 也是一门很难精通的语言,因为简洁的语法背后隐藏了许多黑 ...
- 关于Gevent的使用指北
关于Gevent的使用指北 只是看了入门指南,和一个翻译文档.写一下个人读书心得. 其实看完之后,第一个反映就是asyncio这个系统库,感觉gevent现在所做的一些事情是与asyncio很像的,但 ...
- [转] iOS开发者的Weex伪最佳实践指北
[From] http://www.cocoachina.com/ios/20170601/19404.html 引子 这篇文章是笔者近期关于Weex在iOS端的一些研究和实践心得,和大家一起分享分享 ...
- git宝典—应付日常工作使用足够的指北手册
最近公司gitlab又迁移,一堆git的命令骚操作,然鹅git命令,感觉还是得复习下——其实,git现在界面操作工具蛮多,比如intellij 自带的git操作插件就不错,gitlab github ...
- Python 简单入门指北(二)
Python 简单入门指北(二) 2 函数 2.1 函数是一等公民 一等公民指的是 Python 的函数能够动态创建,能赋值给别的变量,能作为参传给函数,也能作为函数的返回值.总而言之,函数和普通变量 ...
- 可能比文档还详细--VueRouter完全指北
可能比文档还详细--VueRouter完全指北 前言 关于标题,应该算不上是标题党,因为内容真的很多很长很全面.主要是在官网的基础上又详细总结,举例了很多东西.确保所有新人都能理解!所以实际上很多东西 ...
- 关于supervisor的入门指北
关于supervisor的入门指北 在目前这个时间点(2017/07/25),supervisor还是仅支持python2,所以我们要用版本管理pyenv来隔离环境. pyenv 根据官方文档的讲解, ...
- Celery入门指北
Celery入门指北 其实本文就是我看完Celery的官方文档指南的读书笔记.然后由于我的懒,只看完了那些入门指南,原文地址:First Steps with Celery,Next Steps,Us ...
随机推荐
- 谷歌蜂鸟算法对网站seo优化有何影响
http://www.wocaoseo.com/thread-89-1-1.html 谷歌在过去三个月里,非常低调的推出了蜂鸟算法,据谷歌技术员表示,此种方法一出,将影响90%网站的排名, ...
- 深入学习redis 的线程模型
一.redis 的线程模型 redis 内部使用文件事件处理器 file event handler,它是单线程的,所以redis才叫做单线程模型.它采用IO多路复用机制同时监听多个 socket,将 ...
- Redis秒杀实战-微信抢红包-秒杀库存,附案例源码(Jmeter压测)
导读 前二天我写了一篇,Redis高级项目实战(点我直达),SpringBoot整合Redis附源码(点我直达),今天我们来做一下Redis秒杀系统的设计.当然啦,Redis基础知识还不过关的,先去加 ...
- 加载PHP的配置扩展文件
一.在PHP的安装目录中去创建一个 php.ini 的配置文件 复制php.ini-development文件,修改成php.ini文件(注意:不要多加一个空格或标点什么的).其中 php.ini ...
- Lua索引、伪索引、引用
索引:堆栈的索引 伪索引:一个类似于索引,但是有着特殊存储的索引,使用方式和索引一样,看上去像在操作堆栈 引用:LUA_REGISTRYINDEX伪索引下的表的整数键
- POJ - 3851-Wormholes(SPFA判负环)
A friend of yours, an inventor, has built a spaceship recently and wants to explore space with it. D ...
- 跟着兄弟连系统学习Linux-【day02】
day02-20200528 p6.vmvare安装与使用 官网下载安装包,个人学习的时候要求不高,所以不用安装最新版本,用不到那么多的功能,保证稳定版本就好了,然后傻瓜式安装.注意安 ...
- springboot x.x.x RELEASE pom 第一行报错解决办法
springboot x.x.x RELEASE pom 第一行报错解决办法 在pom.xml 文件的properties中加入maven jar插件的版本号 <properties> & ...
- leetcode刷题-64最小路径和
题目 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小. 说明:每次只能向下或者向右移动一步. 示例: 输入:[ [1,3,1], [1,5, ...
- Java单例模式的实现与破坏
单例模式是一种设计模式,是在整个运行过程中只需要产生一个实例.那么怎样去创建呢,以下提供了几种方案. 一.创建单例对象 懒汉式 public class TestSingleton { // 构造方法 ...