Skip to content

Commit dbb181c

Browse files
authored
Merge pull request #2 from dramforever/answers-backtrace
调用栈相关编程题答案
2 parents d66b176 + bfa95a6 commit dbb181c

File tree

3 files changed

+228
-6
lines changed

3 files changed

+228
-6
lines changed

source/chapter1/7exercise.rst

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,83 @@
2323

2424
1. `*` 应用程序在执行过程中,会占用哪些计算机资源?
2525
2. `*` 请用相关工具软件分析并给出应用程序A的代码段/数据段/堆/栈的地址空间范围。
26-
3. `*` 请用分析并给出应用程序C的代码段/数据段/堆/栈的地址空间范围。
26+
3. `*` 请用分析并给出应用程序C的代码段/数据段/堆/栈的地址空间范围。
2727
4. `*` 请结合编译器的知识和编写的应用程序B,说明应用程序B是如何建立调用栈链信息的。
2828
5. `*` 请简要说明应用程序与操作系统的异同之处。
2929
6. `**` 请基于QEMU模拟RISC—V的执行过程和QEMU源代码,说明RISC-V硬件加电后的几条指令在哪里?完成了哪些功能?
3030
7. `*` RISC-V中的SBI的含义和功能是啥?
31-
8. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议?
31+
8. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议?
3232
9. `**` 请简要说明从QEMU模拟的RISC-V计算机加电开始运行到执行应用程序的第一条指令这个阶段的执行过程。
3333
10. `**` 为何应用程序员编写应用时不需要建立栈空间和指定地址空间?
34+
11. `***` 现代的很多编译器生成的代码,默认情况下不再严格保存/恢复栈帧指针。在这个情况下,我们只要编译器提供足够的信息,也可以完成对调用栈的恢复。
35+
36+
我们可以手动阅读汇编代码和栈上的数据,体验一下这个过程。例如,对如下两个互相递归调用的函数:
37+
38+
.. code-block::
39+
40+
void flip(unsigned n) {
41+
if ((n & 1) == 0) {
42+
flip(n >> 1);
43+
} else if ((n & 1) == 1) {
44+
flap(n >> 1);
45+
}
46+
}
47+
48+
void flap(unsigned n) {
49+
if ((n & 1) == 0) {
50+
flip(n >> 1);
51+
} else if ((n & 1) == 1) {
52+
flap(n >> 1);
53+
}
54+
}
55+
56+
在某种编译环境下,编译器产生的代码不包括保存和恢复栈帧指针 ``fp`` 的代码。以下是 GDB 输出的本次运行的时候,这两个函数所在的地址和对应地址指令的反汇编,为了方便阅读节选了重要的控制流和栈操作(省略部分不含栈操作):
57+
58+
.. code-block::
59+
60+
(gdb) disassemble flap
61+
Dump of assembler code for function flap:
62+
0x0000000000010730 <+0>: addi sp,sp,-16 // 唯一入口
63+
0x0000000000010732 <+2>: sd ra,8(sp)
64+
...
65+
0x0000000000010742 <+18>: ld ra,8(sp)
66+
0x0000000000010744 <+20>: addi sp,sp,16
67+
0x0000000000010746 <+22>: ret // 唯一出口
68+
...
69+
0x0000000000010750 <+32>: j 0x10742 <flap+18>
70+
71+
(gdb) disassemble flip
72+
Dump of assembler code for function flip:
73+
0x0000000000010752 <+0>: addi sp,sp,-16 // 唯一入口
74+
0x0000000000010754 <+2>: sd ra,8(sp)
75+
...
76+
0x0000000000010764 <+18>: ld ra,8(sp)
77+
0x0000000000010766 <+20>: addi sp,sp,16
78+
0x0000000000010768 <+22>: ret // 唯一出口
79+
...
80+
0x0000000000010772 <+32>: j 0x10764 <flip+18>
81+
End of assembler dump.
82+
83+
启动这个程序,在运行的时候的某个状态将其打断。此时的 ``pc``, ``sp``, ``ra`` 寄存器的值如下所示。此外,下面还给出了栈顶的部分内容。(为阅读方便,栈上的一些未初始化的垃圾数据用 ``???`` 代替。)
84+
85+
.. code-block::
86+
87+
(gdb) p $pc
88+
$1 = (void (*)()) 0x10752 <flip>
89+
90+
(gdb) p $sp
91+
$2 = (void *) 0x40007f1310
92+
93+
(gdb) p $ra
94+
$3 = (void (*)()) 0x10742 <flap+18>
95+
96+
(gdb) x/6a $sp
97+
0x40007f1310: ??? 0x10750 <flap+32>
98+
0x40007f1320: ??? 0x10772 <flip+32>
99+
0x40007f1330: ??? 0x10764 <flip+18>
100+
101+
根据给出这些信息,调试器可以如何复原出最顶层的几个调用栈信息?假设调试器可以理解编译器生成的汇编代码 [#dwarf]_ 。
102+
34103

35104

36105
实验练习
@@ -92,7 +161,7 @@ lab1 的工作使得我们从硬件世界跳入了软件世界,当看到自己
92161
.. code-block:: rust
93162
94163
// 这段代码输出了 os 内存空间布局,这到这些信息对于编写 os 十分重要
95-
164+
96165
info!(".text [{:#x}, {:#x})", s_text as usize, e_text as usize);
97166
debug!(".rodata [{:#x}, {:#x})", s_rodata as usize, e_rodata as usize);
98167
error!(".data [{:#x}, {:#x})", s_data as usize, e_data as usize);
@@ -111,7 +180,7 @@ lab1 的工作使得我们从硬件世界跳入了软件世界,当看到自己
111180
- 完成实验指导书中的内容并在裸机上实现 ``hello world`` 输出。
112181
- 实现彩色输出宏(只要求可以彩色输出,不要求 log 等级控制,不要求多种颜色)
113182
- 隐形要求
114-
183+
115184
可以关闭内核所有输出。从 lab2 开始要求关闭内核所有输出(如果实现了 log 等级控制,那么这一点自然就实现了)。
116185

117186
- 利用彩色输出宏输出 os 内存空间布局
@@ -154,7 +223,7 @@ challenge: 支持多核,实现多个核的 boot。
154223
tips
155224
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
156225

157-
- 对于 Rust, 可以使用 crate `log <https://docs.rs/log/0.4.14/log/>`_ ,推荐参考 `rCore <https://github.com/rcore-os/rCore/blob/master/kernel/src/logging.rs>`_
226+
- 对于 Rust, 可以使用 crate `log <https://docs.rs/log/0.4.14/log/>`_ ,推荐参考 `rCore <https://github.com/rcore-os/rCore/blob/master/kernel/src/logging.rs>`_
158227
- 对于 C,可以实现不同的函数(注意不推荐多层可变参数解析,有时会出现不稳定情况),也可以参考 `linux printk <https://github.com/torvalds/linux/blob/master/include/linux/printk.h#L312-L385>`_ 使用宏实现代码重用。
159228
- 两种语言都可以使用 ``extern`` 关键字获得在其他文件中定义的符号。
160229

@@ -163,7 +232,7 @@ tips
163232

164233
1. 请学习 gdb 调试工具的使用(这对后续调试很重要),并通过 gdb 简单跟踪从机器加电到跳转到 0x80200000 的简单过程。只需要描述重要的跳转即可,只需要描述在 qemu 上的情况。
165234

166-
2. tips:
235+
2. tips:
167236

168237
- 事实上进入 rustsbi 之后就不需要使用 gdb 调试了。可以直接阅读代码。`rustsbi起始代码 <https://github.com/rustsbi/rustsbi-qemu/blob/main/rustsbi-qemu/src/main.rs#L146>`_ 。
169238
- 可以使用示例代码 Makefile 中的 ``make debug`` 指令。
@@ -185,3 +254,5 @@ tips
185254
- 由于彩色输出不好自动测试,请附正确运行后的截图。
186255
- 完成问答问题。
187256
- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。
257+
258+
.. [#dwarf] 对编译器如何向调试器提供生成的代码的信息,有兴趣可以参阅 `DWARF 规范 <https://dwarfstd.org>`_

source/chapter1/8answer.rst

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,81 @@
4848
4949
5050
2. `***` 实现一个linux应用程序B,能打印出调用栈链信息。(用C或Rust编程)
51+
52+
以使用 GCC 编译的 C 语言程序为例,使用编译参数 ``-fno-omit-frame-pointer`` 的情况下,会保存栈帧指针 ``fp`` 。
53+
54+
``fp`` 指向的栈位置的负偏移量处保存了两个值:
55+
56+
* ``-8(fp)`` 是保存的 ``ra``
57+
* ``-16(fp)`` 是保存的上一个 ``fp``
58+
59+
.. TODO:这个规范在哪里?
60+
61+
因此我们可以像链表一样,从当前的 ``fp`` 寄存器的值开始,每次找到上一个 ``fp`` ,逐帧恢复我们的调用栈:
62+
63+
.. code-block:: c
64+
65+
#include <inttypes.h>
66+
#include <stdint.h>
67+
#include <stdio.h>
68+
69+
// Compile with -fno-omit-frame-pointer
70+
void print_stack_trace_fp_chain() {
71+
printf("=== Stack trace from fp chain ===\n");
72+
73+
uintptr_t *fp;
74+
asm("mv %0, fp" : "=r"(fp) : : );
75+
76+
// When should this stop?
77+
while (fp) {
78+
printf("Return address: 0x%016" PRIxPTR "\n", fp[-1]);
79+
printf("Old stack pointer: 0x%016" PRIxPTR "\n", fp[-2]);
80+
printf("\n");
81+
82+
fp = (uintptr_t *) fp[-2];
83+
}
84+
printf("=== End ===\n\n");
85+
}
86+
87+
但是这里会遇到一个问题,因为我们的标准库并没有保存栈帧指针,所以找到调用栈到标准的库时候会打破我们对栈帧格式的假设,出现异常。
88+
89+
我们也可以不做关于栈帧保存方式的假设,而是明确让编译器告诉我们每个指令处的调用栈如何恢复。在编译的时候加入 ``-funwind-tables`` 会开启这个功能,将调用栈恢复的信息存入可执行文件中。
90+
91+
有一个叫做 `libunwind <https://www.nongnu.org/libunwind>`_ 的库可以帮我们读取这些信息生成调用栈信息,而且它可以正确发现某些栈帧不知道怎么恢复,避免异常退出。
92+
93+
正确安装 libunwind 之后,我们也可以用这样的方式生成调用栈信息:
94+
95+
.. code-block:: c
96+
97+
#include <inttypes.h>
98+
#include <stdint.h>
99+
#include <stdio.h>
100+
101+
#define UNW_LOCAL_ONLY
102+
#include <libunwind.h>
103+
104+
// Compile with -funwind-tables -lunwind
105+
void print_stack_trace_libunwind() {
106+
printf("=== Stack trace from libunwind ===\n");
107+
108+
unw_cursor_t cursor; unw_context_t uc;
109+
unw_word_t pc, sp;
110+
111+
unw_getcontext(&uc);
112+
unw_init_local(&cursor, &uc);
113+
114+
while (unw_step(&cursor) > 0) {
115+
unw_get_reg(&cursor, UNW_REG_IP, &pc);
116+
unw_get_reg(&cursor, UNW_REG_SP, &sp);
117+
118+
printf("Program counter: 0x%016" PRIxPTR "\n", (uintptr_t) pc);
119+
printf("Stack pointer: 0x%016" PRIxPTR "\n", (uintptr_t) sp);
120+
printf("\n");
121+
}
122+
printf("=== End ===\n\n");
123+
}
124+
125+
51126
3. `**` 实现一个基于rcore/ucore tutorial的应用程序C,用sleep系统调用睡眠5秒(in rcore/ucore tutorial v3: Branch ch1)
52127

53128
注: 尝试用GDB等调试工具和输出字符串的等方式来调试上述程序,能设置断点,单步执行和显示变量,理解汇编代码和源程序之间的对应关系。
@@ -96,7 +171,11 @@
96171
8. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议?
97172
9. `**` 请简要说明从QEMU模拟的RISC-V计算机加电开始运行到执行应用程序的第一条指令这个阶段的执行过程。
98173
10. `**` 为何应用程序员编写应用时不需要建立栈空间和指定地址空间?
174+
11. `***` 现代的很多编译器生成的代码,默认情况下不再严格保存/恢复栈帧指针。在这个情况下,我们只要编译器提供足够的信息,也可以完成对调用栈的恢复。(题目剩余部分省略)
99175

176+
* 首先,我们当前的 ``pc`` 在 ``flip`` 函数的开头,这是我们正在运行的函数。返回给调用者处的地址在 ``ra`` 寄存器里,是 ``0x10742`` 。因为我们还没有开始操作栈指针,所以调用处的 ``sp`` 与我们相同,都是 ``0x40007f1310`` 。
177+
* ``0x10742`` 在 ``flap`` 函数内。根据 ``flap`` 函数的开头可知,这个函数的栈帧大小是 16 个字节,所以调用者处的栈指针应该是 ``sp + 16 = 0x40007f1320``。调用 ``flap`` 的调用者返回地址保存在栈上 ``8(sp)`` ,可以读出来是 ``0x10750`` ,还在 ``flap`` 函数内。
178+
* 依次类推,只要能理解已知地址对应的函数代码,就可以完成恢复操作。
100179

101180
实验练习
102181
-------------------------------

source/chapter2/6answer.rst

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,78 @@
1212
编程题
1313
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1414
1. `***` 实现一个裸机应用程序A,能打印调用栈。
15+
16+
以 rCore tutorial ch2 代码为例,在编译选项中我们已经让编译器对所有函数调用都保存栈指针(参考 ``os/.cargo/config`` ),因此我们可以直接从 `fp` 寄存器追溯调用栈:
17+
18+
.. code-block:: rust
19+
:caption: ``os/src/stack_trace.rs``
20+
21+
use core::{arch::asm, ptr};
22+
23+
pub unsafe fn print_stack_trace() -> () {
24+
let mut fp: *const usize;
25+
asm!("mv {}, fp", out(reg) fp);
26+
27+
println!("== Begin stack trace ==");
28+
while fp != ptr::null() {
29+
let saved_ra = *fp.sub(1);
30+
let saved_fp = *fp.sub(2);
31+
32+
println!("0x{:016x}, fp = 0x{:016x}", saved_ra, saved_fp);
33+
34+
fp = saved_fp as *const usize;
35+
}
36+
println!("== End stack trace ==");
37+
}
38+
39+
之后我们将其加入 ``main.rs`` 作为一个子模块:
40+
41+
.. code-block:: rust
42+
:caption: 加入 ``os/src/main.rs``
43+
:emphasize-lines: 4
44+
45+
// ...
46+
mod syscall;
47+
mod trap;
48+
mod stack_trace;
49+
// ...
50+
51+
作为一个示例,我们可以将打印调用栈的代码加入 panic handler 中,在每次 panic 的时候打印调用栈:
52+
53+
.. code-block:: rust
54+
:caption: ``os/lang_items.rs``
55+
:emphasize-lines: 3,9
56+
57+
use crate::sbi::shutdown;
58+
use core::panic::PanicInfo;
59+
use crate::stack_trace::print_stack_trace;
60+
61+
#[panic_handler]
62+
fn panic(info: &PanicInfo) -> ! {
63+
// ...
64+
65+
unsafe { print_stack_trace(); }
66+
67+
shutdown()
68+
}
69+
70+
现在,panic 的时候输入的信息变成了这样:
71+
72+
.. code-block::
73+
74+
Panicked at src/batch.rs:68 All applications completed!
75+
== Begin stack trace ==
76+
0x0000000080200e12, fp = 0x0000000080205cf0
77+
0x0000000080201bfa, fp = 0x0000000080205dd0
78+
0x0000000080200308, fp = 0x0000000080205e00
79+
0x0000000080201228, fp = 0x0000000080205e60
80+
0x00000000802005b4, fp = 0x0000000080205ef0
81+
0x0000000080200424, fp = 0x0000000000000000
82+
== End stack trace ==
83+
84+
这里打印的两个数字,第一个是栈帧上保存的返回地址,第二个是保存的上一个 frame pointer。
85+
86+
1587
2. `**` 扩展内核,实现新系统调用get_taskinfo,能显示当前task的id和task name;实现一个裸机应用程序B,能访问get_taskinfo系统调用。
1688
3. `**` 扩展内核,能够统计多个应用的执行过程中系统调用编号和访问此系统调用的次数。
1789
4. `**` 扩展内核,能够统计每个应用执行后的完成时间。

0 commit comments

Comments
 (0)