Werden Datentypdeklaratoren wie "int" und "char" im RAM gespeichert, wenn ein C-Programm ausgeführt wird?

74

Wenn ein C-Programm ausgeführt wird, werden die Daten auf dem Heap oder dem Stack gespeichert. Die Werte werden in RAM-Adressen gespeichert. Aber was ist mit den Typindikatoren (z. B. intoder char)? Werden sie auch gespeichert?

Betrachten Sie den folgenden Code:

char a = 'A';
int x = 4;

Ich habe gelesen, dass A und 4 hier in RAM-Adressen gespeichert sind. Aber was ist mit aund x? Am verwirrendsten ist, woher die Ausführung weiß, dass aes sich um ein Zeichen und xein Int handelt. Ich meine, ist das intund charirgendwo im RAM erwähnt?

Angenommen, ein Wert wird irgendwo im RAM als 10011001 gespeichert. Wenn ich das Programm bin, das den Code ausführt, woher weiß ich dann, ob es sich bei dieser 10011001 um eine charoder eine handelt int?

Was ich nicht verstehe, ist, wie der Computer weiß, wenn er den Wert einer Variablen von einer Adresse wie 10001 liest, ob es sich um eine intoder handelt char. Stellen Sie sich vor, ich klicke auf ein Programm namens anyprog.exe. Der Code wird sofort ausgeführt. Enthält diese ausführbare Datei Informationen darüber, ob die gespeicherten Variablen vom Typ sind intoder char?

user16307
quelle
24
Diese Informationen gehen zur Laufzeit vollständig verloren. Sie (und Ihr Compiler) müssen vorher sicherstellen, dass der Speicher korrekt interpretiert wird. Ist das die Antwort, nach der Sie gesucht haben?
5gon12eder
4
Das tut es nicht. Da davon ausgegangen wird, dass Sie wissen, was Sie tun, nimmt es alles, was es an der von Ihnen angegebenen Speicheradresse findet, und schreibt es an stdout. Wenn das, was geschrieben wurde, einem lesbaren Zeichen entspricht, wird es irgendwann auf der Konsole eines anderen als lesbares Zeichen angezeigt. Wenn dies nicht der Fall ist, wird es als Kauderwelsch oder möglicherweise als zufällig lesbares Zeichen angezeigt.
Robert Harvey
22
@ user16307 Die kurze Antwort lautet, dass der Compiler in statisch typisierten Sprachen beim Ausdruck eines Zeichens einen anderen Code erzeugt als beim Ausdruck eines Int. Zur Laufzeit gibt es kein Wissen mehr, xdas ein Zeichen ist, aber es ist der Zeichen-Druckcode, der ausgeführt wird, weil der Compiler dies ausgewählt hat.
Ixrec
13
@ user16307 Es wird immer als Binärdarstellung der Zahl 65 gespeichert. Ob es als 65 oder als A ausgegeben wird, hängt vom Code ab , den Ihr Compiler zum Ausdrucken erstellt hat. Neben den 65 gibt es keine Metadaten, die besagen, dass es sich tatsächlich um ein Zeichen oder ein Int handelt (zumindest nicht in statisch typisierten Sprachen wie C).
Ixrec
2
Die voll verstehen die Konzepte Sie hier stellen und implementieren sie selbst, möchten Sie vielleicht einen Compiler Kurs nehmen, zum Beispiel Coursera ist ein
mucaho

Antworten:

122

Um die Frage zu beantworten, die Sie in mehreren Kommentaren gepostet haben (die Sie meines Erachtens in Ihrem Beitrag bearbeiten sollten):

Was ich nicht verstehe, ist, wie der Computer wissen kann, wenn er den Wert einer Variablen ausliest und Adressen wie 10001, wenn es sich um ein int oder ein char handelt. Stellen Sie sich vor, ich klicke auf ein Programm namens anyprog.exe. Der Code wird sofort ausgeführt. Enthält diese exe-Datei Informationen darüber, ob die Variablen als in oder char gespeichert sind?

Fügen wir also Code hinzu. Angenommen, Sie schreiben:

int x = 4;

Und nehmen wir an, dass es im RAM gespeichert wird:

0x00010004: 0x00000004

Der erste Teil ist die Adresse, der zweite Teil ist der Wert. Wenn Ihr Programm (das als Maschinencode ausgeführt wird) ausgeführt wird, 0x00010004wird nur der Wert angezeigt 0x000000004. Der Typ dieser Daten ist nicht bekannt, und es ist nicht bekannt, wie sie verwendet werden sollen.

Wie findet Ihr Programm das Richtige heraus? Betrachten Sie diesen Code:

int x = 4;
x = x + 5;

Wir haben hier ein Lesen und ein Schreiben. Wenn Ihr Programm xaus dem Speicher liest , wird es 0x00000004dort gefunden. Und Ihr Programm kann es ergänzen 0x00000005. Und der Grund, warum Ihr Programm "weiß", dass dies eine gültige Operation ist, liegt darin, dass der Compiler durch Typensicherheit sicherstellt, dass die Operation gültig ist. Ihr Compiler hat bereits überprüft, ob Sie 4und 5zusammen hinzufügen können . Wenn Ihr Binärcode (die Exe) ausgeführt wird, muss er diese Überprüfung nicht durchführen. Es führt jeden Schritt einfach blind aus, vorausgesetzt, alles ist in Ordnung (schlechte Dinge passieren, wenn sie tatsächlich sind, nicht in Ordnung).

So kann man es sich auch vorstellen. Ich gebe Ihnen diese Informationen:

0x00000004: 0x12345678

Gleiches Format wie zuvor - Adresse links, Wert rechts. Welcher Typ ist der Wert? Zu diesem Zeitpunkt kennen Sie genau so viele Informationen zu diesem Wert wie Ihr Computer, wenn er Code ausführt. Wenn Sie 12743 zu diesem Wert hinzufügen sollten, könnten Sie es tun. Sie haben keine Ahnung, welche Auswirkungen diese Operation auf das gesamte System haben wird, aber das Hinzufügen von zwei Zahlen ist etwas, in dem Sie wirklich gut sind, also können Sie es tun. Macht das den Wert an int? Nicht unbedingt - Sie sehen nur zwei 32-Bit-Werte und den Additionsoperator.

Vielleicht liegt ein Teil der Verwirrung dann darin, die Daten wieder herauszuholen. Wenn wir haben:

char A = 'a';

Woher weiß der Computer, dass er ain der Konsole angezeigt wird? Nun, dazu gibt es viele Schritte. Das erste ist, zu As Speicherort im Speicher zu gehen und ihn zu lesen:

0x00000004: 0x00000061

Der ahexadezimale Wert für in ASCII ist 0x61, so dass der obige Wert möglicherweise im Speicher angezeigt wird. Unser Maschinencode kennt jetzt also den ganzzahligen Wert. Woher weiß es, dass der ganzzahlige Wert in ein Zeichen umgewandelt werden muss, um ihn anzuzeigen? Einfach ausgedrückt, der Compiler hat dafür gesorgt, dass alle erforderlichen Schritte für diesen Übergang ausgeführt wurden. Ihr Computer selbst (oder das Programm / die Exe-Datei) hat jedoch keine Ahnung, um welche Art von Daten es sich handelt. Das 32-Bit - Wert könnte alles sein - int, die charHälfte ein double, einen Zeiger, einen Teil einer Anordnung, die Teil eines string, einen Teil eines Befehls usw.


Hier ist eine kurze Interaktion, die Ihr Programm (exe) möglicherweise mit dem Computer / Betriebssystem hat.

Programm: Ich möchte anfangen. Ich brauche 20 MB Speicher.

Betriebssystem: Findet 20 MB freien Speicher, der nicht verwendet wird, und übergibt sie

(Die wichtige Anmerkung ist , dass diese zurückkehren konnten alle 20 kostenlosen MB Speicher, sie haben nicht einmal zusammenhängend sein müssen. Zu diesem Zeitpunkt kann das Programm nun im Speicher arbeiten hat , ohne auf die OS im Gespräch)

