Ein kleiner Ausflug in das Land des (Linux) Dynamic Loaders

Ein (nicht ganz so) kleiner Beitrag der tief in die Welt des Linux Dynamic Loaders mit seinen Helferchen GOT und PLT hinabsteigt, damit man mal sehen kann wie das eigentlich mit den Shared Librarys bei Linux funktioniert…

Wenn man heutzutage programmiert schreibt man viele Funktionen gar nicht selbst, denn viele Funktionen werden von Librarys bereitgestellt.
Librarys sind dabei Teile von Programmcode (die nicht alleine gestartet werden können) die jedes Programm verwenden kann.
Das ist nicht nur einfacher (und fehlerfreier) als alle Funktionen selbst zu schreiben, es ist auch sparsamer – denn wenn mehrere Programme die gleichen Funktionen aus der gleichen Library verwenden so muss diese Library nur einmal in den RAM geladen werden.
Bei Linux enden solche Programmteile auf ‚.so‘ was soviel wie Shared Library (Update: Shared Object!) heißt.

Jetzt bauen wir uns erstmal unsere eigene Shared Library und dann nehmen wir sie auseinander. Los gehts…

Zuerst erzeugen wir eine Datei ‚libfoo.c‘. Dies ist unsere Shared Library.
Sie ist in der Programmierspache C geschrieben, bekommt einen String (einen Satz) den sie auf dem Bildschirm ausgibt, und gibt einen Integer (Zahl) als Wert zurück, hier ist der Programmcode:

#include <stdio.h>

int foo(char* myval)
{
  printf("I should tell: %s\n", myval);
  return 42;
}

Und dazu haben wir ein Programm welches diese Library aufruft, testprog.c:

extern int foo(char*);

#include <stdio.h>

int main(void)
{
  int ret;

  printf("Calling my Lib!\n");
  ret = foo("Do what I tell you!");
  printf("My Lib told me: %d\n", ret);
  return 0;
}

Hier fällt auf dass wir oben in unser Programm ‚extern int foo(char*);‘ geschrieben haben, das bedeutet dass wir eine Funktion ‚foo‘ in unserem Programm verwenden, die einen Integer zurückgibt und einen String (char*) bekommt. Wir definieren hier nicht wo das ist, denn beim bauen unseres Programmes wird der Linker das für uns auflösen (Symbol Resolving).

Jetzt nehmen wir erstmal unseren Gnu C-Compiler und sagen ihm dass wir aus der Datei ‚libfoo.c‘ eine Datei ‚libfoo.so‘ haben möchten. Diese Datei soll Prozessorcode beinhalten (also kompiliert werden) und sie soll eine Shared Library sein. Außerdem soll der Prozessorcode Positionsunabhängig sein. Während unser Programm nämlich Linux sagen kann ‚lade mich an Adresse 0x100000‘ und somit weiß an welcher Speicheradresse seine Programmteile stehen kann unsere Library das nicht. Unter anderem aus Sicherheitsgründen wird sie irgendwo im Speicher plaziert (ASLR = Address Space Layout Randomization). Da muss man beim Erzeugen des Assembler/Prozessorcodes gut aufpassen dass man keine absoluten Adressen verwendet, sondern nur relative.

gcc -shared -fPIC -o libfoo.so libfoo.c

Nachdem es nun hoffentlich keine Fehler gab (meine Beispiele sind selbstverständlich geprüft und fehlerfrei *hust* *hust*) compilieren wir nun noch unser Programm. Wir sagen dem Gnu Compiler diesmal dass wir alle Warnungen ausgegeben haben möchten wenn wir die Datei ‚testprog.c‘ zum Programm ‚testprog‘ kompilieren. Außerdem geben wir ihm an dass wir die Library ‚libfoo‘ verwenden möchten, und dass er diese im aktuellen Verzeichnis ‚.‘ suchen soll. Wenn wir das weglassen so wird sich der Linker beschweren dass wir eine Funktion ‚foo‘ nutzen wollen, er sie aber nirgendwo finden konnte!

gcc -Wall -o testprog testprog.c -L. -lfoo

Jetzt starten wir mal unser Programm mit dem Befehl ‚./testprog‘, und was passiert?

./testprog: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

