Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

11 Dezember, 2009

Tag 12: Die eigene Toolbox und Code debugging

TL;DR:

Heute wollen wir richtig in die Programmierung mit C einsteigen. Die Umgebung steht, die IDE ist aufgebohrt, der Build-Prozess automatisiert und der Rahmen steht auch schon. Nun gehen wir daran, uns ein kleines Set an Grafikbefehlen zusammenzubauen, mit denen wir Linien, Flächen, Rechtecke oder Kreise zeichnen können. Spätestens jetzt werden wir zwangsläufig Fehler machen. Damit wir die Fehler auch schnell lokalisieren und beheben können, schauen wir uns den Debugger gdb an.

Zuerst geht es an unsere Grafik Bibliothek. Da wir keine Grafiken einbinden wollen, brauchen wir ein paar andere Hilfsmittel, um eine grafische Komponente in das Spiel zu bringen. Dafür wäre es nicht schlecht, wenn wir Grundelemente wie Linien, Rechtecke (gefüllt oder nicht) und Kreise zeichnen können. Machen wir uns solche Funktionen!

Zuerst wollen wir uns an der Linie versuchen. Hier wollen wir eine Linie zwischen zwei Punkten ziehen. Leider reicht es nicht ganz aus, eine Geradengleichung mit der Zwei Punkte Form aufzustellen und diese zu zeichnen, denn je nach Neigungswinkel der Geraden müssen wir sie anders zeichnen. Das folgende Bild verdeutlicht das etwas:

Gerade

Wir können also nicht in X-Richtung iterieren, wenn die Gerade zu steil verläuft. Aus diesem Grunde müssen wie entsprechend wechseln. (Deshalb auch die signum-Funktion und der kompliziertere Aufbau). Die Funktion ist nicht komplett von mir, teile hab ich von diesem alten Dokument übernommen.

int _sign(float a) {
  return a > 0 ? 1 : (a << 0 ? -1 : 0);
}

void drawLine(int x1, int y1, int x2, int y2, int color) {
  float u, s, v, d1x, d1y, d2x, d2y, m, n;
  int x = x1, y = y1;

  u = x2-x1;
  v = y2-y1;

  d1x = d2x = _sign(u);
  d1y = _sign(v);
  d2y = 0;
  m = abs(u);
  n = abs(v);

  if (m <= n) {
    d2x = 0;
    d2y = _sign(v);
    m = abs(v);
    n = abs(u);
  }

  s = (int)(m/2);

  for (int i=0; i<round(m); i++) {
    drawPixel(x, y, color);
    s += n;
    if (s >= m) {
      s -= m;
      x += d1x;
      y += d1y;
    } else {
      x += d2x;
      y += d2y;
    }
  }
}

Mit der Möglichkeit, eine Gerade zu zeichnen, können wir uns dem nächsten Problem widmen: Dem Rechteck. Wollen wir ein gefülltes Rechteck, zeichnen wir einfach Y viele Linien mit X Länge unter einander. Wollen wir nur die Umrandung, zeichnen wir vier einfache Linien. Ziemlich einfache Geschichte.

void drawRect(int x1, int y1, int x2, int y2, int color, bool filled) {
  int tmp;

  if (x1 > x2) { tmp = x1; x2 = x1; x1 = tmp; }
  if (y1 > y2) { tmp = y1; y2 = y1; y1 = tmp; }

  if (filled) {
    for (int i=y1; i<=y2; i++) drawLine(x1, i, x2, i, color);
  } else {
    drawLine(x1, y1, x2, y1, color);
    drawLine(x1, y1, x1, y2, color);
    drawLine(x2, y1, x2, y2, color);
    drawLine(x1, y2, x2, y2, color);
  }
}

Beim Zeichnen des Kreises zähle ich auf euch! (Vorsicht Spoiler!) Zwei Lösungsansätze: Entweder man zeichnet den Kreis wie ein Stern mit sehr vielen Zacken. Also eine Linie Zeichnen, diese Linie um 2 Grad nach oben rotieren und dort zeichnen usw. Eine weitere Möglichkeit ist es, einen Kreis mit Durchmesser 1 zu zeichnen, dann mit Durchmesser 3 usw.

Mit diesen Funktionen können wir schon recht viel anstellen. In der drawScreen()-Funktion habe ich ein kleines Testbild gebastelt, die die Funktionen testet. Wollen wir das Testen etwas weiter treiben. Probieren wir mal das hier aus:

for (int i=0; i<=600; i++) drawLine(0, i, 799, i, white);

Compilieren wir den Source uns starten es, schmiert es nach dem Start direkt mit der Meldung Segmentation fault ab. Hiermit können wir natürlich nicht viel anfangen. Ein erster Schritt wäre es jetzt, in den Sourcecode zu schauen und nach dem Fehler zu suchen. Doch ohne Anhaltspunkt gestaltet sich dies recht schwer. (Klar, wir wissen ja schon, dass es irgend was mit der gerade eingefügten Zeile zu tun haben muss, hat man aber mehrere Änderungen gemacht, die sich wieder auf andere Codeteile auswirken, ist es nicht mehr so offensichtlich).

Der zweite Ansatz wäre, ein paar printf()-Funktionsaufrufe in den Code zu streuen und sich dann durch den Output zu wühlen um daraus Schlüsse zu ziehen. In unserem Fall ist dies allerdings etwas ungünstig, da wir viele Ausgaben in jedem Frame (30-100 pro Sekunde?) haben. Die Ausgaben würden nur so über den Bildschirm rasen. Eine bessere Lösung muss her.

gdb ist ein Debugger, der den Binärcode eines Binaries so zerpflückt, dass man die Anwendung kontrollieren kann. Damit gdb so viele Informationen wie möglich aus den compilierten Binary ziehen kann, kann man gcc die Option -ggdb mitgeben. Damit werden Debugging-Information ins Binary mit eingebaut. Das compilierte Binary wird dadurch etwas größer, doch dieses kann man mit strip wieder schrumpfen lassen.

Starten wir das Programm noch einmal mit gdb:

$ gdb ./bin/game
GNU gdb 6.8
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 "i686-pc-linux-gnu"...
(gdb)

Wir landen in einer interaktiven Console, mit der wir nun das Programm steuern können. Mit dem Befehl run starten wir es.

(gdb) run
Starting program: .../bin/game 
[Thread debugging using libthread_db enabled]
[New Thread 0xb7bbc6d0 (LWP 19035)]

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xb7bbc6d0 (LWP 19035)]
0x0804899f in drawPixel (x=256, y=600, color=16777215) at src/draw.c:13
13        p[lineOffset+x] = color;

Hier sehen wir schon etwas mehr. Wieder den Segmentation fault Abbruch, doch nun die Zeile, in der es kracht. Hier sieht man auch, mit welchen Parametern die Funktion aufgerufen wurde. Mit backtrace können wir uns den Call-Stack anschauen, also die Reihenfolge, in der die fehlerhafte Funktion aufgerufen wurde.

(gdb) backtrace 
#0  0x0804899f in drawPixel (x=256, y=600, color=16777215) at src/draw.c:13
#1  0x08048b6e in drawLine (x1=0, y1=600, x2=799, y2=600, color=16777215)
    at src/draw.c:43
#2  0x08048fd4 in drawScreen () at src/draw.c:92
#3  0x0804907b in main () at src/main.c:23

Jetzt wird das Problem klar: In der drawScreen()-Funktion haben wir in Zeile 43 unsere Schleife, in der wir drawLine(0, 600, 799, 600, 16777215) aufrufen. Da die for-Schleife bis 600 läuft, stürtzt das Programm im letzten Durchlauf ab. Der eigentliche Fehler tritt dann in Zeile 13 in der Funktion drawPixel() auf. Diese wird mit x=256 und y=600 aufgerufen. Hier verbirgt sich das Problem. Da wir unseren screen nur mit 800x600 initialisiert haben (also x=0..799 und y=0..599) gibt es hier einen Überlauf. Dies wurde uns nicht gleich bewusst, da es sich um ein eindimensionales Array handelt. Wer mag sich um dieses Problem kümmern? :)

Bei kniffligeren Fällen kann man auch Breakpoints (Haltepunkte) einfügen und Schrittweise durch den Code steppen. Mehr dazu findet ihr in diesem kleinen Tutorial.

So, dies waren wieder viele Informationen. Am besten alles mal ausprobieren und etwas herumspielen. Vielleicht hat sich ja noch der ein oder andere Fehler eingeschlichen. Den kompletten Sourcecode habe ich natürlich wieder auf Github hochgeladen. Übermorgen werden wir dann eine Landschaft generieren.