Subject:
|
Design by Contract (long post)
|
Newsgroups:
|
lugnet.robotics.rcx
|
Date:
|
Thu, 8 Jan 2004 07:03:04 GMT
|
Viewed:
|
3287 times
|
| |
| |
Hi, people.
The reason for the flood of posts is that Ive been trying to get authenticated
on LUGNET for the last week :( But as you can see, Im authenticated now :)
On to the topic of this post (sorry for such a long post):
I have created an assert() macro which works under BrickOS. For those
unfamiliar with assert(), it is a standard C macro whose purpose is to help find
bugs. It is integral to the design by contract style of programming pioneered
by Bertrand Meyer.
Briefly, assert() expects a boolean expression as an argument. If the argument
evaluates true, nothing happens. If it evaluates false, that is considered to
be a program error, and the program terminates, identifying which assert()
statement failed.
How would you use it ? Well I noticed that the BrickOS code almost never checks
the malloc return value. If malloc() is unable to allocate the requested
memory, it should return a null pointer. So you might write
assert (myptr = malloc (sizeof (mystruct));
Now, if the memory cant be allocated, the program will still crash, but it will
do it noisily, rather than silently, and you have some chance of tracking down
what failed. This is a facility that finds bugs for you automatically !
Using assertions serves a documentation purpose, enforces design assumptions and
is a useful tool for writers of modules to protect themselves from poorly
behaved clients. There are plenty of books and magazine articles that can tell
you more.
Cost
But what about the overhead ? I hear you saying. The assert() macro is
controlled by a preprocessor constant: If NDEBUG is defined (no debugging)
assert() statements vanish from the code. When assert() is enabled, Each time
it is used it costs an expression evaluation, and a function call.
Description:
I modified the helloworld demo program to show how assert is used and
implemented:
#include <conio.h>
#include <unistd.h>
//#include "assert.h"
//#define NDEBUG
void assert_fail (void);
__asm__ ("
.text
.global _cputw
_assert_fail:
mov.w @r7, r0
jsr @_cputw
loop:
bra loop
");
#ifdef NDEBUG
#define assert(x) ((void)0)
#else
#define assert(x) if (x) {} else { assert_fail(); }
#endif
int main(int argc, char **argv) {
cputs("hello");
sleep(1);
cputs("world");
sleep(1);
assert (1==0);
cputs ("Here");
sleep(1);
return 0;
}
Here should not be displayed. Normally, the macro definition would be in a
file called assert.h, and the assert_fail function would be in any convenient
module.
How does it work ? When NDEBUG is defined, the macro does nothing. When not
defined, assert calls assert_fail() if its condition evaluates false.
assert_fail() displays its function return address and halts. The function
return address is the point in the code where the macro was inserted.
What use is the function return address ? If you know the program load address,
and have the linker map files handy, you can work out which assert() was the one
that failed.
The program load address is tricky, because it might be different each time the
program is loaded. I added the following code to program_run() to display the
program load address:
static void program_run(unsigned nr) {
if(program_valid(nr)) {
program_t *prog=programs+nr;
// *IM ADDED*
#ifdef DISPLAY_START_ADDR // help track down assert()
cputs ("Addr"); msleep (500);
cputw (prog->text); msleep (1000);
#endif
// *IM ADDED END*
When I run the test program on my RCX, it prints out:
Addr B68C
Hello
World
B6B6
And then stops. So the assert statement was B6B6-B68C = 0x2A from the start of
my program. How do I relate that to the source code ? Use the linker map file:
The Gnu linker, ld will create a map file if you ask it to edit the makefile
in the demo directory, so that it includes a dmap target:
PROGRAMS= assert_test.lx assert_test.dmap
When the program is maked, the assert_test.dmap file that I got includes the
following lines:
0000ae2a ? _mm_start
0000b000 t _assert_fail
0000b000 t .text
0000b000 T ___text
0000b006 t loop
0000b008 t .bf
0000b008 T _main
0000b03c t .ef
As best as I can work out 0xb000 is a virtual address that is simply a place
holder for the ultimate load address (which varies at run time). You can see in
this listing that main() occupies addresses 0x0008 .. 0x003c from the start of
the program. As we saw above, 0x2A is in this range, so the assert() that fired
was in main.
You may have several assert()s in the same function. If you generate an
assembler listing file, you get a mixed C-assembly listing with op-codes and
addresses. You can use the listing file to track the assert() down to the exact
line of code. To get the listing file, I edited the following line in
makefile.common in the main BrickOS directory:
COPT =-O2 -fno-builtin -fomit-frame-pointer -g -Wa,-alh=$*.lst
I agree that its all a bit of a hassle to go to my hex calculator is getting
more of a work out than it has in a long time. But if you are committed to
assert() (I am), then its a small price to pay for something that will find your
bugs for you.
Enjoy !
Iain
|
|
Message has 1 Reply: | | Re: Design by Contract (long post)
|
| (...) Hi Iain! (...) Cool! (...) I have a question about this: If NDEBUG is defined, shouldn't it still execute x? That is, shouldn't the code in assert.h (or wherever) be: #ifdef NDEBUG #define assert(x) (x) #else #define assert(x) if (x) else (...) (21 years ago, 8-Jan-04, to lugnet.robotics.rcx, FTX)
|
7 Messages in This Thread:
- Entire Thread on One Page:
- Nested:
All | Brief | Compact | Dots
Linear:
All | Brief | Compact
|
|
|
|