Fügt ein Betriebssystem beim Öffnen eines Programms seinen eigenen Computercode ein?


32

Ich lerne CPU's und weiß, wie es ein Programm aus dem Speicher liest und seine Anweisungen ausführt. Ich verstehe auch, dass ein Betriebssystem Programme in Prozessen trennt und sie dann so schnell abwechselt, dass Sie glauben, dass sie gleichzeitig ausgeführt werden, aber tatsächlich läuft jedes Programm alleine in der CPU. Aber wenn das Betriebssystem auch eine Menge Code in der CPU ausführt, wie kann es dann die Prozesse verwalten?

Ich habe nachgedacht und die einzige Erklärung, die ich denken könnte, ist: Wenn das Betriebssystem ein Programm aus dem externen Speicher in den Arbeitsspeicher lädt, fügt es seine eigenen Anweisungen in die Mitte der ursprünglichen Programmanweisungen ein, damit das Programm ausgeführt wird, das Programm kann das Betriebssystem aufrufen und einige Dinge tun. Ich glaube, es gibt eine Anweisung, die das Betriebssystem zum Programm hinzufügt, damit die CPU einige Zeit zum Betriebssystemcode zurückkehren kann. Außerdem glaube ich, dass das Betriebssystem beim Laden eines Programms prüft, ob es einige vorab gesendete Anweisungen gibt (die zu verbotenen Adressen im Speicher springen würden) und diese dann beseitigt.

Denke ich ernst? Ich bin kein CS-Student, sondern ein Mathematik-Student. Wenn möglich, würde ich mir ein gutes Buch darüber wünschen, da ich niemanden gefunden habe, der erklärt, wie das Betriebssystem einen Prozess verwalten kann, wenn das Betriebssystem auch eine Menge Code in der CPU enthält und nicht gleichzeitig ausgeführt werden kann Uhrzeit des Programms. Die Bücher sagen nur, dass das Betriebssystem Dinge verwalten kann, aber jetzt wie.


7
Siehe: Kontextwechsel Das Betriebssystem führt einen Kontextwechsel zur App durch. Die App kann dann OS-Dienste anfordern, die einen Kontext zum OS zurückgeben. Wenn die App endet, wechselt der Kontext zurück zum Betriebssystem.
Guy Coder

4
Siehe auch "syscall".
Raphael


1
Wenn die Kommentare und Antworten Ihre Frage nicht zu Ihrem Verständnis oder Ihrer Zufriedenheit beantworten, fordern Sie bitte weitere Informationen als Kommentar an und erläutern Sie, was Sie denken oder wo Sie verloren sind oder worüber Sie spezifisch mehr Informationen benötigen.
Guy Coder

2
Ich denke, dass Interrupt , Hooking (eines Interrupts), Hardware-Timer (mit Scheduling- Handling-Hook) und Paging (Teilantwort auf Ihre Bemerkung zum verbotenen Speicher) die wichtigsten Schlüsselwörter sind, die Sie benötigen. Das Betriebssystem muss eng mit dem Prozessor zusammenarbeiten, damit der Code nur bei Bedarf ausgeführt werden kann. Daher kann der größte Teil der CPU-Leistung für die eigentliche Berechnung verwendet werden, nicht für die Verwaltung.
Palec

Antworten:


35

