Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit f205ebb

Browse files
committed
doc: update exception control flow
1 parent d127ace commit f205ebb

1 file changed

Lines changed: 118 additions & 2 deletions

File tree

docs/lab/lab3.md

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
### 实验简介
88

9-
栈帧相关实验
9+
栈帧与程序控制流相关实验
1010

1111
本学期,我们仍然将金老师 ICS 第三个 Lab 回炉重造,减轻代码工作量并添加更多讲解和提示,以加深各位同学对栈帧和协程的理解,并探索一些相关应用,丰富同学们的知识面。
1212

@@ -105,7 +105,7 @@ void foo() {
105105

106106
- 不同输入函数的“截断”不同,截断指的是输入函数在读取到哪些字符时会停止读取。比如,`gets` 函数会读取到换行符为止,所以它也会读入 `\0` 这种非常特殊的字符。具体可以参照 [CTF中常见的C语言输入函数截断属性总结](https://xuanxuanblingbling.github.io/ctf/pwn/2020/12/16/input/)
107107

108-
- 一个示例的构造payload的方法
108+
- 一个示例的构造 payload 的方法
109109

110110
假设 buffer 距离返回地址的偏移为 0x10 ,且我们想让程序返回到地址 `0x4005d6`,则我们可以编写一个如下的 Python 程序:
111111

@@ -545,6 +545,122 @@ def game():
545545

546546
![进度条](stacklab/task3_result.gif)
547547

548+
## 五、控制流和栈帧的进一步思考
549+
550+
> [!TIP]
551+
>
552+
> 如果你在做 Lab 时候做到了这里,那么恭喜你,你已经完成了 80% 以上的工作,接下来的任务主要是深入的思考和问答。有些问题较为开放,没有标准答案,充分思考回答即可。例如对于问题“为什么需要 Scope Table Ptr?”,回答“我不认为需要 STP,下面是一种更好的实现方法 blablah……”也是可以接受的。希望大家积极思考,放过 LLM,开动自己的脑筋。
553+
554+
### 历史的回顾
555+
556+
回顾一下,这个 Lab 的前一部分到底在做什么?我们为什么需要费时费力地写 `save`/`restore``try`/`catch`/`throw``yield`……?实际上这是因为,人类的编程思路是跳跃的。我们脑子里在想,“先干什么然后做很多次这个然后如果这样则跳到那里干那个”,但是笨笨的 CPU 只会一条一条执行指令。这就产生了相当大的矛盾。
557+
558+
> [!NOTE]
559+
>
560+
> 你知道吗,其实函数式编程语言更加贴近人类的思考过程。你有学过函数式编程语言吗?你能在 AI 的帮助下面的这一段 Haskell 代码吗?你觉得函数式编程语言和过程式语言的区别是什么?请在报告中回答。
561+
>
562+
> ```haskell
563+
> safe :: Int -> [Int] -> Int -> Bool
564+
> safe _ [] _ = True
565+
> safe x (x1:xs) n = x /= x1 && x /= x1 + n && x /= x1 - n && safe x xs (n+1)
566+
>
567+
> queens :: Int -> [[Int]]
568+
> queens 0 = [[]]
569+
> queens n = [ x:y | y <- queens (n-1), x <- [1..8], safe x y 1]
570+
> ```
571+
572+
为了让计算机能够跟上人类跳跃的思维我们逐步发明了这些东西
573+
574+
* 我们往汇编语言里加入了 `JMP`/`JXX`
575+
576+
终于不用像计算器那样只能一条一条执行计算了我们可以撰写更丰富的内容了
577+
578+
* 我们实现了 `CALL`/`RET`
579+
580+
可以编写函数了代码更加的整齐和规范了我们现在也有了递归编写代码更容易了
581+
582+
* 出现了高级语言我们可以使用 `if`/`else` 等控制流了
583+
584+
终于不用被各种看不懂的 `JMP` 折磨了程序更易阅读了
585+
586+
* 引入了异常处理控制流 `try`/`catch`
587+
588+
当程序运行出错的时候能更好的追踪错误让程序继续运行而不是哐的一下崩溃了
589+
590+
* 引入了生成器 `generator`/`yield`
591+
592+
终于不用让程序干等着了数据加载器占用的内存也更少了
593+
594+
* 引入了异步原语 `async`/`await`
595+
596+
编写异步代码更加容易了程序运行吞吐量更高了
597+
598+
* ……
599+
600+
当然这些概念是无穷无尽的你也可以发明自己的控制流只要它 make sense)。当然就有一门学科专门研究计算机语言的相关理论不管是新特性新语法或者是传统的安全性执行效率还是听上去奇奇怪怪的类型论生命周期理论都是这门学科所涉及的这门学科就是编程语言理论PL, Programming Language Theory)”。
601+
602+
相信在前面半学期的磨练中大家对编程语言的理解提升了不少接下来就带大家以 PL 领域学者的视角重新看一看这个 Lab
603+
604+
### 异常处理流到底是什么?
605+
606+
在某种意义上这像是程序的第二条控制流”,它只有在程序运行产生异常的时候才会被唤起在运行一段之后又会返回原先的控制流继续运行在上面的代码中我们使用了异常处理堆栈和 C Macro 来模拟了这一操作但是现代的 C++ 编译器却不是这么实现的
607+
608+
> [!NOTE]
609+
>
610+
> 回忆一下内存布局栈区和堆区的区别是什么我们的异常处理堆栈在运行时存放在栈区还是堆区这是好的还是不好的程序正常运行时的栈帧存放在栈区还是堆区
611+
>
612+
> 查阅资料并在报告中回答上述问题相信你在回答完上述问题之后应该已经清楚为什么现代 C++ 编译器不是像我们这样处理异常控制流了
613+
614+
事实上现代的 C++ 编译器此处特指 GCCClang),使用了 DWARF 标准其内部写明了 Itanium C++ ABI 的工作方式对于有 `try`/`catch` 的函数会在其栈帧添加异常处理记录Exception Registration Record)”。具体如下
615+
616+
对于一个普通的函数来说它的栈帧大概是这样的
617+
618+
```
619+
High Address
620+
+-------------------+
621+
| Caller's Frame |
622+
|-------------------|
623+
| Return Address |
624+
|-------------------|
625+
| Saved RBP |
626+
|-------------------|
627+
| Local Variables |
628+
|-------------------|
629+
| Callee-Saved Regs |
630+
+-------------------+
631+
Low Address
632+
```
633+
634+
当进入 try 块时,程序会在栈顶压入以下的结构:
635+
636+
```
637+
+-------------------+
638+
| Next Record Ptr |
639+
|-------------------|
640+
| Handler Function | // 指向异常处理函数(如__gxx_personality_v0)
641+
|-------------------|
642+
| Scope Table Ptr | // 指向.tdata/.eh_frame的异常范围表
643+
+-------------------+
644+
```
645+
646+
`Next Record Ptr` 指向上一个记录,从而形成错误处理堆栈。`Handler Function` 指向异常处理函数,而 `Scope Table Ptr` 指向 `.tdata` / `.eh_frame` 的异常范围表。
647+
648+
> [!NOTE]
649+
>
650+
> 请仔细阅读实验文档、查阅资料,并在报告中回答问题:为什么需要 `Scope Table Ptr`?如果你查阅了资料依然无法理解,可以尝试在 C++ 中写一个带有异常处理的函数,然后使用 gdb 调试它。
651+
652+
在程序 throw 之后,一些运行时库(例如 libunwind)会自动遍历异常记录链表,然后找到能处理这个异常的函数并进入。对于无法处理这个异常的栈帧,会直接摧毁。举个例子来说,如果 A 函数中 try/catch 了,然后在 try 块中调用了 B 函数,B 函数调用了 C 函数,C 函数 throw 了一个异常,则程序会依次查找异常记录链表,当它找到了 A 函数之后,B 函数的栈帧会自动被摧毁,之后再也无法恢复里面的局部变量等信息了。
653+
654+
> [!NOTE]
655+
>
656+
> 请仔细阅读实验文档、查阅资料,并在报告中回答问题:摧毁函数 B 的栈帧是好的吗?在什么情况下是好的?在什么情况下是不好的?为什么异常处理控制流被用作异常处理而不是其它用途(比如它有哪些设计是专门为异常处理而设计的)?
657+
>
658+
> Itanium C++ ABI 的做法有哪些和我们的实验不一样?二者各自的优缺点是什么?
659+
660+
> [!TIP]
661+
>
662+
> 你知道吗,MSVC 编译器会使用不同于 Clang 和 GCC 的异常处理策略,它使用的策略叫做 SEH(Structured Exception Handling),通过 FS:[0] 注册异常回调。
663+
548664
## 参考资料与鸣谢
549665
550666
- CMU 原版 [Attack Lab](http://csapp.cs.cmu.edu/3e/labs.html)

0 commit comments

Comments
 (0)