Vorsicht mit Arrayindices

Für alles, was in den übrigen Lazarusthemen keinen Platz, aber mit Lazarus zutun hat.
Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Vorsicht mit Arrayindices

Beitrag von Warf »

In einem anderen Thema hier kamen wir auf die gefahren von Arrays mit startindices zu sprechen. Hiermit Lager ich das ganze in ein separates Thema aus.

Kurze Zusammenfassung vom vorherigen Thema: Statische Arrays werden über eine Range StartIndex..Endindex angegeben. Oftmals schreibt der Entwickler der nicht so oft mit Statischen Arrays arbeitet seine For schleifen allerdings

Code: Alles auswählen

for i:=0 to Length(Arr)-1 do
  Arr[i] := XYZ;


Das Problem ist jetzt aber, ohne range Checks frisst das der Computer meist ohne zu murren auch wenn der Array nicht mit 0 anfängt. Das kann zu Fehlern führen die man nicht erwartet. Hier sind ein paar Beispiele (Getestet auf Mac OSX High Sierra, FPC 3.0.4, x86-64, ist hochgradig Plattform abbhängig):

Code: Alles auswählen

program test;
 
{$Mode ObjFPC}{$H+}
 
type
  TFoo = class
  private
    FSomeValue: Integer;
    FSomeArray: Array[17..20] of Integer;
  public
    constructor Create();
    procedure Bar();
    property SomeValue: Integer read FSomeValue;
  end;
 
constructor TFoo.Create();
begin
  FSomeValue := 42;
end;
 
procedure TFoo.Bar();
var i: Integer;
begin
  for i:=0 to Length(FSomeArray)-1 do
    FSomeArray[i] := -1;
end;
 
var
  foo: TFoo;
  bar: TFoo;
begin
  foo := TFoo.Create;
  bar := TFoo.Create;
  try
    bar.bar;
    WriteLn(foo.SomeValue);
  finally
    foo.Free;
    bar.Free;
  end;
end.

Ausgabe: -1
Bei diesem Code wird die SomeValue Variable von Foo durch Code von Bar geändert, obwohl diese beiden Objekte eigentlich nichts voneinander wissen (dürfen und sollen), nur weil zufällig die beiden Objekte nebeneinander gelegt wurden.

Der Pascal Memory manager setzt memory blöcke verschiedener Klassen weit auseinander. Wenn man jetzt allerdings z.B. den C memory manager verwendet (was auch öfter mal vorkommt) wird es noch lustiger:

Code: Alles auswählen

program test;
 
{$Mode ObjFPC}{$H+}
 
uses
  cmem;
 
type
  TFoo = class
  private
    SomeOtherStuff1: array[0..10] of Integer;
    FSomeValue: Integer;
    SomeOtherStuff2: array[0..10] of Integer;
  public
    constructor Create();
    property SomeValue: Integer read FSomeValue;
  end;
 
  TBar = class
  private
    FSomeArray: Array[20..30] of Integer;
  public
    constructor Create();
  end;
 
constructor TFoo.Create();
begin
  FSomeValue := 42;
end;
 
constructor TBar.Create();
var i: Integer;
begin
  for i:=0 to Length(FSomeArray)-1 do
    FSomeArray[i] := -1;
end;
 
var
  foo: TFoo;
  bar: TBar;
begin
  foo := TFoo.Create;
  bar := TBar.Create;
  try
    WriteLn(Foo.SomeValue);
  finally
    foo.Free;
    bar.Free;
  end;
end.

Ausgabe: -1

Jetzt kann das Objekt der Klasse Foo den Speicher von einem Objekt der Klasse Bar verändern. Selbst wenn Foo und Bar in komplett unterschiedlichen Units definiert wären wäre dies Möglich.

Wir können auch uns lokal den Speicher komplett kaputt machen, z.B. diese Endlosschleife:

Code: Alles auswählen

program test;
var
  i: Integer;
  arr: array[1..10] of Integer;
begin
  for i:=0 to length(arr)-1 do
    arr[i]:=-1;
end.


Und wenn wir eine Globale Variable haben können wir alle anderen Globalen Variablen kaputt machen:
SomeTestUnit.pas:

Code: Alles auswählen

Unit SomeTestUnit;
 
interface
 
var SomeVal: Integer;
 
implementation
 
end.


Test.pas:

Code: Alles auswählen

program test;
 
uses
  SomeTestUnit;
 
var
  arr: array[20..50] of Integer;
  i: Integer;
begin
  for i:=0 to Length(arr)-1 do arr[i] := -1;
  WriteLn(SomeVal);
