Hallo Leute.
Da wir in der Firma mit dem ASF (Atmel Software Framework) auf dieses
Problem gestoßen sind, wollte ich hier nachfragen, ob Ihr das hier mal
näher beleuchten könnte, warum dass so ist.
Konkretes Beispiel:
1
// include datei: test.h
2
typedefstruct
3
{
4
intx;
5
}FOO_T;
6
7
voidbar(void);
8
--------------------------------
9
10
// test.c
11
#include"test.h"
12
13
FOO_Tvar;
14
15
voidbar(void)
16
{
17
var.x=5;
18
}
19
--------------------------------
20
21
// main.c
22
#include<stdio.h>
23
#include"test.h"
24
25
FOO_Tvar;
26
27
intmain(void)
28
{
29
bar();
30
printf("%i\n",var.x);
31
}
Bei dem printf() in main wird nun 5 ausgegeben, da das var aus test.c
genommen wird und nicht var aus main.
Frage 1:
Warum gibt es hier keine warnung, nach dem Motto: Hey, du hast da zwei
variablen, vom selben typ, mit dem selben namen, die global sind und
nicht static und auch nicht als extern deklariert, ist das wirklich das
was du willst?
Denn genau das hatten wir nun:
Code aus einem Atmel Quick-Start-Guide kopiert und gewundert das nichts
geht... bis wir dann mal raus gefunden haben, das es in den tiefsten
tiefen des ASF nunmal globale variablen vom selben Typ mit dem selben
Namen gab.
Wäre z.B. FOO_T var in main als extern deklariert, könnte ich das
verhalten ja voll und ganz nachvollziehen. Wenn aber der compiler das eh
so macht, erschließt sich mir die Sinnhaftigkeit von extern nicht so
ganz :-/
Frage 2:
Gibt es eine festgelegte reihenfolge in der die symbole genommen werden,
oder wird aus den beiden symbolen einfach ein symbol gemacht?
Frage 3:
Ist das Verhalten gewollt? Ist es "undefined behavior"? Ist es
"Implementation-defined behavior"?
Das verhalten lässt sich mit dem ARM-GCC aus dem Atmel Studio 7 (müsste
der 4.9.3 sein) als auch mit einem aktuellen GCC 6.1.1 20160501 unter
Linux reproduzieren.
Um das noch kurz klar zu stellen:
Ich möchte nur verstehen warum das so ist. Dass es kein Compiler-Bug
ist, da bin ich mir sicher. Es ist wie immer ein Layer 8 Problem, das
ist klar. :o)
Grüße
Probier mal -fno-common
Nicht initialisierte Variablen werden in manchen Compilern als
sogenannte "commons" ausgeworfen. Eine gleichlautende initialisierte
Definition hat gerät dann nicht in Konflikt mit nicht initialisierten
Definitionen, sondern ersetzt sie kommentarlos.
lalala schrieb:> Um das zu vermeiden gibt es bei globalen Variablen das Schlüsselwort> 'static'. Damit wird ein exportieren der Variable verhindert.
Mit 'static' klappt es. Nur das Verhalten ist nicht korrekt. Denn die
beiden Variablen sollten auch ohne 'static' nur im jeweiligen Modul
gelten.
Sultan schrieb:> Nur das Verhalten ist nicht korrekt.
Nein. Das Verhalten ist verbreitet.
> Denn die> beiden Variablen sollten auch ohne 'static' nur im jeweiligen Modul> gelten.
Das wäre definitiv nicht korrekt.
Etwas Hintergrund zum Thema gibts unter 6.2.2 in
www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf
Wenn ich (genau) das Beispiel in Xcode zu übersetzen versuche, dann
meckert der Linker. Wenn ich das einfach im Terminal übersetze - ohne
extra Flags, dann funkt. es tadellos. Da wird wohl noch ein Linker-Flag
gesetzt.
Es gibt schlicht beides. Das strikte Modell, wie es hier clang zeigt,
aber auch gcc mit -fno-common, und die verbreitete Erweiterung aus
J.5.11 (C99).
Da ziemlich viel Code die Erweiterung verwendet, hat der Standard darauf
verzichtet, das pauschal zu verdammen.
Kaj G. schrieb:> Ist das Verhalten gewollt?
Ja.
> Ich möchte nur verstehen warum das so ist.
Das Zauberwort heißt "Linkage" (C99, §6.2.2).
Zunächst ein paar andere Begriffe, die damit zusammenhängen
Scope of Identifiers (C99, §6.2.1)
1
1 An identifier can denote an object; a function; a tag or a member
2
of a structure, union, or enumeration; a typedef name; a label name; a
3
macro name; or a macro parameter. The same identifier can denote
4
different entities at different points in the program. [...]
5
6
2 For each different entity that an identifier designates, the identifier
7
is visible [...] only within a region of program text called its scope.
8
[...] There are four kinds of scopes: function, file, block, and
9
function prototype.
Aus .2 lernen wir: Der größte Scope in C ist "file scope"! Es gibt
keinen Scope in C, der größer ist als der einer "translation unit"!
Die Verbindung zwischen gleichnamigen Objekten (entities) in
unterschiedlichen Translation Units wird in C (und C++) durch deren
Linkage hergestellt (C99, §6.2.2):
1
1 An identifier declared in different scopes or in the same scope more
2
than once can be made to refer to the same object or function by a
3
process called linkage. There are three kinds of linkage: external,
4
internal, and none.
5
6
2 In the set of translation units and libraries that constitutes an
7
entire program, each declaration of a particular identifier with
8
external linkage denotes the same object or function. [...]
9
10
4 For an identifier declared with the storage-class specifier extern in
11
a scope in which a prior declaration of that identifier is visible, if
12
the prior declaration specifies internal or external linkage, the
13
linkage of the identifier at the later declaration is the same as the
14
linkage specified at the prior declaration. If no prior declaration is
15
visible, or if the prior declaration specifies no linkage, then the
16
identifier has external linkage.
17
18
5 [...] If the declaration of an identifier for an object has file scope
19
and no storage-class specifier, its linkage is external.
In deinem Beispiel sind var (test.c) und var (main.c) also file-scope
Identifier, die qua "external Linkage" ein und dasselbe Objekt
bezeichnen.
Innerhalb der GNU Compiler Collection (GCC) u.a.m. ist der Linker für
die Linkage verantwortlich (formal kein Teil von GCC sondern der GNU
Binutils), und hier ist auch das Object-Format von Bedeutung, denn nicht
jedes Object-Format kann gleich viel, insbesondere was Sections angeht.
Zu Declarations (C99 §6.7):
1
5 A declaration specifies the interpretation and attributes of a set
2
of identifiers. A definition of an identifier is a declaration for that
3
identifier that:
4
— for an object, causes storage to be reserved for that object;
5
— [...]
Für var wird also Speicher allokiert (qua "FOO_T var"), für dessen
Storage Duration (C99 $6.2.4) gilt:
1
3 An object whose identifier is declared with external or internal
2
linkage, or with the storage-class specifier static has static storage
3
duration. Its lifetime is the entire execution of the program and its
4
stored value is initialized only once, prior to program startup.
Das "initialized only once" bezieht sich auf das Objekt, d.h. die
Toolchain muss sicherstellen, dass var nur 1x initialisiert wird — auch
dann, wenn mehrere (tentative) Definitions vorhanden sind. Für
"tentative Definitions" siehe C99 §6.9.2 External Object Definitions.
Anstatt einer tentative Definition verwendet man oft besser eine
explizit extern Deklaration, d.h. mit Schlüsselwort "extern". Falls es
keine Definition des Objekts gibt, beschwert sich der Linker. Falls es
hingegen mehrer Definitionen gibt, wird ein Standard foo-gcc das Objekt
nach COMMON legen, d.h. es ist keiner Input-Section zugeordnet
("Section" ist ein Konzept, das nicht Gegenstand des C-Standard ist,
dito für "Symbol").
Erst wenn dem Objekt eine Input-Section zugeordnet ist (z.B. per
-fno-common), kann sich der Linker über Mehrfachdefinitionen beschweren.
Allerdings ist anzumerken, dass mehrere tentative Definitionen keine
Mehrfachdefinition in diesem Sinne sind, siehe obigen Auszug aus dem
Standard. Die "Mehrfachdefinition" entsteht da erst im Kopf des
Anwenders, wenn dieser mehrfach vorhandene tentative Definitions
unterschiedlichen Objekten zuordnet...
Selbst wenn in einem Modul "int x" und in einem anderen "float x" steht,
beschwert sich der Linker (mit -fcommon) nicht, selbst wenn wie bei
avr-gcc sizeof(int) != sizeof(float) ist: Der Linker reserviert einfach
das Maximum an benötigtem Platz. Erst Linken mit -flto liefert eine
Diagnostic, welche dann allerdings vom Compiler kommt, der die globale
Sicht oder zumindest Modulgrenzen überschreitende Sicht hat.
A. K. schrieb:> Da ziemlich viel Code die Erweiterung verwendet, hat der Standard darauf> verzichtet, das pauschal zu verdammen.
Die common-Überlagerung ist einfach das historisch ältere Konzept
gewesen, daher ist es als implementierungsabhängige Option nach wie
vor vorhanden. Wenn man die älteren C-Handbücher von Dennis Ritchie
liest, war es wohl ursprünglich die Annahme, dass man nur in der
C-Implementierung auf der PDP-11 das "extern" weglassen könne, offenbar
ein Tribut an den damaligen Linker.
Jörg W. schrieb:> Die common-Überlagerung ist einfach das historisch ältere Konzept> gewesen, daher ist es als implementierungsabhängige Option nach wie> vor vorhanden.
Dem C99 Rationale kann man entnehmen, dass Ritchie das nicht einfach
bloss übernahm weil eh schon vorhanden, sondern direkt beabsichtigte.
Und zwar in noch krasserer Form als heute üblich, mit einem common
record unabhängig davon ob mit "extern" oder ohne:
6.2.2 zum Common Modell: "Every object declaration with external
linkage, regardless of whether the keyword extern appears in the
declaration, creates a definition of storage. When all of the modules
are combined together, each definition with the same name is located at
the same address in memory. (The name is derived from common storage in
Fortran.) This model was the intent of the original designer of C,
Dennis Ritchie."
A. K. schrieb:> Dem C99 Rationale kann man entnehmen, dass Ritchie das nicht einfach> bloss übernahm weil eh schon vorhanden, sondern direkt beabsichtigte.
Wirkliche Aussagen von ihm selbst finde ich nicht dazu. Allerdings
fällt auf, wenn man sich die Beschreibung von BCPL durchliest, dass
dort "extrn" noch verpflichtend war. Das lässt darauf schließen, dass
es Absicht war, es in C nicht mehr verpflichtend zu benötigen.
Andererseits sagt das alte C-Manual ausdrücklich, dass das nur bei
"PDP-11 C" so sei:
1
11.2 Scope of externals
2
[…]
3
In PDP-11 C, it is explicitly permitted for (compatible) external
4
definitions of the same identifier to be present in several of the
5
separately-compiled pieces of a complete program, or even twice within
6
the same program file, with the important limitation that the
7
identifier may be initialized in at most one of the definitions. In
8
other operating sys- tems, however, the compiler must know in just
9
which file the storage for the identifier is allocated, and in which
10
file the identifier is merely being referred to. In the
11
implementations of C for such systems, the appearance of the ex- tern
12
keyword before an external definition indicates that storage for the
13
identifiers being declared will be allocated in another file. Thus in
14
a multi-file program, an external data definition without the extern
15
specifier must appear in exactly one of the files. Any other files
16
which wish to give an external definition for the identifier must
17
include the extern in the definition. The identifier can be
18
initialized only in the file where storage is allocated.