Recent

Author Topic: Crash on changing a string  (Read 601 times)

LemonParty

  • Full Member
  • ***
  • Posts: 202
Crash on changing a string
« on: June 18, 2025, 07:04:11 pm »
I get crash on running this code:
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$H+}
  2.  
  3. procedure Modify(var S: AnsiChar);
  4. begin
  5.   S:= '2';
  6. end;
  7.  
  8. var
  9.   S: AnsiString = '12345';
  10. begin
  11.   Modify(PAnsiChar( @S[1] )^);
  12. end.
Is this a bug or expected behavior?
Lazarus v. 4.99. FPC v. 3.3.1. Windows 11

Nicole

  • Hero Member
  • *****
  • Posts: 1211
Re: Crash on changing a string
« Reply #1 on: June 18, 2025, 07:24:54 pm »
I am not very good at pointers.
However, I found in general, that Lazarus is not too good in resolving nestings.
In several cases a value between helped me.

So please try something like this:

Var s1: string;

s1:=...
Modify(s1);

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 11457
  • Debugger - SynEdit - and more
    • wiki
Re: Crash on changing a string
« Reply #2 on: June 18, 2025, 07:47:25 pm »
Expected behaviour.

Taking the address of a string element prevents  the "copy on write" for the string.
And by that they also circumvent making the "constant write accessible".

Use "UniqueString" as in the last example below.




In Detail.

The string '12345' is a constant. The variable "s" is not a constant, but the initial value is.
That initial value '12345' is (maybe depending on OS) in write protected memory.

Taking the address of @s[1] => takes the address of the initial data, in the write protected memory.

Taking the address, also looses the info that this is a string.
A string has the special property that the variable itself is a pointer, and that the data can be in protected memory. If you accessed this without typecasts, then that data would be copied (copy on write) to write-able memory.

If you had a "var c: char" variable, that issue could never arise. As the value (the char) is always in the variable (without internal pointer).



You can trigger the error much simpler:
Code: Pascal  [Select][+][-]
  1. pchar(@s[1])^ := '2';


And you can do your call, if you ensure the data is not in write-protected memory
Code: Pascal  [Select][+][-]
  1.   s := s + IntToStr(Random(9));
  2.       Modify(PAnsiChar( @S[1] )^);
or
Code: Pascal  [Select][+][-]
  1.   UniqueString(s);
  2.   Modify(PAnsiChar( @S[1] )^);



Mind that
Code: Pascal  [Select][+][-]
  1. var
  2.   S, S2: AnsiString;
  3. begin
  4.   s := '1111' + IntToStr(Random(9));
  5.   s2 := s;
  6.   pchar(@s[1])^ := '2';
  7.   writeln(s);
  8.   writeln(s2);
  9.  

Has the same issue.

Both strings will be modified.

Because again, taking the address prevents "copy on write". And again "UniqueString" before taking the address is the correct solution.

LemonParty

  • Full Member
  • ***
  • Posts: 202
Re: Crash on changing a string
« Reply #3 on: June 18, 2025, 08:01:52 pm »
Thank you, Nicole.
Thank you, Martin_fr. Very good explanation.

It is a bit dissapointing that constants placed in unwritable memory. But it is logical in sence that we can't change them.
Lazarus v. 4.99. FPC v. 3.3.1. Windows 11

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 11457
  • Debugger - SynEdit - and more
    • wiki
Re: Crash on changing a string
« Reply #4 on: June 18, 2025, 08:11:34 pm »
It is a bit dissapointing that constants placed in unwritable memory. But it is logical in sence that we can't change them.

Well, if you look at the s/s2 example => the write protected mem is just one way this manifests.
You get the error also, if the string is referenced more than once (unless you want to change all references?).

The issue is, that before modifying string contents via a pointer, you must make sure that the string is uniquely referenced: UniqueString

Warfley

  • Hero Member
  • *****
  • Posts: 1930
Re: Crash on changing a string
« Reply #5 on: June 19, 2025, 11:56:45 am »
Pascal strings do lazy copy, meaning they only create a copy when necessary. This is for example when you change the string:
Code: Pascal  [Select][+][-]
  1. MyString[3] := 'c';
The problem is, for this the Compiler needs to know that you are changing the string. You cast the string to a raw pointer, which means the FPC cannot know what you are doing with that. Instead do the following:
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$H+}
  2.  
  3. procedure Modify(var S: AnsiChar);
  4. begin
  5.   S:= '2';
  6. end;
  7.  
  8. var
  9.   S: AnsiString = '12345';
  10. begin
  11.   Modify(S[1]);
  12. end.  

Now it works, because now the FPC knows you are changing the string, because you don't go through the abstraction of the pointer.

To see why look at the generated assembly:
Code: Pascal  [Select][+][-]
  1. procedure Modify(var S: AnsiChar);
  2. begin
  3.   S:= '2';
  4. end;
  5.  
  6. procedure PtrAccess;
  7. var
  8.   S: AnsiString = '12345';
  9. begin
  10.   Modify(PAnsiChar(@S[1])^);
  11. end;
  12.  
  13. procedure DirectAccess;
  14. var
  15.   S: AnsiString = '12345';
  16. begin
  17.   Modify(S[1]);
  18. end;

Code: ASM  [Select][+][-]
  1. ptraccess():
  2.         [...]
  3.         leaq    -8(%rbp),%rdi
  4.         call    fpc_ansistr_assign
  5.         movq    -8(%rbp),%rdi
  6.         call    modify(char)
  7.         [...]
  8.  
  9. directaccess():
  10.         [...]
  11.         call    fpc_ansistr_assign
  12.         leaq    -8(%rbp),%rdi
  13.         call    fpc_ansistr_unique // Lazy Copy
  14.         movq    %rax,%rdi
  15.         call    modify(char)
  16.         [...]

With the direct access without the pointer, the fpc internally calls fpc_ansistr_unique, which it does not do when going through the pointer. This is because it knows that the value will be modified so it must create a (lazy) copy of the string.
« Last Edit: June 19, 2025, 12:02:45 pm by Warfley »

Thaddy

  • Hero Member
  • *****
  • Posts: 17420
  • Ceterum censeo Trumpum esse delendum (Tnx Charlie)
Re: Crash on changing a string
« Reply #6 on: June 19, 2025, 12:42:35 pm »
Yes, well explained.
You could have added that
Code: Pascal  [Select][+][-]
  1.   Modify(PAnsiChar( @S[1] )^);
is to the majority of programmers the same as shooting yourself in the foot...

Further more, the above does not even point to a Pascal string type....
« Last Edit: June 19, 2025, 04:46:47 pm by Thaddy »
Due to censorship, I changed this to "Nelly the Elephant". Keeps the message clear.

 

TinyPortal © 2005-2018