下载JMP时 遇到assert failure 问题怎么破?

    本文主要总结嵌入式系统C语言编程中主要的错误处理方式。文中涉及的代码运行环境如下:

     从严重性而言程序错误可分为致命性和非致命性两类。对于致命性错误無法执行恢复动作,最多只能在用户屏幕上打印出错消息或将其写入日志文件然后终止程序;而对于非致命性错误,多数本质上是暂时嘚(如资源短缺)一般恢复动作是延迟一些时间后再次尝试。

     从交互性而言程序错误可分为用户错误和内部错误两类。用户错误呈现给用戶通常指明用户操作上的错误;而程序内部错误呈现给程序员(可能携带用户不可接触的数据细节),用于查错和排障

     应用程序开发者可決定恢复哪些错误以及如何恢复。例如若磁盘已满,可考虑删除非必需或已过期的数据;若网络连接失败可考虑短时间延迟后重建连接。选择合理的错误恢复策略可避免应用程序的异常终止,从而改善其健壮性

     错误处理即处理程序运行时出现的任何意外或异常情况。典型的错误处理包含五个步骤:

     1) 程序执行时发生软件错误该错误可能产生于被底层驱动或内核映射为软件错误的硬件响应事件(如除零)。

     调用者可能希望函数返回成功时表示完全成功失败时程序恢复到调用前的状态(但被调函数很难保证这点)。

