In particular, the client was trying to make the error dialog to appear only for certain exceptions. He tried to do this with the following code:
if AShowDlg then EI.Options.ExceptionDialogType := edtEurekaLog else EI.Options.ExceptionDialogType := edtNone;and was testing it like this:
procedure TForm1.Button1Click(Sender: TObject); var List: TStringList; begin try List.Add('Test'); except on E: Exception do HandleError(E); end; end;where
HandleError
is the client's code for handling exceptions.At the same time, the client claimed that "everything works correctly without EurekaLog", and "with EurekaLog: after processing the test exception, (another) Access Violation is raised, and the application disappears from the monitor, while still showing in the task manager".
Do you see a problem in the above code?
The problem is that the test case uses an uninitialized
List
variable. This means that it can be any value (which will be an arbitrary garbage from the stack left after execution of the previous code). This sample/test code essentially calls an arbitrary code from your app. It is like:
var P: procedure; begin P := random; P(); end;Depending on how "lucky" you are, this code can:
- Fail immediately when calling an invalid location. An Access Violation exception will be raised. EurekaLog will catch the exception. This is what usually (but not always!) happens.
- A called location could happen to be valid, so your code will start executing a random code in your app. Things could go two ways:
- Code will start doing things, but fail in the end. Some exception will be raised. EurekaLog will catch the exception. However, the code may "manage" to do several things before failing.
- Code will start doing things and it may actually succeed in the end. Since code did something sucessfully - there will be no exception, EurekaLog would not catch it. That is highly unlikely, but it could happen.
It is important to realize that if your application has some code - this code could be called when you are calling a random address. Any code. For example, if your app has a code that formats your hard drive - well, the
List.Add
might successfully format your hard drive. Of course, chances for that are extremelly small: your random address should not only match the start of your hard drive formatting code perfectly, but other values (environment, arguments) should also happen to have sane values allowing the code to execute. However, there is no 100% guarantee that it will not happen. It is just extremely unlikely.Specifically, customer are calling his test code from a button click on the form. It is not that surprising that values in the CPU stack could contain some values related to the form or the button.
It just so happened (read: "lucky") that in an application without EurekaLog, this garbage pointed to an invalid memory area, so an attempt to call the
List.Add
threw an Access Violation exception immediately when the code tried to call the method. The message looked like this: 'Access violation at address 00000001 in module 'Project1.exe'. Read of address 00000001'. Note that both addresses match and are invalid.However, when EurekaLog was added to the application, the situation changed, and a different garbage value appeared on the stack. While adding EurekaLog caused a change in the behaviour in the application, but it is not like EurekaLog caused this directly. It is an indirect effect. If your application has a bug that depends on a random trash from memory, it is not surprising that adding a large code (any code, not just EurekaLog) could affect these trash values.
Specifically, here is what happens in customer's test app (with or without EurekaLog) on our machine:
- The
List.Add
line actually calls theTForm.Create
method, passing the actualForm1
form instead of a class pointer; - The
TForm.Create
calls theSystem.@ClassCreate
, because its second argument is non-zero random trash value; - The
System.@ClassCreate
expects a class reference, but the class pointer is still theForm1
object; - The
System.@ClassCreate
calls theNewInstance
method on a class reference. The pointer to theNewInstance
method is stored at a negative offset to the class. Specifically, it is usually 12 bytes before the class (for a 32-bit modern Delphi).
So, the
Form1
is a heap allocated dynamic block. And the System.@ClassCreate
is trying to read a function's pointer preceding the Form1
. In other words, it reads a value placed before a heap allocated block.What happens next depends on what value is stored before a heap allocated block. As you can imagine, this heavily depends on what memory manager you are using. These values will be different when:
- Classic memory manager (think of Delphi 7) is used;
- FastMM (shipped with modern Delphi) is used;
- Memory shim of EurekaLog is used.
For example, the size of the
Form1
should be around 1'000 bytes, which makes it a "small block" in terms of FastMM. It means FastMM will allocate it in a pool of small blocks. Therefore values before Form1
could be values from other small blocks.On the other hand, if you compile your app with EurekaLog, it has a memory checks option enabled by default. This causes EurekaLog to install a memory filter, which will add a debugging header immediately before (and after) your allocated blocks. Therefore values before
Form1
will be values from EurekaLog's debugging header.What this value will be exactly - it is hard to tell, could be anything:
- When FastMM is used - the value could come from another small memory block allocated before the
Form1
. It could be different in different Delphi versions (different object's sizes and structures, different order of creating objects), could be different from run to run (say, a background thread could slip allocation of his own object immediately before allocating theForm1
); - When EurekaLog is used - the value actually comes from a call stack for the
Form1
. Here is a debugging header placed by EurekaLog immediately beforeForm1
:ETypes.TPointerRecord = packed record ... CallStack: TCallStackArray; // - System.@ClassCreate reads from here GuardBlock: DWord; end; // Form1 starts here
So the actual value depends on your VCL version, as well as Windows version, and possibly also installed software on your PC! For example, a software can install some shell extension or other component that could be loaded into your process and therefore could appear in the saved call stack.
While adding EurekaLog to customer's application caused the behaviour change, it is important to understand that it is not about EurekaLog. For example, using a different memory manager could also cause a change in behaviour (could be different or the same).
We don't know what this value was on the client's machine, but when we checked, the VCL code called on garbage values eventually crashed when trying to register the created form with a non-existent Owner. The message looked like this: 'Access violation at address 004092F9 in module 'Project1.exe'. Read of address E8C78BD6'. Note that the addresses are different; and the first address is correct, but the second is not.
EurekaLog has catched this exception (which the client mistook for an exception when trying to call the
List.Add
) and handled it correctly. However, the VCL was already in a corrupted state. Namely, when the EurekaLog dialog was shown (or after it was closed), the message loop accessed the saved garbage data, which led to the second ("incorrect") Access Violation exception (which EurekaLog also caught).The application "disappeared" for the reason that the main form was "overwritten" when it was passed as
Self
in TCustomForm.Create
.That is why, if you want to test throwing exceptions, then you should do it like this:
List := nil; // - added List.Add('Test');
This example also perfectly illustrates why you should restart or close your application immediately after handling an exception: your application may be in a invalid state. Indeed, if it were in the correct state, then the unexpected exception would not have occurred. And once something unexpected happened, the application is no longer in the expected state. This means that its further behavior is undefined. An attempt to continue execution can lead to data corruption, the throwing of other (extremely difficult to diagnose) exceptions, and so on.
Of course, it is extremely important to separate really "unexpected" exceptions from "expected" ones. Unfortunately, this is a vast topic that is beyond the scope of this post.
P.S. Read more stories like this one or read feedback from our customers.