 Ja, willkommen zurück zu Programmierparadigmen und diesem zweiten Teil des Moduls Names, Scopes und Bindings. Was wir hier jetzt machen wollen, ist uns ein bisschen genauer anschauen, wie lange diese Objekte, die im Programmiersprachen Namen haben, denn überhaupt leben und wie genau die Programmiersprache all diese benannten Objekte ablegt im Speicher und was für unterschiedliche Arten die Objekte im Speicher abzulegen, es denn überhaupt gibt. Jedes Objekt, was es in so einer Programmiersprache gibt, hat eine bestimmte Lebenszeit. Wenn ich Objekt sage, meine ich hiermit nicht unbedingt die Objekte, die man vielleicht aus objektorientierten Programmiersprachen kennt, die meine ich auch, aber eben nicht nur diese, sondern ich meine im Prinzip alles, was in irgendeiner Form in diesem Programm existiert oder insbesondere was in irgendeiner Form im Speicher gespeichert wird. Und jedes diese Objekte hat jetzt eine Lebensdauer, die irgendwo in der Länge dieses Programms beziehungsweise der Programmausführung ist, also zum Beispiel haben globale Variablen in der Regel eine Lebensdauer, die genauso lang sind wie die Ausführung des Programms, das heißt die existieren von Anfang an und existieren bis zum Ende, wohin gegen lokale Variablen in der Regel nur während der Ausführung einer bestimmten Funktion existieren, das heißt gern Lebensdauer ist in der Regel viel, viel kürzer. Was man auch unterscheiden muss, einfach von den Mikriffen her, ist die Lebensdauer des Objekts, also die Objekt-Lifetime und die Lebensdauer des Bindings dieses Objekts, also die Binding-Lifetime. Und was hier üblicherweise stattfindet, ist, dass ein bestimmtes Objekt während der Ausführung des Programms verschiedene Namen haben kann, das heißt das Objekt lebt eine gewisse Dauer und hat aber dann kürzere Binding-Lifetimes in seinem Leben, weil eben bestimmte Namen zu bestimmten Zeiten während der Ausführung auf dieses eine Objekt zeigen. Was auch passieren kann, ist, dass diese Bindings nebeneinander her existieren, das heißt wir können zu einem bestimmten Zeitpunkt während der Programmausführung mehrere Bindings haben, die eben genau auf dasselbe Objekt zeigen. So, dann schauen wir uns zur Verteutigung, vielleicht einfach mal zwei kleine Beispiele an und fangen hier mit dem ersten an. In dem Beispiel verwende ich so eine kleine Spielprogrammiersprache, die jede beliebige Programmiersprache mit Funktionen im Prinzip sein könnte, wo ich hier erst mal eine Funktion f definiere und in diese Funktion f haben wir dann eine Zuweisung vom Wert 3 an eine Variable namens x und schlussendlich gibt diese Funktion f diese Variable x, beziehungsweise den Wert darin zurück und was wir dann außerhalb der Funktion machen ist, dass wir diese Funktion einmal aufrufen und deren Rückgabewert, also den Wert 3 in Variable y schreiben. Wenn wir uns jetzt hier die Lebensdauer oder die Lifetime der verschiedenen Objekte, die es hier gibt, mal anschauen, dann haben wir einmal natürlich erst mal die Programmausführung, die sozusagen, dass die gesamte Zeit, die hier in irgendeiner Form relevant ist, darstellt. Und innerhalb dieser Programmausführung gibt es jetzt die Lebenszeiten der einzelnen Dinge, die wir hier so sehen. Also zum einen gibt es da diesen Wert 3, der irgendwo seine Lebenszeit hat und der existiert relativ lang in unserem Beispiel während dieser Programmausführung und innerhalb der Lebenszeit vom Wert 3 gibt es dann einmal die Lebenszeit von dieser Variable x, die zu einem bestimmten Zeitpunkt, nämlich dann, wenn die Funktion f gerade ausgeführt wird an den Wert 3 gebunden ist und dann haben wir auch noch die Lebenszeit von unserer Variable y, die auch irgendwann auf diesen Wert 3 zeigt und deshalb auch innerhalb der Lebenszeit vom Wert 3 ist, aber eben nicht mit der Lebenszeit von x überlappt, weil y erst dann auf 3 zeigt und auch erst dann existiert, wenn f schon wieder zurückgekehrt ist, also x nicht mehr existiert. Schauen wir uns mal noch ein zweites Beispiel an und dieses zweite Beispiel wird eine Situation illustrieren, die normalerweise nicht so auftreten sollte, also eigentlich ein Bug im Programm ist. Als das was am längsten lebt, haben wir hier natürlich auch wieder die Programmausführung und innerhalb dieser Programmausführung haben wir jetzt irgendein Objekt, das kann so ähnliches sein wie dieser Wert 3, den wir gerade hatte oder vielleicht auch ein komplexeres Objekt, was irgendwo im Speicher abgespeichert ist und außerdem gibt es ein Binding auf dieses Objekt, also zum Beispiel wieder eine Variable, wie wir es gerade eben schon gesehen haben und was jetzt hier aber auffüllt ist, dass es da irgendwo ein Zeitraum innerhalb der Programmausführung gibt, in dem das Binding existiert, sprich der Name auf der Name, der auf dieses Objekt zeigt existiert, aber das Objekt hört ja hier auf zu existieren, sprich das Binding zeigt eigentlich auf etwas, was es gar nicht mehr gibt und das ist so in den gängigen Programmiersprachen ein Fehler, also ein Bug und heißt zum Beispiel dann Dending Reference, das heißt ich habe eine Referenz auf etwas, das es aber gar nicht mehr gibt und das ist je nach Programmiersprache tatsächlich ein größeres Problem, also in der Sprache WC zum Beispiel kann das sogar ausgenutzt werden, um bestimmte Angriffe auszuführen, nämlich indem ich diese sogenannte Use After Free Vulnerability, also diese Schwachstelle, die es mir erlaubt, Dinge zu benutzen, nachdem sie eigentlich schon im Speicher freigegeben wurden, ausnutze, um die entsprechende Attacke dann zu machen und die nennt sich dann Use After Free Attack. Wie genau die funktioniert werde ich jetzt hier nicht erklären, aber das was da im Grunde ausgenutzt wird, ist eben diese Tatsache, dass das Binding länger existiert und eine längere Lebensdauer hat als das Objekt und wie das genau funktioniert, kann man gerne mal woanders nachschauen. So jetzt haben wir uns angeschaut, wie lange bestimmte Objekte oder auch die Bindings, die diese Objekte dann mit einem Namen referenzieren überhaupt existieren, das heißt wir haben sozusagen die Zeitdimension angeschaut und was wir jetzt noch anschauen wollen, ist die räumliche Dimension, wo wir uns anschauen wollen, wie diese Objekte überhaupt im Speicher abgelegt werden und was das dann auch für einen Einfluss auf den Namen hat, mit dem man in der Programmiersprache auf die Objekte im Speicher zugreifen kann. Es gibt im Prinzip üblicherweise drei Arten von Speicherobjekten, die in den meisten Programmiersprachen in der einen oder anderen Form auftreten. Das eine ist, was man Static Memory nennt, also statischen Speicher, das heißt wir haben da irgendwelche Adressen im Speicher, die während der kompletten Ausführung ihre Gültigkeit behalten, das heißt zum Beispiel eine Konstante ist irgendwo im Speicher abgelegt und die liegt da die ganze Zeit und die eine Adresse, mit der ich diese Konstante referenzieren kann, die ist während der kompletten Programmausführung weiterhin gültig. Die zweite Art und Weise, wie Dinge im Speicher abgelegt werden, ist auf dem Stack. Der Stack wird üblicherweise für Subroutinen oder Funktionen verwendet, wie genau, schauen wir uns noch ein bisschen genauer an, aber die Grunde Idee ist, dass jeder Aufruf einer Funktion dann ein Stück weiteres Speicherstück auf dem Stack hinzufügt und da dann all die Dinge, die zu dieser Funktion gehören, drin gespeichert werden. Das heißt wir haben, wenn die Funktion aufgerufen wird, die Allokation des Speichers und wenn die Funktion dann wieder zurückkehrt, wird diese entsprechend dieses Stück Speicher auf dem Stack wieder freigegeben. Die dritte Variante ist der Hieb und hier kann im Prinzip das Programm zu jeder beliebigen Zeit weiteren Speicher nutzen oder Speicher auch wieder freigeben und dadurch hat man im Prinzip den maximalen Grad an Freiheit, weil man kann bestimmte Dinge, die auf dem Hieb gespeichert sind über die Laufzeit von Funktionen hinweg zum Beispiel am Leben erhalten, man kann aber auch innerhalb einer Funktion irgendwann sagen, so das brauche ich jetzt nicht mehr, das kann jetzt wieder freigegeben werden. So jetzt schauen wir uns mal diese drei Arten von Speicherobjekten ein bisschen genauer an und fangen wir mit dem statisch annäzierten Speicher oder dem Static Memory an. Je nach Programmiersprache wird es für verschiedene Dinge verwendet. Was ich jetzt sage, ist so das, was in den meisten Programmiersprachen gilt, aber es gibt natürlich wie immer Ausnahmen und es mag sein, dass eine bestimmte Programmiersprache jetzt eben Dinge, die ich jetzt hier aufzähle, eben nun gerade nicht in den statischen Speicher ablegt, aber im Großen und Ganzen ist das dann schon so. Das eine Beispiel hier sind globale Variablen, also Variablen, die während der kompletten Laufzeit des Programms eine Gültigkeit haben, kann man im Prinzip im statischen Speicher ablegen, weil es gibt ja keinen Grund, die irgendwann mal zu verschieben oder irgendwann zu löschen, weil sie ja die ganze Zeit global von allen Funktionen benutzt werden können und deswegen auch die ganze Zeit existieren. Das zweite Beispiel hier sind Literale, also Literale sind im Prinzip all die Dinge, die ja wortwörtlich so im Programm Code drinstehen, zum Beispiel ein String, den ich da vielleicht reinschreib, so in Entführungszeichen oder vielleicht auch eine Zahl wie 120, die einfach direkt im Programm Code drinstehen und was üblicherweise dann in dem Programm passiert, ist, dass eben dieses Literal, dieser Wert, so wie er ist, auch im Speicher steht und weil dieser diese Werte als Literal gegeben wird, während der kompletten Programmausführung benutzt werden kann, schreibt man den üblicherweise auch in den statischen Speicher. Das dritte hier sind die Symboltabellen, was überhaupt eine Symboltabelle, das ist im Prinzip eine Abbildung von Dingen, die einen Namen haben in der Sprache, zum Beispiel Funktionen, zu dem, was dahinter steckt. Also ich habe in vielen Programmen so eine Symboltabelle, die mir eben zum Beispiel Funktionsnamen auf die Adresse im Programm abbildet, wo dann die Funktionsimplimierung tatsächlich steht. Abpro-Pro-Funktionsimplimierung, das Viertel auf der Liste hier ist der Programmcode selbst, also sozusagen die Implementierung, von der ich gerade gesprochen habe und das heißt, alles was alle Instruktionen, die irgendwo in dem Programm mal ausgeführt werden könnten, stehen natürlich auch irgendwo im Speicher und das ist üblicherweise auch in diesem statischen Speicher, eben weil sich dieser Programmcode in den meisten Fällen ja nicht ändert. Und schlussendlich gibt es da noch die Compile-Time-Constants, also Konstanten, die zur Zeit des Kompilierens konstant sind. Das können die Literale sein, die ich gerade eben schon angesprochen habe, können aber auch andere Dinge sein. Also wenn ich in meinem Programm zum Beispiel schreibe, x ist gleich 3 plus 4, dann weiß der Kompiler, dass 3 plus 4 gleich 7 ist, also ist 7 in dem Moment auch eine Compile-Time-Constant, obwohl es vielleicht gar kein Literal in meinem Programm ist. Und diese Konstanten, selbst wenn sie lokal innerhalb einer Funktion sind, landen auch üblicherweise auf dem statischen Speicher. Nach dem statischen Speicher schauen wir uns jetzt mal die zweite Art von Speicherobjekten an, nämlich all die Dinge, die im Programm auf dem Stack gespeichert werden. Also schauen wir uns die Stack-Based-Allocation an. Um das Ganze ein bisschen an dem Beispiel zu verdeutlichen, schreibe ich erstmal so ein kleines Spielprogramm in unserer ausgedachten Spielsprache wieder auf. In dem Fall haben wir da eine Funktion C, die irgendwas macht, was genau ist hier erstmal nicht so wichtig. Dann haben wir eine Funktion B, die je nachdem wie ein if oder die Bedingung eines ifs hier evaluiert entweder nochmal B aufruft oder unsere Funktion C, die oben drüber steht, aufruft. Dann haben wir noch eine Funktion A und was die macht, ist ganz einfach B aufrufen. Und schlussendlich muss das Programm auch noch irgendwo losgehen und dafür haben wir irgendwo unsere Main-Funktion oder unser Hauptprogramm und was da hier passiert, ist, dass wir A aufrufen. Während dieses Programm jetzt ausgeführt wird, wird irgendwo im Speicher der Stack abgespeichert und dieser Stack ist im Grunde einfach nur ein Stück Speicher, der in eine bestimmte Richtung immer weiter wächst und in unserem Fall ist dieses Wachstum von unten nach oben. Also die Richtung dieses Stack-Wachstums ist hier nach oben. Was auf dem Stack passiert ist, dass wir für jede Ausführung einer Funktion ein Stück Speicher haben, indem alles, was zu dieser Ausführung der Funktion gehört und was genau das ist, werden wir uns gleich anschauen, gespeichert wird. Also in unserem Fall geht es in Main los mit dem Aufruf von der Funktion A, sprich wir haben hier erstmal ein Stück Speicher für diese Ausführung von A. Was A jetzt macht, ist B aufzurufen, das heißt oben drauf kommt als nächstes ein Stück Speicher für diesen Funktionsaufruf von B. In B gibt es jetzt zwei Varianten, entweder wir rufen nochmal B auf oder wir rufen C auf, gehen wir mal davon aus, dass wir zunächst erstmal nochmal B aufrufen, das heißt wir werden auf dem Stack ein weiteres Stück Speicher reservieren für diesen zweiten Aufruf von B. In diesem zweiten Aufruf von B gehen wir mal davon aus, dass als nächstes C aufgerufen wird, das heißt hier kommt noch so ein Segment oben drauf für C und so weiter und sofort hier nach dem, was C dann macht, kann dieser Stack jetzt noch weiter wachsen oder er kann dann natürlich auch wieder schrumpfen, wenn die Funktionen entsprechend zurückkehren. Schauen wir uns mal ein bisschen genauer an, was in diesen Stücken von Speicher, die jeweils für die Funktionsausführung reserviert sind, eigentlich genau passiert. Also wenn man hier so ein bisschen reinsumt, dann sieht man, dass da so verschiedene Sachen drin gespeichert sind und das genauer schauen wir uns gleich an. Dieses gesamte Ding hier nennt man den Activation Record, weil er alles speichert, was speziell für diese Funktion notwendig ist, während diese Funktion eben aktiv ist. Was ist da alles drin in diesem Activation Record? Also eine Sache, die da üblicherweise gespeichert wird, ist die Adresse, zu der wir wieder zurückspringen müssen, also die Adresse von im Code, zu der wir wieder zurückspringen müssen, wenn diese Funktion zurückkehrt, also wenn sie schlussendlich fertig ist mit ihrer Ausführung. Für diese erste Ausführung von der Funktion B hier zum Beispiel, würden wir anschließend wieder in die Funktion A zurückkehren und zwar genau an den Punkt hier in der Ausführung hinter dem Aufruf von B und eben genau diese Adresse würde dann hier in dem Activation Record gespeichert. Dann gibt es hier so ein Stück in dem Activation Record, wo so verschiedenste Sachen drin sind, die nötig sind, um diesen ganzen Stack überhaupt zu managen. Was das genau ist, schauen wir uns dann mal in einem späteren Modul an und da gehe ich jetzt erst mal nicht weiter drauf ein. Ein recht großer Teil unter Umständen in diesem Activation Record ist für die lokalen Variablen. Also all die Variablen, die ihren Scope innerhalb dieser Funktion, in dem Fall der Funktion B haben und was wir dann hier auf dem Stack haben, sind eben genau die Werte dieser Variablen oder gegebenenfalls auch nur die Adresse der Objekte, wo dann die eigentlichen Werte drinstehen. Falls während der Ausführung der Funktion vielleicht irgendwelche temporären Werte mal entstehen, dann werden die ebenfalls auf dem Stack gespeichert und schlussendlich, bevor wir dann die nächste Funktion aufrufen, wollen wir vielleicht noch Argumente übergeben an die nächste Funktion, also wenn unser B jetzt noch irgendwelche Argumente nehmen würde, dann würden wir die auch auf den Stack schreiben, sodass dann der nächste Aufruf von B oder auch von jeder anderen Funktion, die aufgerufen wird, eben diese Werte oder diese Argumente lesen kann. Also was hier ganz oben drauf kommt, sind die Argumente, die übergeben werden an diese Funktion, die anschließend hier noch aufgerufen werden. Also für dieses Beispiel von dem Stack-Segment, das ich hier gerade aufmalle, wäre das eben ein Argument, was wir an den zweiten Aufruf von B übergeben oder wenn wir dann hier bei diesem Stack-Segment sind, wo B zum zweiten Mal aufgerufen ist, rufen wir dann ja C auf, sprich da werden dann noch die Argumente, die wir vielleicht an C übergeben drin. So neben dem statisch allizierten Speicher und dem Stack, wo all die Dinge für die Funktionsausführungen drauf liegen, haben wir jetzt noch diese dritte Art von Speicherobjekten und das ist alles, was auf dem heap gespeichert wird. Wofür benutzt man diesen heap? Der heap wird vor allem für dynamisch allizierte Datenstrukturen verwendet. Was heißt dynamisch hier? Dynamisch bedeutet, dass wir erst zur Laufzeit wissen, ob und wie oft und wie viel und wie groß diese Datenstrukturen überhaupt sanken müssen. Vielleicht als kleines Beispiel, wenn ich in einem Programm eine Schleife habe und das Programmeingaben von den Benutzer bekommt, die festlegen, wie oft oder ob überhaupt diese Schleife ausgeführt wird. Und wenn innerhalb dieser Schleife vielleicht Datenstrukturen angelegt werden, weil ich zum Beispiel ein neues Objekt erstelle, dann weiß ich ja, bevor das Programm ausgeführt wird, nicht wie viel Speicher ich dafür überhaupt verwende. Und das ist dann etwas, was eben genau auf dem heap gemacht wird, weil dann zur Laufzeit des Programms überhaupt erst feststeht, ob ich diese Objekte brauche und auch wie groß diese Objekte eigentlich sind. Ein typisches Beispiel dafür sind die Objekte und jetzt meine ich wirklich die Objekte im Sinne von objektorientierter Programmierung, die man in Java üblicherweise hat, weil die eben oft dynamisch, also erst zur Laufzeit, alliziert werden und deshalb auf dem heap landen. Je nach Programmiersprache hat oder hat man eben kein sogenanntes Managed Memory, was bedeutet es? Das bedeutet, dass die Programmiersprache selbst sich um die Verwaltung dieser dynamisch allizierten Speicherobjekte in irgendeiner Form kümmern. Wie genau das funktioniert, schauen wir uns später in einem anderen Modul noch genauer an. Aber so die Grundidee ist, dass das in diesem Managed Memory die Programmiersprache zum Beispiel Speicher dann wieder frei gibt, indem sie garbage collection, also das Aufräumen von nicht mehr gebrauchten Speicher übernimmt. Das hat zwei konkrete Folgen für die Programmausführung. Zum einen nehme ich das Objekte, die nicht mehr erreichbar sind, also für dies kein aktives Binding mehr gibt, was eben auf diese Objekte zeigt, dass die automatisch die alliziert werden. Das heißt, alles was man nicht mehr braucht, weil es keinen Namen mehr gibt, der darauf zeigt, wird automatisch aufgeräumt. Die andere Folge ist, dass wir einerseits natürlich weniger Kontrolle haben, denn der Programmiere kann jetzt nicht mehr festlegen, wann genau welcher Speicher dann wie freigegeben wird, aber gleichzeitig eine bestimmte Klasse von Fehlern einfach ausgeräumt ist, weil die einfach nicht mehr auftreten können. Und insbesondere können eben diese Use after free Probleme und die Schwachstellen, die damit einhergehen, die ich vorhin schon mal kurz angesprochen habe, gar nicht auftreten entsprachen, die Managed Memory haben. Zum Abschluss machen wir noch ein kleines Quiz, in dem jetzt einfach mal die Sassungen, die wir uns gerade angeschaut haben, nochmal am Beispiel von dem Java-Programm so ein bisschen illustriert und noch abgefragt werden sollen. Was wir hier haben, ist einfach ein bisschen Java, indem wir einmal eine Klasse definieren, die heißt Person und hat selber ein paar Felder und dann haben wir hier so diese Main Funktion, in der wir verschiedene Sachen machen und unter anderem eben eine Instanz dieser Klasse erstellen und in diese Variable P speichern. Und die Frage für Sie ist jetzt, wo bestimmte Datenobjekte, die während dieser Ausführung des Programms entstehenden gespeichert werden? Also sind die Static Memories, sind die auf dem Heap oder sind die vielleicht auf dem Stack? Und zwar konkret wollen wir das für diese vier Objekte wissen, nämlich einmal dieser Integer-Wert 23, der String, in dem der Name der Person drin steht, also John, das Person-Objekt überhaupt und dann auch diese Variable P, die dann schlussendlich irgendwann hier unten mal auf dieses Person-Objekt zeigt. Und die Frage für Sie ist jetzt, wo werden die eigentlich genau abgelegt? Und ich würde Sie bitte an der Stelle das Video mal kurz zu unterbrechen und dann im Elias abzustimmen, einfach, dass Sie sehen, ob Sie das Ganze auch verstanden haben und dann vielleicht auch die Lösung ein bisschen besser mit durchdenken können. So, schauen wir uns mal die Lösung an. Zum einen gibt es jetzt zwei Sachen, die auf dem Stack gespeichert werden und zwar in dem jeweiligen oder in dem Allocation Frame dieser Main Funktion, die natürlich auch erstmal nur eine Funktion ist und dementsprechend in Java zumindest auch ein Allocation Frame hat, nämlich zum einen dieser Wert 23 und zwar weil das ein primitiver Wert ist, der in einer lokalen Variable gespeichert ist, deswegen landet das in Java auf dem Stack und zum anderen, der die Referenz auf die die Variable P zeigt, also das kann eine Adresse sein oder hier initial ist es natürlich erst mal der Wert Null und dieser Wert Null bzw. später dann die Adresse werden auf dem Stack gespeichert. Die anderen beiden Dinge, die hier abgefragt wurden, landen in Java beide auf dem Heap, wie gesagt, das variiert ein bisschen von Programmiersprache zu Programmiersprache. Das zweite, nämlich dieses Person-Objekt ist, glaube ich, recht klar, ganz einfach, weil das ein dynamisch analysiertes Objekt ist, wo die Größe und auch ob man es vielleicht braucht, im Allgemeinen zur Laufzeit erst bekannt werden und eben nicht statisch bekannt sind, deswegen landet dieses Objekt auf dem Heap. Vielleicht etwas überraschend ist, der String, in dem der Name John drin steht. Das ist eine Besonderheit von Java, wo alle String-Konstanten, die so im Code verwendet werden, auch auf dem Heap gespeichert werden und zwar in einem String Pool. Was den Vorteil hat, dass, wenn der selber String mehrmals auftaucht in einem größeren Programm, also wenn ich jetzt zum Beispiel irgendwo in diesem Programm noch mal einen String namens John hätte, dann würde das beides zum selben Speicherobjekt zeigen und das funktioniert aus dem Grund in Java gut, weil Strings konstant sind. Das heißt, ich kann diesen String nie ändern, ich kann da jetzt nicht Johnny draus machen oder wenn ich das machen würde, wäre das ein neuer String und deswegen kann der auch wieder verwendet werden und die meisten Java Implementierung machen das eben so, dass sie einen String Pool haben, in dem diese Strings dann auf dem Heap gespeichert werden und diese P Name Variable würde dann den Pointer in diesen String Pool beinhalten. Das war es schon wieder für diesen zweiten Teil des Modules Names, Scopes und Bindings. Also Sie wissen jetzt, wie lange eigentlich diese Namen und diese Bindings innerhalb von Programmiersprachen existieren und vor allem, dass die eine begrenzte Lebensdauer, die eben nicht so lange ist wie die Lebensdauer des kompletten Programmes haben und Sie haben auch gesehen, wo bestimmte Dinge im Speicher überhaupt abgespeichert werden und dass es da diese drei Arten von Speicher gibt, nämlich Static Memory, Stack Memory und Heap Vielen Dank fürs Zuhören und bis zum nächsten Mal.