GCC: Adresse einer bestimmten Codestelle

OP #3325453
Lesenswert?

Hi,

ich habe gerade ein Problem zu lösen, das sich reichlich kniffelig gibt:

Gegeben sein ein C-Code, wie er bei kleinen µC recht häufig auftritt:
1
int main(void) {
2
  // Init-Anweisungen
3
  ...
4
  
5
  // Idle-Loop
6
  while(1) {
7
    // eventuell Anweisungen in der Idle-Loop
8
    ...
9
  }
10
  // kommt niemals hier her
11
}

Aufgabe ist nun, die Adresse einer bestimmten Codestelle innerhalb der 
Idle-Loop zu ermitteln. Das soll aber - weil in einem automatisierten 
Test genutzt - nicht von Hand ermittelt werden, sondern im ELF-File als 
Label auslesbar sein. Eine erste Idee war, das "Label as value"-Feature 
des GCC zu verwenden, einen Pointer damit zu beschreiben und diesen dann 
während des Programmablaufs im z.B. Simulator auszulesen:
1
void *ptrLabel;
2

3
int main(void) {
4
  // Label-Adresse in Pointer eintragen
5
  ptrLabel = &&LABEL;
6
  // Init-Anweisungen
7
  ...
8

9
  // Idle-Loop
10
  while(1) {
11
    // gesuchte Zieladresse
12
    LABEL:
13
    // weitere Anweisungen in der Idle-Loop
14
    ...
15
  }
16
  // kommt niemals hier her
17
}

Problem: _das funktioniert nicht_! Es wird dem Pointer eine Adresse 
zugewiesen, allerdings die Startadresse von main! Nicht die innerhalb 
der Idle-Loop. (nachweisbar im Assemblercode!)

Also ist nun eine Idee gesucht, diese Nuss doch noch zu knacken: wie 
kann ich den C-Code (!) gestalten, das ich die Adresse der gewünschten 
Code-Stelle auslesen kann? Bedingungen:

- der Programmfluss _innerhalb_ der Idle-Loop soll nicht verändert 
werden, also z.B. kein Call an der entsprechenden Stelle auf eine 
spezielle Funktion, deren Adresse man aus dem ELF-File ja auslesen kann.
- nach Möglichkeit sollte die Adresse direkt auslesbar sein, alternativ, 
wie oben ja schon genutzt, z.B. durch eine (Pointer-)Variable.
- wenn möglich ein Konstrukt, das nicht nur speziell für den GCC 
verwendbar ist.

Irgendjemand eine Idee?

Schönen Tag noch,
Thomas
Gast #3325498
Lesenswert?

Wenn ich mir so vorstelle, was erzeugte Assemblercode noch mit dem 
C-Code zu tun hat, nachdem der Optimierer seinen Arbeit getan hat, bin 
ich mir nicht so sicher, ob das, was du vorhast, wirklich das ist, was 
du willst.

Was willst du denn mit dem Test nachprüfen?

Oliver
#3325695
Lesenswert?

1
/*volatile*/ unsigned char nTime = 0;
2

3
void *ptrLabel;
4

5
int main(void)
6
{
7
   ptrLabel = &&LABEL;
8

9
  while(1)
10
  {
11
    LABEL: nTime++;
12
  }
13
}

