|
6 | 6 |
|
7 | 7 | ### 实验简介 |
8 | 8 |
|
9 | | -栈帧相关实验。 |
| 9 | +栈帧与程序控制流相关实验。 |
10 | 10 |
|
11 | 11 | 本学期,我们仍然将金老师 ICS 第三个 Lab 回炉重造,减轻代码工作量并添加更多讲解和提示,以加深各位同学对栈帧和协程的理解,并探索一些相关应用,丰富同学们的知识面。 |
12 | 12 |
|
@@ -105,7 +105,7 @@ void foo() { |
105 | 105 |
|
106 | 106 | - 不同输入函数的“截断”不同,截断指的是输入函数在读取到哪些字符时会停止读取。比如,`gets` 函数会读取到换行符为止,所以它也会读入 `\0` 这种非常特殊的字符。具体可以参照 [CTF中常见的C语言输入函数截断属性总结](https://xuanxuanblingbling.github.io/ctf/pwn/2020/12/16/input/) |
107 | 107 |
|
108 | | -- 一个示例的构造payload的方法: |
| 108 | +- 一个示例的构造 payload 的方法: |
109 | 109 |
|
110 | 110 | 假设 buffer 距离返回地址的偏移为 0x10 ,且我们想让程序返回到地址 `0x4005d6`,则我们可以编写一个如下的 Python 程序: |
111 | 111 |
|
@@ -545,6 +545,122 @@ def game(): |
545 | 545 |
|
546 | 546 |  |
547 | 547 |
|
| 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++ 编译器(此处特指 GCC 和 Clang),使用了 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 | +
|
548 | 664 | ## 参考资料与鸣谢 |
549 | 665 |
|
550 | 666 | - CMU 原版 [Attack Lab](http://csapp.cs.cmu.edu/3e/labs.html) |
|
0 commit comments