end.

Ausgabe: -1

------------------------------------------------------------------------------------------------------------

Worauf ich hinaus möchte ist, es kann sehr viel kaputt gehen. Um genau zu sein kann man durch die Startindex unterschiede jede beliebige stelle im Speicher treffen. All diese Beispiele oben laufen ohne Fehlermeldung ab (solange range Checks aus sind) und weder Klassen kapeslung bringt etwas, noch verschiedene Units, oder ähnliches bringen etwas. Das einzige was man dagegen tuen kann ist die Funktionen Low und High zu verwenden. Das Problem ist allerdings wer neu in Pascal ist (z.B. von Java kommt) weiß das natürlich nicht. In java iteriert man immer von 0 bis n-1. Dann reicht ein ungünstig gewählter Startindex, und etwas Pech im Memory mapping, und das was ich mir oben als Beispiele zusammengehackt hat wird Wirklichkeit. Und das plötzlich in einer Total fremden Klasse aus einer ganz anderen Unit ein wert geändert wurde, ist das finden dieses Fehlers sehr aufwendig. Wenn man noch Threading verwendet wird das ganze sogar noch weniger deterministisch, und das Fehler finden kann man praktisch komplett vergessen, da die resultierenden Fehler nicht reproduzierbar sind.

Man selbst kann zwar immer Low und High verwenden, allerdings ist man sicher das jeder der am Projekt mitwirkt dies auch tut.

Darum nun mein Appell an all die Pascal Programmierer hier, bitte fangt eure Arrays doch mit 0 an.

Timm Thaler
Beiträge: 1224
Registriert: So 20. Mär 2016, 22:14
OS, Lazarus, FPC: Win7-64bit Laz1.9.0 FPC3.1.1 für Win, RPi, AVR embedded
CPU-Target: Raspberry Pi 3

Re: Vorsicht mit Arrayindices

Beitrag von Timm Thaler »

Vergiss es!

Wenn ich Arrays immer mit Null anfangen müsste, könnte ich ja gleich so einen retardierten Quatsch wie C nehmen.

Oder wie es ein Freund oft auszudrücken pflegt: Man muss sich ja nicht nach unten orientieren.

Und warum zum Henker sollte ich range checks ausschalten?

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2636
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Vorsicht mit Arrayindices

Beitrag von m.fuchs »

Warf hat geschrieben:All diese Beispiele oben laufen ohne Fehlermeldung ab (solange range Checks aus sind)

In den Unittests sind sie aber nicht aus. Und schwupps schlägt der Test fehl.

Code: Alles auswählen

Range check error
Exception class: ERangeError
at   $000000000046A3F8 line 41 of testcase1.pas


So, welches Problem ist jetzt noch zu lösen?
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

siro
Beiträge: 730
Registriert: Di 23. Aug 2016, 14:25
OS, Lazarus, FPC: Windows 11
CPU-Target: 64Bit
Wohnort: Berlin

Re: Vorsicht mit Arrayindices

Beitrag von siro »

Ich hab in meinem ganzen Leben noch kein Array gehabt, was nicht bei 0 angefangen hat. Programmiere schon 30 Jahre
wo benötigt man denn so etwas ?
Ich hätte auch zu viel "Angst" dass die Speicherzugriffe auf das Array "etwas" unkontrollert erfolgen könnten....
auf Deutsch, ich trau der Sache nicht übern weg und hab auch keine wirkliche Anwendung dafür

Siro
Grüße von Siro
Bevor ich "C" ertragen muß, nehm ich lieber Lazarus...

mischi
Beiträge: 206
Registriert: Di 10. Nov 2009, 18:49
OS, Lazarus, FPC: macOS, 10.13, lazarus 1.8.x, fpc 3.0.x
CPU-Target: 32Bit/64bit

Re: Vorsicht mit Arrayindices

Beitrag von mischi »

siro hat geschrieben:Ich hab in meinem ganzen Leben noch kein Array gehabt, was nicht bei 0 angefangen hat. Programmiere schon 30 Jahre
wo benötigt man denn so etwas ?
Ich hätte auch zu viel "Angst" dass die Speicherzugriffe auf das Array "etwas" unkontrollert erfolgen könnten....
auf Deutsch, ich trau der Sache nicht übern weg und hab auch keine wirkliche Anwendung dafür

Siro