Aha! Immer wenn wir unser Programm starten sorgt ein Teil von Linux (der Loader) dafür dass alle Shared Librarys die unser Programm braucht auch verfügbar sind. Aber warum findet er unsere Library nicht?
Ganz einfach: Bei Linux werden Shared Librarys immer in bestimmten Ordnern erwartet (‚/usr/local/lib‘). Wenn wir dem Loader nun sagen dass der Ordner ‚.‘ mit verwendet werden soll geht es. Das machen wir ganz einfach über die Umgebungsvariable LD_LIBRARY_PATH, die wir erweitern:

export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

Und schon läuft unser Programm:

Calling my Lib!
I should tell: Do what I tell you!
My Lib told me: 42

Nun sind wir aber sehr neugierige Menschen und wollen wissen wie das eigentlich genau funktioniert:

Und zwar hat der Linker beim erzeugen des Programmes zwei Programmteile (Sections) erzeugt die hier verwendet werden:
1. Die PLT (Procedure Linkage Table)
2. Die GOT (Global Offset Table)

Diese können wir uns ansehen. Zuerst sehen wir uns aber unseren Programmcode als Assemblercode an ‚objdump –disassemble ./testprog‘.
Interessant wird es ab der Funktion ‚0000000000400716 <main>‘,
die einzige Zeile die für uns wichtig ist, ist diese hier:

  40072d:       e8 ce fe ff ff          callq  400600 <foo@plt>

Hier wird also unsere Funktion ‚foo‘ aufgerufen (das wird im Assemblercode mit einem ‚call‘ gemacht, dieser geht immer bis zum nächsten ‚ret‘). Hier callt er also Adresse 400600. Was steht dort?

0000000000400600 <foo@plt>:
  400600:       ff 25 2a 0a 20 00       jmpq   *0x200a2a(%rip)        # 601030 <_GLOBAL_OFFSET_TABLE_+0x30>
  400606:       68 03 00 00 00          pushq  $0x3
  40060b:       e9 b0 ff ff ff          jmpq   4005c0 <_init+0x28>

Anhand des ‚foo@plt‘ erkennen wir dass die Adresse in der PLT liegt.
Der Programmcode nimmt zuerst die Adresse die an der Adresse 601030 steht und springt diese an (‚jmpq‘). Danach setzt er einen Übergabeparameter (‚pushq‘) und springt zu einer anderen Adresse (‚jmpq‘). Diese zweite Adresse zu der er springt (‚4005c0‘) ist die Adresse des Linux Dynamic Loaders. Das müsst ihr mir jetzt einfach mal glauben.
Der Parameter den er davor übergibt ist die Nummer der Funktion die wir laden möchten (Nummer 3 ist eben unser foo()).

Mittels ‚readelf –relocs ./testprog‘ können wir alle Funktionen die wir beim Start des Programmes laden müssen ansehen:

Relocation section '.rela.plt' at offset 0x538 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000601020  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601028  000400000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601030  000600000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0

Bleibt also die Frage: Wo springt der obere jumpq hin? Gucken wir uns die Zieladresse ‚601030‘ die in der GOT liegt an, ‚readelf -x .got ./testprog‘:

Hex dump of section '.got':
  0x00600ff8 00000000 00000000                   ........

Hmmm, ziemlich leer diese GOT. Ist auch logisch, weil diese erst beim laufenden Programm gefüllt wird. Lassen wir das Programm laufen und schauen uns dann an was hier steht, ‚gdb ./testprog‘:

set disassembly-flavor intel
break main
run
disassemble

Wir sehen nun wieder unseren Call als Assemblercode:

   0x000000000040072d <+23>:    call   0x400600 <foo@plt>

Schauen wir uns den Assembler-Code an Adresse 0x400600 also an,
‚x /3i 0x400600‘:

   0x400600 <foo@plt>:  jmp    QWORD PTR [rip+0x200a2a]        # 0x601030
   0x400606 <foo@plt+6>:        push   0x3
   0x40060b <foo@plt+11>:       jmp    0x4005c0

Aha, wieder der Sprung zu 0x601030. Die dortige Adresse wird als Zieladresse für den Sprung genommen (Pointer, sieht man im Assemblercode daran dass die Adresse in eckigen Klammern steht). Was steht also da für eine Adresse? ‚x /1xg 0x601030‘:

0x601030:       0x0000000000400606

Aha, ein Sprung zu 400606. Also eigentlich einfach in die nächste Zeile. Häh? Lassen wir das Programm mal bis zum Ende laufen und dann gucken wir nochmal was für eine Zieladresse bei 601030 steht,
‚break *0x400749‘, ‚continue‘, ‚x /1xg 0x601030‘:

0x601030:       0x00007ffff7bd56a0

Aha! Da steht jetzt eine andere Adresse. Und wo geht die hin?
‚info proc mappings‘:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /root/test/testprog
            0x600000           0x601000     0x1000        0x0 /root/test/testprog
            0x601000           0x602000     0x1000     0x1000 /root/test/testprog
            0x602000           0x623000    0x21000        0x0 [heap]
      0x7ffff780c000     0x7ffff79cc000   0x1c0000        0x0 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7ffff79cc000     0x7ffff7bcb000   0x1ff000   0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7ffff7bcb000     0x7ffff7bcf000     0x4000   0x1bf000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7ffff7bcf000     0x7ffff7bd1000     0x2000   0x1c3000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7ffff7bd1000     0x7ffff7bd5000     0x4000        0x0
      0x7ffff7bd5000     0x7ffff7bd6000     0x1000        0x0 /root/test/libfoo.so
      0x7ffff7bd6000     0x7ffff7dd5000   0x1ff000     0x1000 /root/test/libfoo.so
      0x7ffff7dd5000     0x7ffff7dd6000     0x1000        0x0 /root/test/libfoo.so
      0x7ffff7dd6000     0x7ffff7dd7000     0x1000     0x1000 /root/test/libfoo.so
      0x7ffff7dd7000     0x7ffff7dfd000    0x26000        0x0 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7ffff7fe7000     0x7ffff7fea000     0x3000        0x0
      0x7ffff7ff6000     0x7ffff7ff8000     0x2000        0x0
      0x7ffff7ff8000     0x7ffff7ffa000     0x2000        0x0 [vvar]
      0x7ffff7ffa000     0x7ffff7ffc000     0x2000        0x0 [vdso]
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x25000 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7ffff7ffd000     0x7ffff7ffe000     0x1000    0x26000 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7ffff7ffe000     0x7ffff7fff000     0x1000        0x0
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

Von Adresse ‚0x7ffff7bd5000‘ bis ‚0x7ffff7bd6000‘ ist die Library ‚/root/test/libfoo.so‘. Ein Disassemblen zeigt das auch,
‚x /10i 0x00007ffff7bd56a0‘:

   0x7ffff7bd56a0 <foo>:        push   rbp
   0x7ffff7bd56a1 <foo+1>:      mov    rbp,rsp
   0x7ffff7bd56a4 <foo+4>:      sub    rsp,0x10
   0x7ffff7bd56a8 <foo+8>:      mov    QWORD PTR [rbp-0x8],rdi
   0x7ffff7bd56ac <foo+12>:     mov    rax,QWORD PTR [rbp-0x8]
   0x7ffff7bd56b0 <foo+16>:     mov    rsi,rax
   0x7ffff7bd56b3 <foo+19>:     lea    rdi,[rip+0x1b]        # 0x7ffff7bd56d5
   0x7ffff7bd56ba <foo+26>:     mov    eax,0x0
   0x7ffff7bd56bf <foo+31>:     call   0x7ffff7bd5580 <printf@plt>
   0x7ffff7bd56c4 <foo+36>:     mov    eax,0x2a

Okay, vielleicht müsst ihr mir jetzt einfach glauben dass das der Programmcode unserer Library ist 😛

Und die Erklärung?
Ganz einfach! Das erste Mal springen wir einfach eine Zeile weiter, damit wird der Linux Loader aufgefordert uns die Adresse der Shared Library in der GOT zu hinterlegen. Das zweite Mal ist die Adresse da und wir kommen garnicht mehr zum Programmcode des Linux Loaders weil wir direkt unsere Lib aufrufen. Das ganze nennt sich ‚Lazy Binding‘ – es macht ja keinen Sinn wenn der Linux Loader alle Library Funktionen suchen und bereitstellen würde, obwohl man sie garnicht nutzt.

