Umstieg von C auf C++

bei der Programmierung von Microcontrollern

I/O Register

/* C-Source */
uint8_t c;
typedef uint32_t volatile device_register;

// use pointer-placement to declare a POINTER to the device register
device_register *const UTXBUF0 = (device_register *)0x03ffd00c;

*UTXBUF0 = c;	// OK: send value of c to the UART
/* C++-Source */
uint8_t c;
typedef uint32_t volatile device_register;

// use REFERENCE-placement instead of pointer-placement
device_register &UTXBUF0 = *(device_register *)0x03ffd00c;

// now we can use UTXBUF0 as the register itself, not a pointer
UTXBUF0 = c;	// OK: send value of c to the UART

C und C++ gemischt

Wenn man z.B. eine Library so gestalten möchte, dass sie sowohl mit einem C als auch mit einem C++ Compiler verwendet werden kann, muss man ganz zu Beginn des Header files (in dem alle relevanten Funktions- prototypen aufgeführt sind) folgenden Code einfügen:

#ifdef __cplusplus
	extern "C" {
#endif

und ganz am Schluss der Datei

#ifdef __cplusplus
}
#endif

Fremden C-Code einbinden (z.B. Library)

extern "C" void func1(int);
// oder für mehrere
extern "C" {
	int func2(char *s);
	double func3(int x);
}

im include File (oder wo immer die Funktionsprototypen stehen) verwenden. Am Code der jeweiligen Funktionen muss nichts verändert werden - allerdings daran denken dass für diese nun C++ Regeln gelten.

C++ Funktionen für Verwendung in C exportieren

Eine Deklaration mit

extern "C" void func1(int);

verhindert, dass der Compiler die Symbolnamen durch die Mangel dreht. Dies ist z.B. bei Interrupthandlern zwingend notwendig.

Compiler Flags

Da sich Ausnahmebehandlung und RTTI schlecht ohne dynamische Speicherverwaltung realtisieren lassen, sollte man darauf verzichten. Dem Compiler kann man das mit folgenden Flags mitteilen:

-fno-exceptions -fno-non-call-exceptions -fno-rtti

Globale Destruktoren und anderen Code zum Programmausstieg kann man mit

-fno-use-cxa-atexit

abschalten.

-ffreestanding # programm hat evtl kein main()
-fno-common # sicherstellen dass globale variablen # nur einmal deklariert werden
-ffunction-sections und -fdata-sections

stellen sicher, dass Funktionen und Daten in eigenen ELF-Sektionen landen. Zusammen mit --gc-sections kann unbenutzer Code besser entfernt werden.

-finline-small-functions

Wenn man mit -Os compiliert, zieht der Compiler in Erwägung, kleine Funktionen dann zu inlinen, wenn sie weniger Instruktionen enthalten als der Aufruf verursachen würde.

-findirect-inlining

Der Compiler berücksichtigt verschachteltes inlining besser. z.B. wenn main() func1() aufruft und diese wiederum func2() und beide Kandidaten zum inlinen wären...

Hinweis: Die Flags sind nicht nur für C++, sondern auch für C verwendbar.

-fstack-protector-strong

macht durchaus auch auf embedded Systemen Sinn, um Stack overflows (besser) erkennen zu können. Details dazu siehe weiter unten.

Verwendung globaler Konstruktoren

Um globale Konstruktoren verwenden zu können, müssen ein paar Vorbereitungen getroffen werden (zumindest in einer Bare-Metal Umgebung). GCC plaziert diese in einem Array mit dem Symbol .init_array.

Deshalb muss man im Linker Script sicherstellen, dass dieses ebenfalls im Flash landet und Symbole für den Zugriff definiert werden:

. = ALIGN(4);
.init_array :
{
	__init_array_start = .;
	KEEP (*(.init_array*))
	__init_array_end = .;
} > FLASH

Im Startup Code müssen diese dann aufgerufen werden:

static void call_constructors()
{
	extern void (*__init_array_start)();	// defined in linker script
	extern void (*__init_array_end)();	// defined in linker script

	// Call each function in the init_array.
	// We have to take the address of the symbols, as __init_array_start
	// *is* the first function pointer, not the address of it.
	for (void (**p)() = &__init_array_start; p < &__init_array_end; ++p) {
		(*p)();
	}
}

Eine andere Frage ist, wann diese Konstruktoren aufgerufen werden sollten. Wenn dies vor der Initialisierung der Hardware geschieht, muss man dies bei der Implementierung der jeweiligen Konstruktoren berücksichtigen.

Vererbung

Vererbung und Laufzeit polymorphismus sind eine nützliche Sache. Allerdings setzt dies das Vorhandensein des operator delete vorraus (sobald wir in einer Basisklasse einen virtuellen Destruktor definieren), sogar dann wenn wir nie ein Objekt dieser Klasse auf dem Heap anlegen (und das vielleicht auch gar nicht wollen). Die libgcc geht einfach von einem "normalen" Unix-Userspace aus. Daher müssen wir eine solche Funktion definieren. Wenn wir keine dynamische Speicherverwaltung implementiert haben, kann man annehmen dass der Aufruf des delete operator ein schwerwiegender Fehler ist.

// Für C++11 oder älter
void operator delete(void* p)
{
	PANIC("delete called on pointer %p (was an object heap-allocated?)", p);
}

// Für C++14
// (siehe http://en.cppreference.com/w/cpp/memory/new/operator_delete)
void operator delete(void* p, size_t t)
{
	PANIC("delete called on pointer %p, size %zu", p, t);
}

Wenn das Objekt auf dem Stack angelegt wird, wird eine Variante des Destrukors ohne den delete Operators aufgerufen.

Noch besser, man vermeidet virtuelle Destruktoren ganz, denn sie führen auch dazu, dass eine vtable angelegt wird (vergrössert code).

Was man vermeiden sollte

Exceptions

Das Exception-Handling von C++ benötigt dynamische Speicher allocation, die wir meist nicht haben (es sei denn, wir verwenden glibc - aber dann explodiert die Codegrösse).

Stack overflow erkennen

Der Compiler kann uns dabei unterstützen, mit relativ wenig Overhead (eine Lese- und eine Schreiboperation pro Funktionsaufruf ?) relativ sicher Stack überläufe zu erkennen:

int func1(int l)
{
	uintptr_t _canary = __stack_chk_guard;
	char array_on_stack[16];

	// ... read and write to the array ...

	if (_canary != __stack_chk_guard) {
		// We accidently corrupted the stack
		__stack_chk_fail();
	}
}

Mit den verschiedenen -fstack-protector-xxx Varianten fügt der Compiler den entsprechenden Code automatisch bei Bedarf den Funktionen hinzu. Dazu muss man allerdings diese beiden Symbole definieren:

extern "C" {
	// our canary value
	extern const uintptr_t __stack_chk_guard = 0xcafebabe;

	// Called if the check fails
	[[noreturn]]
	void __stack_chk_fail()
	{
		PANIC("Stack overrun!");
	}
}

den Rest erledigt der Compiler! Erwähnenswert ist dabei, dass ein korrupter Stack nicht erkannt wird, wenn wir zwar über die Grenzen des Arrays schreiben, dabei aber den canary auslassen...