Nein. Das Betriebssystem spielt nicht mit dem Code des Programms, der neuen Code einfügt. Das hätte eine Reihe von Nachteilen.

  1. Dies wäre zeitaufwändig, da das Betriebssystem die gesamte ausführbare Datei durchsuchen und ihre Änderungen vornehmen müsste. Normalerweise wird ein Teil der ausführbaren Datei nur bei Bedarf geladen. Das Einfügen ist außerdem teuer, da Sie eine Menge Material aus dem Weg räumen müssen.

  2. Aufgrund der Unentscheidbarkeit des Halteproblems ist es unmöglich zu wissen, wo Sie die Anweisungen zum Zurückspringen zum Betriebssystem einfügen müssen. Wenn der Code beispielsweise so etwas enthält while (true) {i++;}, müssen Sie auf jeden Fall einen Haken in diese Schleife einfügen, aber die Bedingung in der Schleife ( truehier) kann willkürlich kompliziert sein, sodass Sie nicht entscheiden können, wie lange sie wiederholt wird. Andererseits wäre es sehr ineffizient, Hooks in jede Schleife einzufügen : Zum Beispiel würde ein Zurückspringen zum Betriebssystem for (i=0; i<3; i++) {j=j+i;}den Prozess erheblich verlangsamen. Und aus dem gleichen Grund können Sie keine kurzen Schleifen erkennen, um sie in Ruhe zu lassen.

  3. Aufgrund der Unentscheidbarkeit des Halteproblems ist es unmöglich zu wissen, ob die Code-Injektionen die Bedeutung des Programms geändert haben. Angenommen, Sie verwenden Funktionszeiger in Ihrem C-Programm. Das Einfügen von neuem Code würde die Positionen der Funktionen verschieben, sodass Sie, wenn Sie einen durch den Zeiger aufrufen, an die falsche Stelle springen würden. Wenn der Programmierer krank genug wäre, um berechnete Sprünge auszuführen, würden auch diese fehlschlagen.

  4. Es würde mit jedem Antiviren-System eine Menge Spaß machen, da es auch den Virencode ändern und all Ihre Prüfsummen durcheinander bringen würde.

Sie können das Problem des Anhaltens umgehen, indem Sie den Code simulieren und Hooks in jede Schleife einfügen, die mehr als eine festgelegte Anzahl von Malen ausgeführt wird. Dies würde jedoch eine extrem teure Simulation des gesamten Programms erfordern, bevor es ausgeführt werden konnte.

Wenn Sie Code einfügen möchten, ist der Compiler der richtige Ort dafür. Auf diese Weise müssten Sie es nur einmal tun, aber aus dem zweiten und dritten oben genannten Grund würde es immer noch nicht funktionieren. (Und jemand könnte einen Compiler schreiben, der nicht mitspielt.)

Es gibt drei Hauptmethoden, mit denen das Betriebssystem die Kontrolle über Prozesse zurückerhält.

  1. In kooperativen (oder nicht präemptiven) Systemen gibt es eine yieldFunktion, die ein Prozess aufrufen kann, um dem Betriebssystem die Kontrolle zurückzugeben. Wenn dies Ihr einziger Mechanismus ist, sind Sie natürlich darauf angewiesen, dass sich die Prozesse gut verhalten, und ein Prozess, der nicht nachgibt, belastet die CPU, bis er beendet wird.

  2. Um dieses Problem zu vermeiden, wird ein Timer-Interrupt verwendet. CPUs ermöglichen es dem Betriebssystem, Rückrufe für alle Arten von Interrupts zu registrieren, die die CPU implementiert. Das Betriebssystem verwendet diesen Mechanismus, um einen Rückruf für einen Timer-Interrupt zu registrieren, der regelmäßig ausgelöst wird und der es ihm ermöglicht, seinen eigenen Code auszuführen.

  3. Jedes Mal, wenn ein Prozess versucht, aus einer Datei zu lesen oder auf andere Weise mit der Hardware zu interagieren, fordert er das Betriebssystem auf, dafür zu arbeiten. Wenn das Betriebssystem von einem Prozess aufgefordert wird, etwas zu tun, kann es entscheiden, diesen Prozess anzuhalten und einen anderen Prozess auszuführen. Das hört sich vielleicht etwas machiavellistisch an, ist aber das Richtige: Die Datenträger-E / A ist langsam, sodass Sie auch Prozess B ausführen lassen können, während Prozess A darauf wartet, dass sich die sich drehenden Metallklumpen an die richtige Stelle bewegen. Netzwerk-E / A ist noch langsamer. Die Tastatur-E / A ist eiskalt, weil Menschen keine Gigahertz-Wesen sind.


5
Können Sie mehr über Ihren letzten 2. Punkt herausfinden? Ich bin neugierig auf diese Frage und habe das Gefühl, dass die Erklärung hier übersprungen wird. Mir scheint die Frage zu sein, "wie das Betriebssystem die CPU aus dem Prozess zurücknimmt", und Ihre Antwort lautet "Das Betriebssystem handhabt sie". aber wie? Nehmen Sie die Endlosschleife in Ihrem ersten Beispiel: Wie friert es den Computer nicht ein?
BiAiB

