c++代码pr编译影片时出错导出错误没问题,调试时提示sigfpe错误


· 超过33用户采纳过TA的回答

所按照格式应该前面加 typedef

朋友请【采纳答案】您采纳我答题力没明白请追问谢谢

抱歉啊没看懂。能具体指出我的代码哪里错了吗

你对这个回答的評价是

下载百度知道APP,抢鲜体验

使用百度知道APP立即抢鲜体验。你的手机镜头里或许有别人想知道的答案

Unix跟Windows等那些”对于开发者易于使用”的OS比起来在信号和线程的利用方面有诸多的限制。但是即使不知道这些知识就做构架设计和实现的情况也随处可见这个就是那些经瑺不能再现的bug的温床吧。

因此我想分成几回来写一些准则来防止陷入到这些圈套里。

准则1:不依赖于信号收发的设计

·给其他进程以及自己发送异步信号并改变处理流程的设计不要做

  • 简单的使用忽略信号(SIG_IGN)则没有问题

·不要把线程和信号一起使用 

  • 这将使程序动作的预测和调試变得很困难

SIGFPE异步信号就是这些以外的信号。在什么时机发送异步信号并不能被预测出来我们会在程序里追加收到某些信号时做一些特殊处理(信号处理函数)的函数。那么根据收到的信号就跳到信号处理函数的程序就叫做”在任意代码处都能发生跳转”的程序这样的程序往往隐藏这下面的那些问题:

  1. 容易引入BUG。”任意的代码”虽然也包含”执行C/C++里面的一条语句的过程中”的意思但这很容易跳出程序员嘚正常思维以及默认的假定条件。编写程序的时候往往需要考虑比C++异常分支还要多得多的分支情况
  2. 使测试项目激增。即使根据白盒测试達成100%的分支覆盖,也不能网罗到因为接受信号而发生的跳转分支处理也就是说做到100%的网罗信号跳转分支的测试是不能全部实现的。一般的加上要考虑” 在实行某个特定代码时因为接受到信号而发生的误操作”这样的BUG会经常发生的这种情况,测试困难往往就是导致软件的品質低下的诱因

根据经验,”当检查到子进程结束(接收到SIGCHLD信号)时要做必要的处理”像这样的信号处理不管做什么都是有必要的情况会有,但是除此以外的信号处理例如

  • 把自己的状态用信号告诉其他进程
  • 主线程在输入输出函数里发送信号给被阻塞的子线程,并解除阻塞

等是应该事先好好考虑过后再去做实际的实现。前者的话如果不强制在”普通的”进程间进行通信的话可能会很好,后者是特意要使用線程也要应该按照即使阻塞了也不能发生问题那样再设计。

不管怎么样如果必须要使用信号的话,也要先全部理解这些陷阱以及和哆线程软件设计的场合一样或者说比它更严格的制约。注意事项都需要铭记在心里

:暂时先掌握”准则2”:-)

准则2: 要知道信号处理函数中可鉯做那些处理

· 在用sigaction函数登记的信号处理函数中可以做的处理是被严格限定的

· 仅仅允许做下面的三种处理

  1. 调用异步信号安全的相关函数(鈳重入函数)

· 以外的其他处理不要做!

因为在收到信号时要做一些处理,那通常是准备一个信号处理函数并用sigaction函数把它和信号名进行关联嘚话就OK了但是,在这个信号处理函数里可以做的处理是像上面那样被严格限定的没有很好掌握这些知识就随便写一些代码的话就会引起下面那样的问题:

   o  死锁是一个比较典型的例子,除此之外还能引起函数返回值不正确以及在某一函数内执行时突然收到SEGV信号等的误操莋。
   译者注1:SEGV通常发生在进程试图访问无效内存区域时(可能是个NULL指针或超出进程空间之外的内存地址)。当bug原因和SEGV影响在不同时间呈现时它们特别难于捕获到。

· 问题2: 由于pr编译影片时出错导出错误器无意识的优化操作有导致程序紊乱的危险 
   o  这是跟pr编译影片时出错導出错误器以及pr编译影片时出错导出错误器优化级别有关系的bug。它也是“pr编译影片时出错导出错误器做了优化处理而不能正常动作”“洇为inline化了程序不能动作了”,“变换了OS了程序也不能动作”等这些解析困难bug产生的原因

还是一边看具体的代码一边解说吧。在下面的代碼里至少有三个问题根据环境的不同很可能引起不正确的动作、按照次序来说明里面的错误。

      在上面的代码里有竞争条件在sigaction函数被调鼡后、在gSignaled还未被赋值成0值之前,如果接受到SIGINT信号了那会变得怎么样呢? 在信号处理函数中被覆写成1后的gSignaled会在信号处理函数返回后被初始化成0、在后面的while循环里可能会变成死循环

    · 但是,就像下面的代码描述的那样main函数是把gSignaled的值存放到了寄存器里。在while循环之前仅仅是做了┅次拷贝变量gSignaled内存上的值到寄存器里、而在while循环里只是参照这个寄存器里的值。

     在不执行优化的情况下pr编译影片时出错导出错误后pr编译影爿时出错导出错误器有可能不会生成上面那样的伪代码但Gcc当使用-O2选项做优化pr编译影片时出错导出错误时,生成的实际那样的汇编代码产苼的危害并不仅仅是像上面说的威胁那样简单这方面的问题,是设备驱动的开发者所要知道的常识但现实情况是对于应用程序的设计鍺.开发者几乎都不知道这些知识。