Wie ich schon im Vorgänger-Thread erwähnte, fangen in Fortran alle Arrays mit 1 an. Fortran wird zwar von den meisten als Nischensprache der Physik angesehen, aber es geht mir nur um ein Beispiel. Die Elemente einer Matrix in der Mathematik werden auch meistens mit 1 beginnend benannt. Die Stockwerke eines mehrgliedrigen Gebäudekomplexes an einem Hang fangen nicht unbedingt mit Erdgeschoss an. Auch bei anderen Bauten hat das Kellergeschoss die -1. Aus der Praxis gibt es da schon einige Beispiele, wo die Null nicht der naheliegende Start ist. Dass sich Arr[0] als Pointer-Ersatz durchgesetzt hat, sollte mit der Todesstrafe geahndet werden ;-)
MiSchi macht die fink-Pakete

siro
Beiträge: 730
Registriert: Di 23. Aug 2016, 14:25
OS, Lazarus, FPC: Windows 11
CPU-Target: 64Bit
Wohnort: Berlin

Re: Vorsicht mit Arrayindices

Beitrag von siro »

Ich habe mich bisher darauf verlassen und spekuliere auch damit, dass Array[0] IMMER der Start des Speichers ist. :oops:
Das liegt vermutlich daran, das ich früher ausschließlich und heute auch noch viel in Assembler programmiere.

Das Fortran mit 1 als index beginnt wuste ich nicht. Da muss man natürlich schon aufpassen.

Wenn das Verhalten "eindeutig" in der Programmiersprache festgelegt ist, dann sollte man sich natürlich auch auf die "richtige" Funktionsweise verlassen können und wenn man der Meinung ist,
dass es zur Übersichtlichkeit beiträgt, sollte man es wohl auch benutzen.
Viele Wege führen ja bekanntlich zum Ziel und es ist sicher eine "persönliche" Note wie ein Array angelegt bzw. verwaltet wird.

Wenn ich das richtig in Erinnerung habe, liegt bei der neueren Stringverwaltung ja auch einiges an "Verwaltung" unter dem
eigentlichen Array[0].

Es gibt also sicher berechtigte Anwendungen dafür.
Grüße von Siro
Bevor ich "C" ertragen muß, nehm ich lieber Lazarus...

Timm Thaler
Beiträge: 1224
Registriert: So 20. Mär 2016, 22:14
OS, Lazarus, FPC: Win7-64bit Laz1.9.0 FPC3.1.1 für Win, RPi, AVR embedded
CPU-Target: Raspberry Pi 3

Re: Vorsicht mit Arrayindices

Beitrag von Timm Thaler »

Monat 1..12
Tag 1..31
Wochentag 1..7 aus RTC
Heizung Profil Wochentag [1..7, 1..6]
Sensoren Kanäle 1..8, 9..16, 17..24
Pumpen 1..10
Phase U, I, P 1..3

Ja natürlich kann man das alles auf Null umrechnen. Man kanns aber auch lassen. Und funktioniert alles auch auf dem AVR.

Zum Beispiel werden Messkanäle lesbar als 1..24 per RS232 übertragen. Da muss ich im AVR von 0 auf 1 umrechnen, und am PC wieder von 1 auf 0. Nicht nur dass das unnötig ist, es sorgt auch regelmäßig für Verwirrung: Das was im AVR Kanal 0 ist, ist bei der Übertragung dann Kanal 1, im Programm am PC wieder Kanal 0 und beim Speichern in der CSV bekommt es wieder Kanal 1.

Nee, da bin ich froh weg von C zu sein und mit Pascal diesen Krampf nicht mehr zu haben. Eine Hochsprache soll sich ja dem Menschen anpassen, sonst kann ich gleich in Assembler schreiben.

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Vorsicht mit Arrayindices

Beitrag von Warf »

m.fuchs hat geschrieben:
Warf hat geschrieben:All diese Beispiele oben laufen ohne Fehlermeldung ab (solange range Checks aus sind)

In den Unittests sind sie aber nicht aus. Und schwupps schlägt der Test fehl.

Code: Alles auswählen

Range check error
Exception class: ERangeError
at   $000000000046A3F8 line 41 of testcase1.pas


So, welches Problem ist jetzt noch zu lösen?


Manchmal deaktiviert man die Range Checks aber auch, z.B. Wenn man nicht möchte das das einen false positive wirft: Move(arr[0], dst[0], len) für len = 0. z.B. In einem Daten parser oder Netzwerkprotokoll nahezu unvermeidliche

Da ist es oft einfacher rangechecks für diese Unit auszuschalten

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2636
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Vorsicht mit Arrayindices

Beitrag von m.fuchs »

Du kannst sie ja auch deaktivieren, aber nicht in den Unittests. Damit sicherst die Korrektheit deines Quellcodes auf unterster Ebene. Wenn es da nicht zu Problemen kommt, dann läuft auch darüber alles.
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Vorsicht mit Arrayindices

