300 lines
15 KiB
Plaintext
300 lines
15 KiB
Plaintext
chapter 15 "Debugging"
|
|
Intermediate LPC
|
|
Descartes of Borg
|
|
November 1993
|
|
|
|
Chapter 7: Debugging
|
|
|
|
7.1 Types of Errors
|
|
By now, you have likely run into errors here, there, and everywhere. In
|
|
general, there are three sorts of errors you might see: compile time
|
|
errors, run time errors, and malfunctioning code. On most muds you
|
|
will find a personal file where your compile time errors are logged. For
|
|
the most part, this file can be found either in your home directory as the
|
|
file named "log" or ".log", or somewhere in the directory "/log" as a file
|
|
with your name.. In addition, muds tend to keep a log of run time errors
|
|
which occur while the mud is up. Again, this is generally found in
|
|
"/log". On MudOS muds it is called "debug.log". On other muds it may
|
|
be called something different like "lpmud.log". Ask your administrators
|
|
where compile time and run time errors are each logged if you do not
|
|
already know.
|
|
|
|
Compile time errors are errors which occur when the driver tries to load
|
|
an object into memory. If, when the driver is trying to load an object
|
|
into memory, it encounters things which it simply does not understand
|
|
with respect to what you wrote, it will fail to load it into memory and log
|
|
why it could not load the object into your personal error log. The most
|
|
common compile time errors are typos, missing or extra (), {}. [], or "",
|
|
and failure to declare properly functions and variables used by the
|
|
object.
|
|
|
|
Run time errors occur when something wrong happens to an object in
|
|
memory while it is executing a statement. For example, the driver
|
|
cannot tell whether the statement "x/y" will be valid in all circumstances.
|
|
In fact, it is a valid LPC expression. Yet, if the value of y is 0, then a
|
|
run time error will occur since you cannot divide by 0. When the driver
|
|
runs across an error during the execution of a function, it aborts
|
|
execution of the function and logs an error to the game's run time error
|
|
log. It will also show the error to this_player(), if defined, if the player
|
|
is a creator, or it will show "What?" to players. Most common causes
|
|
for run time errors are bad values and trying to perform operations with
|
|
data types for which those operations are not defined.
|
|
|
|
The most insideous type of error, however, is plain malfunctioning
|
|
code. These errors do not log, since the driver never really realizes that
|
|
anything is wrong. In short, this error happens when you think the code
|
|
says one thing, but in fact it says another thing. People too often
|
|
encounter this bug and automatically insist that it must be a mudlib or
|
|
driver bug. Everyone makes all types of errors though, and more often
|
|
than not when code is not functioning the way you should, it will be
|
|
because you misread it.
|
|
|
|
7.2 Debugging Compile Time Errors
|
|
Compile time errors are certainly the most common and simplest bugs to
|
|
debug. New coders often get frustrated by them due to the obscure
|
|
nature of some error messages. Nevertheless, once a person becomes
|
|
used to the error messages generated by their driver, debugging compile
|
|
time errors becomes utterly routine.
|
|
|
|
In your error log, the driver will tell you the type of error and on which
|
|
line it finally noticed there was an error. Note that this is not on which
|
|
line the actual error necessarily exists. The most common compile time
|
|
error, besides the typo, is the missing or superfluous parentheses,
|
|
brackets, braces, or quotes. Yet this error is the one that most baffles
|
|
new coders, since the driver will not notice the missing or extra piece
|
|
until well after the original. Take for example the following code:
|
|
|
|
1 int test(string str) {
|
|
2 int x;
|
|
3 for(x =0; x<10; x++)
|
|
4 write(x+"\n");
|
|
5 }
|
|
6 write("Done.\n");
|
|
7 }
|
|
|
|
Depending on what you intended, the actual error here is either at line 3
|
|
(meaning you are missing a {) or at line 5 (meaing you have an extra }).
|
|
Nevertheless, the driver will report that it found an error when it gets to
|
|
line 6. The actual driver message may vary from driver to driver, but no
|
|
matter which driver, you will see an error on line 6, since the } in line 5
|
|
is interpreted as ending the function test(). At line 6, the driver sees that
|
|
you have a write() sitting outside any function definition, and thus
|
|
reports an error. Generally, the driver will also go on to report that it
|
|
found an error at line 7 in the form of an extra }.
|
|
|
|
The secret to debugging these is coding style. Having closing } match
|
|
up vertically with the clauses they close out helps you see where you are
|
|
missing them when you are debugging code. Similarly, when using
|
|
multiple sets of parentheses, space out different groups like this:
|
|
if( (x=sizeof(who=users()) > ( (y+z)/(a-b) + (-(random(7))) ) )
|
|
As you can see, the parentheses for the for() statement, are spaced out
|
|
from the rest of the statement. In addition, individual sub-groups are
|
|
spaced so they can easily be sorted out in the event of an error.
|
|
|
|
Once you have a coding style which aids in picking these out, you learn
|
|
which error messages tend to indicate this sort of error. When
|
|
debugging this sort of error, you then view a section of code before and
|
|
after the line in question. In most all cases, you will catch the bug right
|
|
off.
|
|
|
|
Another common compile time error is where the driver reports an
|
|
unknown identifier. Generally, typos and failure to declare variables
|
|
causes this sort of error. Fortunately, the error log will almost always
|
|
tell you exactly where the error is. So when debugging it, enter the
|
|
editor and find the line in question. If the problem is with a variable and
|
|
is not a typo, make sure you declared it properly. On the other hand, if
|
|
it is a typo, simply fix it!
|
|
|
|
One thing to beware of, however, is that this error will sometimes be
|
|
reported in conjunction with a missing parentheses, brackets, or braces
|
|
type error. In these situations, your problem with an unknown identifier
|
|
is often bogus. The driver misreads the way the {} or whatever are
|
|
setup, and thus gets variable declarations confused. Therefore make
|
|
sure all other compile time errors are corrected before bothering with
|
|
these types of errors.
|
|
|
|
In the same class with the above error, is the general syntax error. The
|
|
driver generates this error when it simply fails to understand what you
|
|
said. Again, this is often caused by typos, but can also be caused by not
|
|
properly understanding the syntax of a certain feature like writing a for()
|
|
statement: for(x=0, x<10, x++). If you get an error like this which is
|
|
not a syntax error, try reviewing the syntax of the statement in which the
|
|
error is occurring.
|
|
|
|
7.3 Debugging Run Time Errors
|
|
Run time errors are much more complex than their compile time
|
|
counterparts. Fortunately these errors do get logged, though many
|
|
creators do not realise or they do not know where to look. The error log
|
|
for run time errors are also generally much more detailed than compile
|
|
time errors, meaning that you can trace the history of the execution train
|
|
from where it started to where it went wrong. You therefore can setup
|
|
debugging traps using precompiler statements much easier using these
|
|
logs. Run time errors, however, tend to result from using more
|
|
complex codign techniques than beginners tend to use, which means you
|
|
are left with errors which are generally more complicated than simple
|
|
compile time errors.
|
|
|
|
Run time errors almost always result from misusing LPC data types.
|
|
Most commonly, trying to do call others using object variables which are
|
|
NULL, indexing on mapping, array, or string variables which are
|
|
NULL, or passing bad arguments to functions. We will look at a real
|
|
run time error log from Nightmare:
|
|
|
|
Bad argument 1 to explode()
|
|
program: bin/system/_grep.c, object: bin/system/_grep
|
|
line 32
|
|
' cmd_hook' in ' std/living.c' ('
|
|
std/user#4002')line 83
|
|
' cmd_grep' in ' bin/system/_grep.c' ('
|
|
bin/system/_grep')line 32
|
|
Bad argument 2 to message()
|
|
program: adm/obj/simul_efun.c, object: adm/obj/simul_efun
|
|
line 34
|
|
' cmd_hook' in ' std/living.c' ('
|
|
std/user#4957')line 83
|
|
' cmd_look' in ' bin/mortal/_look.c' ('
|
|
bin/mortal/_look')line 23
|
|
' examine_object' in ' bin/mortal/_look.c' ('
|
|
bin/mortal/_look')line 78
|
|
' write' in 'adm/obj/simul_efun.c' ('
|
|
adm/obj/simul_efun')line 34
|
|
Bad argument 1 to call_other()
|
|
program: bin/system/_clone.c, object: bin/system/_clone
|
|
line 25
|
|
' cmd_hook' in ' std/living.c' ('
|
|
std/user#3734')line 83
|
|
' cmd_clone' in ' bin/system/_clone.c' ('
|
|
bin/system/_clone')line 25
|
|
Illegal index
|
|
program: std/monster.c, object:
|
|
wizards/zaknaifen/spy#7205 line 76
|
|
' heart_beat' in ' std/monster.c'
|
|
('wizards/zaknaifen/spy#7205')line
|
|
76
|
|
|
|
All of the errors, except the last one, involve passing a bad argument to a
|
|
function. The first bug, involves passing a bad first arument to the efun
|
|
explode(). This efun expects a string as its first argment. In debugging
|
|
these kinds of errors, we would therefore go to line 32 in
|
|
/bin/system/_grep.c and check to see what the data type of the first
|
|
argument being passed in fact is. In this particular case, the value being
|
|
passed should be a string.
|
|
|
|
If for some reason I has actually passed something else, I would be done
|
|
debugging at that point and fix it simply by making sure that I was
|
|
passing a string. This situation is more complex. I now need to trace
|
|
the actual values contained by the variable being passed to explode, so
|
|
that I can see what it is the explode() efun sees that it is being passed.
|
|
|
|
The line is question is this:
|
|
borg[files[i]] = regexp(explode(read_file(files[i]), "\n"), exp);
|
|
where files is an array for strings, i is an integer, and borg is a mapping.
|
|
So clearly we need to find out what the value of read_file(files[i]) is.
|
|
Well, this efun returns a string unless the file in question does not exist,
|
|
the object in question does not have read access to the file in question, or
|
|
the file in question is an empty file, in which cases the function will
|
|
return NULL. Clearly, our problem is that one of these events must
|
|
have happened. In order to see which, we need to look at files[i].
|
|
|
|
Examining the code, the files array gets its value through the get_dir()
|
|
efun. This returns all the files in a directory if the object has read access
|
|
to the directory. Therefore the problem is neither lack of access or non-
|
|
existent files. The file which caused this error then must have been an
|
|
empty file. And, in fact, that is exactly what caused this error. To
|
|
debug that, we would pass files through the filter() efun and make
|
|
sure that only files with a file size greater than 0 were allowed into the
|
|
array.
|
|
|
|
The key to debugging a run time error is therefore knowing exactly what
|
|
the values of all variables in question are at the exact moment where the
|
|
bug created. When reading your run time log, be careful to separate the
|
|
object from the file in which the bug occurred. For example, the
|
|
indexing error above came about in the object /wizards/zaknaifen/spy,
|
|
but the error occured while running a function in /std/monster.c, which
|
|
the object inherited.
|
|
|
|
7.4 Malfunctioning Code
|
|
The nastiest problem to deal with is when your code does not behave the
|
|
way you intended it to behave. The object loads fine, and it produces no
|
|
run time errors, but things simply do not happen the way they should.
|
|
Since the driver does not see a problem with this type of code, no logs
|
|
are produced. You therefore need to go through the code line by line
|
|
and figure out what is happening.
|
|
|
|
Step 1: Locate the last line of code you knew successfully executed
|
|
Step 2: Locate the first line of code where you know things are going
|
|
wrong
|
|
Step 3: Examine the flow of the code from the known successful point to
|
|
the first known unsuccessful point.
|
|
|
|
More often than not, these problems occurr when you are using if()
|
|
statements and not accounting for all possibilities. For example:
|
|
|
|
int cmd(string tmp) {
|
|
if(stringp(tmp)) return do_a()
|
|
else if(intp(tmp)) return do_b()
|
|
return 1;
|
|
}
|
|
|
|
In this code, we find that it compiles and runs fine. Problem is nothing
|
|
happens when it is executed. We know for sure that the cmd() function
|
|
is getting executed, so we can start there. We also know that a value of
|
|
1 is in fact being returned, since we do not see "What?" when we enter
|
|
the command. Immediately, we can see that for some reason the
|
|
variable tmp has a value other than string or int. As it turns out, we
|
|
issued the command without parameters, so tmp was NULL and failed
|
|
all tests.
|
|
|
|
The above example is rather simplistic, bordering on silly.
|
|
Nevertheless, it gives you an idea of how to examine the flow of the
|
|
code when debugging malfunctioning code. Other tools are available as
|
|
well to help in debugging code. The most important tool is the use of
|
|
the precompiler to debug code. With the code above, we have a clause
|
|
checking for integers being passed to cmd(). When we type "cmd 10",
|
|
we are expecting do_b() to execute. We need to see what the value of
|
|
tmp is before we get into the loop:
|
|
|
|
#define DEBUG
|
|
int cmd(string tmp) {
|
|
#ifdef DEBUG
|
|
write(tmp);
|
|
#endif
|
|
if(stringp(tmp)) return do_a();
|
|
else if(intp(tmp)) return do_b();
|
|
else return 1;
|
|
}
|
|
|
|
We find out immediately upon issuing the command, that tmp has a
|
|
value of "10". Looking back at the code, we slap ourselves silly,
|
|
forgetting that we have to change command arguments to integers using
|
|
sscanf() before evaluating them as integers.
|
|
|
|
7.5 Summary
|
|
The key to debugging any LPC problem is always being aware of what
|
|
the values of your variables are at any given step in your code. LPC
|
|
execution reduces on the simplest level to changes in variable values, so
|
|
bad values are what causes bad things to happen once code has been
|
|
loaded into memory. If you get errors about bad arguments to
|
|
functions, more likely than not you are passing a NULL value to a
|
|
function for that argument. This happens most often with objects, since
|
|
people will do one of the following:
|
|
1) use a value that was set to an object that has since destructed
|
|
2) use the return value of this_player() when there is no this_player()
|
|
3) use the return value of this_object() just after this_object() was
|
|
destructed
|
|
|
|
In addition, people will often run into errors involving illegal indexing or
|
|
indexing on illegal types. Most often, this is because the mapping or
|
|
array in question was not initialized, and therefore cannot be indexed.
|
|
The key is to know exactly what the full value of the array or mapping
|
|
should be at the point in question. In addition, watch for using index
|
|
numbers larger than the size of given arrays
|
|
|
|
Finally, make use of the precompiler to temporarly throw out code, or
|
|
introduce code which will show you the values of variables. The
|
|
precompiler makes it easy to get rid of debugging code quickly once you
|
|
are done. You can simply remove the DEBUG define when you are
|
|
done.
|
|
|
|
Copyright (c) George Reese 1993
|