AddrVal Concept of Usage addresses with a size or value information

AddrVal Concept of Usage addresses with a size or value information

Inhalt


Topic:.AddrVal.

Last changed: 2019-09-26

Often an memory address (pointer) should be stored, but the memory address is associated with an integer value. This integer value may be typical:

In ordinary C programs the size or length information is stored anywhere other. Hence the cohesion between this two informationis are not obviously.

The header file compl_adaption.h defines (should define) a macro for definition of a struct which bundles a desired pointer type and a value in a unique form. For different platforms this definition may be slightly different. This struct should be small and compact, so some comiler uses only registers for this struct for local storage. Hence a returned per value is optimized using only CPU registers.

The other intension for the OS_PtrVal_DEF(NAME, TYPE) is offer a unique form to store this typical construct with different pointer types.


1 Definition of the definition macro in compl_adaption.h

Topic:.AddrVal.STRUCT_ADDR_VAL_emC.

The following macro defines the definition style of the pointer-value struct:

/**This macro defines a struct with a memory address to the given type and a integer number.
 * Usual it can be used to describe exactly an 1-dimensional array. The integer number is the number of elements,
 * the size in memory is (sizeof(TYPE) * numberOfElements).
 * This struct should pass with 2 register for call by value or return by value, usual supported by the compiler.
 */
#define STRUCT_ADDR_VAL_emC(NAME, TYPE) struct NAME##_T { TYPE* addr; int32 lenVal; } NAME

In some user or emC sources it can be used to define a concretely struct:

typedef STRUCT_ADDR_VAL_emC(StringJc, char const);

This C-source line defines the type StringJc. The expansion of the line with this macro is:

typedef struct StringJc_T { char const* ref; int32 val; } StringJc;

It should be clear. The use of the macro clarifies that it is such a pointer-value type. Without usage of the macro it is any ordinary struct maybe or not accordingly this pattern.

The source code should be clear. The macro nature of the following "MACRO (...)" should be recognized by capital letters. Otherwise it seems to be a function (...) type definition if the macro is not found while compiling. But then the argument list is incorrect and forces a compiler error. Hence the writing form in the source is clarified.


2 A common definition for any desired pointer type and value

Topic:.AddrVal.AddrVal_emC.

Often an undefined pointer and an associated integer value is need. In ordinary C sources a void* variable is used. But the void* has the disadvantage that no content is shown while debugging. Hence it is better to use a simple type instead void.

There is a second problem: Older implementations especially for Intel X86 CPUs support any byte position in memory as valid address (1-byte-boundary). But this is not optimal in runtime, because a 64-bit-Memory should access with a 8-byte-boundary address, adequate a 32 bit memory (for embedded processors) as 4.byte-boundary. For complatibility a char* pointer is usual on 1-byte-boundary, but a pointer to any more complex struct should be optimized to 4- or 8-byte-boundary. Hence the char const* is not a proper candidate as common pointer type instead void*. Therefore a common struct type Addr8_emC is given which contains an array of integer to view it in debug. This type is used for

/**The type AddrVal_emC handles with a address (pointer) for a 8 byte alignment. */
typedef STRUCT_ADDR_VAL_emC(AddrVal_emC, Addr8_emC);