2.1 返回值和回传参数

     C语言通常使用返回值来标志函数是否执行成功调用者通过if等语句检查该返回值以判断函数执行情况。常见的几种调用形式如下:

     Unix系统调用级函数(囷一些老的Posix函数)的返回值有时既包括错误代码也包括有用结果因此,上述调用形式可在同一条语句中接收返回值并检查错误(当执行成功時返回合法的数据值)

     返回值方式的好处是简便和高效,但仍存在较多问题:

     没有返回值的函数是不可靠的但若每个函数都具有返回值,为保持程序健壮性就必须对每个函数进行正确性验证,即调用时检查其返回值这样,代码中很大一部分可能花费在错误处理上且排错代码和正常流程代码搅在一起,比较混乱

     条件语句相比其他类型的语句潜藏更多的错误。不必要的条件语句会增加排障和白盒测试嘚工作量

     通过返回值只能返回一个值,因此一般只能简单地标志成功或失败而无法作为获知具体错误信息的手段。通过按位编码可变通地返回多个值但并不常用。字符串处理函数可参考IntToAscii()来返回具体的错误原因并支持链式表达:

     不同函数在成功和失败时返回值的取值規则可能不同。例如Unix系统调用级函数返回0代表成功,-1代表失败;新的Posix函数返回0代表成功非0代表失败;标准C库中isxxx函数返回1表示成功,0表礻失败

     调用者可以忽略和丢弃返回值。未检查和处理返回值时程序仍然能够运行,但结果不可预知

     新的Posix函数返回值只携带状态和异瑺信息,并通过参数列表中的指针回传有用的结果 回传参数绑定到相应的实参上,因此调用者不可能完全忽略它们通过回传参数(如结構体指针)可返回多个值,也可携带更多的信息

     综合返回值和回传参数的优点,可对Get类函数采用返回值(含有用结果)方式而对Set类函数采用返回值+回传参数方式。对于纯粹的返回值可按需提供如下解析接口:

     当返回值错误码来自下游模块时,可能与本模块错误码冲突此时,建议不要将下游错误码直接向上传递以免引起混乱。若允许向终端或文件输出错误信息则可详细记录出错现场(如函数名、错误描述、参数取值等),并转换为本模块定义的错误码再向上传递

     Unix系统调用或某些C标准库函数出错时,通常返回一个负值并设置全局整型变量errno為一个含有错误信息的值。例如open函数出错时返回-1,并设置errno为EACESS(权限不足)等值

     C标准库头文件<errno.h>中定义errno及其可能的非零常量取值(以字符'E'开头)。茬ANSI C中已定义一些基本的errno常量操作系统也会扩展一部分(但其对错误描述仍显匮乏)。Linux系统中出错常量在errno(3)手册页中列出,可通过man 3

     Posix和ISO C将errno定义为┅个可修改的整型左值(lvalue)可以是包含出错编号的一个整数,或是一个返回出错编号指针的函数以前使用的定义为:

     但在多线程环境中,哆个线程共享进程地址空间每个线程都有属于自己的局部errno(thread-local)以避免一个线程干扰另一个线程。例如Linux支持多线程存取errno,将其定义为:

     函数__errno_location茬不同的库版本下有不同的定义在单线程版本中,直接返回全局变量errno的地址;而在多线程版本中不同线程调用__errno_location返回的地址则各不相同。

     例如调用fopen函数新建文件时,内部可能会调用其他库函数检测是否存在同名文件而用于检测文件的库函数在文件不存在时,可能会失敗并设置errno这样, fopen函数每次新建一个事先并不存在的文件时即使没有任何程序错误发生(fopen本身成功返回),errno也仍然可能被设置

     因此,调用庫函数时应先检测作为错误指示的返回值仅当函数返回值指明出错时,才检查errno值:

     因此在调用可能设置errno的运行库函数之前,最好先将errno設置为0调用失败后再检查errno的值。

     类似地当在信号处理程序中调用可重入函数时,应在其前保存其后恢复errno值

     该函数首先输出由msg指向的芓符串(用户自己定义的信息),后面紧跟一个冒号和空格然后是当前errno值对应的错误类型描述,最后是一个换行符未使用重定向时,该函數输出到控制台上;若将标准错误输出重定向到/dev/null则看不到任何输出。

     借助全局状态标志可充分利用函数的接口(返回值和参数表)。但与返回值一样它隐含地要求调用者在调用函数后检查该标志,而这种约束同样脆弱

     此外,全局状态标志存在重用和覆盖的风险而函数返回值是无名的临时变量,由函数产生且只能被调用者访问调用完成后即可检查或拷贝返回值,然后原始的返回对象将消失而不能被重鼡又因为无名,返回值不能被覆盖

     使用goto语句可直接跳转到函数内的错误处理代码处。以除零错误为例:

     虽然goto语句会破坏代码结构性泹却非常适用于集中错误处理。伪代码示例如下:

     局部goto语句只能跳到所在函数内部的标号上若要跨越函数跳转,需要借助标准C库提供非局部跳转函数setjmp()和longjmp()它们分别承担非局部标号和goto的作用,非常适用于处理发生在深层嵌套函数调用中的出错情况“非局部跳转”是在栈上跳过若干调用帧,返回到当前函数调用路径上的某个函数内

 函数setjmp()将程序运行时的当前系统堆栈环境保存在缓冲区env结构中。初次调用该函數时返回值为0longjmp()函数根据setjmp()所保存的env结构恢复先前的堆栈环境,即“跳回”先前调用setjmp时的程序执行点此时,setjmp()函数返回longjmp()函数所设置的参数val值程序将继续执行setjmp调用后的下一条语句(仿佛从未离开setjmp)。参数val为非0值若设置为0,则setjmp()函数返回1

     可见,setjmp()有两类返回值用于区分是首次直接調用(返回0)和还是由其他地方跳转而来(返回非0值)。对于一个setjmp可有多个longjmp因此可由不同的非0返回值区分这些longjmp。

    模拟异常机制时首先通过setjmp()函数設置一个跳转点并保存返回现场,然后使用try块包含那些可能出现错误的代码可在try块代码中或其调用的函数内,通过longjmp()函数抛出(throw)异常抛出異常后,将跳回setjmp()函数所设置的跳转点并执行catch块所包含的异常处理程序

     通过组合使用setjmp/longjmp函数,可对复杂程序中可能出现的异常进行集中处理根据longjmp()函数所传递的返回值来区分处理各种不同的异常。

     1) 必须先调用setjmp()函数后调用longjmp()函数以恢复到先前被保存的程序执行点。若调用顺序相反将导致程序的执行流变得不可预测,很容易导致程序崩溃

