Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 40 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
40
Dung lượng
312,64 KB
Nội dung
252 PANICS A solution is to run deliberately badly-behaved client code in a separate test thread, programmatically checking the resulting exit reasons and categories of the panicked thread against those you would expect to have occurred. You should disable just-in-time debugging for the duration of the test, so that only the test thread, rather than the emulator, is terminated. For example: enum TChilliStrength { ESweetPepper, EJalapeno, EScotchBonnet }; void EatChilli(TChilliStrength aStrength) { _LIT(KTooStrong, "Too Strong!"); __ASSERT_ALWAYS(EScotchBonnet!=aStrength, User::Panic(KTooStrong, KErrAbort); // Omitted for clarity } // Thread function TInt TestPanics(TAny* /*aData*/) {// A panic occurs if code is called incorrectly EatChilli(EScotchBonnet); return (KErrNone); } void TestDefence() { // Save current just-in-time status TBool jitEnabled = User::JustInTime(); // Disable just-in-time debugging for this test User::SetJustInTime(EFalse); _LIT(KPanicThread, "PanicThread"); // Create a separate thread in which to run the panic testing RThread testThread; TInt r = testThread.Create(KPanicThread, TestPanics, KDefaultStackSize, NULL, NULL); ASSERT(KErrNone==r); // Request notification of testThread’s death (see Chapter 10) TRequestStatus tStatus; testThread.Logon(tStatus); testThread.Resume(); User::WaitForRequest(tStatus); // Wait until the thread dies ASSERT(testThread.ExitType()==EExitPanic); // Test the panic reason is as expected ASSERT(testThread.ExitReason()==KErrAbort); testThread.Close(); // Set just-in-time back to previous setting User::SetJustInTime(jitEnabled); } Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com SUMMARY 253 15.5 Faults, Leaves and Panics A fault is raised if a critical error occurs such that the operating system cannot continue normal operation. On hardware, this results in a reboot. A fault can only occur in kernel-side code or a thread which is essential to the system, for example the file server, so typically you will not encounter them unless you are writing device drivers or uncover a bug in the OS. In effect, a fault is another name for a serious system panic. Chapter 2 discusses leaves in more detail, but, in essence, they occur under exceptional conditions such as out of memory or the absence of a communications link. Unlike a panic, a leave should always be caught (”trapped”) by code somewhere in the call stack, because there should always be a top-level TRAP. However, if a leave is not caught, this implies that the top-level TRAP is absent (a programming error) and the thread will be panicked and terminate. 15.6 Summary This chapter discussed the use of panics to terminate the flow of execution of a thread. Panics cannot be ”caught” like an exception and are severe, resulting in a poor user experience. For that reason, panics are only useful to track down programming errors and, on Symbian OS, are typically combined with an assertion statement, as discussed in the next chapter. This chapter described the best way to identify panics and illustrated how to test the panics that you’ve added to your own code for defensive programming. It gave a few examples of commonly-encountered system panics and directed you to the Panics section of the system documentation for a detailed listing of Symbian OS system panics. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 16 Bug Detection Using Assertions On the contrary! As Ibsen lay dying, his nurse brought him some visitors. ”Our patient is feeling much better today,” she told them. Ibsen woke up, made the exclamation above, and died. In C++, assertions are used to check that assumptions made about code are correct and that the state, for example, of objects, function parameters or return values, is as expected. Typically, an assertion evaluates a statement and, if it is false, halts execution of the code and perhaps prints out a message indicating what failed the test, or where in code the failure occurred. On Symbian OS, you’ll find the definition of two assertion macros 1 in e32def.h: #define __ASSERT_ALWAYS(c,p) (void)((c)||(p,0)) #if defined(_DEBUG) #define __ASSERT_DEBUG(c,p) (void)((c)||(p,0)) #endif As you can see from the definition, if the assertion of condition c is false, procedure p is called; this should always halt the flow of execution, typically by panicking (panics are described in detail in Chapter 15). You can apply the assertion either in debug code only or in both debug and release builds. I’ll discuss how you decide which is appropriate later in the chapter. 1 At first sight, these definitions seem more complex than they need to be, when the following simpler definition could be used: #define __ASSERT_ALWAYS(c,p) ((c)||(p)) The reason for the (p,0) expression is for cases where the type returned from p is void (the case when p is a typical Panic() function) or a value that can’t be converted to an integer type for evaluation. The cast to void is present to prevent the return value of the expression being used inadvertently. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 256 BUG DETECTION USING ASSERTIONS You’ll notice that the assertion macros do not panic by default, but allow you to specify what procedure to call should the assertion fail. This gives you more control, but you should always terminate the running code and flag up the failure, rather than return an error or leave. Assertions help you detect invalid states or bad program logic so you can fix your code as early as possible. It makes sense to stop the code at the point of error, thus forcing you to fix the problem (or remove the assertion statement if your assumption is invalid). If the assertion simply returns an error on failure, not only does it alter the program flow, but it also makes it harder to track down the bug. You should always raise a panic when an assertion statement fails. 16.1 −−− ASSERT − DEBUG Here’s one example of how to use the debug assertion macro: void CTestClass::TestValue(TInt aValue) { #ifdef _DEBUG _LIT(KPanicDescriptor, "TestValue"); // Literal descriptor #endif __ASSERT_DEBUG((aValue>=0), User::Panic(KMyPanicDescriptor, KErrArgument)); } Of course, this is somewhat awkward, especially if you expect to use a number of assertions to validate your code, so you’ll probably define a panic utility function for your module, with its own panic category string and a set of panic enumerators specific to the class. So, for example, you’d add the following enumeration to CTestClass, so as not to pollute the global namespace: enum TTestClassPanic { EInvalidData, // =0 EUninitializedValue // =1 }; Then define a panic function, either as a member of the class or as a static function within the file containing the implementation of the class: Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com −−− ASSERT − DEBUG 257 static void Panic(TInt aCategory) { _LIT(KTestClassPanic, "CTestClass-Panic"); User::Panic(KTestClassPanic, aCategory); } You could then write the assertion in TestValue() as follows: void CTestClass::TestValue(TInt aValue) { __ASSERT_DEBUG((aValue> =0), Panic(EInvalidTestValueInput)); } The advantage of using an identifiable panic descriptor and enu- merated values for different assertion conditions is traceability, both for yourself and clients of your code, when an assertion fails and a panic occurs. This is particularly useful for others using your libraries, since they may not have access to your code in its entirety, but merely to the header files. If your panic string is clear and unique, they should be able to locate the appropriate class and use the panic category enumeration to find the associated failure, which you will have named and documented clearly to explain why the assertion failed. There may be cases where there’s nothing more a client programmer can do other than report the bug to you, the author of the code; alternatively, the problem could be down to their misuse of your library, which they’ll be able to correct. I’ll discuss the pros and cons of using assertions to protect your code against badly-programmed calling code later in this chapter. If you don’t want or need an extensive set of enumerated panic values, and you don’t expect external callers to need to trace a panic, you may consider using a more lightweight and anonymous assertion. A good example of this is to test the internal state of an object, which could not possibly be modified by an external caller, and thus should always be valid unless you have a bug in your code. Assertions can be added early in the development process, but left in the code, in debug builds, to validate the code as it is maintained and refactored. In these cases, you may consider using the ASSERT macro, defined in e32def.h as follows: #define ASSERT(x) __ASSERT_DEBUG(x, User::Invariant()) I like this macro because it doesn’t need you to provide a panic category or descriptor. If condition x is false, in debug builds only, it calls User::Invariant() which itself panics with category USER and reason 0. The macro can be used as follows: Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 258 BUG DETECTION USING ASSERTIONS ASSERT(iClanger>0); As an alternative to using ASSERT to test the internal state of your object, you may wish to consider using the __TEST_INVARIANT macro, which I discuss in more detail in Chapter 17. An alternative, useful definition of ASSERT which you may see in some Symbian OS code is as follows: #ifdef _DEBUG #ifdef ASSERT #undef ASSERT #endif #define __ASSERT_FILE__(s) _LIT(KPanicFileName,s) #define __ASSERT_PANIC__(l) User::Panic(KPanicFileName().Right(12),l) #define ASSERT(x) { __ASSERT_FILE__(__FILE__); __ASSERT_DEBUG(x, __ASSERT_PANIC__(__LINE__) ); } #endif This slightly alarming construction is actually quite simple; in debug builds, if condition x is false, the code is halted by a panic identifying the exact place in code (in the panic descriptor – which contains the last 12 characters of the filename) and the panic category (which contains the line of code at which the assertion failed). The disadvantage of using this construct is that you are coupling the compiled binary directly to the source file. You cannot later modify your code file, even to make non-functional changes to comments or white space lines, without recompiling it to update the assertion statements. The resulting binary will differ from the original, regardless of the nature of the changes. Depending on how you deliver your code, this limitation may prohibit you from using this macro. Let’s move on from how to use the Symbian OS assertion syntax to consider when you should use assertions and, perhaps more importantly, when you should not. Firstly, don’t put code with side effects into assertion statements. By this, I mean code which is evaluated before a condition can be verified. For example: __ASSERT_DEBUG(FunctionReturningTrue(), Panic(EUnexpectedReturnValue)); __ASSERT_DEBUG(++index<=KMaxValue, Panic(EInvalidIndex)); The reason for this is clear; the code may well behave as you expect in debug mode, but in release builds the assertion statements are removed by the preprocessor, and with them potentially vital steps in your pro- gramming logic. Rather than use the abbreviated cases above, you should perform the evaluations first and then pass the returned values into the Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com −−− ASSERT − DEBUG 259 assertion. You should follow this rule for both __ASSERT_DEBUG and __ASSERT_ALWAYS statements, despite the fact that the latter are com- piled into release code, because, while you may initially decide the assertion applies in release builds, this may change during the develop- ment or maintenance process. You could be storing up a future bug for the sake of avoiding an extra line of code. You must also make a clear distinction between programming errors (”bugs”) and exceptional conditions. Examples of bugs might be contra- dictory assumptions, unexpected design errors or genuine implementation errors, such as writing off the end of an array or trying to write to a file before opening it. These are persistent, unrecoverable errors which should be detected and corrected in your code at the earliest opportunity. An exceptional condition is different in that it may legitimately arise, although it is rare (hence the term ”exceptional”) and is not consistent with typical or expected execution. It is not possible to stop exceptions occurring, so your code should implement a graceful recovery strategy. A good example of an exceptional condition that may occur on Symbian OS is an out-of-memory failure, because it is designed to run constantly on devices with limited resources for long periods of time without a system reset. To distinguish between bugs and exceptions, you should consider the following question. Can a scenario arise legitimately, and if it can, is there anything you should or could do to handle it? If your answer is ”yes”, you’re looking at an exceptional condition – on Symbian OS, this is exhibited as a leave (leaving is discussed in Chapter 2). If the answer is ”no”, you should consider the situation to be caused by a bug which should be tracked down and fixed. The rest of this chapter will focus on the use of assertions to highlight such programming errors. When code encounters a bug, it should be flagged up at the point at which it occurs so it can be fixed, rather than handled or ignored (which can at best complicate the issue and, at worst, make the bug more difficult to find or introduce additional defects as you ”code around it”). You could consider assertions as an annoying colleague, leaning over your shoulder pointing out defects for you as your code runs. They don’t prevent problems, but make them obvious as they arise so you can fix them. If you add assertion statements liberally as you write code, they effectively document assumptions you make about your program logic and may, in addition, flag up unexpected problems. As you consider which assertions to apply, you are actually asking yourself what implicit assumptions apply to the code and how you can test them. By thinking about each piece of code you write in this way, you may well discover other conditions to test or eliminate that would not have been immediately obvious. Frequent application of assertions as you code can thus help you to pre-empt bugs, as well as catch those already in existence. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 260 BUG DETECTION USING ASSERTIONS And there’s more! Another benefit of assertions is the confidence that your code is behaving correctly, and that you are not ignoring defects which may later manifest themselves where they are hard to track down, for example intermittently or through behavior seemingly unrelated to the code that contains the error. Say you write some library code containing no assertions and then create some code to test it, which runs and returns no errors; are you confident that everything behaves as you expect and the test code checks every boundary condition? Are you sure? Certain? Adding assertions to test fundamental assumptions in your code will show you immediately if it is swallowing or masking defects – and it should give you more confidence that the test results are valid. Sure, you might hit some unpleasant surprises as the test code runs for the first time, but once you’ve ironed out any failures, you can be more confident about its overall quality. What’s more, the addition of assertions also protects your code against any regressions that may be introduced during maintenance and refactoring but which would not otherwise be picked up by your test code. What more could you ask? The cases so far could be considered as ”self defense”, in that I’ve discussed using assertions to catch bugs in your code. Let’s move on to consider defensive programming in general. Defensive programming is not about retorting ”It works OK on my machine” after being informed that your code doesn’t work as expected. It’s based on defending your code against irresponsible use or downright abuse by code that calls it. Defensive code protects functions against invalid input, by inspecting data passed in and rejecting corrupt or otherwise flawed parameters, such as strings that are too long or out-of-range numerical values. You’ll need to consider how to handle bad parameters depending on how your code is called; for example, you may want to assert that the data is good, terminating with a panic if it is not. Alternatively, you may decide to continue the flow of execution, so instead of assertions, you’ll check each incoming parameter (e.g. using if statements) and return to the caller if invalid data is detected – either with an error value or a leave code. Another method would be to check incoming data and, if a parameter is invalid, substitute it with a default parameter or continue with the closest legal value. What you don’t want to do is ignore invalid input and carry on regardless, since this could lead to problems later on, such as data corruption. Whatever method you use to handle illegal input, it should be consistent throughout your code. Your clients should be testing with debug versions of your libraries and thus you could use __ASSERT_DEBUG statements to alert them of invalid input, or other misuse, so they can correct it. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com −−− ASSERT − ALWAYS 261 __ASSERT_DEBUG assertions can be added early in the develop- ment process to highlight programming errors and can be left in to validate the code as it is maintained and refactored – acting as a means to ”design by contract”. 16.2 −−− ASSERT − ALWAYS You still need to be defensive by checking for illegal usage in release builds too, but the case for using __ASSERT_ALWAYS isn’t clear cut. Remember, your assertions will terminate the flow of execution and panic the library, displaying a nasty ”program closed” dialog to the user, which is generally best avoided where possible. Additionally, you should consider the impact on the speed and size of your code if you apply assertion statements liberally in release builds. If you decide not to use __ASSERT_ALWAYSto check incoming values, you should use another defensive technique to guard against illegal input, such as a set of if statements to check values and return error codes or leave when data is unusable. You could use these in combination with asetof__ASSERT_DEBUG statements to alert the client programmer to invalid use in debug builds, but often it is preferable to keep the flow of execution the same in both debug and release builds. In such cases, I suggest you don’t use debug assertions to check input, but instead use if statement checking in both modes, and document each expected return value for your functions. Client programmers should understand their responsibility to interpret the return value and act accordingly. I’ll illustrate this with an example later in this chapter. To determine whether you should use __ASSERT_ALWAYS or another, less terminal, defense, I recommend that you consider whether the calling code may be able to take a different action if you do return an error. Invalid input is a bug from the perspective of your code, but may be caused by an exceptional condition in the calling code which can be handled. A simplistic example would be a call to your code to open and write to a file, where the caller passes in the full file name and path, as well as the data to be written to the file. If the file does not exist, it is probably more appropriate to return this information to the caller through a returned error code or leave value than to assert in a release build. Client code can then anticipate this and deal with it, without the need for your library to panic and alarm the user accordingly. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]... versions of Symbian OS Compatibility must be managed carefully between major Symbian OS product releases For developers delivering Symbian OS code that is not built into ROM, there is the additional complexity of managing compatibility between releases that are built on the same Symbian OS platform Let’s consider the basics of delivering a software library – a binary component which performs a well-defined... useful debug macros and test classes on Symbian OS for tracking down programming errors such as memory leaks and invalid internal state You can find more information about handling leaves (Symbian OS exceptions) in Chapter 2 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 17 Debug Macros and Test Classes If you have built castles in the air, your work need not be lost; that is... the evolution of Symbian OS This chapter defines some of the basic terminology of compatibility It then moves on to discuss some of the practical aspects, which will help you to develop code on Symbian OS which can be extended in a controlled manner without having a negative impact on your external dependents 18. 1 Forward and Backward Compatibility Compatibility works in two directions – forward and backward... UHEAP_MARKEND macros, an ALLOC panic will be raised by User:: DbgMarkEnd(), which is called by UHEAP_MARKEND So what should you do if you encounter such a panic? How do you debug the code to find out what was leaked so you can fix the problem? 2 Incidentally, Symbian OS also provides a set of KHEAP_X macros, which look identical to the UHEAP_X macros, but can be used to check the kernel heap, for example... can find more information about how to use the debugger to find a memory leak on the Symbian Developer Network website (on www .symbian. com/developer, navigate to the Knowledgebase and search for ”memory leak”) It’s good practice to use the UHEAP_X macros to verify that your code does not leak memory You can put UHEAP_MARK and UHEAP_MARKEND pairs in your test code and, since the macros are not compiled... create an active scheduler, as described in Chapters 8 and 9 17.4 Summary Memory is a limited resource on Symbian OS and must be managed carefully to ensure it is not wasted by memory leaks In addition, an application must handle gracefully any exceptional conditions arising when memory resources are exhausted Symbian OS provides a set of debug-only macros that can be added directly to code to check both... before the first UHEAP_MARK macro: void PushLotsL() {// Expand the cleanup stack for 500 pointers { TInt* dummy = NULL; for (TInt index =0; index . debug macros and test classes on Symbian OS for tracking down programming errors such as memory leaks and invalid internal state. You can find more information about handling leaves (Symbian OS exceptions). code for defensive programming. It gave a few examples of commonly-encountered system panics and directed you to the Panics section of the system documentation for a detailed listing of Symbian OS. the macros I’ve discussed won’t work. There are at least three possible options here: • In addition to the __UHEAP_X macros, there is also a set of equivalent __RHEAP_X macros which perform checking