Programm: Ich gehe davon aus, dass der erste Punkt im Speicher eine 32-Bit-Ganzzahlvariable ist x.

(Der Compiler stellt sicher, dass Zugriffe auf andere Variablen diese Stelle im Speicher niemals berühren. Es gibt nichts auf dem System, was besagt, dass das erste Byte eine Variable ist x, oder dass diese Variable xeine Ganzzahl ist. Eine Analogie: Sie haben eine Tasche. Sie sagen dies den Leuten Sie werden nur gelbe Kugeln in diese Tasche legen. Wenn jemand später etwas aus der Tasche zieht, dann wäre es schockierend, wenn er etwas Blaues oder einen Würfel herausziehen würde - etwas ist schrecklich schief gelaufen. Gleiches gilt für Computer: Ihre Das Programm geht jetzt davon aus, dass der erste Speicherplatz die Variable x und eine Ganzzahl ist. Wenn jemals etwas anderes über dieses Byte des Speichers geschrieben wurde oder angenommen wird, dass es sich um etwas anderes handelt, ist etwas Schreckliches passiert nicht passieren)

Programm: Ich werde jetzt 2auf die ersten vier Bytes schreiben , bei denen ich davon ausgehe, dass xes sich um handelt.

Programm: Ich möchte 5 hinzufügen x.

  • Liest den Wert von X in ein temporäres Register

  • Fügt dem temporären Register 5 hinzu

  • Speichert den Wert des temporären Registers zurück in das erste Byte, das immer noch angenommen wird x.

Programm: Ich gehe davon aus, dass das nächste verfügbare Byte die Variable char ist y.

Programm: Ich werde ain Variable schreiben y.

  • Eine Bibliothek wird verwendet, um den Bytewert für zu finden a

  • Das Byte wird an die Adresse geschrieben, von der das Programm ausgeht y.

Programm: Ich möchte den Inhalt von anzeigen y

  • Liest den Wert im zweiten Speicherpunkt

  • Verwendet eine Bibliothek, um aus dem Byte ein Zeichen zu konvertieren

  • Verwendet Grafikbibliotheken zum Ändern des Konsolenbildschirms (Einstellen der Pixel von Schwarz auf Weiß, Scrollen um eine Zeile usw.)

(Und es geht weiter von hier)

Woran werden Sie wahrscheinlich hängen bleiben x? Was passiert, wenn der erste Punkt in der Erinnerung nicht mehr vorhanden ist ? oder ist die zweite nicht mehr y? Was passiert, wenn jemand xals charoder yals Zeiger liest ? Kurz gesagt, schlimme Dinge passieren. Einige dieser Dinge haben ein genau definiertes Verhalten, andere undefiniertes. Undefiniertes Verhalten ist genau das - alles kann passieren, von nichts bis zum Absturz des Programms oder des Betriebssystems. Sogar genau definiertes Verhalten kann böswillig sein. Wenn ich xzu einem Zeiger auf mein Programm wechseln und Ihr Programm dazu bringen kann, ihn als Zeiger zu verwenden, kann ich Ihr Programm dazu bringen, mein Programm auszuführen - genau das tun Hacker. Der Compiler ist da, um sicherzustellen, dass wir nicht int xalsstringund solche Dinge. Der Maschinencode selbst kennt keine Typen und tut nur das, was in den Anweisungen angegeben ist. Es gibt auch eine große Menge an Informationen, die zur Laufzeit entdeckt werden: Welche Bytes an Speicher darf das Programm verwenden? Beginnt xam ersten Byte oder am 12.?

Aber Sie können sich vorstellen, wie schrecklich es wäre, Programme wie dieses zu schreiben (und das können Sie auch in der Assemblersprache). Sie beginnen mit der 'Deklaration' Ihrer Variablen - Sie sagen sich, dass Byte 1 xByte 2 ist y, und wenn Sie jede Codezeile schreiben, Register laden und speichern, müssen Sie sich (als Mensch) merken, welches ist xund welches Eines ist y, weil das System keine Ahnung hat. Und Sie (als Mensch) müssen sich merken, welche Typen xund welche ysind, denn auch hier hat das System keine Ahnung.

Shaz
quelle
Erstaunliche Erklärung. Nur der Teil, den Sie geschrieben haben "Woher weiß es, dass der ganzzahlige Wert in ein Zeichen umgewandelt werden muss, damit er angezeigt wird? Einfach ausgedrückt, der Compiler hat alle erforderlichen Schritte für diesen Übergang ausgeführt." ist immer noch neblig für mich. Nehmen wir an, die CPU hat 0x00000061 aus dem RAM-Register abgerufen. An diesem Punkt, sagen Sie, gibt es andere Anweisungen (in der exe-Datei), die den Übergang zu dem machen, was wir auf dem Bildschirm sehen?
user16307
2
@ user16307 ja, es gibt zusätzliche anweisungen. Jede Codezeile, die Sie schreiben, kann möglicherweise in viele Anweisungen umgewandelt werden. Es gibt Anweisungen, um herauszufinden, welches Zeichen verwendet werden soll, es gibt Anweisungen, welche Pixel geändert werden müssen und in welche Farbe sie geändert werden sollen usw. Es gibt auch Code, den Sie nicht wirklich sehen. Wenn Sie beispielsweise std :: cout verwenden, bedeutet dies, dass Sie eine Bibliothek verwenden. Ihr Code zum Schreiben in die Konsole besteht möglicherweise nur aus einer Zeile. Die aufgerufenen Funktionen bestehen jedoch aus mehreren Zeilen, und jede Zeile kann in mehrere Maschinenanweisungen umgewandelt werden.
Shaz
8
@ user16307 Otherwise how can console or text file outputs a character instead of int Da es eine andere Reihenfolge von Anweisungen für die Ausgabe des Inhalts eines Speicherorts als Ganzzahl oder als alphanumerische Zeichen gibt. Der Compiler kennt die Variablentypen, wählt zur Kompilierungszeit die entsprechende Anweisungsfolge aus und zeichnet sie in der EXE auf.
Charles E. Grant
2
Ich würde einen anderen Ausdruck für "Der Bytecode selbst" finden, da sich der Bytecode (oder Bytecode) normalerweise auf eine Zwischensprache (wie Java Bytecode oder MSIL) bezieht, die diese Daten möglicherweise tatsächlich speichert, damit sie zur Laufzeit genutzt werden können. Außerdem ist nicht ganz klar, worauf sich "Bytecode" in diesem Zusammenhang beziehen soll. Ansonsten nette Antwort.
jpmc26
6
@ user16307 Machen Sie sich keine Sorgen um C ++ und C #. Was diese Leute sagen, liegt weit über Ihrem derzeitigen Verständnis der Funktionsweise von Computern und Compilern. Für die Zwecke, die Sie zu verstehen versuchen, weiß die Hardware nichts über Typen, Zeichen oder Int oder was auch immer. Als Sie dem Compiler mitgeteilt haben, dass eine Variable ein Int ist, hat er ausführbaren Code generiert, um einen Speicherort so zu behandeln, als ob es ein Int wäre. Der Speicherort selbst enthält keine Informationen über Typen. Es ist nur so, dass Ihr Programm beschlossen hat, es als int zu behandeln. Vergessen Sie alles, was Sie über Informationen zum Laufzeit-Typ gehört haben.
Andres F.
43

Ich denke, Ihre Hauptfrage scheint zu lauten: "Wenn der Typ zur Kompilierungszeit gelöscht und zur Laufzeit nicht beibehalten wird, wie kann der Computer dann feststellen, ob er Code ausführen soll, der ihn als einen interpretiert, intoder ob er Code ausführen soll, der ihn als einen interpretiert char? "