为了解决上面的问题全局变量gSignaled的类型要像下面那样声明。

      volatile则是提示pr编译影片时出错导出错误器不要像仩面那样做优化处理变成每次循环都要参照该变量内存里的值那样进行pr编译影片时出错导出错误。所以在信号处理函数里把该变量的值修改后也能真实反映到main函数的while循环里

是根据CPU类型使用typedef来适当定义的整数值,例如x86平台是int类型就是指”用一条机器指令来更新内存里的朂大数据“。在信号处理函数里要被引用的变量必须要定义成sig_atomic_t类型那么不是sig_atomic_t类型的变量(比如x86平台上的64位整数),就得使用两条机器指令来唍成更新动作如果在执行一条机器指令的时候突然收到一个信号而程序执行被中断,而且在信号处理函数中一引用这个变量的话就只能看到这个变量的部分的值。另外由于字节对齐的问题不能由一条机器指令来完成的情况也会存在。把该变量的类型变成sig_atomic_t的话这个变量被更新时就只需要一条机器指令就可以完成了。所以在信号处理函数里即使使用了该变量也不会出现任何问题

(参见§7.18.3/2)。有无符号是跟具体的实现有关考虑到移植性取值在0~127之间是比较合适的。C99也支持这个取值范围C++规范()里也有同样的描述、确切的位置是§1.9/9这里。在SUSv3的楿关描述请参考这里此外、虽然在GCC的参考手册里也把指针类型更新成原子操作,但在标准C/C++却没有记载

错误3: 在信号处理函数里调用了不鈳重入的函数

     上述的样例代码中调用了printf函数,但是这个函数是一个不可重入函数所以在信号处理函数里调用的话可能会引起问题。具体嘚是在信号处理函数里调用printf函数的瞬间,引起程序死锁的可能性还是有的但是,这个问题跟具体的时机有关系所以再现起来很困难,也就成了一个很难解决的bug了

知道上面的流程的话、像这样的由于信号中断引起的死锁就能被理解了吧。为了修正这个bug在信号处理函數里就必须调用可重入函数。可重入函数的一览表在UNIX规范 (SUSv3)有详细你一定会惊讶于这个表里的函数少吧。

另外一定不要忘记以下的几点:

  1. 虽然在SUSv3里有异步信号安全(async-signal-safe)函数的一览,但根据不同的操作系统某些函数是没有被实现的。所以一定要参考操作系统的手册
  2. 第三者做成嘚函数如果没有特别说明的场合,首先要假定这个函数是不可重入函数不能随便在信号处理函数中使用。
  3. 调用不可重入函数的那些函數就会变成不可重入函数了

异步信号安全(async-signal-safe)”函数异步信号安全函数是指”在该函数内部即使因为信号而正在被中断,在其他的地方该函數再被调用了也没有任何问题”如果函数中存在更新静态区域里的数据的情况(例如,malloc)一般情况下都是不全的异步信号函数。但是即使使用静态数据,如果在这里这个数据时候把信号屏蔽了的话它就会变成异步信号安全函数了。

译者注3:不可重入函数就不是异步信號安全函数

准则3:多线程程序里不准使用fork

      在多线程程序里在”自身以外有线程存在的状态”下一使用fork的话,就可能引起各种各样的问题比较典型的例子就是,fork出来的子进程可能会死锁请不要在不能把握问题的原委的情况下就在多线程程序里fork子进程.

那看看实例吧.一执行丅面的代码,在子进程的执行开始处调用doit()时,发生死锁的机率会很高.

以下是说明死锁的理由.

一般的,fork做如下事情

  1. 父进程的内存数据会原封不动嘚拷贝到子进程中
  2. 子进程在单线程状态下被生成 

在内存区域里静态变量mutex的内存会被拷贝到子进程里,而且父进程里即使存在多个线程,但它们也不会被继承到子进程里fork的这两个特征就是造成死锁的原因.

译者注: 死锁原因的详细解释 ---

  1. mutex变量的内容会原样拷贝到fork出来的子进程Φ(在此之前,mutex变量的内容已经被线程改写成锁定状态). 
  2. 子进程再次调用doit的时候,在锁定互斥体mutex的时候会发现它已经被加锁,所以就一直等待,直到拥囿该互斥体的进程释放它(实际上没有人拥有这个mutex锁). 
  3. 线程的doit执行完成之前会把自己的mutex释放,但这是的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响.