A user source can use this type AddrVal_emC for general purpose addresses, for example (see

There are some macros to access and set the information:

The difference to MemC is: The val element is any number, not a memory size information in any case. It is a sematically difference only, not for C-syntax. But that's why the user should honor it.


3 MemC: Definition of a memory address and its size

Topic:.AddrVal..

Routines such as malloc returns a (void*) pointer to the memory area, the originally size information is given as argument for malloc, but it is stored anywhere other.

The type MemC bundles a memory address with its size. It is based on the STRUCT_ADDR_VAL_emC(...) definition, but the value is a size information for the memory area in any case. The size information counts in the sizeof(...) units, often designated as size_t. Because the STRUCT_ADDR_VAL_emC(...) definition determines the type of value maybe to int32, there is a practicable limitation of memory sizes up to 2 GByte. It is the size of one data element, never the size of a large memory area.

There are some macros to get the address with a given type, get the size etc.

There are some macros to access and set the information:

This macros should be used because it covers the internal irrelevant type of the addr element in this struct.

The difference to AddrVal is: The val element is always the size information of the memory location.


4 Different specifities of a address-value pair in a struct

Topic:.AddrVal.spec.

The definition uses the above shown macro

typedef STRUCT_ADDR_VAL_emC(MyTypeAddrVal, Type);

It is used especially and for example:

typedef STRUCT_ADDR_VAL_emC(StringJc, Type);
typedef STRUCT_ADDR_VAL_emC(int8ARRAY, int8);
typedef STRUCT_ADDR_VAL_emC(int16ARRAY, int16);
typedef STRUCT_ADDR_VAL_emC(int32ARRAY, int32);
typedef STRUCT_ADDR_VAL_emC(int64ARRAY, int64);
typedef STRUCT_ADDR_VAL_emC(floatARRAY, float);
typedef STRUCT_ADDR_VAL_emC(doubleARRAY, double);

The last ones are definitions of arrays with a size information inside the struct. It is a basicly necessity for a safety programming style to know the size of an array in its context.

The access to the elements are written direct:

int32ARRAY myArray;
myArray->addr =givenArray;
myArray->size = givenArrayNumberofelements;
....
if(ix < 0 || ix >) myArray->size) {
  THROW_s0(IndexOutOfBoundsException, "exception text", ix, myArray->size);
  ...

The rule for direct access to the STRUCT_AddrVal elements is:

In conclusion, a common MemC or a common AddrVal_emC should use access macros, all other, especially StringJc should access direct.


5 Safety check with MemC

Topic:.AddrVal..

C hat den Vorteil der Maschinennähe: Es kann alles formuliert werden, was im Maschinencodeablauf notwendig ist. Das ist die Vorausetzung dafür, dass auf Treiberebene oder hardwareorientiert notwendige Dinge programmiert werden können. Aber mit dieser Eigenschaft der Sprache ist die Programmierung in C und gleichermaßen in C++ sensibel bei nicht entdeckten Softwarefehlern.

Im folgenden wird an einem Beispiel-Softwarefehler, der dem Verfasser einige Tage Arbeit gekostet hat, gezeigt, was passieren kann. Folgend wird eine Vermeidungsstrategie im emC-Konzept vorgestellt.

Das MemC-Konzept wurde vom Verfasser schon ab ca. 2005 eingesetzt, aber jetzt innerhalb emC konsequent verbessert. Dabei stand MemC nicht Pate für emC wie man vermuten könnte. emC ist wesentlich mehr.

Das Prinzip:


6 Entstehung von Softwarefehlern mit Speicher-Querschläger

Topic:.AddrVal.error.

Der einfache Softwarefehler, ein falscher Algorithmus, ist an den Ergebnissen erkennbar und debugbar. Dafür gibt es Tests.

Problematisch wird es, wenn ein Softwarefehler nicht beim Test auffällt da er in einem falschen Speicherbereich schreibt, aber im Praxiseinsatz genau deswegen schwer nachvollziehbare Effekte hervorruft.

Wenn eine Datenstruktur

typedef struct MyType_t { int a,b,c;} MyType_s;

oder auch eine

class MyClass { int a, virtual void op(); };

mittels

MyType_s* refs = (MyType_s*) calloc(sizeof(MyType_s, 1);
MyClass* refc  = new MyClass();

angelegt wird, und ohne Casting und sonstige Besonderheiten zugegriffen wird:

refs->a = 123;

dann kann nichts passieren. Die Programme sind fehlerfrei hinzubekommen bezüglich Richtigkeit des Speicherzugriffs. Schreibfehler die bei älteren Compilern auch schonmal zu Zugriffsfehlern geführt haben werden vom Compiler erkannt und als Compilerfehler gemeldet.

Anders sieht es dagegen aus wenn Arrays im Spiel sind, deren Größe noch dazu von Parametern abhängen und/oder die Compilerfehlerprüfung mit void*-Nutzung abgeschaltet wird:

int nrofData = getItFromArguments();
int32* myData = calloc(sizeof(int32) * nrofData, 1);
...
if(ix >=0 && ix < nrofData) {
  myData[ix] = value;
}
if(ix >=0 && ix < (nrofData - (zValue>>2)) ) {
  memcpy(&myData[ix], src, zValue);
}

Im letzten Fall sollen eine beliebige Anzahl von Bytes aus einer Quelle auf das Datenfeld kopiert werden, beispielsweise um verschiedene Wertetypen zu speichern (int, float, double, complex, arrays). Daher wird memcpy benutzt. Der Index wird unmittelbar zuvor getestet. Dies sollte eine unbedingte Notwendigkeit sein und nicht nach der Designrichtlinie Design by Contract vorausgesetzt werden, dass kein falscher Index von der Anwendung oder vom davorliegenden Programm vorgegeben werden darf. Besser ist immer die Wareneingangskontrolle als Synonym/Stichwort.

Im konkreten Fall ist nun folgendes passiert: Nachdem der Algorithmus mit dem int32* myData-Array wie oben gezeigt getestet und durchprogrammiert wurde, ist die Software erweitert worden. Es sind für den Datenspeicher einige Kopfdaten hinzugekommen, so dass das Ganze etwa wie folgt aussah:

typedef struct MyData_t {
  HeadData head;
  int32 data[10];
} MyData;
.....
int nrofData = getItFromArguments();
.....
int bytesAlloc = sizeof(MyData) + (nrofData - 10) * sizeof(int32);
MyData* myData = calloc(sizeof(bytesAlloc, 1);
.....
if(ix >=0 && ix < (nrofData - (zValue>>2)) ) {
  memcpy(&myData->data[ix], src, zValue);
}

Es ist nichts dagegen einzuwenden, dass in der struct MyData ein Array mit 10 Elementen definiert wird und dann mehr als 10 Elemente benutzt werden. Dies ist beim Allokieren berücksichtigt. Der Vorteil ist, für das Debugging sieht man die ersten 10 Werte direkt in der Debuganzeige. Das Ganze ist schlüssig lesbar. Dieses Pattern kann immer empfohlen werden, wenn es sich um ein Array mit variabler Länge zur Runtime handelt. Das ist nicht das vorzustellende Problem.

Bei der memcpy-Zeile ist nur eine kleine Änderung hinzugekommen:

  memcpy(&myData[ix], src, zValue);
  memcpy(&myData->data[ix], src, zValue);

Diese Änderung wurde in der gesamten Software ordentlich durchgeführt, auf Richtigkeit getestet und - fertig. Außer, es wurde ein Softwaremodul nicht beachtet, vergessen, nicht angeschaut. Die Compilierung stellt den Fehler nicht fest. Die sich hier ergebende fälschliche Verwendung einer Nicht-Array-Referenz als Array hätte vom Compiler erkannt werden können, aber nicht in C und auch nicht in C++ (so weit bekannt). In der Sprache C wurde zu Anfang die Handhabung von Zeigerarithmetik und Array-Indizierung sehr einfach und 'anwenderfreundlich ?' designed, damals gab es noch nicht 'autocompletion', und nie verbessert. Aus heutiger Sicht ist das eigentlich nicht mehr tragbar. Aufgrund des unentdeckten Fehlers in der Quelle wurde zur Runtime über den allokierten Speicher heraus in irgendeinen danebenliegenden Speicher geschrieben.

Der Einsatz des Programms erfolge dann in einer Simulink-Umgebung mit diesen in C geschriebenen S-Functions. Das Gesamtsystem einschließlich noch anderer FBlocks mit diversen Funktionalitäten ist dann schon ein größeres System. Die Software ist modular vorgetestet (in einer Testmodul-C-Umgebung), dann für Simulink compiliert, dann im Simulink-Einsatz getestet.

Ergebnis, vieles geht, aber selten - brachialer Absturz des Simulink ohne erkennbare Verursachung.

Die erste Frage ist: Liegt es überhaupt an der eigenen Software, und an welcher Komponente. Es gibt auch schonmal die Empfehlung, ein Tool neu zu installieren bzw. den Grafiktreiber zu aktualisieren ... Es kommt also eine Unsicherheit auf. Der Absturz passiert offensichtlich nicht in direktem Zusammenhang mit den Sfunction-Blocks.

Weiterarbeit: Einkreisen (Weglassen von Teilen, wann tritt es auf) und Debuggen im 'Attache to Process'-Mode. Hierbei ist es mit Mathworks-Simulink lobenswert, dass die Arbeit des Simulink in den eigenen S-Functions auf C- oder Maschinenebene Schritt-debugged werden kann. Aber das Ganze ist sehr arbeitsaufwendig. Debuggen an sich sowieso (in welchem Heuhaufen sucht man die Nadel). Die Compilierungs- und Startzeiten sind erheblich (einige Sekunden für eine testhalber-Änderung in der C-Umgebung versus Minute(n) für das Compilieren der mex-dll und weitere Minute(n) für das Neustarten, Aufrufen des Testmodells, Einstellen der Bedingungen). Es ist keine sichtliche Korrelation zwischen dem Fehler, bei dem man nicht weiß in welchem Modul er steckt, und der Reaktion erkennbar: Absturz Simulink. Man kann nur in den Zig-Tausend Quellzeilen aller Module vorsichtigt debuggen, nicht finden, wieder ein Absturz mit Neustart.


7 Grundsatzdiskussion: Sichere Ablaufumgebung

Topic:.AddrVal.java.

Es gibt mehrere Möglichkeiten, Programme sicher zu machen:

So die Theorie. Diese Probleme sind nicht neu. Anfang der 90-ger Jahre, genauer zwischen 1990 und 1995 hat sich ein Entwicklerteam einer Aufgabe gestellt: Für die Vielzahl der damals aufkommenden und absehbaren Softwareanwendungen mit verschiedenen CPUs sollte eine einheitliche und sichere Ablaufumgebung entwickelt werden. Beim Start dieses Projektes war die Assemblerprogrammierung mit allen Problemen verbreitet, teils wurde mit Pascal, Turbo-Pascal, auch mit PL/M und solchen Dingen programmiert. Für die ablaufsichere Umgebung wurde ein Meta-Maschinencode definiert, der den Ablauf beschreibt und direkt interpretierbar ist. Bei der Interpretation können Sicherheitschecks für Speicherbereichtests berücksichtigt werden, ohne dass die Anwenderprogrammierung dies selbst (aufwändig) berücksichtigen muss. Damit ist die Ablaufumgebung auch dann sicher, wenn sich ein solcher Fehler wie oben gezeigt eingeschlichen hat und noch nicht gefunden wurde. Das Sicherheitprinzip ist: Es wird nur immer der eigene Speicherbereich beschrieben.

Als diese Entwicklung fertiggestellt war, 1995, hat sich mittlerweile C mit C++ als Programmiersystem auch für Embedded Prozessoren etabliert. Das Programmiersystem, genannt Java, passte aber auch für die aufkommende Variablilität der Internetanwendungen und hat sich also stattdessen dort verbreitet.

Später wurde dieses System 'Java' verbessert: Ein 'Just in Time'-Compiler hat den universellen Meta-Maschinencode, genannt Bytecode vor dem Start des Programms auf den Maschinencode des Zielsystems umgesetzt, damit keine Zeitverluste der Interpretation bestehen. Lediglich die Sicherheitschecks werden auch im Zielcode durchgeführt. Dabei kann man aber optimieren. In einer Schleife mit wohldefinierten Index-Werten braucht man nicht jeden Arrayzugriff zu überprüfen sondern kann sich in dem überschaubaren Bereich auf die Überprüfung der eigentlichen Eingangswerte zu Beginn der Schleife reduzieren. Damit ist der Ablauf von Software in Java systembedingt nur um wenige Prozent langsamer. Böse Zungen behaupten zwar dennoch, Java sei langsam. Der Abarbeitungs-Unterschied zwischen nicht ganz optimal programmierten C++ und Java dürfte größer sein.

Java hat um ca. 2000 einen Stiefbruder bekommen, C#. Auch andere verbreitete Programmiersysteme wie Python sind entsprechend sicher und ähnlich gestaltet.

Java und adäquate Programmiersprachen haben zwei Grenzen, die den Einsatz von C(++) notwendig machen:

Der Punkt a) bedeutet, dass C seine Berechtigung hat wenn es um Treiber und Hardware geht. Das Laufzeitsystem des Java ist selbst in C programmiert.

Der Punkt b) schränkt die Echtzeitfähigkeit ein. Pausenzeiten von einigen zig Millisekunden können vorkommen, die als Response von Serveranwendungen nicht stören, wohl aber in getakteten Systemen der Echtzeit-Signalverarbeitung. Es gibt allerdings echtzeitfähige Garbage-Collector-Lösungen, beispielsweise bei der Jamaica-VM (aicas, https:www.aicas.com).

Ergo: Für reine algorithmische Anwendungen ohne Hardwarebezug sollte man in Java, C# & co programmieren und nicht in C++ und C.

Dennoch braucht man C und C++ für die Hardware- und Treiberebene. Dabei kann man sich nicht auf einfache Algorithmen beschränken, die also insgesamt überschaubar sind, sondern es wird komplexer. Daher braucht es auch in C/++ nebst dem Codereview eine Sicherung von Ablaufumgebungen. Die Grenze ist wie folgt legbar:


8 Nebenbemerkung void* und casting

Topic:.AddrVal.voidPtr.

Es gehört zum Thema sichere Programmierung, nicht direkt zum Thema MemC:

Zeiger mit dem Type void* können mit allen Zeigern versorgt werden. Das schafft eine hohe Universalität, die aber den Compiler-Typecheck ausschaltet. void*-Zeiger sollte also nur in wohlbegründeten Ausnahmefällen benutzt werden. Zu einer Verwendung von void* muss es immer entweder eine einfache markante Verwendungsintension geben, bei memcpy sind die Zeiger die direkten Speicheradressen. Das ist eine einfache Verwendungsintension. Oder es muss eine klare Zusatzangabe zur Verwendung des Zeigers geben, beispielsweise ein enum-Wert zur Auswahl spezifischer Castings. Das sollte aber nur bei unbedingter Notwendigkeit erfolgen. Für die Vereinfachung der Parameterübergabe zu komplexen Anwendungsfunktionen ist void* teils verbreitet, aber nicht zu empfehlen.

In C++ gibt es das static_cast<Type*> und dynamic_cast<Type*>. Das erstere ist immer einsetzbar, das dynamic.. nur dann wenn der Typ aufgrund weiterer Angaben gesichert ist. Das ist eine ähnliche aber abgeschwächte Variante der void* -Problematik. In Java & co sind diese beiden cast-Formen erlaubt, wobei Java beim dynamic-cast immer den Typ zur Laufzeit testet und keine Fehler zulässt, kostet ein Minimum an Rechenzeit. Der Typ ist bekannt aufgrund der Basisklasse java.lang.Object und der darin enthaltenen Type-Referenz. Das reinterpret_cast<Type*> in C++ entspricht dem einfachen C-cast. Damit ist es als Problemstelle so notiert gut erkennbar. Das ist aber nicht der Grund der Einteilung der cast-Formen in C++. Der Grund für static_cast und dynamic_cast ist vielmehr: Bei virtuellen Methoden in einer class und Mehrfachvererbung wird der Zeigerwert entsprechend angepasst. Ohne static_cast oder dynamic_cast, also mit der einfachen (Type*)myRef - Schreibweise gibt es für C++-classes gröbste Fehler.

Ein cast sollte immer im Comment begründet werden. Es sollte nur eingesetzt werden, wenn es nicht vermeidbar ist. Diverse SIL-Vorschriften (Software mit Sicherheitslevel) sind im einen oder anderen Fall zu beachten.


9 Sicherheitscheck zur Rumtime in emC mit MemC

Topic:.AddrVal.usg.

Das Prinzip:


9.1 Definition einer typisierten MemC-Struktur

Topic:.AddrVal.usg.STRUCT_ADDR_VAL_emC.

Mit Verwendung des Makros

STRUCT_ADDR_VAL_emC(MyType) myData;

wird eine MemC-Struktur typgerecht angelegt. Es entspricht fast der einfachen Referenzdeklaration MyType* myData. Man kann auf die Referenz zugreifen mit

myData.ref->...

anstelle des direkten myData->.... Die Anwendungssoftware ändert sich kaum mit dem Einführen einer MemC-Struktur statt einer Referenz.

Man kann die typisierte MemC-struct auch ohne Makro anlegen. Makros sind teils nicht zu empfehlen, da sie verdecken, was der Compiler tatsächlich sieht. Andererseits vereinfachen sie die Schreibweise und auch die Lesbarkeit wenn das Makro bekannt ist.

struct MemC_MyType_t { MyType* ref; intptr_t size; } myData;

Es ist hierbei zu empfehlen, die Bezeichnungen ref und size so zu verwenden. Die Verwendung intptr_t kommt daher, eine reguläre struct in 2 Registern der Adressierungsbreite (64 bit für High-End-Anwendungen) zu verwenden und das Alignment für optimalen Speicherzugriff einzuhalten. Der Platzbedarf gegenüber einem 32-bit-int ist meist kein Problem. Für einen 'normalen' 32-bit-Prozessor in einer Embedded-Anwendung sind dies je 32 bit, auch für einen 16-bit-Prozessor mit 32-bit-Adressraum.

Man kann mit

typedef STRUCT_ADDR_VAL_emC(MyType) Mem_MyType;

auch einen wiederverwendeten Anwendertyp mit MemC-Ansatz definieren und direkt verwenden. Das ist zu empfehlen, wenn die Daten als MemC weitergegeben werden:

Mem_MyType myRefM;
...
Mem_MyType myRef2;  //anywhere other
...
myRef2 = myRef;     //association of references

Die Programmierung ist identisch mit der unter Verwendung einfacher Referenzen, eine Zuweisung für Assoziationen. Es wird hierbei ref und size kopiert.


9.2 Die Standard MemC-Definition

Topic:.AddrVal.usg.MemCdef.

In emc/source/MemC_emC.h wird definiert:

typedef STRUCT_ADDR_VAL_emC(MemUnit) MemC;

Der Type MemUnit ist in der jeweiligen compl_adaption.h so definiert dass er einem Adress-Schritt entspricht. Das ist meist der char-Typ. Bei einigen Prozessoren aber beispielsweise auch ein int-Typ. Man kann daher mit der ref in einer MemC-definition direkt Adressrechnungen ausführen. Das ist nicht für den User-Bereich gedacht sondern für die check-Funktionen.

Die Standard-MemC-Definition soll also nicht frei in Anwenderprogrammen verwendet werden sondern stellt lediglich die unifizierte Darstellung eines beliebig getypten Zeigers mit der Länge für MemC-Systemfunktionen dar. Bei einem check-Aufruf:

void* addr = getSpecialAddress(myRefM.ref, further_Args);
checkAddress_MemC(&myRefM, addr, nrofBytes)

wird die Referenz auf eine anwenderdefinierte typisierte MemC-struct entsprechend dem Beispiel oben als void*-Zeiger intern einen untypisierten MemC zugeordnet. So kann die Beliebigkeit der typisierten MemC-Definitionen einheitlich verarbeitet werden. Die Verwendung eines void* im Argument der checkAddress_MemC(...)-Routinen kann damit begründet werden und ist eine einfache markante Verwendungsintension nach den Ausführungen in Topic:.MemC.voidPtr.. Ein Fehler der Zuweisung wird zur Laufzeit sehr wahrscheinlich erkannt da bei beliebig falschen Daten die check-Funktionen sehr wahrscheinlich false erkennt und mit der entsprechenden Exception reagiert. Die Checkfunktion wird in Topic:.MemC.usg.check. vorgestellt.


9.3 Speicherallokierung mit MemC

Topic:.AddrVal.usg..

Für die Allokierung steht ein Makro bereit:

ALLOC_MemC(myData, sizeof(MyType));
ALLOC_MemC(myData, sizeof(*myData->ref));

Die zweite Form entnimmt den Typ von der Definition der Referenz und vermeidet damit zusätzlich Verwechslungs-Fehler. Insbesondere kann aber die allokierte Länge dynamisch sein wenn Arrays mit parameterabhängigen Längen im Spiel sind.

Das Makro ist definiert mit

#define ALLOC_MemC(M, SIZE) { *(void**)(&(M).ref) = alloc_MemC(SIZE); (M).size = SIZE; }

Das casting der Referenz auf void* ist hier zulässig weil beim Allokieren zunächst nur eine Speicheradresse entsteht. Die Richtigkeit der Typisierung kann letzlich nur aufgrund der angegebenen size sichergestellt werden. Das ist bei allen Allokierungen so. Auch beim new in C++ wird intern die size der class als Argument dem low-level-Allokierungsaufruf übergeben.

Die alternative Nicht-Makro-Version dazu kann wie folgt aussehen:

int sizeAlloc = sizeof(MyType);
myData.ref = (MyType*) alloc_MemC(sizeAlloc);
myData.size = sizeAlloc;

Wichtig ist, dass der selbe Wert für size gespeichert wird der auch beim Allokieren angebenen wurde.

Das Allokieren mit alloc_MemC(...) entspricht etwa dem bekannten malloc mit folgenden Unterschieden:

Damit ist unabhängig von der unmittelbaren Prüfung der size und der Adresse beim Speicherzugriff ein gewisser Sicherheitscheck vorhanden.


9.4 Check direkt beim Zugriff auf den Speicher

Topic:.AddrVal.usg.check.

Bei einem einfachen Datenzugriff über wohldefinierte Elemente einer struct oder class braucht man nicht extra checken, da eine falsche Adresse sich nicht ergeben kann wenn die size beim Allokieren stimmt und kein falsches casting verwendet wird:

myData.ref->member = 124; //simple, without check

Dagegen ist eine Prüfung der Zugriffsadresse dann zweckmäßig, wenn beispielsweise in einer inneren Routine die Herkunft der Daten nicht genügend bekannt ist:

void myRoutine(float* dataAddr) {
  dataAddr[0] = p1;
  dataAddr{1] = p2;
}

In einer Definition über Design by Contract würde hier stehen, dass eine gültige Adresse mit 2 Elementen belegt wird. Aber wer sichert in dieser Routine, dass die Adresse richtig ist. Es hängt vom Aufrufer ab. Dazwischen kann sich ein Casting, eine Array-Indizierung oder sonst etwas befinden. Das Problem an Softwarefehlern ist, dass sie nicht bekannt sind. Sie schleichen sich ein. Besser ist es daher, wie folgt zu arbeiten:

void myRoutine(void* memC, float* dataAddr) {
  if(checkAddress_MemC(memC, dataAddr, 8)) {
    dataAddr[0] = p1;
    dataAddr{1] = p2;
  }
}

Das Design by Contract regelt in diesem Fall, dass zusätzlich der Speicherbereich der Daten angegeben werden muss, und zwar als Zeiger auf eine typisierte MemC-Struktur. Dieses Aufrufargument sollte direkt aus dem Alloc- oder Definitionsteil der Daten kommen ohne Zwischenverarbeitung. Damit ist einfach Quelltext-reviewbar, dass dies korrekt ist. Die Übergabe als void* ist hier notwendig, da der Zeigertyp auf einen Allgemeintyp für checkAddress_MemC(...) reduziert werden muss. Er ist nicht kritisch, da direkt auf die korrekte oder vermeintliche MemC zugegriffen wird und nur ein unwahrscheinlicher Fehler nicht entdeckt wird.

Die Routine arbeitet inline und damit im Nicht-Fehlerfall rechenezeitoptimiert:

inline bool checkAddress_MemC(void* memC, void* addr, int nrofBytes){
  MemC* mem = (MemC*)memC;   //Note: the mem as param can have any Type of reference.
  if (addr >= mem->ref && addr <= (mem->ref + mem->size - nrofBytes)) return true;
  else {
    __errorAddress_MemC(mem, addr, nrofBytes);
    return false;
  }
}

Nur im Fehlerfall wird zu einer Subroutine geprungen, die eine Exception aufruft. Die Exception kann entsprechend der Zielsystemdefinition in applstdef_emC.h entweder ein C++- throw sein, damit in eine Fehlerbehandlung einlaufen, oder das Programm beenden, oder auch lediglich einen Fehlervermerk ausgegeben und dennoch weiterlaufen. Daher wird hier der Zugriff auf dataAddr in die if(check...) eingefasst. Der Zugriff wird also vermieden, das Programm aber nicht angehalten. Damit sind freilich Folgefehler vorhanden, aber kein Überschreiben falscher Speicherbereiche. Man kann/sollte in die __errorAddress_MemC(...)-Routine einen Debug-Break setzen. Zur Fehlerbehandlung siehe ThCxt-ExcH_emC.html.


9.5 memcpy mit doppeltem Boden

Topic:.AddrVal.usg.memcpy.

Ein memcpy ist ein lo-level-Speicherzugriff und im Treiber-, Hardware- und Spezialalgorithmus-Bereich durchaus notwendig. In diesen Bereichen ist aber häufig die Adressbildung komplex, insbesondere wenn Daten aufbereitet werden in Telegrammen und dergleichen. Beim memcpy direkt ist jeglicher Check abgeschaltet. Lediglich die hardwareseitige Speicherüberwachung (Protected Mode) kann grobe Fehler erkennen.

Ist der Speicherbereich über eine typisierte oder nicht typisierte MemC-Struktur beschrieben mit Adresse und Länge, unabhängig von der Komplexität der Adressbildung intern in diesem Speicherbereich, dann kann dies einfach genutzt werden für ein gesichertes memcpy:

memcpy_MemC(&myData, addr, src, len);

Gegenüber dem normalen memcpy gibt es lediglich noch die Angabe des MemC-Objektes das wie beim checkAddress_MemC(...) durchgereicht und gut reviewbar ist.

Die interne Realisierung ist ähnlich wie checkAddress_MemC nur dass das memcpy implizit gerufen wird:

inline void memcpy_MemC(void* memC, void* addr, void const* src, int size) {
  MemC* mem = (MemC*)memC;
  if (addr < mem->ref || addr >= (mem->ref + mem->size - size)) {
    __errorAddress_MemC(mem, addr, size);
  } else {
    memcpy(addr, src, size);
  }
}

10 Fazit

Topic:.AddrVal.fazit.

Nach Einbau dieser Sicherheitsmechanismen wurde der oben beschriebene Softwarefehler entdeckt.

Die Sicherheitsmechanismen sind weder rechenzeitintensiv noch arbeitsaufwändig. Es lohnt sich an kritischen Stellen oder fast überall mit diesen Mechanismen zu arbeiten. Zumeist ist die Arbeitsaufwand eines Speicherquerschreiber-Softwarefehlers hoch, die Wahrscheinlichkeit dessen Aufretens bei intensiven Codereview zwar gering, aber das Produkt des Auftretens mit dem Aufwand höher als die konsequente Verwendung des MemC zumindestens an kritischen Stellen in C-Programmen.

Für C++ in Zusammenhang mit new muss der operator new entsprechend überladen werden, das wurde noch nicht ausgeführt. Häufig werden aber komplexe Daten-Zusammenstellungen auch in C++ mit struct gehandhabt.

In C(++) hat man keinen doppelten Boden, man muss sich diesen selbst schaffen.