Ergibt:
1
int main(void)
2
{
3
   ptrLabel = &&LABEL;
4
  de:  85 e7         ldi  r24, 0x75  ; 117
5
  e0:  90 e0         ldi  r25, 0x00  ; 0
6
  e2:  90 93 66 00   sts  0x0066, r25
7
  e6:  80 93 65 00   sts  0x0065, r24
8
  ea:  ff cf         rjmp  .-2        ; 0xea <__stack+0xb>

0x75 ist die Wordadresse = Byteadresse 0xEA von LABEL.
Also von der while-Schleife, da der sinnlose Zähler wegoptimiert wird. 
main hat die Adresse 0x6F(0xDE).

Grundsätzlich tut es, was es tun soll.

Was willst du eigentlich damit bezwecken? Die Adresse steht doch ohnehin 
nicht unter ptrLabel im *.elf, da sie erst zur Laufzeit in den Pointer 
geschrieben wird.

mfg.
#3325978
Lesenswert?

Hallo, ich nocheinmal:

Definiert in "assembler volatile" die Marke.
Im Code selbst implementierst du eine externe Variable:
1
extern void* Codeadresse __asm__("mein_einzigartiger_Labelname_als_Symbol");

Nach dem Linken des Codes enthaelt
1
&Codeaddresse
 die Adresse.

MfG


Nachtrag:
Ausgabe:
1
Hallo Welt
2
Sprungmarke bei 0x000000000040059d
Dump(ausschnitt):
1
0000000000400584 <main>:
2
  400584:       55                      push   %rbp
3
  400585:       48 89 e5                mov    %rsp,%rbp
4
  400588:       48 83 ec 10             sub    $0x10,%rsp
5
  40058c:       89 7d fc                mov    %edi,-0x4(%rbp)
6
  40058f:       48 89 75 f0             mov    %rsi,-0x10(%rbp)
7
  400593:       bf ac 06 40 00          mov    $0x4006ac,%edi
8
  400598:       e8 d3 fe ff ff          callq  400470 <puts@plt>
9

10
000000000040059d <mein_einzigartiger_Labelname_als_Symbol>:
11
  40059d:       ba 9d 05 40 00          mov    $0x40059d,%edx
12
  4005a2:       b8 b7 06 40 00          mov    $0x4006b7,%eax
13
  4005a7:       48 89 d6                mov    %rdx,%rsi
14
  4005aa:       48 89 c7                mov    %rax,%rdi
15
  4005ad:       b8 00 00 00 00          mov    $0x0,%eax
16
  4005b2:       e8 c9 fe ff ff          callq  400480 <printf@plt>
17
  4005b7:       b8 00 00 00 00          mov    $0x0,%eax
18
  4005bc:       c9                      leaveq 
19
  4005bd:       c3                      retq   
20
  4005be:       66 90                   xchg   %ax,%ax
Angehängte Dateien:
Persönliche Seite #3326004
Lesenswert?

Stephan B. schrieb:
> __asm__("mein_einzigartiger_Labelname_als_Symbol");

Die böse Überraschung wartet bereits:

- falls das Label in einer Scheife ist, die aufgerollt wird

- falls das Label in einer Funktion ist, die (mehrfach)
  evtl. nur partiell geinlinet wird

- falls das Label in einem Block steht, der durch Optimizer
  kopiert wird, um teure Sprünge zu sparen

- etc.
Persönliche Seite #3326059
Lesenswert?

Stephan B. schrieb:
> Johann L. schrieb:
>> Die böse Überraschung wartet bereits
>
> Deine genannten Moeglichkeiten sollten in einem Compilerfehler
> resultieren?

Nein, beschweren wird sich erst der Assembler, weil das Label dann 
mehrfach definiert wird.  Für den Compiler ist Inline-Asm ne Blackbox.

Zudem definiert der Ansazu lediglich ein Label, aber den Wert (die 
Adresse) hat man dadurch noch nicht.
Gast #3326085
Lesenswert?

A. K. schrieb:
> Die wird hier aber benutzt. Nämlich um in main() die Adresse vom Label
> rein zu schreiben.

da sie aber nie wieder ausgelesen wird, muss der compiler den wert dort 
nicht reinschreiben. (sonst wird es das volatile problem mit einer ISR 
gar nicht geben).

Da es die funktion vom Programm nicht ändert, darf er sie wegoptimieren. 
Ob er es macht ist eine andere Frage.
#3326090
Lesenswert?

Peter II schrieb:
> da sie aber nie wieder ausgelesen wird, muss der compiler den wert dort
> nicht reinschreiben.

Dazu müsste er das aber wissen. Da es sich um eine globale Variable 
handelt weiss er es jedoch nicht, solange klassisch File für File 
compiliert wird. Er kann es nur wissen, wenn das gesamte Programm 
geschlossen übersetzt wird (z.B. -flto oder -fwhole-program).
Gast #3326152
Lesenswert?

A. K. schrieb:
> Peter II schrieb:
>> der Compieler kennt doch wohl die Adresse, damit damit kann er es doch
>> wegoptimieren.
>
> Dazu müsste er wissen, dass vor main() niemand auf ptrLabel zugreift.
> Denn vor der Zuweisung muss der Inhalt von ptrLabel 0 sein.

Das wäre in ISO C ja kein Problem, weil man da gar keinen Code schreiben 
kann, der vor main() ausgeführt wird.
#3326158
Lesenswert?

Rolf Magnus schrieb:
> Das wäre in ISO C ja kein Problem, weil man da gar keinen Code schreiben
> kann, der vor main() ausgeführt wird.

In C nicht, in C++ schon. Und auf dieser Ebene wird der Compiler 
möglicherweise keinen Unterschied mehr dazwischen machen.

Wobei mindestens manche Startup-Szenarien von C Compilern für 
Microcontroller sehr wohl C Code vor main() ausführen. Da müsste man in 
dem Fall eine Extrawurst braten, um das dem Compiler beizubringen.
#3326193
Lesenswert?

Matthias Lipinsky schrieb:
> Das alles erklärt aber noch nicht die Frage, was damit bezweckt werden
> soll...

Man kann soetwas gebrauchen, wenn man z.B. innerhalb von einem 
aligned-Codeblock virtuell verschiedene Funktionen haben moechte (ohne 
dabei jede Funktion aus Speicherplatzgruenden alignen zu muessen).
Z.B. um ein Muster mit harting Timings zu interpretieren, wie es im 
Beitrag "Re: AVR ASM ws2811 / ws2812 Ansteuerung mit FastPWM" 
(burstsequence_8mhz.h und ws2812.*) getan wurde.

MfG
OP #3326650
Lesenswert?

Hi @all,

wow, da geht ja echt die Post ab. :-) Ok, hier die Auflösung des Rätsels 
... wobei, das ist gar nicht so rätselhaft. Und mit etwas Phantasie 
sollte man sogar selbst drauf kommen können. Also ...