Beitrag von Warf »

m.fuchs hat geschrieben:Du kannst sie ja auch deaktivieren, aber nicht in den Unittests. Damit sicherst die Korrektheit deines Quellcodes auf unterster Ebene. Wenn es da nicht zu Problemen kommt, dann läuft auch darüber alles.


Nicht mal über Compiler Switches? Das würde mich nämlich stark wundern

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2636
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Vorsicht mit Arrayindices

Beitrag von m.fuchs »

Ah, das klang missverständlich. Mit "du kannst sie ja auch deaktivieren" meine ich, dass du das gerne in deinem fertigen Programm machen kannst/darfst. In den Tests sollen sie aber immer an sein.
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Vorsicht mit Arrayindices

Beitrag von Warf »

Timm Thaler hat geschrieben:Monat 1..12
Tag 1..31
Wochentag 1..7 aus RTC
Heizung Profil Wochentag [1..7, 1..6]
Sensoren Kanäle 1..8, 9..16, 17..24
Pumpen 1..10
Phase U, I, P 1..3

Ja natürlich kann man das alles auf Null umrechnen. Man kanns aber auch lassen. Und funktioniert alles auch auf dem AVR.

Zum Beispiel werden Messkanäle lesbar als 1..24 per RS232 übertragen. Da muss ich im AVR von 0 auf 1 umrechnen, und am PC wieder von 1 auf 0. Nicht nur dass das unnötig ist, es sorgt auch regelmäßig für Verwirrung: Das was im AVR Kanal 0 ist, ist bei der Übertragung dann Kanal 1, im Programm am PC wieder Kanal 0 und beim Speichern in der CSV bekommt es wieder Kanal 1.

Nee, da bin ich froh weg von C zu sein und mit Pascal diesen Krampf nicht mehr zu haben. Eine Hochsprache soll sich ja dem Menschen anpassen, sonst kann ich gleich in Assembler schreiben.


Aber auch eine kleine Verschiebung wie die um 1 kann schwere Fehler haben. Die oben gezeigte Endlosschleife ist da nur ein Beispiel
Du kannst damit Parameter, andere lokale Variablen sowie die rücksprungaddresse oder den Self Pointer überschreiben, da die lokalen Variablen auf dem Stack alle nah bei einander liegen

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2636
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Vorsicht mit Arrayindices

Beitrag von m.fuchs »

Warf hat geschrieben:Aber auch eine kleine Verschiebung wie die um 1 kann schwere Fehler haben. Die oben gezeigte Endlosschleife ist da nur ein Beispiel
Du kannst damit Parameter, andere lokale Variablen sowie die rücksprungaddresse oder den Self Pointer überschreiben, da die lokalen Variablen auf dem Stack alle nah bei einander liegen

Das kann dir alles aber auch bei null-basierten Arrayindices passieren. Mit dem so beliebten Fehler:

Code: Alles auswählen

for i := 0 to Length(MyArray) do (*...*)
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

mischi
Beiträge: 206
Registriert: Di 10. Nov 2009, 18:49
OS, Lazarus, FPC: macOS, 10.13, lazarus 1.8.x, fpc 3.0.x
CPU-Target: 32Bit/64bit

Re: Vorsicht mit Arrayindices

Beitrag von mischi »

siro hat geschrieben:Wenn ich das richtig in Erinnerung habe, liegt bei der neueren Stringverwaltung ja auch einiges an "Verwaltung" unter dem
eigentlichen Array[0].

Das ist auf jeden Fall immer von der Implementierung im Compiler abhängig. Deshalb würde ich so etwas immer vermeiden, wie der Teufel das Weihwasser. Eigentlich gehört da zumindest eine entsprechende Kommentar-Notiz dazu. Oft lässt sich das Problem "sauber" lösen, in dem man einen tatsächlichen Pointer verwendet.
MiSchi macht die fink-Pakete

mischi
Beiträge: 206
Registriert: Di 10. Nov 2009, 18:49
OS, Lazarus, FPC: macOS, 10.13, lazarus 1.8.x, fpc 3.0.x
CPU-Target: 32Bit/64bit

Re: Vorsicht mit Arrayindices

Beitrag von mischi »

m.fuchs hat geschrieben:

Code: Alles auswählen

for i := 0 to Length(MyArray) do (*...*)

Das hat stinkt so nach C, dass eigentlich jedem ordentlichen Pascal-Programmierer schlecht werden müsste ;-)
MiSchi macht die fink-Pakete

Antworten