3
Einige Betriebssysteme tun dies, die meisten Betriebssysteme spielen zumindest mit dem Code, der zum "Verknüpfen" verwendet wird, herum, sodass das Programm unter einer beliebigen Adresse geladen werden kann
Ian Ringrose,

1
@BiAiB Das Schlüsselwort hier ist "Interrupt". Die CPU ist nicht nur etwas, das einen bestimmten Befehlsstrom verarbeitet, sondern kann auch asynchron von einer separaten Quelle unterbrochen werden - am wichtigsten für uns, E / A- und Takt-Interrupts. Da nur Kernel-Space-Code Interrupts verarbeiten kann, kann Windows sicher sein, dass die Arbeit von jedem laufenden Prozess jederzeit "gestohlen" werden kann. Die Interrupt-Handler können jeden gewünschten Code ausführen, einschließlich "Speichern der CPU-Register und Wiederherstellen von hier (ein anderer Thread)". Extrem vereinfacht, aber das ist der Kontextwechsel.
Luaan

1
Hinzufügen zu dieser Antwort; Der in den Punkten 2 und 3 genannte Multitasking-Stil wird als "preemptives Multitasking" bezeichnet. Der Name bezieht sich auf die Fähigkeit des Betriebssystems, einen laufenden Prozess zu verhindern. Kooperatives Multitasking wurde häufig auf älteren Betriebssystemen verwendet. Unter Windows wurde zumindest das präemptive Multitasking erst unter Windows 95 eingeführt. Ich habe von mindestens einem heute verwendeten industriellen Steuerungssystem gelesen, das Windows 3.1 ausschließlich für sein kooperatives Echtzeit-Multitasking-Verhalten verwendet.
Jason C

3
@BiAiB Eigentlich liegst du falsch. Desktop-CPUs führen seit dem i486 keinen Code sequenziell und synchron aus. Aber auch ältere CPUs hatten noch asynchrone Eingänge - Interrupts. Stellen Sie sich eine Hardware-Interrupt-Anfrage (IRQ) wie einen Pin auf der CPU selbst vor - wenn sie eintrifft 1, stoppt die CPU, was auch immer sie tut, und beginnt mit der Interrupt-Verarbeitung (was im Grunde bedeutet: "Zustand erhalten und zu einer Adresse im Speicher springen"). Die Interrupt-Behandlung selbst ist kein x86oder welcher Code auch immer, sie ist buchstäblich fest verdrahtet. Nach dem Springen wird erneut (beliebiger) x86Code ausgeführt. Threads sind eine viel höhere Abstraktion.
Luaan

12

Die Antwort von David Richerby ist zwar gut, aber es ist eine Art Glasur darüber, wie moderne Betriebssysteme bestehende Programme stoppen. Meine Antwort sollte für die x86- oder x86_64-Architektur zutreffend sein, die die einzige ist, die üblicherweise für Desktops und Laptops verwendet wird. Andere Architekturen sollten ähnliche Methoden haben, um dies zu erreichen.

Beim Start des Betriebssystems wird eine Interrupt-Tabelle erstellt. Jeder Eintrag in der Tabelle verweist auf ein Stück Code im Betriebssystem. Wenn Interrupts auftreten, die von der CPU gesteuert werden, wird diese Tabelle überprüft und der Code aufgerufen. Es gibt verschiedene Interrupts, z. B. Division durch Null, ungültiger Code und einige vom Betriebssystem definierte Interrupts.

Auf diese Weise kommuniziert der Benutzerprozess mit dem Kernel, z. B. wenn er auf die Festplatte oder auf etwas anderes, das der Betriebssystemkernel steuert, lesen / schreiben möchte. Ein Betriebssystem richtet auch einen Timer ein, der nach Beendigung einen Interrupt aufruft, sodass der Ausführungscode zwangsweise vom Benutzerprogramm in den Betriebssystemkern geändert wird und der Kernel andere Aktionen ausführen kann, z. B. andere Programme in die Warteschlange stellen.

Wenn dies geschieht, muss der Betriebssystem-Kernel aus dem Speicher speichern, wo sich der Code befand, und wenn der Kernel das getan hat, was er tun muss, wird der vorherige Status des Programms wiederhergestellt. Somit weiß das Programm nicht einmal, dass es unterbrochen wurde.