Und die Antwort ist ... der Computer tut es nicht. Allerdings ist der Compiler nicht wissen, und es wird einfach den richtigen Code in den binären in erster Linie gesetzt hat. Wenn die Variable wie folgt eingegeben würde char, würde der Compiler den Code für die Behandlung als a nicht intin das Programm einfügen, sondern den Code für die Behandlung als a char.

Es gibt Gründe, den Typ zur Laufzeit beizubehalten:

  • Dynamische Typisierung: Bei der dynamischen Typisierung erfolgt die Typprüfung zur Laufzeit, daher muss der Typ natürlich zur Laufzeit bekannt sein. C ist jedoch nicht dynamisch typisiert, sodass die Typen sicher gelöscht werden können. (Beachten Sie jedoch, dass dies ein ganz anderes Szenario ist. Dynamische Typen und statische Typen sind nicht dasselbe. In einer Sprache mit gemischten Schreibweisen können Sie die statischen Typen dennoch löschen und nur die dynamischen Typen beibehalten.)
  • Dynamischer Polymorphismus: Wenn Sie unterschiedlichen Code basierend auf dem Laufzeittyp ausführen, müssen Sie den Laufzeittyp beibehalten. C hat keinen dynamischen Polymorphismus (es hat eigentlich überhaupt keinen Polymorphismus, außer in einigen speziellen hartcodierten Fällen, z. B. dem +Operator), weshalb der Laufzeittyp nicht benötigt wird. Der Laufzeit-Typ unterscheidet sich jedoch ohnehin von dem statischen Typ. In Java können Sie beispielsweise die statischen Typen theoretisch löschen und den Laufzeit-Typ für den Polymorphismus beibehalten. Beachten Sie auch, dass Sie, wenn Sie den Typ-Lookup-Code dezentralisieren und spezialisieren und in das Objekt (oder die Klasse) einfügen, den Laufzeit-Typ nicht unbedingt benötigen, z. B. C ++ vtables.
  • Laufzeitreflexion: Wenn Sie dem Programm erlauben, seine Typen zur Laufzeit zu reflektieren, müssen Sie die Typen natürlich zur Laufzeit beibehalten. Mit Java, das zur Laufzeit Typen erster Ordnung beibehält, aber zur Kompilierungszeit Typargumente in generische Typen löscht, können Sie dies leicht erkennen, sodass Sie nur den Typkonstruktor ("roher Typ"), nicht aber das Typargument berücksichtigen können. Auch in C gibt es keine Laufzeitreflexion, sodass der Typ zur Laufzeit nicht beibehalten werden muss.

Der einzige Grund, den Typ zur Laufzeit in C beizubehalten, ist das Debuggen. Das Debuggen wird jedoch normalerweise mit der verfügbaren Quelle durchgeführt. Anschließend können Sie den Typ einfach in der Quelldatei nachschlagen.

Typ Löschen ist ganz normal. Dies wirkt sich nicht auf die Typensicherheit aus: Die Typen werden beim Kompilieren überprüft. Wenn der Compiler überzeugt ist, dass das Programm typensicher ist, werden die Typen (aus diesem Grund) nicht mehr benötigt. Dies hat keinen Einfluss auf den statischen Polymorphismus (auch als Überladung bezeichnet): Sobald die Überladungsauflösung abgeschlossen ist und der Compiler die richtige Überladung ausgewählt hat, werden die Typen nicht mehr benötigt. Typen können auch als Leitfaden für die Optimierung dienen. Sobald der Optimierer die Optimierungen basierend auf den Typen ausgewählt hat, werden sie nicht mehr benötigt.

Das Beibehalten von Typen zur Laufzeit ist nur erforderlich, wenn Sie etwas mit den Typen zur Laufzeit tun möchten.

Haskell ist eine der strengsten, strengsten und typsichersten statisch typisierten Sprachen, und Haskell-Compiler löschen normalerweise alle Typen. (Die Ausnahme ist die Übergabe von Methodenwörterbüchern für Typklassen, glaube ich.)

Jörg W. Mittag
quelle
3
Nein! Warum? Wofür würden diese Informationen benötigt? Der Compiler gibt den Code zum Einlesen von a charin die kompilierte Binärdatei aus. Es gibt keinen Code für ein aus int, es gibt keinen Code für ein aus byte, es gibt keinen Code für einen Zeiger aus, es gibt einfach nur den Code für ein aus char. Es werden keine Laufzeitentscheidungen basierend auf dem Typ getroffen. Sie brauchen den Typ nicht. Es ist völlig irrelevant. Alle relevanten Entscheidungen wurden bereits zum Zeitpunkt der Kompilierung getroffen.
Jörg W Mittag
2
Gibt es nicht Der Compiler fügt einfach Code zum Drucken eines Zeichens in die Binärdatei ein. Zeitraum. Der Compiler weiß, dass an dieser Speicheradresse ein Zeichen vorhanden ist, daher wird der Code zum Drucken eines Zeichens in der Binärdatei abgelegt. Wenn der Wert an dieser Speicheradresse aus irgendeinem seltsamen Grund kein Zeichen ist, dann bricht die Hölle los. So funktioniert im Grunde eine ganze Klasse von Sicherheits-Exploits.
Jörg W Mittag
2
Denken Sie darüber nach: Wenn die CPU irgendwie über die Datentypen von Programmen Bescheid wüsste, müsste jeder auf dem Planeten jedes Mal eine neue CPU kaufen, wenn jemand einen neuen Typ erfindet. public class JoergsAwesomeNewType {};Sehen? Ich habe gerade einen neuen Typ erfunden! Sie müssen eine neue CPU kaufen!
Jörg W Mittag
9
Nein, tut es nicht. Der Compiler weiß, welchen Code er in die Binärdatei schreiben muss. Es macht keinen Sinn, diese Informationen aufzubewahren. Wenn Sie ein int drucken, gibt der Compiler den Code zum Drucken eines int ein. Wenn Sie ein Zeichen drucken, gibt der Compiler den Code zum Drucken eines Zeichens ein. Zeitraum. Aber es ist nur ein kleines Muster. Der Code zum Drucken eines Zeichens interpretiert das Bitmuster auf eine bestimmte Weise, der Code zum Drucken eines Int interpretiert das Bit auf eine andere Weise, aber es gibt keine Möglichkeit, ein Bitmuster, das ein Int ist, von einem Bitmuster, das ein Int ist, zu unterscheiden ist ein Zeichen, es ist eine Folge von Bits.
Jörg W Mittag
2
@ user16307: "Enthält die exe-Datei keine Informationen darüber, welche Adresse welche Art von Daten enthält?" Vielleicht. Wenn Sie mit Debug-Daten kompilieren, enthalten die Debug-Daten Informationen zu Variablennamen, Adressen und Typen. Und manchmal werden diese Debug-Daten in der EXE-Datei gespeichert (als binärer Stream). Es ist jedoch nicht Teil des ausführbaren Codes und wird nicht von der Anwendung selbst verwendet, sondern nur von einem Debugger.
Ben Voigt
12

Der Computer "weiß" nicht, welche Adressen was sind, sondern was in die Anweisungen Ihres Programms eingearbeitet ist.

Wenn Sie ein C-Programm schreiben, das eine char-Variable schreibt und liest, erstellt der Compiler Assembly-Code, der diese Daten als char schreibt, und es gibt einen anderen Code, der eine Speicheradresse liest und als char interpretiert. Das einzige, was diese beiden Operationen zusammenhält, ist der Ort dieser Speicheradresse.

Wenn es Zeit zum Lesen gibt, heißt es in den Anweisungen nicht "Sehen, welcher Datentyp vorhanden ist", sondern "Laden Sie diesen Speicher als Float". Wenn die Adresse, von der gelesen werden soll, geändert wurde oder etwas diesen Speicher mit etwas anderem als einem Float überschrieben hat, lädt die CPU diesen Speicher ohnehin glücklich als Float, und alle möglichen seltsamen Dinge können als Ergebnis auftreten.