例如,请试着考虑下面那样的执行流程,就明白为什么在上面多线程程序里不经意地使用fork就造成死鎖了.

  1. 线程1调用doit函数
  2. 在这儿程序处理切换到线程2
  3. 线程2调用fork函数
  4. 这时,子进程的doit函数用的mutex处于”锁定状态”,而且,解除锁定的线程在子进程里不存茬
  5. 子进程调用doit函数
  6. 子进程再次锁定已经是被锁定状态的mutex,然后就造成死锁

     直到目前为止,已经写上了thread+fork是危险的,但是有一个特例需要告诉大家.”fork後马上调用exec的场合,是作为一个特列不会产生问题的”. 什么原因呢..? exec函数一被调用,进程的”内存数据”就被临时重置成非常漂亮的状态.因此,即使在多线程状态的进程里,fork后不马上调用一切危险的函数,只是调用exec函数的话,子进程将不会产生任何的误动作.但是,请注意这里使用的”马上”這个词.即使exec前仅仅只是调用一回printf(“I’m child

译者注:exec函数里指明的命令一被执行,改命令的内存映像就会覆盖父进程的内存空间.所以,父进程里的任何數据将不复存在.

为了在多线程的程序中安全的使用fork,而规避死锁问题的方法有吗?试着考虑几个.

规避方法1:做fork的时候,在它之前让其他的线程完全終止.

      在fork之前,让其他的线程完全终止的话,则不会引起问题.但这仅仅是可能的情况.还有,因为一些原因而其他线程不能结束就执行了fork的时候,就会昰产生出一些解析困难的不具合的问题.

规避方法2:fork后在子进程中马上调用exec函数
( 追记一些忘了写的东西)

     不用使用规避方法1的时候,在fork后不调用任哬函数(printf等)就马上调用execl等,exec系列的函数.如果在程序里不使用”没有exec就fork”的话,这应该就是实际的规避方法吧.

译者注:笔者的意思可能是把原本子进程应该做的事情写成一个单独的程序,pr编译影片时出错导出错误成可执行程序后由exec函数来调用.

规避方法3:”其他线程”中,不做fork-unsafe的处理

除了调用fork嘚线程,其他的所有线程不要做fork-unsafe的处理.为了提高数值计算的速度而使用线程的场合,这可能是fork-safe的处理,但是在一般的应用程序里则不是这样的.即使仅仅是把握了那些函数是fork-safe的,做起来还不是很容易的.fork-safe函数,必须是异步信号安全函数(可重入函数),而他们都是能数的过来的.因此,malloc/new,printf这些函数是不能使用的.

规避方法4:使用pthread_atfork函数,即将fork之前调用事先准备的回调函数.

      使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数,在这个回调函数内,协商清除進程的内存数据.但是关于OS提供的函数(例:malloc),在回调函数里没有清除它的方法.因为malloc里使用的数据结构在外部是看不见的.因此,pthread_atfork函数几乎是没有什么實用价值的.

规避方法5:在多线程程序里,不使用fork

:生成子进程的系统调用
:全局变量和函数内的静态变量
:如果使用Linux的话,查看pthread_atfork函数的man手册比较恏.关于这些流程都有一些解释.
:从fork后到exec执行的这段时间

:仅仅做四则演算的话就是fork-safe的

准则4: 请不要做线程的异步撤消的设计

  • 线程的异步撤销昰指:某个线程的执行立刻被其他线程给强制终止了
  • 请不要单单为了让“设计更简单”或者“看起了更简单”而使用线程的异步撤消

咋一看还是挺简单的但是搞不好可能会引起各种各样的问题。请不要在不能把握问题的实质就做出使用线程的异步撤消的设计!

在pthread的规格说奣中允许一个线程可以强制中断某个线程的执行。这就是所说的异步撤消

线程的撤消有下面的两种方式。

    • 撤消动作是让线程的处理┅直被延迟到撤消点才会去执行

还有,到底是用哪种撤消方式不是撤消者,而是被撤销者能够决定的另外,在被撤销者也能够选择完铨禁止撤消的这种方式 

      那么,让我看看乱用线程的异步撤消会引起什么问题呢看过准则3的人可能会知道,在下面的脚本里被撤销线程以外的任意一个线程会被死锁。

1.   线程1中调用malloc函数正在做内存分配的过程中线程2异步撤消了线程1的处理

3.   后面的任意一个线程如果再次调鼡malloc函数的话就会马上导致该线程死锁

      反之,即使做了异步撤消也没有问题的函数也有少数存在的、我们把它们叫做「async-cancel safe函数」或者「异步撤消安全函数」在一些商用UNIX中、OS提供的api函数的文档说明中有async-cancel safety的记载、但是在Linux(glibc)里就很遗憾,几乎没有相关的说明

     在多线程编程中为了安全嘚使用异步撤消处理、有没有回避死锁的方法呢?我们试着想了几个他们与准则3里的线程+fork的场合的回避策很相似。

回避方法1: 被撤销线程Φ只能使用异步撤消安全函数

首先,被撤销线程中只能使用异步撤消安全函数。但是这个方法

  • 在规格说明中只有3个异步撤消安全的函數
  • 这些以外的函数是不是异步撤消安全(商用UNIX)、因为没有说明文档我们不清楚(Linux)

中有以上的两点所以这个回避方法几乎不现实。

回避方法2: 被撤销线程中在做非异步撤消安全处理的过程中,先把撤消方式设置成「延迟」或者是「禁止」

第二个是被撤销线程在做非异步撤消安铨处理的过程中,把撤消方式再设定成「延迟」或者「禁止」对于这个方法

  • 就像方法1写的那样、要把我那个函数是异步撤消安全的一时還是挺麻烦的
  • 在任意的场所并不能保证撤消动作会被马上执行
    • 例如,再设定成「延迟」后的一段时间内如果撤消发生时、某个正在阻塞的I/O函数是否能够被解除阻塞还是挺微妙的
    • 如果设定成撤消禁止的话则撤消会被屏蔽掉

有上面样的问题、会导致「一精心设计撤消方式的替換,从一开始就使用延迟撤消还不够好」这样的结果所以这几乎是不好的一个回避策。

回避方法3: 使用pthread_cleanup_push函数登录异步撤消时的线程数據清除的回调函数

第三种则是,用pthread_cleanup_push函数、登录一个在异步撤消发生时的数据清除的回调函数这和在准则3中介绍的pthread_atfork函数有点儿类似。用这個函数登录的回调函数来清除线程的数据和锁就可以回避死锁了。

回避方法4: 不要执行异步撤消处理

最后是、不要执行异步撤消处理反而代之的是、

  • 设计成不依赖使用异步撤消那样的处理
  • 不得不使用线程撤消的话,不做异步撤消而作延迟撤消的处理

这是比较实际的做法是我们值得推荐的。

准则5: 尽可能避免线程中做延迟撤销的处理

  • 线程的异步撤消是指:一个线程发出中断其他线程的处理的一个动作
  • 延遲撤消因为是规格自由度比较高所以根据OS和C库函数的版本它也有各式各样的动作
  1. 要想在不同的环境下都能稳定的动作的话,就必须要详細调查运行环境和对C库函数进行抽象化做必要的条件pr编译影片时出错导出错误
  2. 在C++中,「撤消发生时的对象释放」的实现不具有可移植性
  • 線程撤销要慎重使用在C++里不要使用

在前面我们已经讲过,线程的撤消分为「异步」「延迟」这两种类型并且「异步撤消」也是非常容噫引起各种复杂问题的元凶。

那么现在要在程序中除掉「延迟撤消」。延迟撤消虽然不会像异步撤消那样会引起各种各样的问题、但是注意事项还是有很多的。只有把下面的这些注意事项全部都把握之后才能放心使用

注意事项1: 要好好把握撤消点

      和异步撤消不一样的是:撤消处理一直会被延迟到在代码上明示出来的撤消点之后才会被执行。如果编写了一个具有延迟撤消可能的代码代码中的那条语句是撤消点,必须要正确的把握

      首先,调用过pthread_testcancel函数的地方就变成撤消点了当然这个函数是:仅仅为了「变成延迟撤消」的目的而设置出来嘚函数。除此之外某些标准库函数被调用后会不会变成撤消点是在规格(SUSv3)中决定的。、有下面的函数一览

      看到这些我想已经明白了,但昰在规格中也说明了「能否成为撤消点跟具体的实现相关的函数」也是多数存在的原因是:为了可移植性、保证「在一定的时间内让线程的延迟撤消完成」是很困难的事情。做的不好的话、只要稍微一提升OS的版本就可能让做出来的程序产品不能动作

即使是这样那还想要使用延迟撤消吗?

注意事项2: 实现要知道cleanup函数的必要性

      可能被延迟撤销的线程在运行的过程中要申请资源的场合,一定要考虑到以下的几點否则就会编制出含有资源丢失和死锁的软件产品。

例如编写的下面的函数就不能被安全的延迟撤销掉

      在上面的样例代码中,nanosleep执行的過程中经常会触发延迟撤销的最终动作但是这个时候的mutex锁还处于被锁定的状态。而且线程一被延迟撤消的话就意味着没有人去释放掉這个互斥锁了。因此在下面的main函数中调用同样的cancel_unsafe函数时就会引起死锁了。

注意事项3: 实现要清楚延迟撤消和C++之间的兼容度

      使用C语言的场合利用上面的pthread_cleanup_push/pop函数就能安全地执行延迟撤消的动作,但是在C++语言的场合就会出现其他的问题C++与延迟撤消之间的兼容度是非常差的。具体嘚表现有以下两个问题:

  1. 执行延迟撤消的时候内存栈上的对象的析构函数会不会被调用跟具体的开发环境有关系
    • GCC3版本就不会调用。
    • Solaris和Tru64 UNIX下的原生pr编译影片时出错导出错误器的场合就调用析构函数(好像)
  2. pthread_cleanup_push/pop函数和C++的异常处理机制之间有着怎样的相互影响也能具体环境有关

不调用析構函数,或者在抛出异常的时候不能做cleanup处理经常是发生内存泄漏,资源丢失程序崩溃,死锁等现象的原因令人意外的是对于这个深層次的问题,就连都束手无策

先必须确保对象的自由存储,而后全都让cleanup函数去释放对象的方法也有但是这次是牺牲了异常安全性。

应該说的是在使用C++的工程里不对线程进行延迟撤消处理还是比较实际的。

:异步撤消跟malloc函数的例子很相似

准则6: 遵守多线程编程的常识

  1. 要准確把握在POSIX标准的函数中那些函数是非线程安全的,一定不要使用
  2. 要让自己编写的函数符合线程安全
  • 在访问共享数据/变量之前一定要先锁萣
  • 如果使用C++的话一定要注意函数的同步方法

 (1) 要准确把握那些非线程安全的函数,一定不要使用

      如果在POSIX平台上进行多线程编程时有几个朂基本的知识,也就是所说的“常识”希望大家一定要严格遵守。

      首先我们要理解“线程安全”的意思。线程安全的函数就是指“┅个能被在多个线程同时调用也不会发生问题的函数”。这样的函数通常要满足以下几个的特质

  1. 不要操作局部的静态变量(函数内的static变量)囷全局静态数据(全局变量,函数外的静态变量)而且,也不要调用其他的非线程安全的函数
  2. 如果要操作这样的变量的话事先必须使用互斥锁mutex进行同步,否则一定要限制多个线程同时对它的访问

      那么、在POSIX标准的函数里面也有不满足上述条件的。由于历史遗留问题一些函數的识别标识(signature)的定义没有考虑到线程安全的问题,所以不管怎么做都不能满足上述的条件例如,看看 吧它的定义(signature) 如下:

      localtime 函数是,把一個用整数形式表示的时刻(从到现在为止的秒数)、转换成一个能让人容易明白的年月日形式表示出来的tm结构体并返回给调用者的函数根据規格说明、返回出来的tm结构体是不需要free()掉,也不能释放的这个函数典型的实现就像下面的代码那样:

这个函数如果被像下面那样使用的話,就会有漏洞:

3.   线程A参照ta结构体里的数据 → 就发现这些数据是一些奇怪的值!

      在函数的说明手册里对这个问题也没有做过详细的说明關于这个漏洞,在localtime函数即使使用了mutex锁也不能被回避掉所以,这个函数定义的识别标识是不行滴
[译 者lymons注:在多个线程里调用localtime函数之所以囿问题的原因是,localtime函数里返回的tm构造体是一个静态的结构体所以在 线程A里调用localtime函数时,该结构体被赋予正确的值;而在线程A参照这个结構体之前线程B又调用localtime的话,这个静态的结构体 又被赋予新的一个值因此在线程A对这个结构体的访问都是基于一个错误的值进行的]

这里登载了的非线程安全的函数有如下所示。

      对于在规格中被定义为非线程安全的函数应该制定一个避免使用它们的规则出来,并且制作一個能够自动检查出是否使用了这些函数的开发环境应该是比较好的。

      反之在这里没有被登载的POSIX标准函数都被假定为"shall be thread-safe" 的、所以在实际的使用中可以认为在多线程环境里是没有问题的(而且在使用的平台上没有特别地说明它是非线程安全的话)。

      另外有几个非线程安全的函数,都准备了一个备用的线程安全版本的函数(仅仅是变更了函数的识别标识)像这些函数为了与原版进行区别都在其函数名后面添加了 _r 这个後缀。例如asctime函数就有线程安全版本的函数asctime_r。在规格说明中是否定义了备用函数可以试着点击刚才的里面的函数名就可以看到。点击 就鈳以看到

       用[TSF]这样的文字标记出来的函数吧。这就是备用函数在一览中没有记载出来的函数(备注: 稍微有点儿出入。请参照)、据我所知還有下面的备用函数

     在多线程编程中,不要使用非线程安全的函数而他们的备用函数可以放心地积极的去使用。

:在C言語里函数不能偅载所以只能添加一个新的函数

:跟网络有关的API哪些是新的哪些是旧的,可以参考  这本好书

(2)要让自己编写的函数符合线程安全

      在写哆线程的应用程序时,在多个线程里共享的变量要先锁定然后在更新它那么在多线程里共享的变量主要有全局变量和函数内的静态变量。洏且,即使是short型和int型的共享变量也要先锁定后更新才能保证其安全

      还有,在使用C++编程的场合要注意函数的方步方法。一般的说来下面的写法昰错误的Mutex在函数内被声明成静态变量是不允许的

应该用下面的方式来代替,

把Mutex声明成全局变量的话比较好(稍微比上一个好)

线程安全函數是像下面那样

  1. 不要操作局部的静态变量(函数内的static型的变量)和非局部的静态数据(全局变量)。并且,其它的非线程安全函数不要调用
  2. 要操作这樣的变量的话, 就要使用mutex进行同步处理,来限制多个线程同时对它进行操作
  • 特别是前者, 和被叫做可重入的(reentrant)函数有区别

嗯, 因为比较详细的, 如果不昰在对于执行速度要求比较苛刻的环境中编写代码的话, 单单地意识到「是否线程安全」就足够了,不是吗

声明一点这篇主要是探讨Windows下的異常捕获。

首先要说明一点操作系统中,不论是用户态还是内核态的程序出现了异常情况或者严重错误,操作系统也是按照一定规则處理的并不是随便地终止了程序。不然的话操作系统就会发生资源泄露,或者其他错误

Windows系统检测到我们写的程序发生了未处理异常戓者其他严重错误(具体什么错误呢?)时一般会将其终止掉,在此之前默认会弹出一个应用程序错误对话框(Application Fault Dialog),或者叫GPF(General Protection Fault 通用保護错误)对话框如果系统中配置了JIT(Just-In-time)调试器,那么在崩溃的时候会启动设置的调试器Windows系统中默认的JIT是一个叫做Dr. Watson的JIT调试器(参考,其可執行文件名字在不同代Windows

看msdn上介绍到的Remark部分有提到:

也就是说这个函数会直接触发一个SEH异常。

另外这个函数中的Remark部分还提到具体搜索异瑺的过程:

  1. 系统首先通知进程的调试器,如果存在的话
  2. 如果未调试进程或者关联的调试器不处理这个异常,系统会尝试搜索发生异常的線程的栈帧来定位基于栈帧的异常处理函数系统首先是收缩当前的栈帧,然后向后遍历之前的栈帧(也就是向调用函数的方向回溯)
  3. 洳果找不到基于栈帧的处理函数,或者没有基于栈帧的处理函数处理异常系统将再次尝试通知进程的调试器。
  4. 如果未调试进程或者关聯的调试器未处理异常,则系统将根据异常类型做出默认处理对于大多数异常,默认操作是调用ExitProcess函数

这部分内容主要参考开源软件crashrpt的攵档

我们写程序一般是通过 配合 来完成崩溃的捕获和dump抓取的。 SetUnhandledExceptionFilter的自定义处理函数的返回值中:

  • EXCEPTION_CONTINUE_EXECUTION 在异常发生的代码处继续执行代码,不过囿一个修改异常信息的机会我写的测试代码中,这种处理返回值最后会在系统的Event Viewer中找到崩溃记录。

Windows 10上我测试下来,自定义未处理异瑺函数的返回值与SetErrorMode的配合对系统事件(Event Viewer)中应用程序错误产生记录的对应关系是:

  1. 自定义未处理异常返回 EXCEPTION_EXECUTE_HANDLER,设置错误模式为 SetErrorMode(SEM_NOGPFAULTERRORBOX)生成dump,不产生倳件记录因为这类异常背后的逻辑是程序预知到了这种错误,处理好了就不需要系统记录了。
  2. 自定义未处理异常返回 EXCEPTION_CONTINUE_EXECUTION设置错误模式為 SetErrorMode(SEM_NOGPFAULTERRORBOX),程序卡死无法产生dump和事件记录卡死的原因是让程序继续执行,但是之前碰到的异常并没有解决导致无法继续执行。

Windows上这个处理过程可以抓取到大多数崩溃参考资料中的也使用了这个方法。

我们C++代码中常见的异常类型有

  1. 访问无效内存比如空指针,内存访问越界
  1. 棧耗尽,比如无限递归导致Stack overflow错误
  1. 缓存溢出,大数据块写入小数据块导致内存非法访问。现在VC++pr编译影片时出错导出错误器一般会启用Buffer Security Check : pr编譯影片时出错导出错误选项

  2. 调用C++的纯虚指针,参考 的示例代码

  3. 内存耗尽,申请内存失败 目前的操作系统中由于使用了虚拟内存的技术一般不会碰到

  4. 非法参数传入C++系统函数
    参考 里面的示例代码。


 
 
 
  1. CRT检测到异常并请求强制退出进程

Windows中的有两种可以捕获的异常:

  1. 另外一种是使鼡SEH这个是VC++pr编译影片时出错导出错误器独有的,不可用于移植的代码__try{}__catch{}结构的,而且SEH只能用于C类型的函数不能用于C++类内部。

不过SEH的异瑺可以通过_set_se_translator()转为C++的异常,参考:里面的示例代码将一个除零的异常,转为了自定义的C++类型异常 SE_Exception

对于使用SEH不能保护的代码,就属于unhandled Exception范畴叻可以使用SetUnhandledExceptionFilter设置函数来处理,这个函数是SEH的top-level处理过程不过使用这个函数要注意,如果异常处理函数是在DLL中并且这个DLL还没有加载,那麼行为是未定义的

XP中引入的VEH()是对SEH的扩展。如果你想监控所有类型的异常就像是调试器那样,那么VEH是非常适合的不过问题是你要決定哪些异常要处理,哪些不要处理

除了C++类型异常和SEH异常之外,还有CRT异常crt遇到C++类型的异常之后会调用terminate()函数,所以你最好用set_terminate()设置一个错誤处理过程
CRT错误处理过程可以设置:

  • 比如printf设置了空缓存区,参考示例代码

以上主要在 MSDN 查找资料

ANSI标准中一共有六种:

  1. SIGFPE Floating-point error,当浮点运算出错時由CRT调用一般情况下不会生成。Windows系统 默认关闭 了这个信号取而代之的是生成一个NaN或者无限大的数字,可以通过_controlfp_s函数打开这个异常参栲
  2. SIGINT CTRL+C signal, win32程序 不支持 这个信号当CTRL+C中断发生时,Win32系统会生成一个新的线程处理该中断这样的话,比如一些在unix上的单线程可能会变成多线程並出现不可知的错误。这里强调了UNIX中的单线程程序我试了一下及时创建一个最简单的console程序也会有3个线程(一个Main

这部分可以参考 msdn 函数介绍

NT鈈会生成,留着只是为了兼容ANSI但是如果在主线程中设置了SIGSEGV信号函数,那么就会由CRT而不是SEH设置的SetUnhandledExceptionFilter()过滤函数来调用并且有一个全局的变量_pxcptinfoptrs包含异常信息。如果是在其他线程的话异常处理过程是由SEH的SetUnhandledExceptionFilter()过滤函数调用的。(这部分删除掉是因为我现在(2020.10)没在msdn上找到这个说法可能昰后来有变动,毕竟这篇文档成文早于2010年之后Windows上的CRT有较大的变化)

除了函数之外,还有pr编译影片时出错导出错误链接选项上的一些事情CRT可以以MD(动态链接)和MT(静态链接)的方式pr编译影片时出错导出错误进模块(exe/DLL)里面。参考:/MT、/MDpr编译影片时出错导出错误选项以及可能引起在不同堆中申请、释放内存的问题,/MD, /MT, /LD (Use Run-Time Library)
MD的方式时推荐的,多个模块公用一个CRT的DLL库的方式;以MT的方式使用CRT的话需要把函数写成static,并苴使用/NODEFAULTLIB链接标记链接到所有模块中,还需要每个模块中都注册CRT错误处理过程

上面提到这几种异常也不全,可以参考操作系统的IDT表项看看系统支持哪些中断/异常处理比如说还有:

为什么调试器可以抓到所有崩溃?

这部分参考《软件调试》中介绍的内容。这里再次向张老师致敬

简单地说,因为Windows提供的中断和异常管理机制中如果碰到有未处理的异常,或者是CRT中发生错误的时候会先判断进程是否在调试器下運行,是的话就把控制权交给了调试器 也就是说程序的执行流程中会主动请求调试器协助。

详细一点说的话需要从Windows的中断和异常说起。

Windows保护模式下如果发生中断或者异常会通过查找IDT(中断描述符)表来寻找处理函数,而Windows启动早期会初始化这个表

IDT中有三种表项,也就昰所谓的门描述符(Gate Descriptor)结构

  • 任务门 切换任务用;由于x64架构不支持硬件方式的任务切换,所以没有任务门了
  • 陷阱门 描述异常处理例程入ロ

本篇提到的中断和异常处理,主要靠后二者

Windows中的异常,除了CPU产生的硬件异常外还有软件模拟出的异常,比如调用RaiseException或者语言层面throw抛出嘚异常Windows会以统一的方式处理这两类软硬件异常。用于描述异常的数据结构是struct _EXCEPTION_RECORD

  • 软件异常 是通过内核服务 NtRaiseException 产生的,用户态代码可以通过RaiseException来間接调用这个函数

从上图中可以看出本节简述部分的内容。因为这个函数会尝试请求调试器介入所以调试器可以捕获所以的异常。

那麼如果进程执行中发生一个异常如何捕获并处理异常,Windows提供了哪些机制

Windows中创建一个进程的过程中会调用一个函数BaseProcessStart,这个函数的实现比較简单但是整个函数体被一个SEH的__try包裹着,这里是Windows进程为异常发生托底的最后一重关卡由于很多程序有全局变量,还有使用了CRT的程序還需要处理CRT的初始化,处理命令行参数等而crt的实现中还会做基于信号的异常处理,所以CRT也有一层SEH的__try块这个由pr编译影片时出错导出错误器负责插入这段代码,我电脑上是在路径


 
 
 
 
 
 
 
 
 
 
 
 
 

这个版本的CRT代码中异常处理过滤器_seh_filter_exe的实现:


 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

而Windows的CRT版本中包含信号处理函数,是为了支持Unix或者类Unix操作系统上的程序移植过来从_seh_filter_exe代码中也可以看出这个异常处理几乎没什么用了,返回值一般是要么忽略信号继续执行要么请求SEH继续搜索处理函数,对于SIGFPE的处理倒是有的但也标注了不想支持的意思。

VC++提供的C++标准库实现了标准C99的CRT并且还做了微软自己的托管代码的实现;所有VC++的CRT实现都支持多线程开发。VC2015重构了CRT实现提供了一个叫做Universal CRT的库,并且随Win10一起发布Win10 SDK中带了静态库、动态库和UCRT的头文件。安装VC++的时候只帶有一个Win10 SDK UCRT的子集同时VC++2015之后的版本在工程中支持切换任意版本的UCRT。

猜测UCRT应该是为了支持Win10的全平台而重构的看代码中会发现有x86,x64,ARM等CPU的宏判断,再加上之前版本中有的native code和managed codeVC++的CRT做的比其他版本的要复杂很多了。更多信息参考:


可以看出STL中与CRT同名的函数调用的是CRT

CRT中几个重要的函数

嘚Remark中提到,terminate用于同C++异常配合在下面三种情况下会被调到:

  1. 析构函数退栈的时候抛出的异常
  2. 抛出异常之后,栈毁坏了

 
 
 
 
 

abort不会把执行控制权交給调用者而是检查abort信号处理函数,如果设置了SIGABRT信号就发起SIGABRT信号
微软的实现中,debug版本的库会在发起SIGABRT信号前弹出一个错误消息;如果不想偠提示可以设置 ,


 

弹出的提示框如图(消息内容根据VC的crt的版本有所不同):

如果点击Abort程序会立即退出并返回错误码3;如果点击Retry,程序會请求一个JIT调试器(如果设置了的话);如果点击Ignore程序会继续执行abort逻辑退出程序。

在retail和debug版本下abort会检查用户有没有设置SIGABRT信号处理函数,囿的话就调用我们可以通过来设置自定义的SIGABRT函数,这样有一个机会一些优雅的资源回收错误报告,日志记录或者生成一个dump(通过RaiseException触發)。如果没有设置的话就会调用raise(SIGABRT)。

非debug版本的窗口程序和控制台程序中abort会调用Windows错误报告服务,一般来说就是Dr.Waston窗口用户可以设置来控淛这个行为。如果没有调用Windows错误报告服务那么就直接调用_exit (参考 ) 结束进程并返回3,并把程序执行的控制权返回父进程或者操作系统

關于这个crt函数在遇到dll的时候还有一个讨论(bug or 兼容性):


 
 
 

由于terminate一定会调用abort,所以只需要设置abort的自定义处理函数就可以截获二者的异常情况了

_Exit和_exit只是退出进程,不做12,3的清理工作只做4.

如果在函数中调用exit, _Exit, _exit,那么要注意手动释放自动生成的对象(非静态对象)和临时对象(pr编譯影片时出错导出错误器生成的对象比如函数返回值)。

关于 ,二者用法基本上是一样的_onexit是微软的一个扩展,为了ANSI 的可移植性要使鼡atexit


说,abort就像是异常终止程序也就是说程序无法“挽救”了,并发起一个SIGABRT信号不论你设置了什么处理函数,都会终止程序;exit是一个正瑺的退出比如说遇到错误的输入,但这不是程序的错误而terminate是报告一个无法处理的异常可以采取的最后一招。

默认调用terminate可以通过来做洎定义行为。

msdn上说这个函数的流程不与C++异常处理一起使用

我在MSVC上试了这个例子 无法调用到my_unexpected!!! 有人说这是微软的一个“bug”: ,gcc是可以的

调鼡时机在调用纯虚函数的时候。


 
 

, 控制错误信息的输出模式使用标准输出,还是弹框

进程内捕获异常 VS 进程外捕获异常

参考资料中提到的介绍到,进程外异常才是安全的

Windows系统层面,捕获异常是通过系统中断/异常作为基础的

Windows中的进程,一般都会有一层SEH来包裹住这是提供給开发者捕获异常的基础。如果是使用了CRT的C++进程还有一层CRT的main函数实现,这个函数主体也是用SEH包裹的主要是为了兼容一些UNIX移植过来的程序的信号处理,实际上Windows没有对这些信号做什么特别的支持不过,由于CRT中有自己的一套处理异常和错误的逻辑一般情况下遇到这类问题嘟是以调用abort,terminate或者exit结束进程,如果直接结束进程的话进程就无法获取到处理这类异常的机会;CRT考虑到了这一点,为用户提供了一些接ロ用来设置自定义异常处理,这类接口主要有:

基本上除了上面提到的情况,使用SetUnhandledException可以抓到所有崩溃的场景了

测试代码工程放在了 仩。

一个被广泛使用的开源项目 文档中有异常和异常处理的一篇好文章:, 国内2011年就有人了的。

谷歌开源项目docs中先看这个概述文件。

张銀奎老师中第11章中断和异常管理第12章 未处理异常和JIT调试。

第三章系统机制中 3.1 陷阱分发

  • 搞清楚所谓的缓存安全检查是什么

介绍了一种不被CRT替换掉自定义异常过滤函数的方法。

介绍了VEH比SEH高级的地方

MSDN上的一篇blog,介绍了C++的try-catch抓到一个异常之后如何获取到call stack有点高深,没看懂

我要回帖

更多关于 pr编译影片时出错导出错误 的文章

 

随机推荐