Der Prozess kann die Interrupt-Tabelle aus zwei Gründen nicht ändern. Der erste Grund ist, dass sie in einer geschützten Umgebung ausgeführt wird. Wenn also versucht wird, bestimmten geschützten Assembly-Code aufzurufen, löst die CPU einen weiteren Interrupt aus. Der zweite Grund ist der virtuelle Speicher. Der Speicherort der Interrupt-Tabelle liegt im realen Speicher zwischen 0x0 und 0x3FF. Bei Benutzerprozessen wird dieser Speicherort jedoch normalerweise nicht zugeordnet, und der Versuch, nicht zugeordneten Speicher zu lesen, löst einen weiteren Interrupt aus, also ohne die geschützte Funktion und die Fähigkeit, in den realen RAM zu schreiben kann der Benutzerprozess nicht ändern.


4
Die Interrupts werden nicht vom Betriebssystem definiert, sondern von der Hardware vorgegeben. Die meisten aktuellen Architekturen verfügen über spezielle Anweisungen zum Aufrufen des Betriebssystems. i386 hat dafür einen (durch Software generierten) Interrupt verwendet, bei seinen Nachfolgern ist dies jedoch nicht mehr der Fall.
Vonbrand

2
Ich weiß, dass die Interrupts von der CPU definiert werden, aber der Kernel setzt die Zeiger. Ich habe es möglicherweise schlecht erklärt. Ich dachte auch, dass Linux Int 9 verwendet, um noch mit dem Kernel zu sprechen, aber vielleicht gibt es jetzt bessere Möglichkeiten.
Programmdude

Dies ist eine ziemlich irreführende Antwort, obwohl die Vorstellung, dass präventive Scheduler durch Timer-Interrupts gesteuert werden, richtig ist. Zunächst ist zu beachten, dass sich der Timer in Hardware befindet. Um zu verdeutlichen, dass der Vorgang "Speichern ... Wiederherstellen" als Kontextwechsel bezeichnet wird, werden unter anderem alle CPU-Register (einschließlich des Befehlszeigers) gespeichert. Prozesse können auch Unterbrechungstabellen effektiv ändern, dies wird als "geschützter Modus" bezeichnet, der auch den virtuellen Speicher definiert, und ist seit 286 bekannt - ein Zeiger auf die Unterbrechungstabelle wird in einem beschreibbaren Register gespeichert.
Jason C

(Auch die Real-Modus-Interrupt-Tabelle wurde seit dem 8086 verschoben - nicht auf die erste Seite des Speichers gesperrt.)
Jason C

1
Diese Antwort vermisst ein kritisches Detail. Wenn ein Interrupt ausgelöst wird, wechselt die CPU nicht direkt zum Kernel. Stattdessen werden zuerst die vorhandenen Register gespeichert und dann auf einen anderen Stapel umgeschaltet. Erst dann wird der Kernel aufgerufen. Es wäre eine ziemlich schlechte Idee, den Kernel mit einem zufälligen Stack aus einem zufälligen Programm aufzurufen. Auch der letzte Teil ist irreführend. Es wird kein Interrupt ausgegeben, der versucht, nicht zugeordneten Speicher zu lesen. es ist einfach unmöglich. Sie lesen von virtuellen Adressen und der nicht zugeordnete Speicher hat einfach keine virtuelle Adresse.
MSalters

5

Der Betriebssystemkern erhält aufgrund des CPU-Taktinterrupt-Handlers die Kontrolle über den laufenden Prozess zurück, nicht durch das Einfügen von Code in den Prozess.

Sie sollten sich über Interrupts informieren , um mehr über deren Funktionsweise und den Umgang mit OS-Kernels zu erfahren und verschiedene Funktionen zu implementieren.


Nicht nur die Uhr unterbrechen: jede Unterbrechung. Und auch Anweisungen zum Ändern des Modus.
Gilles 'SO - hör auf, böse

3

Es gibt eine ähnliche Methode wie die, die Sie beschreiben: kooperatives Multitasking . Das Betriebssystem fügt keine Anweisungen ein, aber jedes Programm muss so geschrieben sein, dass es Betriebssystemfunktionen aufruft, die möglicherweise einen anderen der kooperativen Prozesse ausführen. Dies hat die von Ihnen beschriebenen Nachteile: Ein Programmabsturz bringt das gesamte System zum Erliegen. Windows bis einschließlich 3.0 funktionierte so; 3.0 im "protected mode" und höher nicht.