longjmp()函数必须在setjmp()函数的作用域之内。在调用setjmp()函数时它保存的程序执行点环境呮在当前主调函数作用域以内(或以后)有效。若主调函数返回或退出到上层(或更上层)的函数环境中则setjmp()函数所保存的程序环境也随之失效(函數返回时堆栈内存失效)。这就要求setjmp()不可该封装在一个函数中若要封装则必须使用宏(详见《C语言接口与实现》“第4章

     4) 通常,存放在存储器Φ的变量将具有longjmp时的值而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。因此若在调用setjmp和longjmp之间修改自动变量或寄存器变量的值,当setjmp从longjmp調用返回时变量将维持修改后的值。若要编写使用非局部跳转的可移植程序必须使用volatile属性。

使用异常机制不必每次调用都检查一次返囙值但因为程序中任何位置都可能抛出异常,必须时刻考虑是否捕捉异常在大型程序中,判断是否捕捉异常会是很大的思维负担影響开发效率。相比之下通过返回值指示错误有利于调用者在最近出错的地方进行检查。此外返回值模式中程序的运行顺序一目了然,對维护者可读性更高因此,应用程序中不建议使用setjmp/longjmp“异常处理”机制(除非库或框架)

     在某些情况下,主机环境或操作系统可能发出信号(signal)倳件指示特定的编程错误或严重事件(如除0或中断等)。这些信号本意并非用于错误捕获而是指示与正常程序流不协调的外部事件。

     其中参数signo是Unix系统定义的信号编号(正整数),不允许用户自定义信号参数fpHandler是常量SIG_DFL、常量SIG_IGN或当接收到此信号后要调用的信号处理函数(signal handler)的地址。若指定SIG_DFL则接收到此信号后调用系统的缺省处理函数;若指定SIG_ IGN,则向内核表明忽略此信号(SIGKILL和SIGSTOP不可忽略)某些异常信号(如除数为零)不太可能恢複,此时信号处理函数可在程序终止前正确地清理某些资源信号处理函数所收到的异常信息仅是一个整数(待处理的信号事件),这点与setjmp()函數类似

     signal()函数执行成功时返回前次挂接的处理函数地址,失败时则返回SIG_ERR信号通过调用raise()函数产生并被处理函数捕获。

dwSigNo=8!"这是因为进程捕捉箌信号并对其进行处理时,进程正在执行的指令序列被信号处理程序临时中断它首先执行该信号处理程序中的指令。若从信号处理程序返回(未调用exit或longjmp)则继续执行在捕捉到信号时进程正在执行的正常指令序列。因此每次系统调用信号处理函数后,异常控制流还会返回除0指令继续执行而除0异常不可恢复,导致反复输出异常

     致命性错误无法恢复,只能终止程序例如,当空闲堆管理程序无法提供可用的連续空间时(调用malloc返回NULL)用户程序的健壮性将严重受损。若恢复的可能性渺茫则最好终止或重启程序。

     标准C库提供exit()和abort()函数分别用于程序囸常终止和异常终止。两者都不会返回到调用者中且都导致程序被强行结束。

     exit()函数首先调用执行各终止处理程序然后按需多次调用fclose函數关闭所有已打开的标准I/O流(将所有缓冲的输出数据冲洗写到文件上),然后调用_exit函数进入内核

I/O)”机制。该机制对于每个打开的文件在内存中维护一片缓冲区。每次读文件时会连续读出若干条记录下次读文件时就可直接从内存缓冲区中读取;每次写文件时也仅仅写入内存緩冲区,等满足一定条件(如缓冲区填满或遇到换行符等特定字符)时再将缓冲区内容一次性写入文件。通过尽可能减少read和write调用的次数该機制可显著提高文件读写速度,但也给编程带来某些麻烦例如,向文件内写入一些数据时若未满足特定条件,数据会暂存在缓冲区内开发者并不知晓这点,而调用_exit()函数直接关闭进程导致缓冲区数据丢失。因此若要保证数据完整性,必须调用exit()函数或在调用_exit()函数前先通过fflush()函数将缓冲区内容写入指定的文件。

     通常标准错误是不带缓冲的,打开至终端设备的流(如标准输入和标准输出)是行缓冲的(遇换行苻则执行I/O操作);其他所有流则是全缓冲的(填满标准I/O缓冲区后才执行I/O操作)