Ad 1) Die Lösung von Stephan B. funktioniert! Damit wird ja nichts 
anderes getan, als im vom Compiler erzeugten Assembler-Code ein 
bestimmtes Label einzuführen. Und das taucht auch, wie per readelf ganz 
einfach nachweisbar, eben auch im ELF-File auf, von wo ich es für den 
Test auslesen kann. Die Lösung hat 3 Vorteile: das sollte analog auch 
mit anderen Compilern als dem GCC funktionieren, es wird kein 
zusätzlicher Code eingeführt, vor allem, wie schon geschrieben, eben 
nicht in der Idle-Loop, was essentiell ist. Und drittens, die gesuchte 
Adresse an der bestimmten Code-Stelle steht sofort zur Verfügung und 
nicht, wie die Label-as-value-Variante, erst wenn der definierte Pointer 
geschrieben ist.

Ad 2) nachdem das Rätseln gross ist, wozu das Ganze: Es geht um 
automatisierte Tests, speziell um Performance-Tests. In "grossen" 
Controllern, wie z.B. bei ARM-Cortex-Prozessoren, wenn ich per PLL 
Taktraten von 100MHz und mehr erreiche, kann man den Code für einen 
Performance-Test entsprechend ein-codieren, der Performance-Einbruch ist 
vernachlässigbar. Aber was ist mit Controllern, wie z.B. einem ATTiny? 
Wenig RAM und Flash, keine freien Com-Schnittstellen. Code zur 
Performance-Messung einbauen führt das Ganze ad absurdum.

