1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
\documentclass{lxarticle}
\usepackage{german}
\usepackage[utf8]{inputenc}
\usepackage{lxheaders}
\usepackage{lxextras}
\begin{document}
\section*{Debugging}
Beim Debugging geht es darum, Fehler in einem System zu finden.
Es gibt zwei grundlegend verschiedene Fehlerarten:
\begin{enumerate}
\item die Anwendung beendet sich unerwartet
\begin{description}
\item[auf Grund von Fehlern bei der Speicherveraltung] z.B. durch freigeben
eines Speicherbereichs, welcher zuvor nicht alloziert wurde:
\begin{lstlisting}
mem = malloc(5);
free(((char*)mem)+6000);
\end{lstlisting}
Die Programmausf\"uhrung wird abgebrochen, die glibc liefert alle
Informationen, welche n\"otig sind, um den Fehler zu decodieren:
\begin{lstlisting}
bash-4.0$ ./test
*** glibc detected *** ./test: free(): invalid pointer: 0x083c9778 ***
======= Backtrace: =========
/lib/libc.so.6[0x57c231]
./test[0x804841e]
/lib/libc.so.6(__libc_start_main+0xe6)[0x522a66]
./test[0x8048361]
======= Memory map: ========
0048e000-0048f000 r-xp 0048e000 00:00 0 [vdso]
004e8000-00508000 r-xp 00000000 fd:00 66065 /lib/ld-2.10.1.so
00508000-00509000 r--p 0001f000 fd:00 66065 /lib/ld-2.10.1.so
...
\end{lstlisting}
Mit folgendem Befehl kann festgestellt werden, von wo aus der Fehler
getriggered wurde:
\cmd{addr2line -e test 0x804841e}
/home/linutronix/test.c:16
\item[Speicherzugriffsfehler] z.B. durch Zugriff auf nicht allozierten
Speicher. Die Programmausf\"uhrung wird abgebrochen:
\begin{lstlisting}
bash-4.0$ ./test
Speicherzugriffsfehler
\end{lstlisting}
Solche Fehler lassen sich am Besten mit einer gdb Backtrace Ausgabe (siehe
unten) finden.
\end{description}
\item die Anwendung verh\"alt sich anders als erwartet.
In einem solchen Fall sollte man sich zu erst Gedanken dar\"uber machen,
durch was das Fehlverhalten getriggered werden k\"onnte. Sofort mit einem
Debugger durch den Code zu steppen ist kein guter Ansatz, da so evt. ein
Fehler gefunden und behoben wird der aber nicht zwangsl\"aufig die Ursache
f\"ur das Problem war. Dies kann dazu f\"uhren, dass die Fehlersuche eines
sp\"ater entdeckten Bugs um so komplexer und schwieriger wird.
Hat man sich Gedanken \"uber die Ursachen gemacht, kann man gezielt mit den
nachfolgend beschriebenen Techniken dem Fehler auf den Grund gehen.
\end{enumerate}
\subsection*{Einfach aber wirkungsvoll: \cmd{printf}, \cmd{fprintf}}
Da Ausgaben auf die \cmd{stdout} gecached werden, ist es zuverl\"assiger f\"ur
Debugging \cmd{stderr} zu verwenden und nach jeder Ausgabe noch den cash flush
zu triggern. Ein Debug konstrukt k\"onnte z.B. folgenderma\ss en aussehen:
\begin{lstlisting}
fprintf(stderr, "debug test\n");
fflush(stderr);
\end{lstlisting}
F\"ur gew\"ohnlich wird \cmd{(f)printf} beim Debuggen f\"ur drei Zwecke
verwendet:
\begin{enumerate}
\item Tracing:
\begin{lstlisting}
fprintf(stderr, "%s, called\n", __FUNCTION__);
fflush(stderr);
\end{lstlisting}
\item Ausgabe von Variablenwerten:
\begin{lstlisting}
fprintf(stderr, "Wert: %d, Wert (hex): %x\n", wert, wert);
fflush(stderr);
\end{lstlisting}
\item Ausgabe von Adressen:
\begin{lstlisting}
fprintf(stderr, "Adresse: %p, Wert: %x\n", wert, wert);
fflush(stderr);
\end{lstlisting}
\end{enumerate}
Nat\"urlich k\"onnen die Methoden kombiniert werden.
Soll das Debugging zur Compilezeit zu- und abschaltbar sein, kann z.B. folgendes
Pr\"aprozessor konstrukt hilfreich sein:
\begin{lstlisting}
#ifdef DEBUG
#define debug(...) fprintf(stderr, "%s:%d - ", __FILE__, __LINE__); \
fprintf(stderr, __VA_ARGS__); fflush(stderr)
#else
#define debug(...) ((void)0)
#endif
int main(int argc, char **argv)
{
int val = 3;
debug("addr: %p - val: %d\n", &val, val);
return 0;
}
\end{lstlisting}
Resultat:
\begin{lstlisting}
bash-4.0$ gcc -o test test.c
bash-4.0$ ./test
bash-4.0$
bash-4.0$ gcc -o test -DDEBUG test.c
bash-4.0$ ./test
test.c:22 - test: 0xbfef8694 - val: 3
bash-4.0$
\end{lstlisting}
\subsubsection*{deutlich flexibler: Logging Frameworks}
Wird f\"ur Debugging oder Logging mehr Flexibilit\"at ben\"otigt, sollte ein
Logging Framework verwendet werden. Prominente Vertretter f\"ur verschiedene
Programmiersprachen sind zum Beispiel:
\begin{itemize}
\item Log for C++ (http://log4cpp.sourceforge.net)
\item log4c (http://log4c.sourceforge.net)
\item log4j (http://logging.apache.org/log4j) - f\"ur JAVA
\end{itemize}
Diese Frameworks bieten die M\"oglichkeit das Loggingverhalten zum Startpunkt
der Anwendung, oder teilweise sogar, w\"ahrend der Laufzeit zu ver\"andern.
Es wird beim Loggen mindestens zwischen den Kategorien Debug, Error und Info
unteschieden, welche jeweils an verschiedene Ziele geleitet werden k\"onnen:
Datei, syslog, \dots
\subsubsection*{Backtraces ausgeben}
Die glibc bietet auch die M\"oglichkeit an einer beliebigen Stelle in der
Applikation die Ausgabe eines Function Call Traces (Backtrace) aus zu geben.
Dies ist hilfreich, wenn man herausfinden will, \"uber welche Funktionsaufrufe
die aktuelle Codezeile aufgerufen wurde.
Beispiel:
\begin{lstlisting}
#include <stdio.h>
#include <stdlib.h>
#define DEFAULT_BT_DEPTH 16
void btrace(int depth) {
const void *trace[depth];
char **messages = (char **) NULL;
unsigned int i = 0;
int trace_size = backtrace(trace, depth);
messages = backtrace_symbols(trace, trace_size);
fprintf(stderr, "***BACKTRACE***\n");
for(; i < trace_size; i++)
fprintf(stderr, "%s\n", messages[i]);
fprintf(stderr, "***************\n");
fflush(stderr);
}
void pong() {
#ifdef DEBUG
btrace(DEFAULT_BT_DEPTH);
#endif
}
void ping() {
pong();
}
int main(int argc, char **argv) {
ping();
return 0;
}
\end{lstlisting}
Anwendung:
\begin{lstlisting}
bash-4.0$ gcc -g -o test -DDEBUG test.c
bash-4.0$ ./test
***BACKTRACE***
./test [0x804859b]
./test [0x8048663]
./test [0x8048670]
./test [0x804867d]
/lib/libc.so.6(__libc_start_main+0xe6) [0x522a66]
./test [0x8048441]
***************
bash-4.0$ addr2line -e test 0x804867d
/home/linutronix/test.c:27
bash-4.0$ addr2line -e test 0x8048670
/home/linutronix/test.c:24
\end{lstlisting}
\subsection*{Tracing mit strace}
Mit \cmd{strace} k\"onnen Systemaufrufe getraced werden. Es kann ermittelt
werden welche Systemaufrufe von der Applikation zu welchem Zeitpunkt aufgerufen
wurden und wieviel Zeit der Kernel ben\"otigt um den Aufruf ab zu arbeiten.
Ein \cmd{strace} ist folgenderma\ss en zu interpretieren:
\begin{lstlisting}
bash-4.0$ strace -ftttT ./test
1245687387.873813 execve("./test", ["./test"], [/* 18 vars */]) = 0 <0.000233>
^ ^ ^ ^ ^
| | | | |
| | +- Argumente | +- Dauer
+- Zeitstempel +- syscall +- Rueckgabewert
\end{lstlisting}
Die Option
\begin{itemize}
\item \cmd{-f} sorgt daf\"ur, dass auch Kindprozesse mit getraced werden,
\item \cmd{-ttt} blendet den pr\"azisen Zeitstempel ein,
\item \cmd{-T} gibt die Dauer des Systemaufrufs mit an.
\end{itemize}
\subsection*{Debugging mit gdb}
gdb ist ein textbasierter Debugger. Eine Applikation kann z.B mit dem Befehl
\cmd{gdb ./test} in gdb geladen werden und anschliessend mit dem Befehl
\cmd{run} ausgef\"uhrt werden.
Die 'gdb Reference Card' listet viele n\"utzliche Befehle, z.B. zum Setzen von
Breakpoints und zum Steppen durch die Applikation.
Es gibt auch grafische Frontends f\"ur gdb, wie z.B. \cmd{ddd}
(http://www.gnu.org/software/ddd).
\subsubsection*{Core Dumps analysieren}
\cmd{gdb} eignet sich au\ss erdem hervorragend zur Analyse von so genannten
'core dumps'. Ein 'core dump' entsteht, wenn eine Applikation unerwartet beendet
wird. Gegebenenfalls mu\ss mit \cmd{ulimit -c unlimited} vor dem
Applikationsstart das Limit f\"ur maximal zul\"assige core dumps erh\"oht
werden.
Beispiel:
\begin{lstlisting}
bash-4.0$ ulimit -c unlimited
bash-4.0$ ./test
Speicherzugriffsfehler (Speicherabzug geschrieben)
bash-4.0$ gdb ./test core.12371
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu"...
Reading symbols from /lib/libc.so.6...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `./test'.
Program terminated with signal 11, Segmentation fault.
[New process 12371]
#0 0x08048661 in pong () at test.c:22
22 *null_ptr = 3;
(gdb)
(gdb) bt
#0 0x08048661 in pong () at test.c:22
#1 0x08048671 in ping () at test.c:26
#2 0x0804867b in main (argc=1, argv=0xbf9921f4) at test.c:29
(gdb)
\end{lstlisting}
Der Befehl \cmd{bt} steht f\"ur Backtrace. Die zuletzt aufgerufene Funktion
steht an Position \#0. Zeigt \#0 nicht auf die eigene Applikationen sondern z.B.
in die glibc ist es eher warscheinlich, dass der Fehler durch eine nicht
spezifiezierte Verwendung einer glibc Funktion getriggered wurde. Man sollte in
diesem Fall zuerst untersuchen, ob die erste Funktion in der eigenen Software,
einen korrekten glibc call absetzt, bevor die glibc gedebugged wird.
%\subsubsection*{Remote-Debuggging mit gdbserver}
%Text
\end{document}
|