Präventives Multitasking (die heutzutage übliche Art) beruht auf einer externen Quelle von Interrupts. Interrupts setzen den normalen Kontrollfluss außer Kraft und speichern die Register normalerweise irgendwo ab, damit die CPU etwas anderes tun und das Programm dann transparent fortsetzen kann. Natürlich kann das Betriebssystem das Register "Wenn Sie Interrupts hier fortsetzen" ändern, sodass es in einem anderen Prozess fortgesetzt wird.

(Einige Systeme tun Rewrite Anweisungen in begrenztem Umfang auf Programmladen, genannt „Thunk“ und der Transmeta - Prozessor dynamisch seinen eigenen Befehlssatz neu kompiliert)


AFAICR 3.1 war ebenfalls kooperativ. In Win95 kam das präventive Multitasking zum Einsatz. Im geschützten Modus wurde hauptsächlich der Adressraum isoliert (was die Stabilität verbessert, jedoch aus weitgehend unabhängigen Gründen).
CHAO

Beim Thunking wird der Code nicht neu geschrieben oder in die Anwendung eingefügt. Der geänderte Loader basiert auf dem Betriebssystem und ist kein Produkt der Anwendung. Interpretierende Sprachen, die beispielsweise mit JIT-Compilern kompiliert wurden, ändern weder den Code noch fügen sie etwas in den Code ein. Sie übersetzen Quellcode in eine ausführbare Datei. Dies ist wiederum nicht dasselbe wie das Einfügen von Code in eine Anwendung.
Dave Gordon

Transmeta verwendete ausführbaren x86-Code als Quelle, keine interpretierende Sprache. Und ich habe gedacht , von einem Fall , in dem Code wird injiziert: unter einem Debugger ausgeführt wird . X86-Systeme überschreiben normalerweise die Anweisung am Haltepunkt mit "INT 03", das den Debugger abfängt. Nach dem Fortsetzen wird der ursprüngliche Opcode wiederhergestellt.
pjc50

Beim Debuggen kann kaum jemand eine Anwendung ausführen. darüber hinaus der Entwickler der Anwendung. Ich denke also nicht, dass das dem OP wirklich hilft.
Dave Gordon

3

Multitasking erfordert nichts wie Code-Injection. In einem Betriebssystem wie Windows gibt es eine Komponente des Betriebssystemcodes namens Scheduler, die auf einem Hardware-Interrupt beruht, der von einem Hardware-Timer ausgelöst wird. Dies wird vom Betriebssystem verwendet, um zwischen verschiedenen Programmen und sich selbst zu wechseln, was unserer menschlichen Wahrnehmung zufolge alles gleichzeitig geschieht.

Grundsätzlich programmiert das Betriebssystem den Hardware-Timer so, dass er von Zeit zu Zeit ausgelöst wird ... vielleicht 100 Mal pro Sekunde. Wenn der Timer abläuft, wird ein Hardware-Interrupt generiert - ein Signal, das die CPU auffordert, die Ausführung zu stoppen, den Status auf dem Stack zu speichern, den Modus in einen privilegierteren Modus zu ändern und den Code auszuführen, den sie in einem speziell dafür vorgesehenen Ordner findet in Erinnerung behalten. Dieser Code ist zufällig Teil des Schedulers, der entscheidet, was als nächstes getan werden soll. Es kann sein, dass ein anderer Prozess fortgesetzt wird. In diesem Fall muss ein sogenannter "Kontextwechsel" durchgeführt werden, bei dem der gesamte aktuelle Status (einschließlich der virtuellen Speichertabellen) durch den des anderen Prozesses ersetzt wird. Bei der Rückkehr zu einem Prozess muss der gesamte Kontext dieses Prozesses wiederhergestellt werden.

Die "speziell bezeichnete" Stelle im Speicher muss nur dem Betriebssystem bekannt sein. Die Implementierungen variieren, aber das Wesentliche ist, dass die CPU auf verschiedene Interrupts reagiert, indem sie eine Tabellensuche durchführt. Der Speicherort der Tabelle befindet sich an einer bestimmten Stelle im Speicher (bestimmt durch das Hardware-Design der CPU), der Inhalt der Tabelle wird vom Betriebssystem festgelegt (im Allgemeinen beim Start), und der "Typ" des Interrupts bestimmt, welcher Eintrag vorliegt in der Tabelle ist als "Interrupt-Service-Routine" zu verwenden.

