When disassembling an old Delphi 3 executable, I find some routines that pass arguments in registers EAX, EDX, and on the stack – but not in ECX!
For those routines, ECX never gets set to a 'reasonable' value. This can be seen inside the code of small functions which do use EAX, EDX, and the stack, and also when such a routine is called inside a 'tight' inner block, which ought to be self-containing as far as function arguments go. (This version of Delphi clearly predates call stack optimization.)
This is quite surprising because according to Delphi's current owners (and, thus far, in my own experience), Delphi has always used register
:
Register Convention
Under the register convention, up to three parameters are passed in CPU registers, and the rest (if any) are passed on the stack. The parameters are passed in order of declaration (as with the pascal convention), and the first three parameters that qualify are passed in the EAX, EDX, and ECX registers, in that order.
Initially, I found this in some routines that reside in vcl30.dpl
, the standard library, and so I assumed it was a peculiarity of that particular build (perhaps the library was created with an even older version of Delphi which did not use ECX). But now I also find user routines that are missing ECX! (In both the called function and in calling it, and the function has a number of stack arguments.) Inside a called function an argument may be unused, but the compiler would not know that, and it'd still provide that argument.
This messes up my disassembly; not only I have to provide a dummy argument in the original function's prototype, but also the back-tracking fails because my code cannot find an assignment to ECX, and so it presumes the called function only uses the first 2 arguments.
It seems to violate the strict register
calling convention. Is there a calling convention that uses the other 2 registers but not ECX?
Example – a fragment where ECX gets used and thrashed, prior to calling a library function:
8D4DFC lea ecx, [ebp+local_4]
33D2 xor edx, edx
8BC6 mov eax, esi
8B18 mov ebx, [eax]
FF5350 call [ebx+50h] <- GetSaveFileName; this uses ECX as a proper argument
A144831041 mov eax, [lpEnginePtr]
FF702C push [eax+2Ch] <- probably a local path
6870277355 push (address)"/Saved Games/"
FF75FC push [ebp+local_4]
8D45F8 lea eax, [ebp+local_8]
BA03000000 mov edx, 3
E869EAFCFF call System.@LStrCatN <- wot no ECX?
8B55F8 mov edx, [ebp+local_8]
A144831041 mov eax, [lpEnginePtr]
E860630600 call Engine.SaveFile
...
which I decompile into
call GetSaveFileName (esi, 0, addressof (local_4))
eax = lpEnginePtr
push (eax.field_2C)
push ("/Saved Games/")
push (local_4)
call System.@LStrCatN (addressof (local_8), 3)
call Engine.SaveFile (lpEnginePtr, local_8)
The routine GetSaveFileName
uses, and clobbers, ECX, without saving it:
GetSaveFileName:
53 | push ebx
8BD9 mov ebx, ecx
A140A08F55 mov eax, lpGameSettings
8B90E4000000 mov edx, [eax+0E4h]
8BC3 mov eax, ebx
B944267355 mov ecx, (address)".sav"
E856EBFCFF call System.@LStrCat3
5573263Ah:
5B | pop ebx
C3 | retn
The library function System.@LStrCatN
indeed does not read ECX at all:
System.@LStrCatN:
push ebx
push esi
push edx
push eax <-- not in the Save List
mov ebx, edx
xor eax, eax
mov ecx, [esp+4*edx+10h] <-- overwrite ECX!
test ecx, ecx
jz 41304AA7h
41304AA4h:
add eax, [ecx-4]
41304AA7h:
dec edx
jnz 41304A9Ch
41304AAAh:
call System.@NewAnsiString
...
Other routines that overwrite ECX (write without read) do save ECX in the prolog.
This has been mentioned earlier in Which calling convention to use for EAX/EDX in IDA, but according to the comments that one was a misunderstanding and ECX was used after all.