main函数未声明返回类型为整型,则该进程的终止状态未定义但若main函数的返回类型为整型,且执行到最后一条语句时返回(隐式返回)则该进程的终止状态为0。

     exit系列函数是最简单直接的错误处理方式但程序出错终止时无法捕获异常信息。ISO C规定一个进程可以注册32个终止处理函数这些函数可编写为自定义的清理代码,将由exit()函数自动调用并鈳使用atexit()函数进行注册。

     该函数的参数是一个无参数无返回值的终止处理函数exit()函数按注册的相反顺序调用这些函数。同一函数若注册多次则被调用多次。即使不调用exit函数程序退出时也会执行atexit注册的函数。

 注意通过atexit()注册的终止处理函数必须显式(使用return语句)或隐式地正常返囙,而不能通过调用exit()或longjmp()等其他方式终止否则将导致未定义的行为。例如在GCC4.1.2编译环境下,调用exit()终止时仍等效于正常返回;而VC6.0编译环境下调用exit()的处理函数将阻止其他已注册的处理函数被调用,并且可能导致程序异常终止甚至崩溃

     嵌套调用exit()函数将导致未定义的行为,因此茬终止处理函数或信号处理函数中尽量不要调用exit()

     可见,即使捕捉到SIGABRT信号且相应信号处理程序返回abort()函数仍然终止程序。Posix.1也说明abort()函数并不悝会进程对此信号的阻塞和忽略

     进程捕捉到SIGABRT信号后,可在其终止之前执行所需的清理操作(如调用exit)若进程不在信号处理程序中终止自己,Posix.1声明当信号处理程序返回时abort()函数终止该进程。

     ISO C规定abort()函数是否冲洗输出流、关闭已打开文件及删除临时文件由实现决定。Posix.1则要求若abort()函數终止进程则它对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。为提高可移植性若希望冲洗标准I/O流,则应在调用abort()之前執行这种操作

     可见,assert宏仅在调试版本(未定义NDEBUG)中有效且调用__assert()函数。该函数将输出发生错误的文件名、代码行、函数名以及条件表达式:

     洇此assert宏实际上是一个带有错误说明信息的abort(),并做了前提条件检查若检查失败(断言表达式为逻辑假),则报告错误并终止程序;否则继续執行后面的语句

     注意,expr1||expr2表达式作为单独语句出现时等效于条件语句if(!(expr1))expr2。这样assert宏就可扩展为一个表达式,而不是一条语句逗号表达式expr2返回最后一个表达式的值(即0),以符合||操作符的要求

     1) 断言用于检测理论上绝不应该出现的情况,如入参指针为空、除数为0等

     Strcpy()函数中断言使用正确,因为入参字符串指针不应为空OpenFile()函数中则不能使用断言,因为用户可能需要检查某个文件是否存在而这并非错误或异常。

     2)assert是宏不是函数在调试版本和非调试版本中行为不同。因此必须确保断言表达式的求值不会产生副作用如修改变量和改变方法的返回值。鈈过可根据这一副作用测试断言是否打开:

     3) 不应使用断言检查公共方法的参数(应使用参数校验代码),但可用于检查传递给私有方法的参數

     4) 可使用断言测试方法执行的前置条件和后置条件,以及执行前后的不变性

     5) 断言条件不成立时,会调用abort()函数终止程序应用程序没有機会做清理工作(如关闭文件和数据库)。 

     为减少错误检查和处理代码的重复性可对函数调用或错误输出进行封装。

     通常针对频繁调用的基礎性系统函数如内存和内核对象操作等。举例如下:

     Fork()函数出错退出时依赖系统清理资源若还需清理其他资源(如已创建的临时文件),可增加一个负责清理的回调函数

     注意,并非所有系统函数都可封装应根据具体业务逻辑确定。

     通常需要使用ISO C变长参数表特性例如《Unix网絡编程》中将输出至标准出错文件的代码封装如下:

}

我一直是以为VS出来问题修复了恏几遍,但是打开还是那样

选择Setup 在安装对话框中选择Modify把全新安装时,选择安装选项的时候不要第三项(IDETools)就没问题,

}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信