Dabei handelt es sich nicht um "Code-Injection". Es basiert auf dem im Betriebssystem enthaltenen Code in Zusammenarbeit mit den Hardwarefunktionen der CPU und ihrer unterstützenden Schaltkreise.


2

Ich denke, das realistischste Beispiel für das, was Sie beschreiben, ist eine der von VMware verwendeten Techniken , die vollständige Virtualisierung mithilfe binärer Übersetzung .

VMware fungiert als Schicht unter einem oder mehreren gleichzeitig ausgeführten Betriebssystemen auf derselben Hardware.

Die meisten ausgeführten Anweisungen (z. B. in gewöhnlichen Anwendungen) können mit der Hardware virtualisiert werden, aber ein Betriebssystemkern selbst verwendet Anweisungen, die nicht virtualisiert werden können, da sie "ausbrechen" würden, wenn der Maschinencode des erratenen Betriebssystems unverändert ausgeführt würde "der Steuerung des VMware-Hosts. Beispielsweise muss ein Gastbetriebssystem im privilegiertesten Schutzring ausgeführt und die Interrupt-Tabelle eingerichtet werden. Wäre dies zulässig, hätte VMware die Kontrolle über die Hardware verloren.

VMware schreibt diese Anweisungen vor der Ausführung neu in den Betriebssystemcode und ersetzt sie durch Sprünge in VMware-Code, der den gewünschten Effekt simuliert.

Diese Technik ist also etwas analog zu dem, was Sie beschreiben.


2

Es gibt eine Vielzahl von Fällen, in denen ein Betriebssystem Code in ein Programm einschleusen kann. Die 68000-basierten Versionen des Apple Macintosh-Systems erstellen eine Tabelle aller Segmenteintrittspunkte (die sich unmittelbar vor den statischen globalen Variablen (IIRC) befinden). Wenn ein Programm startet, besteht jeder Eintrag in der Tabelle aus einem Trap-Befehl, gefolgt von der Segmentnummer und dem Versatz in das Segment. Wenn der Trap ausgeführt wird, prüft das System anhand der Wörter nach dem Trap-Befehl, welches Segment und welcher Offset erforderlich sind, lädt das Segment (falls noch nicht geschehen), fügt die Startadresse des Segments zum Offset hinzu und Ersetzen Sie dann die Falle durch einen Sprung zu dieser neu berechneten Adresse.

Auf älteren PC-Software war es üblich, Code mit Trap-Befehlen anstelle von Coprozessor-Mathematikbefehlen zu erstellen, obwohl dies vom "Betriebssystem" technisch nicht durchgeführt wurde. Wenn kein mathematischer Coprozessor installiert wäre, würde der Trap-Handler diesen emulieren. Wenn ein Coprozessor installiert wurde, ersetzt der Handler beim ersten Aufrufen eines Traps den Trap-Befehl durch einen Coprozessor-Befehl. Zukünftige Ausführungen desselben Codes verwenden den Coprozessor-Befehl direkt.


Die FP-Methode wird immer noch auf ARM-Prozessoren verwendet, die im Gegensatz zu x86-CPUs immer noch FP-freie Varianten haben. Dies ist jedoch selten, da die meisten ARM-Anwendungen auf dedizierten Geräten ausgeführt werden. In solchen Umgebungen ist normalerweise bekannt, ob die CPU über FP-Funktionen verfügt.
MSalters

In keinem dieser Fälle hat das Betriebssystem Code in die Anwendung eingefügt. Damit das Betriebssystem Code einspeisen kann, benötigt es eine Lizenz des Softwareherstellers, um die Anwendung zu "modifizieren", die es nicht erhält. Betriebssysteme fügen KEINEN Code ein.
Dave Gordon

@ DaveGordon Eingefangene Anweisungen können vernünftigerweise als das Betriebssystem bezeichnet werden, das Code in die Anwendung einfügt.
Gilles 'SO- hör auf böse zu sein'

@MSalters Aufgefangene Anweisungen treten häufig in virtuellen Maschinen auf.
Gilles 'SO- hör auf böse zu sein'
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.