Schlechte Analogiezeit: Stellen Sie sich ein kompliziertes Versandlager vor, in dem das Lager aus Speicher und die Kommissionierung aus der CPU besteht. Ein Teil des Lagerprogramms legt verschiedene Artikel in das Regal. Ein anderes Programm greift nach Artikeln aus dem Lager und legt sie in Kisten. Wenn sie abgezogen werden, werden sie nicht überprüft, sie gehen einfach in den Behälter. Das gesamte Lager funktioniert so, dass alles synchron läuft und die richtigen Artikel immer zur richtigen Zeit am richtigen Ort sind. Ansonsten stürzt alles ab, genau wie in einem tatsächlichen Programm.

Whatsisname
quelle
Wie würden Sie erklären, wenn die CPU 0x00000061 in einem Register findet und es abruft? und stellen Sie sich vor, das Konsolenprogramm soll dies als Zeichen ausgeben, nicht als int. Meinen Sie, dass es in dieser exe-Datei einige Anweisungscodes gibt, die wissen, dass die Adresse von 0x00000061 ein Zeichen ist und mithilfe der ASCII-Tabelle in ein Zeichen konvertiert wird?
user16307
7
Beachten Sie, dass "alles stürzt ab" eigentlich das Best-Case-Szenario ist. "Seltsame Dinge passieren" ist das zweitbeste Szenario, "subtil seltsame Dinge passieren" ist noch schlimmer, und der schlimmste Fall ist "Dinge passieren hinter deinem Rücken, die jemand absichtlich manipuliert hat, um so zu passieren, wie er es will". aka ein Sicherheits-Exploit.
Jörg W Mittag
@ user16307: Der Code im Programm weist den Computer an, diese Adresse abzurufen und sie dann entsprechend der verwendeten Codierung anzuzeigen. Unabhängig davon, ob es sich bei den Daten am Speicherort um ein ASCII-Zeichen oder einen vollständigen Müll handelt, ist der Computer nicht besorgt. Etwas anderes war dafür verantwortlich, diese Speicheradresse so einzurichten, dass sie die erwarteten Werte enthält. Ich denke, es könnte für Sie von Vorteil sein, eine Assembler-Programmierung auszuprobieren.
Whatsisname
1
@ JörgWMittag: ja. Ich dachte darüber nach, einen Pufferüberlauf als Beispiel zu nennen, entschied aber, dass dies die Dinge nur verwirrender machen würde.
Whatsisname
@ user16307: Das Ding, das Daten auf dem Bildschirm anzeigt, ist ein Programm. Bei herkömmlichen Unixen handelt es sich um ein Terminal (eine Software, die das serielle DEC VT100-Terminal emuliert - ein Hardwaregerät mit einem Monitor und einer Tastatur, die alles, was in das Modem eingeht, auf dem Monitor anzeigt und alles, was über die Tastatur eingegeben wurde, an das Modem sendet). Unter DOS ist es DOS (eigentlich der Textmodus Ihrer VGA-Karte, aber das können wir ignorieren) und unter Windows ist es command.com. Ihr Programm weiß nicht, dass es tatsächlich Zeichenfolgen druckt, sondern druckt nur eine Folge von Bytes (Zahlen) aus.
Slebetman
8

Das tut es nicht. Sobald C zu Maschinencode kompiliert ist, sieht die Maschine nur eine Reihe von Bits. Wie diese Bits interpretiert werden, hängt davon ab, welche Vorgänge an ihnen ausgeführt werden, und nicht von einigen zusätzlichen Metadaten.

Die Typen, die Sie in Ihren Quellcode eingeben, sind nur für den Compiler bestimmt. Es nimmt den von Ihnen angegebenen Datentyp an und versucht nach besten Kräften sicherzustellen, dass diese Daten nur auf sinnvolle Weise verwendet werden. Sobald der Compiler die Logik Ihres Quellcodes so gut wie möglich überprüft hat, konvertiert er ihn in Maschinencode und verwirft die Typdaten, da der Maschinencode keine Möglichkeit hat, dies darzustellen (zumindest auf den meisten Maschinen). .

8bittree
quelle
Was ich nicht verstehe, ist, woher der Computer weiß, wann er den Wert einer Variablen liest und wie z. B. 10001, wenn es sich um ein int oder char handelt. Stellen Sie sich vor, ich klicke auf ein Programm namens anyprog.exe. Der Code wird sofort ausgeführt. Enthält diese exe-Datei Informationen darüber, ob die Variablen als in oder char gespeichert sind? -
user16307
@ user16307 Nein, es gibt keine zusätzlichen Informationen darüber, ob etwas ein Int oder ein Char ist. Ich werde später einige Beispiele hinzufügen, vorausgesetzt, niemand anderes schlägt mich.
8bittree,
1
@ user16307: Die exe-Datei enthält diese Informationen indirekt. Der Prozessor, der das Programm ausführt, interessiert sich nicht für die Typen, die beim Schreiben des Programms verwendet werden, aber ein Großteil davon kann aus den Anweisungen abgeleitet werden, die für den Zugriff auf die verschiedenen Speicherstellen verwendet werden.
Bart van Ingen Schenau
@ user16307 es gibt eigentlich ein wenig extra informationen. Die exe-Dateien wissen, dass eine Ganzzahl 4 Bytes beträgt. Wenn Sie also "int a" schreiben, reserviert der Compiler 4 Bytes für die a-Variable und kann so die Adresse von a und den anderen Variablen danach berechnen.
Esben Skov Pedersen
1
@ user16307 Es gibt keinen praktischen Unterschied (neben der Größe des Typs) zwischen int a = 65und, char b = 'A'sobald der Code kompiliert wurde.
6

Die meisten Prozessoren bieten unterschiedliche Anweisungen für die Arbeit mit Daten unterschiedlichen Typs, sodass Typinformationen normalerweise in den generierten Maschinencode "eingebrannt" werden. Es müssen keine zusätzlichen Typmetadaten gespeichert werden.

Einige konkrete Beispiele könnten helfen. Der folgende Maschinencode wurde mit gcc 4.1.2 auf einem x86_64-System unter SuSE Linux Enterprise Server (SLES) 10 generiert.

Nehmen Sie den folgenden Quellcode an:

int main( void )
{
  int x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

Hier ist das Fleisch des generierten Assembler-Codes, der der obigen Quelle entspricht (unter Verwendung von gcc -S), mit von mir hinzugefügten Kommentaren:

main:
.LFB2:
        pushq   %rbp               ;; save the current frame pointer value
.LCFI0:
        movq    %rsp, %rbp         ;; make the current stack pointer value the new frame pointer value
.LCFI1:                            
        movl    $1, -12(%rbp)      ;; x = 1
        movl    $2, -8(%rbp)       ;; y = 2
        movl    -8(%rbp), %eax     ;; copy the value of y to the eax register
        addl    -12(%rbp), %eax    ;; add the value of x to the eax register
        movl    %eax, -4(%rbp)     ;; copy the value in eax to z
        movl    $0, %eax           ;; eax gets the return value of the function
        leave                      ;; exit and restore the stack
        ret

Es folgen einige zusätzliche Dinge, die retjedoch für die Diskussion nicht relevant sind.

%eaxist ein 32-Bit-Allzweck-Datenregister. %rspist ein 64-Bit-Register, das zum Speichern des Stapelzeigers reserviert ist und die Adresse des zuletzt auf den Stapel geschobenen Objekts enthält. %rbpist ein 64-Bit-Register, das zum Speichern des Rahmenzeigers reserviert ist und die Adresse des aktuellen Stapelrahmens enthält . Beim Eingeben einer Funktion wird auf dem Stapel ein Stapelrahmen erstellt, der Platz für die Argumente und lokalen Variablen der Funktion reserviert. Auf Argumente und Variablen wird mit Offsets vom Frame-Zeiger zugegriffen. In diesem Fall ist der Speicher für die Variable x12 Bytes "unterhalb" der in gespeicherten Adresse %rbp.

Im obigen Code kopieren wir den ganzzahligen Wert von x(1, gespeichert bei -12(%rbp)) in das Register unter %eaxVerwendung des movlBefehls, der zum Kopieren von 32-Bit-Wörtern von einer Stelle zu einer anderen verwendet wird. Wir rufen dann auf addl, wodurch der ganzzahlige Wert von y(gespeichert bei -8(%rbp)) zu dem Wert addiert wird , der bereits in enthalten ist %eax. Wir speichern dann das Ergebnis auf -4(%rbp), das heißt z.

Jetzt wollen wir das ändern, damit wir es mit doubleWerten anstatt mit Werten zu tun inthaben:

int main( void )
{
  double x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

Das gcc -Serneute Laufen gibt uns:

main:
.LFB2:
        pushq   %rbp                              
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movabsq $4607182418800017408, %rax ;; copy literal 64-bit floating-point representation of 1.00 to rax
        movq    %rax, -24(%rbp)            ;; save rax to x
        movabsq $4611686018427387904, %rax ;; copy literal 64-bit floating-point representation of 2.00 to rax
        movq    %rax, -16(%rbp)            ;; save rax to y
        movsd   -24(%rbp), %xmm0           ;; copy value of x to xmm0 register
        addsd   -16(%rbp), %xmm0           ;; add value of y to xmm0 register
        movsd   %xmm0, -8(%rbp)            ;; save result to z
        movl    $0, %eax                   ;; eax gets return value of function
        leave                              ;; exit and restore the stack
        ret

Mehrere Unterschiede. Anstelle von movlund verwenden addlwir movsdund addsd(Zuweisen und Hinzufügen von Gleitkommazahlen mit doppelter Genauigkeit). Anstatt Zwischenwerte in zu speichern %eax, verwenden wir %xmm0.

Dies ist, was ich meine, wenn ich sage, dass der Typ in den Maschinencode "eingebrannt" ist. Der Compiler generiert einfach den richtigen Maschinencode für diesen bestimmten Typ.

John Bode
quelle
4

Historisch betrachtet C das Gedächtnis als aus einer Anzahl von Gruppen von nummerierten Slots des Typs bestehendunsigned char(auch "Byte" genannt, obwohl es nicht immer 8 Bits sein muss). Jeder Code, der irgendetwas verwendet, was im Speicher gespeichert ist, müsste wissen, in welchem ​​Steckplatz oder in welchen Steckplätzen die Informationen gespeichert sind und was mit den Informationen dort zu tun ist [z. B. "interpretiere die vier Bytes ab Adresse 123: 456 als 32-Bit Gleitkommawert "oder" die unteren 16 Bits der zuletzt berechneten Größe ab Adresse 345: 678 in zwei Bytes speichern]. Der Speicher selbst würde weder wissen noch interessieren, was die in den Speicherplätzen gespeicherten Werte "bedeuteten". Wenn Der Code hat versucht, den Speicher mit einem Typ zu schreiben und ihn als einen anderen zu lesen. Die beim Schreiben gespeicherten Bitmuster werden gemäß den Regeln des zweiten Typs interpretiert, mit welchen Konsequenzen auch immer.

Wenn der Code beispielsweise 0x12345678in einem 32-Bit-Format gespeichert werden unsigned intsoll und dann versucht wird, zwei aufeinanderfolgende 16-Bit- unsigned intWerte von seiner Adresse und den oben angegebenen zu lesen, liest unsigned intder Code möglicherweise die Werte , je nachdem, welche Hälfte von wo gespeichert wurde 0x1234 und 0x5678 oder 0x5678 und 0x1234.

Der C99-Standard erfordert jedoch nicht mehr, dass sich der Speicher wie eine Reihe nummerierter Slots verhält, die nichts darüber wissen, was ihre Bitmuster darstellen . Ein Compiler darf sich so verhalten, als ob Speichersteckplätze die darin gespeicherten Datentypen kennen und nur Daten unsigned charlesen dürfen, die mit einem anderen Typ als dem Typ unsigned charoder dem gleichen Typ wie sie geschrieben wurden mit; Compiler dürfen sich weiterhin so verhalten, als ob Speicher-Slots die Fähigkeit und Neigung hätten, das Verhalten eines Programms willkürlich zu verfälschen, das versucht, auf Speicher in einer Weise zuzugreifen, die diesen Regeln widerspricht.

Gegeben:

unsigned int a = 0x12345678;
unsigned short p = (unsigned short *)&a;
printf("0x%04X",*p);

Einige Implementierungen geben möglicherweise 0x1234 und andere 0x5678 aus. Unter dem C99-Standard ist es jedoch zulässig, dass eine Implementierung "FRINK RULES!" oder irgendetwas anderes tun, unter der Annahme, dass es zulässig wäre, dass die Speicherorte aHardware enthalten, die aufzeichnet, welcher Typ zum Schreiben verwendet wurde, und dass diese Hardware auf einen ungültigen Leseversuch in irgendeiner Weise reagiert, einschließlich durch Verursachen "FRINK REGELN!" ausgegeben werden.

Beachten Sie, dass es keine Rolle spielt, ob eine solche Hardware tatsächlich vorhanden ist. Die Tatsache, dass diese Hardware legal vorhanden sein könnte, macht es für Compiler legal, Code zu generieren, der sich so verhält, als würde er auf einem solchen System ausgeführt. Wenn der Compiler bestimmen kann, dass ein bestimmter Speicherort als ein Typ geschrieben und als ein anderer gelesen wird, kann er vorgeben, dass er auf einem System ausgeführt wird, dessen Hardware eine solche Bestimmung vornehmen könnte, und kann mit jedem Grad an Willkür reagieren, den der Compilerautor für angebracht hält .

Der Zweck dieser Regel bestand darin, Compilern, die wussten, dass eine Gruppe von Bytes, die einen Wert eines bestimmten Typs enthielten, zu einem bestimmten Zeitpunkt einen bestimmten Wert enthielt und dass seitdem kein Wert desselben Typs geschrieben wurde, den Rückschluss auf diese Gruppe zu ziehen von Bytes würde immer noch diesen Wert halten. Ein Prozessor hatte beispielsweise eine Gruppe von Bytes in ein Register eingelesen und wollte später die gleichen Informationen wieder verwenden, während sie sich noch im Register befanden. Der Compiler konnte den Inhalt des Registers verwenden, ohne den Wert aus dem Speicher erneut lesen zu müssen. Eine sinnvolle Optimierung. In den ersten zehn Jahren der Regel würde ein Verstoß gegen diese Regel im Allgemeinen bedeuten, dass sich das Schreiben auf den gelesenen Wert auswirken kann, wenn eine Variable mit einem anderen Typ als dem Typ geschrieben wird, der zum Lesen verwendet wird. Ein solches Verhalten kann in einigen Fällen katastrophal sein, in anderen Fällen jedoch harmlos.

Um 2009 haben die Autoren einiger Compiler wie CLANG jedoch festgestellt, dass Compiler in Fällen, in denen Speicher mit einem Typ geschrieben und als anderer gelesen wird, niemals Eingaben erhalten, die sie erhalten könnten, da der Standard Compilern erlaubt, alles zu tun, was sie möchten veranlassen, dass so etwas passiert. Da der Standard angibt, dass der Compiler bei solchen ungültigen Eingaben alles tun darf, was er möchte, sollte Code weggelassen werden, der nur dann Auswirkungen hat, wenn der Standard keine Anforderungen auferlegt (und nach Ansicht einiger Compilerautoren) als irrelevant. Dies ändert das Verhalten von Aliasing-Verstößen dahingehend, dass es wie ein Speicher ist, der bei einer Leseanforderung willkürlich den letzten Wert zurückgibt, der mit demselben Typ wie eine Leseanforderung geschrieben wurde, oder jeden neueren Wert, der mit einem anderen Typ geschrieben wurde.

Superkatze
quelle
1
Das Erwähnen undefinierten Verhaltens beim Beschneiden von Typen für jemanden, der nicht versteht, wie es keine RTTI gibt, erscheint kontraintuitiv
Cole Johnson,
@ColeJohnson: Es ist schade, dass 99% der Compiler vor 2009 keinen formalen Namen oder Standard für den C-Dialekt unterstützen, da sie sowohl aus unterrichtlicher als auch aus praktischer Sicht als grundlegend unterschiedliche Sprachen betrachtet werden sollten. Da sowohl der Dialekt, der über 35 Jahre eine Reihe vorhersehbarer und optimierbarer Verhaltensweisen hervorgebracht hat, denselben Namen erhält, ist es schwierig, Verwirrung zu vermeiden, wenn über Dinge gesprochen wird, die in ihnen anders funktionieren .
Superkatze
Historisch gesehen lief C auf den Lisp-Maschinen, die es nicht erlaubten, so locker mit Typen zu spielen. Ich bin mir ziemlich sicher, dass viele der "vorhersehbaren und optimierbaren Verhaltensweisen", die vor 30 Jahren beobachtet wurden, nur unter BSD Unix auf der VAX funktionierten.
Prosfilaes
@prosfilaes: Vielleicht wären "99% der Compiler, die von 1999 bis 2009 verwendet wurden" genauer? Selbst wenn Compiler Optionen für einige ziemlich aggressive Ganzzahloptimierungen hatten, waren sie genau das - Optionen. Ich weiß nicht, dass ich vor 1999 jemals einen Compiler gesehen habe, der keinen Modus hatte, der nicht garantiert, dass ein bestimmter int x,y,z;Ausdruck x*y > zniemals etwas anderes als 1 oder 0 zurückgibt, oder bei dem Aliasing-Verstöße Auswirkungen haben würden mit der Ausnahme, dass der Compiler willkürlich entweder einen alten oder einen neuen Wert zurückgibt.
Supercat
1
... woher die unsigned charWerte stammen, die zum Erstellen eines Typs "herkommen". Wenn ein Programm einen Zeiger in einen zerlegen soll unsigned char[], zeigen Sie seinen Hex-Inhalt kurz auf dem Bildschirm an, und löschen Sie dann den Zeiger unsigned char[], und akzeptieren Sie später einige Hex-Zahlen von der Tastatur, kopieren Sie sie zurück in einen Zeiger und geben Sie den Zeiger dereferenziert auf In dem Fall, in dem die eingegebene Nummer mit der angezeigten Nummer übereinstimmt, wäre das Verhalten eindeutig.
Superkatze
3

In C ist es nicht. Andere Sprachen (z. B. Lisp, Python) haben dynamische Typen, C ist jedoch statisch. Das bedeutet, dass Ihr Programm wissen muss, welcher Datentyp richtig interpretiert werden soll, und zwar als Zeichen, Ganzzahl usw.

Normalerweise erledigt der Compiler das für Sie, und wenn Sie etwas falsch machen, wird beim Kompilieren ein Fehler (oder eine Warnung) angezeigt.

Mike Harris
quelle
Was ich nicht verstehe, ist, woher der Computer weiß, wann er den Wert einer Variablen liest und wie z. B. 10001, wenn es sich um ein int oder char handelt. Stellen Sie sich vor, ich klicke auf ein Programm namens anyprog.exe. Der Code wird sofort ausgeführt. Enthält diese exe-Datei Informationen darüber, ob die Variablen als in oder char gespeichert sind? -
user16307
1
@ user16307 Im Grunde genommen geht all diese Information vollständig verloren. Es liegt am Maschinencode, gut genug zu entwerfen, um seine Arbeit auch ohne diese Informationen richtig zu machen. Der Computer kümmert sich nur darum, dass die Adresse aus acht Bits in einer Reihe besteht 10001. Es ist entweder Ihre Aufgabe oder die Aufgabe des Compilers , in Abhängigkeit vom Einzelfall mit solchen Aufgaben manuell Schritt zu halten, während Sie Maschinen- oder Assembler-Code schreiben.
Panzercrisis
1
Beachten Sie, dass die dynamische Eingabe nicht der einzige Grund ist, Typen beizubehalten. Java ist statisch typisiert, muss jedoch die Typen beibehalten, da es ermöglicht, den Typ dynamisch zu reflektieren. Außerdem verfügt es über einen Laufzeitpolymorphismus, dh einen Methodenversand basierend auf dem Laufzeittyp, für den es auch den Typ benötigt. C ++ fügt den Dispatch-Code der Methode in das Objekt (oder besser gesagt in die Klasse) selbst ein, sodass der Typ in keiner Weise benötigt wird (obwohl die vtable in gewisser Weise Teil des Typs ist, also zumindest ein Teil davon) Der Typ wird beibehalten. In Java ist der Code für den Methodenversand jedoch zentralisiert.
Jörg W Mittag
Schau dir meine Frage an, die ich geschrieben habe: "Wann wird ein C-Programm ausgeführt?" Werden sie nicht indirekt in der exe-Datei unter den Anweisungscodes gespeichert und befinden sich schließlich im Speicher? Ich schreibe das nochmal für dich: Wenn die CPU 0x00000061 an einem Register findet und es holt; und stellen Sie sich vor, das Konsolenprogramm soll dies als Zeichen ausgeben, nicht als int. Gibt es in dieser exe-Datei (Maschinen- / Binärcode) einige Anweisungscodes, die wissen, dass die Adresse von 0x00000061 ein Zeichen ist und mithilfe der ASCII-Tabelle in ein Zeichen konvertiert wird? Wenn ja, bedeutet dies, dass char int Bezeichner indirekt in der Binärdatei enthalten sind ???
user16307
Wenn der Wert 0x61 ist und als Zeichen deklariert ist (dh 'a') und Sie eine Routine aufrufen, um es anzuzeigen, wird [irgendwann] ein Systemaufruf stattfinden, um dieses Zeichen anzuzeigen. Wenn Sie es als int deklariert haben und die Anzeigeroutine aufrufen, muss der Compiler Code generieren, um 0x61 (Dezimalzahl 97) in die ASCII-Sequenz 0x39, 0x37 ('9', '7') umzuwandeln. Fazit: Der generierte Code ist unterschiedlich, da der Compiler weiß, wie er sie unterschiedlich behandelt.
Mike Harris
3

Man muss zwischen compiletimeund runtimeeinerseits und codeund dataandererseits unterscheiden.

Aus maschineller Sicht ist es kein Unterschied zwischen dem, was Sie anrufen codeoder instructionsund dem, was Sie anrufen data. Auf die Zahlen kommt es an. Aber einige Sequenzen - wie wir es nennen würden code- tun etwas, was wir für nützlich halten, andere nur crashdie Maschine.

Die Arbeit, die von der CPU ausgeführt wird, ist eine einfache 4-Stufen-Schleife:

  • Holen Sie sich "Daten" von einer bestimmten Adresse
  • Dekodiere den Befehl (dh "interpretiere" die Zahl als instruction)
  • Lesen Sie eine gültige Adresse
  • Ergebnisse ausführen und speichern

Dies wird Befehlszyklus genannt .

Ich habe gelesen, dass A und 4 hier in RAM-Adressen gespeichert sind. Aber was ist mit a und x?

aund xsind Variablen, die Platzhalter für die Adressen sind, in denen das Programm den "Inhalt" der Variablen finden konnte. Wenn also die Variable averwendet wird, gibt es effektiv die Adresse des verwendeten Inhalts a.

Am verwirrendsten ist, woher weiß die Ausführung, dass a ein Zeichen und x ein Int ist?

Die Ausführung weiß nichts. Nach dem, was in der Einleitung gesagt wurde, holt die CPU nur Daten und interpretiert diese Daten als Anweisungen.

Die printf-Funktion soll "wissen", welche Art von Eingabe Sie eingeben, dh der resultierende Code gibt die richtigen Anweisungen zum Umgang mit einem speziellen Speichersegment. Natürlich ist es möglich, eine Nonsense-Ausgabe zu generieren: Wenn Sie eine Adresse verwenden, in der keine Zeichenfolge zusammen mit "% s" in gespeichert ist, printf()wird die Nonsense-Ausgabe nur durch einen zufälligen Speicherort gestoppt, an dem eine 0 ( \0) steht.

Gleiches gilt für den Einstiegspunkt eines Programms. Unter dem C64 war es möglich, Ihre Programme in (fast) jeder bekannten Adresse abzulegen. Assembly-Programme wurden mit einer Anweisung gestartet, die sysgefolgt von einer Adresse aufgerufen wurde: sys 49152War ein üblicher Ort, um Ihren Assembler-Code abzulegen. Aber nichts hindert Sie daran, zB grafische Daten zu laden 49152, was zu einem Maschinenabsturz führt, nachdem Sie von diesem Punkt aus "gestartet" haben. In diesem Fall begann der Befehlszyklus mit dem Lesen von "grafischen Daten" und dem Versuch, sie als "Code" zu interpretieren (was natürlich keinen Sinn ergab). die auswirkungen waren erstaunlich;)

Angenommen, ein Wert wird irgendwo im RAM als 10011001 gespeichert. Wenn ich das Programm bin, das den Code ausführt, woher weiß ich dann, ob es sich bei dieser 10011001 um ein Zeichen oder eine Ganzzahl handelt?

Wie gesagt: Der "Kontext" - dh die vorherigen und nächsten Anweisungen - helfen dabei, die Daten so zu behandeln, wie wir es wollen. Aus Sicht der Maschine gibt es keinen Unterschied in Bezug auf den Speicherort. intund charist nur ein Wortschatz, der Sinn ergibt compiletime; Während runtime(auf Baugruppenebene) gibt es kein charoder int.

Was ich nicht verstehe, ist, wie der Computer weiß, wenn er den Wert einer Variablen von einer Adresse wie 10001 liest, ob es sich um ein int oder ein char handelt.

Der Computer weiß nichts. Der Programmierer tut es. Der kompilierte Code generiert den Kontext , der erforderlich ist, um aussagekräftige Ergebnisse für den Menschen zu generieren.

Enthält diese ausführbare Datei Informationen darüber, ob die gespeicherten Variablen vom Typ int oder char sind?

Ja und Nein . Die Information, ob es sich um eine intoder eine charhandelt, geht verloren. Andererseits bleibt der Kontext (die Anweisungen, die angeben, wie mit Speicherstellen umgegangen wird, an denen Daten gespeichert sind) erhalten. also implizit ja, die "information" ist implizit verfügbar.

Thomas Junk
quelle
Schöne Unterscheidung zwischen Kompilierzeit und Laufzeit.
Michael Blackburn
2

Lassen Sie uns diese Diskussion nur auf die C- Sprache beschränken.

Das Programm, auf das Sie sich beziehen, ist in einer höheren Sprache wie C geschrieben. Der Computer versteht nur die Maschinensprache. Höhere Programmiersprachen geben dem Programmierer die Möglichkeit, Logik auf menschlichere Weise auszudrücken, die dann in Maschinencode übersetzt wird, den der Mikroprozessor decodieren und ausführen kann. Lassen Sie uns nun den Code besprechen, den Sie erwähnt haben:

char a = 'A';
int x = 4;

Versuchen wir, jeden Teil zu analysieren:

char / int werden als Datentypen bezeichnet. Diese weisen den Compiler an, Speicher zuzuweisen. In diesem Fall sind chares 1 Byte und int2 Byte. (Bitte beachten Sie, dass diese Speichergröße wiederum vom Mikroprozessor abhängt.)

a / x werden als Bezeichner bezeichnet. Nun können Sie "benutzerfreundliche" Namen für Speicherorte im RAM sagen.

= weist den Compiler an, 'A' am Speicherort aund 4 am Speicherort zu speichern x.

Int / char-Datentypkennungen werden daher nur vom Compiler und nicht vom Mikroprozessor während der Programmausführung verwendet. Sie werden daher nicht gespeichert.

prasad
quelle
ok int / char-Datentyp-IDs werden nicht direkt im Speicher als Variablen gespeichert, sondern werden indirekt in der exe-Datei zwischen den Anweisungscodes gespeichert und finden schließlich im Speicher statt? Ich schreibe das nochmal für dich: Wenn die CPU 0x00000061 an einem Register findet und es holt; und stellen Sie sich vor, das Konsolenprogramm soll dies als Zeichen ausgeben, nicht als int. Gibt es in dieser exe-Datei (Maschinen- / Binärcode) einige Anweisungscodes, die wissen, dass die Adresse von 0x00000061 ein Zeichen ist und mithilfe der ASCII-Tabelle in ein Zeichen konvertiert wird? Wenn ja, bedeutet dies, dass char int Bezeichner indirekt in der Binärdatei enthalten sind ???
user16307
Nein, für die CPU sind alle Zahlen. Für Ihr spezielles Beispiel hängt das Drucken auf der Konsole nicht davon ab, ob die Variable char oder int ist. Ich werde meine Antwort mit einem detaillierten Ablauf darüber aktualisieren, wie das übergeordnete Programm bis zur Ausführung des Programms in die Maschinensprache konvertiert wird.
Prasad
2

Meine Antwort hier ist etwas vereinfacht und bezieht sich nur auf C.

Nein, Typinformationen werden nicht im Programm gespeichert.

intoder charsind keine Typindikatoren für die CPU; nur an den Compiler.

Die vom Compiler erstellte Exe enthält Anweisungen zum Bearbeiten von ints, wenn die Variable als deklariert wurde int. Wenn die Variable als a deklariert wurde char, enthält die exe Anweisungen zum Manipulieren von a char.

In C:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

Dieses Programm gibt seine Meldung aus, da die charund intdie gleichen Werte im RAM haben.

Wenn Sie sich nun fragen, wie printfdie Ausgabe 65für ein intund Afür ein gelingt, charmüssen Sie in der "Formatzeichenfolge" angeben, wie printfder Wert behandelt werden soll .
(Bedeutet zum Beispiel, %cden Wert als a charund %dden Wert als ganze Zahl zu behandeln. In beiden Fällen jedoch den gleichen Wert.)

BenjiWiebe
quelle
2
Ich hatte gehofft, jemand würde ein Beispiel verwenden printf. @OP: int a = 65; printf("%c", a)wird ausgegeben 'A'. Warum? Weil es dem Prozessor egal ist. Alles, was es sieht, sind Bits. Ihr Programm hat den Prozessor angewiesen, 65 (zufällig den Wert von 'A'in ASCII) zu speichern aund dann ein Zeichen auszugeben, was gerne der Fall ist. Warum? Weil es egal ist.
Cole Johnson
aber warum sagt man hier im c # fall nicht die geschichte? Ich las einige andere Kommentare und sie sagten in C # und C ++, dass die Geschichte (Informationen zu Datentypen) anders ist und sogar die CPU nicht rechnet. Irgendwelche Ideen dazu?
user16307
@ user16307 Wenn die CPU nicht rechnet, wird das Programm nicht ausgeführt. :) Was C # betrifft, weiß ich nicht, aber ich denke, dass meine Antwort auch dort gilt. Was C ++ betrifft, weiß ich, dass meine Antwort dort zutrifft.
BenjiWiebe
0

Auf der untersten Ebene gibt es in der tatsächlichen physischen CPU überhaupt keine Typen (ohne Berücksichtigung der Gleitkommaeinheiten). Nur Muster von Bits. Ein Computer manipuliert Bitmuster sehr, sehr schnell.

Das ist alles, was die CPU jemals tun kann. Es gibt keine int oder char.

x = 4 + 5

Wird ausgeführt als:

  1. Laden Sie 00000100 in Register 1
  2. Laden Sie 00000101 in Register 2
  3. IZu Register 1 hinzufügen, um 2 zu registrieren, und in Register 1 speichern

Der Befehl iadd löst Hardware aus, die sich so verhält, als wären die Register 1 und 2 Ganzzahlen. Wenn sie keine ganzen Zahlen darstellen, können später alle möglichen Probleme auftreten. Das beste Ergebnis ist normalerweise ein Absturz.

Es ist Aufgabe des Compilers, die richtige Anweisung basierend auf den in source angegebenen Typen auszuwählen, aber im tatsächlichen Maschinencode, der von der CPU ausgeführt wird, gibt es nirgendwo Typen.

Bearbeiten: Beachten Sie, dass der tatsächliche Maschinencode nirgendwo 4, 5 oder eine Ganzzahl erwähnt. Es sind nur zwei Bitmuster und ein Befehl, der zwei Bitmuster annimmt, dass sie Ints sind, und sie addiert.

Leliel
quelle
0

Kurze Antwort, der Typ ist in den vom Compiler generierten CPU-Anweisungen kodiert.

Obwohl die Informationen über den Typ oder die Größe der Informationen nicht direkt gespeichert werden, verfolgt der Compiler diese Informationen, wenn er auf Werte in diesen Variablen zugreift, diese ändert und diese speichert.

Woher weiß die Ausführung, dass a ein Zeichen und x ein Int ist?

Dies ist nicht der Fall, aber wenn der Compiler den ihm bekannten Maschinencode erzeugt. Ein intund charkönnen unterschiedlich groß sein. In einer Architektur, in der ein Zeichen die Größe eines Bytes und ein Int 4 Bytes hat, befindet sich die Variable xnicht in der Adresse 10001, sondern auch in 10002, 10003 und 10004. Wenn der Code den Wert von xin ein CPU-Register laden muss , es verwendet die Anweisung zum Laden von 4 Bytes. Beim Laden eines Zeichens wird der Befehl zum Laden von 1 Byte verwendet.

Wie wähle ich welche der beiden Anweisungen aus? Der Compiler entscheidet während der Kompilierung, dies geschieht nicht zur Laufzeit, nachdem die Werte im Speicher überprüft wurden.

Beachten Sie auch, dass die Register unterschiedlich groß sein können. Auf Intel x86-CPUs ist der EAX 32 Bit breit, die Hälfte davon ist AX (16 Bit) und AX ist in AH und AL (beide 8 Bit) aufgeteilt.

Wenn Sie also eine Ganzzahl laden möchten (auf x86-CPUs), verwenden Sie den MOV-Befehl für Ganzzahlen, und zum Laden eines Zeichens verwenden Sie den MOV-Befehl für Zeichen. Sie heißen beide MOV, haben aber unterschiedliche Op-Codes. Eigentlich zwei verschiedene Anweisungen. Der Typ der Variablen ist in der zu verwendenden Anweisung codiert.

Dasselbe passiert mit anderen Operationen. Abhängig von der Größe der Operanden und auch wenn sie signiert oder nicht signiert sind, gibt es viele Anweisungen zum Ausführen der Addition. Siehe https://en.wikipedia.org/wiki/ADD_(x86_instruction), in der verschiedene mögliche Ergänzungen aufgeführt sind.

Angenommen, ein Wert wird irgendwo im RAM als 10011001 gespeichert. Wenn ich das Programm bin, das den Code ausführt, woher weiß ich dann, ob es sich bei dieser 10011001 um ein Zeichen oder eine Ganzzahl handelt?

Erstens wäre ein char 10011001, aber ein int wäre 00000000 00000000 00000000 10011001, weil sie unterschiedliche Größen haben (auf einem Computer mit den gleichen Größen wie oben erwähnt). Aber lassen Sie uns den Fall für signed charvs betrachten unsigned char.

Was an einem Speicherort gespeichert ist, kann beliebig interpretiert werden. Es gehört zu den Aufgaben des C-Compilers, sicherzustellen, dass das, was in einer Variablen gespeichert und gelesen wird, auf konsistente Weise erfolgt. Es ist also nicht so, dass das Programm weiß, was in einem Speicherort gespeichert ist, sondern dass es vorher vereinbart, dass es dort immer die gleichen Dinge liest und schreibt. (Dinge wie Casting-Typen nicht mitgerechnet).

frozenkoi
quelle
aber warum sagt man hier im c # fall nicht die geschichte? Ich habe einige andere Kommentare gelesen und sie sagten in C # und C ++, dass die Geschichte (Informationen zu Datentypen) anders ist und sogar die CPU nicht rechnet. Irgendwelche Ideen dazu?
user16307
0

aber warum sagt man hier im c # fall nicht die geschichte? Ich habe einige andere Kommentare gelesen und sie sagten in C # und C ++, dass die Geschichte (Informationen zu Datentypen) anders ist und sogar die CPU nicht rechnet. Irgendwelche Ideen dazu?

In typüberprüften Sprachen wie C # wird die Typüberprüfung vom Compiler durchgeführt. Der Code, den benji schrieb:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

Würde mich einfach weigern zu kompilieren. Ebenso, wenn Sie versucht haben, einen String und eine Ganzzahl zu multiplizieren (ich wollte add sagen, aber der Operator '+' ist mit einer String-Verkettung überladen und es könnte einfach funktionieren).

int a = 42;
string b = "Compilers are awesome.";
double[] c = a * b;

Der Compiler würde es einfach ablehnen, Maschinencode aus diesem C # zu generieren, unabhängig davon, wie sehr Ihre Zeichenfolge darauf geküsst wurde.

Michael Blackburn
quelle
-4

Die anderen Antworten stimmen insofern, als im Wesentlichen jedes Endgerät, auf das Sie stoßen, keine Typinformationen speichert. In der Vergangenheit (und in der Gegenwart im Forschungskontext) gab es jedoch mehrere Hardware-Designs, die eine mit Tags versehene Architektur verwendeten - sie speichern sowohl die Daten als auch den Typ (und möglicherweise auch andere Informationen). Dazu gehören vor allem die Lisp-Maschinen .

Ich erinnere mich vage an eine Hardwarearchitektur für objektorientierte Programmierung, die etwas Ähnliches hatte, aber ich kann sie jetzt nicht finden.

Nathan Ringo
quelle
3
Die Frage ausdrücklich fest , es bezieht sich auf die Sprache C (nicht Lisp) und die C - Sprache ist nicht speichern variable Metadaten. Während es für eine C-Implementierung sicherlich möglich ist, dies zu tun, was der Standard nicht verbietet, geschieht dies in der Praxis nie. Wenn Sie Beispiele haben, die für die Frage relevant sind, geben Sie bitte bestimmte Zitate und Verweise an , die sich auf die Sprache C beziehen .
Nun, Sie könnten einen C-Compiler für eine Lisp-Maschine schreiben, aber heutzutage verwendet niemand mehr Lisp-Maschinen. Die objektorientierte Architektur war übrigens Rekursiv .
Nathan Ringo
2
Ich denke, diese Antwort ist nicht hilfreich. Dies erschwert Dinge, die weit über das derzeitige Verständnis des OP hinausgehen. Es ist klar, dass das OP das grundlegende Ausführungsmodell einer CPU + RAM nicht versteht und wie ein Compiler eine symbolische Quelle auf hoher Ebene in eine ausführbare Binärdatei übersetzt. Tagged Memory, RTTI, Lisp usw. gehen weit über das hinaus, was der Fragesteller meiner Meinung nach wissen muss, und werden ihn nur noch mehr verwirren.
Andres F.
aber warum sagt man hier im c # fall nicht die geschichte? Ich habe einige andere Kommentare gelesen und sie sagten in C # und C ++, dass die Geschichte (Informationen zu Datentypen) anders ist und sogar die CPU nicht rechnet. Irgendwelche Ideen dazu?
user16307