Die Lösung ist eben, in der üblichen Idle-Loop einen Trigger-Punkt zu 
haben, der immer in der Loop ausgeführt wird, unabhängig, was noch an 
Funktionalität in der Idle-Loop ausgeführt wird. Den man per Debugger 
oder Simulator automatisiert (!) einstellen und messen kann, um so Zahl 
der Loops/Zeiteinheit und die jeweilige Zeit einer Loop messen zu 
können.

Ziel: Nachweis, das ich meine Funktionen rechtzeitig abarbeiten kann, 
das die entsprechenden Zeit-Forderungen eingehalten werden können, die 
Möglichkeit, Code-Optimierungen bewerten zu können. Auch nachweisen, das 
der eingesetzte Controller richtig skaliert und nicht zu klein 
(Möglichkeit zur Fehlfunktion) oder zu gross (Kostenaspekt) ist. Oder 
eben auch nachweisen, das unter worst-case-Bedingungen der Code noch 
funktioniert und nicht zu Fehlfunktionen führt.

Vielen Dank an Stephan für die richtige Idee!

Schönen Tag noch, Thomas
Persönliche Seite #3326787
Lesenswert?

Thomas K. schrieb:
> Ziel: Nachweis, das ich meine Funktionen rechtzeitig abarbeiten kann,
> das die entsprechenden Zeit-Forderungen eingehalten werden können,

Autsch.

Mit Laufzeittests bekommst du lediglich Abschätzungen der Laufzeit nach 
unten, was aber benötigt wird sind Abschätzungen nach oben.  Da es 
i.d.R. nicht machbar ist, Zeitmessungen für jede mögliche Eingabe 
durchzuführen, führt an einer statischen Analyse nichts vorbei.
OP #3326995
Lesenswert?

Hi Johann,

Johann L. schrieb:
> Mit Laufzeittests bekommst du lediglich Abschätzungen der Laufzeit nach
> unten, was aber benötigt wird sind Abschätzungen nach oben.  Da es
> i.d.R. nicht machbar ist, Zeitmessungen für jede mögliche Eingabe
> durchzuführen, führt an einer statischen Analyse nichts vorbei.

Schon richtig. Eine Messung der Laufzeit im Komplett-Image kann 
definitiv nicht alle worst-case-Szenarien der Einzelfunktionen abdecken. 
Aber es ist eben ein Baustein von mehreren.

Wenn Du z.B. weist, resp. herausbekommst, das Du jenseits der 90% 
Prozessorauslastung bist, solltest Du zumindest eine Ahnung haben, was 
passieren könnte, wenn der Kunde mit merkwürdigen Fehlerbeschreibungen 
kommt, die keiner nachvollziehen kann. (Ok ich weiss, der Kunde kommt 
IMMER mit merkwürdigen Beschreibungen ... :-) )

Und wenn Du z.B. bei der Ausmessung der Loop-Zeit einer Idle-Loop 
entsprechend hohe Ausreisser nach oben bekommst, die jenseits dessen 
liegen, was als maximale Loop-Zeit definiert oder berechnet wurde, dann 
ist das auch schon ein Indiz dafür, das da noch Arbeit lauert.

Richtig testen ist eben auch eine Kunst für sich. :-) Schönen Tag noch, 
Thomas
Gast #3327028
Lesenswert?

Johann L. schrieb:
> Nein, ptrLabel ist global und wird mit 0 initialisiert.
>
> Das Setzen von ptrLabel geschieht erst zur Laufzeit, und dazu muß die
> Funktion, die das Label enthält, aufgerufen werden.

Da hast du natürlich Recht, für einen Zuweisung schon zur zur 
Compilezeit müsste die Initialisierung bei der Definition gemacht 
werden.

Oliver

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren