Hier spielen zwei Probleme eine Rolle:
Problem Nr. 1: C ist eine statisch typisierte Sprache. Alle Typinformationen werden zur Kompilierungszeit ermittelt. Mit keinem Objekt im Speicher werden Typinformationen gespeichert, sodass Typ und Größe zur Laufzeit 1 bestimmt werden können . Wenn Sie den Speicher an einer bestimmten Adresse untersuchen, während das Programm ausgeführt wird, sehen Sie nur einen Schlamm von Bytes. Es gibt nichts zu sagen, ob diese bestimmte Adresse tatsächlich ein Objekt enthält, welchen Typ oder welche Größe dieses Objekt hat oder wie diese Bytes zu interpretieren sind (als Ganzzahl- oder Gleitkommatyp oder als Zeichenfolge in einer Zeichenfolge usw.). ). Alle diese Informationen werden beim Kompilieren des Codes in den Maschinencode eingebrannt, basierend auf den im Quellcode angegebenen Typinformationen. Zum Beispiel die Funktionsdefinition
void foo( int x, double y, char *z )
{
...
}
Weist den Compiler an, den entsprechenden Maschinencode zu generieren, der x
als Ganzzahl, y
als Gleitkommawert und z
als Zeiger auf behandelt werden soll char
. Beachten Sie, dass Fehlanpassungen in der Anzahl oder Art der Argumente zwischen einem Funktionsaufruf und einer Funktionsdefinition nur erkannt werden, wenn der Code kompiliert wird 2 ; Nur während der Kompilierungsphase werden einem Objekt Typinformationen zugeordnet.
Problem Nr. 2: printf
ist eine variable Funktion; Es werden ein fester Parameter vom Typ const char * restrict
(die Formatzeichenfolge) sowie null oder mehr zusätzliche Parameter verwendet, deren Anzahl und Typ zum Zeitpunkt der Kompilierung nicht bekannt sind :
int printf( const char * restrict fmt, ... );
Die printf
Funktion kann die Anzahl und Art der zusätzlichen Argumente nicht anhand der übergebenen Argumente selbst ermitteln. Es muss sich auf die Formatzeichenfolge verlassen, um zu bestimmen, wie der Byte-Schlamm auf dem Stapel (oder in den Registern) interpretiert werden soll. Noch besser ist , weil es eine variadische Funktion ist, Argumente mit bestimmten Typen werden gefördert , um eine begrenzte Anzahl von Standardtypen (zB short
wird gefördert int
, float
wird gefördert double
, etc.).
Auch hier sind den zusätzlichen Argumenten selbst keine Informationen zugeordnet, die printf
Hinweise auf deren Interpretation oder Formatierung geben könnten. Daher sind die Konvertierungsspezifizierer in der Formatzeichenfolge erforderlich.
Beachten Sie, dass printf
Konvertierungsspezifizierer nicht nur die Anzahl und den Typ zusätzlicher Argumente angeben, sondern auch angeben, printf
wie die Ausgabe formatiert werden soll (Feldbreiten, Genauigkeit, Auffüllung, Ausrichtung, Basis (Dezimal, Oktal oder Hex für Ganzzahltypen) usw.).
Bearbeiten
Um ausführliche Diskussionen in den Kommentaren zu vermeiden (und weil die Chat-Seite für mein Arbeitssystem gesperrt ist - ja, ich bin ein böser Junge), werde ich hier die letzten beiden Fragen beantworten.
WENN ich das mache: float b;
float c;
b=3.1;
c=(5.0/9.0)*(b);
Woher weiß der Compiler in der letzten Anweisung, dass b vom Typ float ist?
Während der Übersetzung hält der Compiler eine Tabelle (oft genannt Symboltabelle ) , dass speichert Informationen über den Namen eines Objekts, Typ, Lagerdauer, Umfang, etc. Sie erklärt b
und c
wie float
, so jederzeit der Compiler sieht einen Ausdruck mit b
oder c
darin, Es wird der Maschinencode generiert, um einen Gleitkommawert zu verarbeiten.
Ich habe Ihren Code oben genommen und ein vollständiges Programm darum gewickelt:
/**
* c1.c
*/
#include <stdio.h>
int main( void )
{
float b;
float c;
b = 3.1;
c = (5.0 / 9.0) * b;
printf( "c = %f\n", c );
return 0;
}
Ich habe die Optionen -g
und -Wa,-aldh
mit gcc verwendet, um eine Liste des generierten Maschinencodes zu erstellen, der mit dem C-Quellcode 3 verschachtelt ist :
GAS LISTING /tmp/ccmGgGG2.s page 1
1 .file "c1.c"
9 .Ltext0:
10 .section .rodata
11 .LC2:
12 0000 63203D20 .string "c = %f\n"
12 25660A00
13 .align 8
14 .LC1:
15 0008 721CC771 .long 1908874354
16 000c 1CC7E13F .long 1071761180
17 .text
18 .globl main
20 main:
21 .LFB2:
22 .file 1 "c1.c"
1:c1.c **** #include <stdio.h>
2:c1.c **** int main( void )
3:c1.c **** {
23 .loc 1 3 0
24 0000 55 pushq %rbp
25 .LCFI0:
26 0001 4889E5 movq %rsp, %rbp
27 .LCFI1:
28 0004 4883EC10 subq $16, %rsp
29 .LCFI2:
4:c1.c **** float b;
5:c1.c **** float c;
6:c1.c **** b = 3.1;
30 .loc 1 6 0
31 0008 B8666646 movl $0x40466666, %eax
31 40
32 000d 8945F8 movl %eax, -8(%rbp)
7:c1.c **** c = (5.0 / 9.0) * b;
33 .loc 1 7 0
34 0010 F30F5A4D cvtss2sd -8(%rbp), %xmm1
34 F8
35 0015 F20F1005 movsd .LC1(%rip), %xmm0
35 00000000
36 001d F20F59C1 mulsd %xmm1, %xmm0
37 0021 F20F5AC0 cvtsd2ss %xmm0, %xmm0
38 0025 F30F1145 movss %xmm0, -4(%rbp)
38 FC
8:c1.c ****
9:c1.c **** printf( "c = %f\n", c );
39 .loc 1 9 0
40 002a F30F5A45 cvtss2sd -4(%rbp), %xmm0
40 FC
41 002f BF000000 movl $.LC2, %edi
41 00
42 0034 B8010000 movl $1, %eax
42 00
43 0039 E8000000 call printf
43 00
10:c1.c **** return 0;
44 .loc 1 10 0
45 003e B8000000 movl $0, %eax
GAS LISTING /tmp/ccmGgGG2.s page 2
11:c1.c **** }
46 .loc 1 11 0
47 0043 C9 leave
48 0044 C3 ret
So lesen Sie die Baugruppenliste:
40 002a F30F5A45 cvtss2sd -4(%rbp), %xmm0
40 FC
^ ^ ^ ^ ^
| | | | |
| | | | +-- Instruction operands
| | | +------------------ Instruction mnemonic
| | +---------------------------------------- Actual machine code (instruction and operands)
| +--------------------------------------------- Byte offset of instruction from subroutine entry point
+------------------------------------------------ Line number of assembly listing
Eine Sache, die hier zu beachten ist. Im generierten Assemblycode gibt es keine Symbole für b
oder c
; Sie sind nur in der Quellcodeliste vorhanden. Bei der main
Ausführung zur Laufzeit wird durch Anpassen des Stapelzeigers Platz für b
und c
(zusammen mit einigen anderen Dingen) vom Stapel zugewiesen:
subq $16, %rsp
Der Code bezieht sich auf diese Objekte durch ihren Versatz vom Rahmenzeiger 4 , b
wobei -8 Bytes von der im Rahmenzeiger gespeicherten Adresse und c
-4 Bytes von dieser wie folgt sind:
7:c1.c **** c = (5.0 / 9.0) * b;
.loc 1 7 0
cvtss2sd -8(%rbp), %xmm1 ;; converts contents of b from single- to double-
;; precision float, stores result to floating-
;; point register xmm1
movsd .LC1(%rip), %xmm0 ;; writes the pre-computed value of 5.0/9.0
;; to floating point register xmm0
mulsd %xmm1, %xmm0 ;; multiply contents of xmm1 by xmm0, store result
;; in xmm0
cvtsd2ss %xmm0, %xmm0 ;; convert result in xmm0 from double- to single-
;; precision float
movss %xmm0, -4(%rbp) ;; save result to c
Da Sie deklariert b
und c
als Gleitkommazahlen angegeben haben, hat der Compiler Maschinencode generiert, um Gleitkommawerte spezifisch zu verarbeiten. die movsd
, mulsd
, cvtss2sd
Anweisungen sind spezifisch für Gleitkommaoperationen, und die Register %xmm0
und %xmm1
werden verwendet , mit doppelter Genauigkeit zum Speichern von Gleitkommazahlen.
Wenn ich den Quellcode ändern , so dass b
und c
ganze Zahlen anstelle von Schwimmern, erzeugt der Compiler verschiedenen Maschinencode:
/**
* c2.c
*/
#include <stdio.h>
int main( void )
{
int b;
int c;
b = 3;
c = (9 / 4) * b; // changed these values since integer 5/9 == 0, making for
// some really boring machine code.
printf( "c = %d\n", c );
return 0;
}
Kompilieren mit gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c
ergibt:
GAS LISTING /tmp/ccyxHwid.s page 1
1 .file "c2.c"
9 .Ltext0:
10 .section .rodata
11 .LC0:
12 0000 63203D20 .string "c = %d\n"
12 25640A00
13 .text
14 .globl main
16 main:
17 .LFB2:
18 .file 1 "c2.c"
1:c2.c **** #include <stdio.h>
2:c2.c **** int main( void )
3:c2.c **** {
19 .loc 1 3 0
20 0000 55 pushq %rbp
21 .LCFI0:
22 0001 4889E5 movq %rsp, %rbp
23 .LCFI1:
24 0004 4883EC10 subq $16, %rsp
25 .LCFI2:
4:c2.c **** int b;
5:c2.c **** int c;
6:c2.c **** b = 3;
26 .loc 1 6 0
27 0008 C745F803 movl $3, -8(%rbp)
27 000000
7:c2.c **** c = (9 / 4) * b;
28 .loc 1 7 0
29 000f 8B45F8 movl -8(%rbp), %eax
30 0012 01C0 addl %eax, %eax
31 0014 8945FC movl %eax, -4(%rbp)
8:c2.c ****
9:c2.c **** printf( "c = %d\n", c );
32 .loc 1 9 0
33 0017 8B75FC movl -4(%rbp), %esi
34 001a BF000000 movl $.LC0, %edi
34 00
35 001f B8000000 movl $0, %eax
35 00
36 0024 E8000000 call printf
36 00
10:c2.c **** return 0;
37 .loc 1 10 0
38 0029 B8000000 movl $0, %eax
38 00
11:c2.c **** }
39 .loc 1 11 0
40 002e C9 leave
41 002f C3 ret
Hier ist dieselbe Operation, jedoch mit b
und c
als Ganzzahlen deklariert:
7:c2.c **** c = (9 / 4) * b;
.loc 1 7 0
movl -8(%rbp), %eax ;; copy value of b to register eax
addl %eax, %eax ;; since 9/4 == 2 (integer arithmetic), double the
;; value in eax
movl %eax, -4(%rbp) ;; write result to c
Dies habe ich vorhin gemeint, als ich sagte, dass Typinformationen in den Maschinencode "eingebrannt" wurden. Wenn das Programm ausgeführt wird, wird der Typ nicht überprüft b
oder c
ermittelt. Es weiß bereits, welcher Typ auf dem generierten Maschinencode basieren soll .
Wenn der Compiler zur Laufzeit den Typ und die Größe bestimmt, warum funktioniert dann das folgende Programm nicht:
float b='H';
printf(" value of b is %c \n",b);
Es funktioniert nicht, weil Sie den Compiler anlügen. Sie sagen, dass dies a b
ist float
, sodass Maschinencode für die Verarbeitung von Gleitkommawerten generiert wird. Wenn Sie es initialisieren, wird das der Konstante entsprechende Bitmuster 'H'
als Gleitkommawert und nicht als Zeichenwert interpretiert.
Sie belügen den Compiler erneut, wenn Sie für das Argument den %c
Konvertierungsspezifizierer verwenden, der einen Wert vom Typ erwartet . Aus diesem Grund wird der Inhalt von nicht richtig interpretiert , und Sie erhalten die Müllausgabe 5 . Auch hier kann die Anzahl oder Art der zusätzlichen Argumente nicht anhand der Argumente selbst ermittelt werden. Alles, was es sieht, ist eine Adresse auf dem Stapel (oder eine Reihe von Registern). Die Formatzeichenfolge muss angeben, welche zusätzlichen Argumente übergeben wurden und welche Typen sie haben. char
b
printf
b
printf
1. Die einzige Ausnahme sind Arrays mit variabler Länge. Da ihre Größe erst zur Laufzeit festgelegt wird, gibt es keine Möglichkeit, sizeof
eine VLA zur Kompilierungszeit auszuwerten .
2. Ab C89 jedenfalls. Zuvor konnte der Compiler nur Fehlanpassungen im Funktionsrückgabetyp abfangen. Es konnten keine Fehlanpassungen in den Funktionsparameterlisten festgestellt werden.
3. Dieser Code wird auf einem 64-Bit-SuSE Linux Enterprise 10-System mit gcc 4.1.2 generiert. Wenn Sie sich in einer anderen Implementierung befinden (Compiler / Betriebssystem / Chip-Architektur), sind die genauen Maschinenanweisungen unterschiedlich, aber der allgemeine Punkt bleibt bestehen. Der Compiler generiert verschiedene Anweisungen zum Behandeln von Floats vs. Ints vs. Strings usw.
4. Wenn Sie eine Funktion in einem laufenden Programm aufrufen, einen Stack-Framewird erstellt, um die Funktionsargumente, lokalen Variablen und die Adresse der Anweisung nach dem Funktionsaufruf zu speichern. Ein spezielles Register, das als Rahmenzeiger bezeichnet wird, wird verwendet, um den aktuellen Rahmen zu verfolgen.
5. Nehmen Sie beispielsweise ein Big-Endian-System an, bei dem das höherwertige Byte das adressierte Byte ist. Das Bitmuster für H
wird gespeichert werden b
als 0x00000048
. Da der %c
Konvertierungsspezifizierer jedoch angibt, dass das Argument a sein soll char
, wird nur das erste Byte gelesen, und es printf
wird versucht, das der Codierung entsprechende Zeichen zu schreiben 0x00
.