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
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)
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.