Und jetzt gibts noch ein kleines spannendes Tool welches sich in die PLT reinhängen kann und alle Library Aufrufe mit aufzeichnet: ‚ltrace‘ – quasi der kleine Bruder von ’strace‘.
strace zeichnet alle System Calls auf, ltrace alle Library Calls.
Wie sieht das also bei uns aus?
‚ltrace ./testprog > /dev/null‘:
(das ‚> /dev/null‘ ist da, damit die Ausgabe unseres Programmes nicht die Ausgabe von ltrace durcheinander bringt!)

__libc_start_main(0x400716, 1, 0x7fff3293d8c8, 0x400750 <unfinished ...>
puts("Calling my Lib!")   = 16
foo(0x4007e4, 0x4007d4, 0x7f0a5710d780, 4096)     = 42
printf("My Lib told me: %d\n", 42)  = 19
+++ exited (status 0) +++

Hmmm, die Ausgabe
‚foo(0x4007e4, 0x4007d4, 0x7f0a5710d780, 4096) = 42‘
ist irgendwie noch etwas unspezifisch. Der Grund liegt auf der Hand: Woher soll ltrace wissen was für Übergabeparameter ‚foo()‘ denn kriegt?
Beim Returnwert Integer (42!) hat es geraten, aber bei den Übergabeparametern zeigt es nur deren Adressen an.

Zum Glück liegt die Lösung nahe: Wenn wir in der Konfigurationsdatei ‚/etc/ltrace.conf‘ unsere Funktion eintragen weiß ltrace Bescheid.
Der Syntax ist fast etwas wie in C, nur dass ein String nicht ‚char*‘ sondern ’string‘ heißt. Also tragen wir ein:

int foo(string)

Und schon:

__libc_start_main(0x400716, 1, 0x7fff68ec92d8, 0x400750 <unfinished ...>
puts("Calling my Lib!")  = 16
foo("Do what I tell you!")   = 42
printf("My Lib told me: %d\n", 42)   = 19
+++ exited (status 0) +++

Übrigens kann ltrace (und strace) auch anzeigen wir lange jeder Aufruf gedauert hat. Damit weiß man was im eigenen Programm viel Zeit kostet.
‚ltrace -r ./testprog > /dev/null‘:

  0.000000 __libc_start_main(0x400716, 1, 0x7ffc4ce25a58, 0x400750 <unfinished ...>
  0.000441 puts("Calling my Lib!")   = 16
  0.001814 foo("Do what I tell you!")  = 42
  0.000368 printf("My Lib told me: %d\n", 42)  = 19
  0.001271 +++ exited (status 0) +++

Und was noch interessanter ist, ltrace (und strace) kann auch kummuliert die Aufrufe zusammenfassen und die verwendete Zeit zusammenrechnen. Dazu nehmen wir mal einen Aufruf wo etwas mehr Arbeit passiert,
‚ltrace -c ls .‘:

% time     seconds  usecs/call     calls      function
------ ----------- ----------- --------- --------------------
 20.50    0.007947         162        49 malloc
 18.89    0.007320        7320         1 setlocale
  9.59    0.003717         154        24 __ctype_get_mb_cur_max
  8.05    0.003119         155        20 __errno_location
  5.42    0.002101         150        14 free
  4.90    0.001901         211         9 getenv
  3.64    0.001410         201         7 __overflow
  3.61    0.001400         140        10 memcpy
  2.70    0.001046         149         7 readdir
  2.40    0.000930         465         2 fclose
  2.33    0.000904         226         4 fwrite_unlocked
  2.17    0.000842         140         6 strlen
  1.86    0.000720         144         5 strcoll
  1.76    0.000684         684         1 bindtextdomain
  1.44    0.000558         139         4 __freading
  1.33    0.000515         515         1 opendir
  1.25    0.000486         486         1 textdomain
  0.93    0.000360         360         1 __xstat
  0.91    0.000354         354         1 closedir
  0.90    0.000350         175         2 _setjmp
  0.73    0.000283         141         2 __fpending
  0.68    0.000263         131         2 fileno
  0.67    0.000261         130         2 fflush
  0.64    0.000249         249         1 isatty
  0.58    0.000226         226         1 ioctl
  0.56    0.000217         217         1 __cxa_atexit
  0.54    0.000211         211         1 getopt_long
  0.34    0.000132         132         1 strrchr
  0.33    0.000127         127         1 freecon
  0.32    0.000125         125         1 realloc
------ ----------- ----------- --------- --------------------
100.00    0.038758                   182 total

Und jetzt mal sehen was davon denn die Systemcalls an Zeit brauchen,
’strace -c ls .‘:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         7           read
  0.00    0.000000           0         1           write
  0.00    0.000000           0        49        40 open
  0.00    0.000000           0        11           close
  0.00    0.000000           0         1           stat
  0.00    0.000000           0        10           fstat
  0.00    0.000000           0        19           mmap
  0.00    0.000000           0        12           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         2           rt_sigaction
  0.00    0.000000           0         1           rt_sigprocmask
  0.00    0.000000           0         2           ioctl
  0.00    0.000000           0         7         7 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         2           getdents
  0.00    0.000000           0         1           getrlimit
  0.00    0.000000           0         2         2 statfs
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         1           set_robust_list
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                   135        49 total

Puhhh, das war ein harter Brocken Arbeit. Dafür haben wir dem Computer ganz ordentlich auf die Finger geschaut. Ich weiß der Beitrag ist lang, schwer und kompliziert und hoffe ich konnte alles einigermaßen erklären.
Fragen aller Art bitte in die Kommentare.

Irgendwann mach ich auch nochmal nen Beitrag über Debugging mit strace/ltrace. Mal sehen…

 

 


Kommentare

11 Antworten zu „Ein kleiner Ausflug in das Land des (Linux) Dynamic Loaders“

  1. Wenn man heutzutage programmiert schreibt man viele Funktionen gar nicht selbst

    Wenn man heutzutage programmiert und sich auf den Code fremder Leute verlassen will, meinst du? – Ich nutze ja meist (bis zu einer gewissen Komplexität) so wenig externe Bibliotheken wie möglich. Das, was der Compiler mir anbietet, reicht mir meist vollkommen. Dafür weiß ich hinterher wenigstens, wo das Problem liegt, wenn was nicht geht.

    (Siehe auch: leftpad-Vorfall.)

    Bei Linux enden solche Programmteile auf ‚.so‘ was soviel wie Shared Library heißt.

    Nö, Shared Object.

    Bei Linux werden Shared Librarys immer in bestimmten Ordnern erwartet (‚/usr/local/lib‘)

    Ja-in. /usr/local/lib ist gar nicht so besonders wichtig, /lib und /usr/lib hingegen schon.

    Der Syntax ist fast etwas wie in C, nur dass ein String nicht ‚char*‘ sondern ’string‘ heißt.

    Unter C heißt ein String natürlich String (siehe string.h in der libc). Ein char* ist kein String, wie du sicher weißt.

    Aber prima, mal was über Assembler zu lesen. Interessiert erschreckend wenige Leute heutzutage.

    1. Thomas

      Hallo tux,

      vielen Dank für die Klarstellung. Einiges wusste ich selbst nicht aus dem Kopf (alle Library Pfade, das ‚.so‘ Shared Object heißt) und manches hatte ich aber bewusst unscharf gelassen.

      Prinzipiell wird ein String in C als ‚char*‘ – also Pointer auf char definiert. Nur dass dieser Pointer auf Char nicht nach dem ersten Char aufhört sondern bis zum NULL weiter geht. Ich muss ehrlich gesagt sagen dass ich das manchmal in C komplizierter finde als in Assembler 🙂
      Jedenfalls nutzt man in C zum definieren eines Strings eigentlich immer ‚char*‘. Das mag in C++ natürlich schon deutlich anders aussehen…

      Wenn ich hier immer noch was falsch verstehe, lasse ich mich gerne korrigieren.

      Thomas

      1. Stringhandling in Assembler ist jetzt natürlich ein, ähem, Sonderfall. 😉

        „Man“ nutzt in C „eigentlich immer“ das, was den wenigsten Aufwand bedeutet. Bedauerlicherweise erwarten die Stringverarbeitungsfunktionen von C tatsächlich meist ein char* (was einem besonders dann wirklich viel Spaß macht, wenn man, wie ich erst letzte Woche wieder, in einer sauberen C++-Anwendung sowas wie libcurl einbauen möchte, das doch sehr rustikalen C-Code voraussetzt), dennoch darfst du gern standardkonform mit einem String arbeiten, wenn du den geringfügigen Mehraufwand unter Umständen in Kauf nimmst.

        Ein Pointer in C zeigt wie in jeder brauchbaren Sprache auf exakt ein einziges Objekt, hört also keinesfalls „nicht auf“. Ein char* zeigt auf ein nullterminiertes Array (!) von chars. Das Nullterminieren (\0 ist das Gleiche wie NUL, aber keinesfalls das Gleiche wie NULL) ist übrigens noch so ein Grund, wieso ich gegen string.h wirklich nichts einzuwenden habe… 😀

        1. Thomas

          Ich sehe schon, da kennt sich jemand deutlich besser aus als ich 🙂
          Mit ‚NULL‘ meinte ich natürlich ein 0x00-Byte welches ein Array von Chars terminiert.

          Das ist der Grund warum ich manchmal Assembler einfach besser mag, wenn ich ‚0x00‘ in Assembler habe weiß ich was das heißt. Wenn ich jetzt in C mit ‚\0‘ oder ‚NUL‘ oder ‚NULL‘ arbeite dann hätte ich jetzt einfach erwartet dass es ein 0x00 Byte sein wird. Wenn ich aber drüber nachdenke müsste es ja noch den Wert 0x00000000 (32Bit) bzw. 0x0000000000000000 (64Bit) geben…

          Jaaa, C ist manchmal kompliziert. Mir hilft es oft in den Assemblercode zu spicken und nachzusehen ob der Compiler auch das gemacht hat was ich erwartet hatte.

          So ist das eben wenn man arbeitstechnisch eher im 4GL Bereich unterwegs ist.

          1. Irgendwas muss ich ja auch mal verstehen. Dafür bin ich scheiße in Assembler (aber ich arbeite dran). Lesen ist da irgendwie deutlich einfacher als schreiben. Das Gegenteil von Perl sozusagen.
            Das 0x00-Byte heißt NUL. 😉

            Die Bitgröße von 0x00 muss dich eigentlich nicht weiter interessieren, ich habe die sanfte Vermutung, dass sie unter jeder Architektur ungefähr identisch ist… in einer Sprache, in der dich das interessieren könnte, musst du vermutlich nie damit arbeiten.

            Assembler lesen, um C-Code zu verstehen. Weia. Masochist von Beruf?

            1. Thomas

              Nicht Masochist von Beruf, Quäle mich aber gerne selber wenn ich bis ins kleinste Bit verstehen will warum etwas so ist wie es ist 🙂

              1. Spaß an der Selbstqual treibt oft die Kreativität voran, so ist es ja nicht. Der einfachste Weg wäre es, irgendwas zu machen, wo der Computer für einen arbeitet und nicht andersrum. Aber das tun wir ja beide aus gutem Grund nicht.

                Aber Assembler? Weia. Ich habe neulich versucht, was Komplizierteres als Hello World in x86 ASM zu schreiben. Ich gebe ja sonst nicht schnell auf, aber das habe ich dann doch erst mal vertagt.

                1. Thomas

                  Ich glaub mein kompliziertestes war eine DCF77 Funkuhr mit LCD Display in AVR-Assembler (lief auf einem Arduino).
                  Herrliches Projekt. Ich glaub es hatte mehr Kommentarzeilen als Assemblercode.
                  Und ich hatte vergessen die Prozessorregister beim Start sauber auf 0 zu setzen. Deshalb funktionierte das LCD nur wenn man den Arduino ein paar Sekunden aus hatte und dann anschaltete. Wenn man nur Reset gedrückt hat gings nicht mehr.
                  Was hab ich danach gesucht…

                  1. Dann sollte dir C doch erst recht gefallen … 😀

                    1. Thomas

                      Immer wenn ich in Versuchung komme C verstehen zu wollen schaue ich bei http://www.ioccc.org/ vorbei und dann ists auch wieder gut 🙂

                    2. Ähm, das kriegst du aber unter allen Sprachen hin. 😉

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Time limit is exhausted. Please reload CAPTCHA.