Using ‘Proc Disassembly’ we can inspect the assembly code produced by the compiler. In the first part of this series we looked at the generated code for a Naked proc. Now we will discuss regular procedures (or subs and functions) that do not produce minimal code. Regular procs support destruction for local dynamic variable types like String, Object, Variant, arrays and hash tables. In addition, they provide the logic to trace code using the Tron and Gfa_Tron statements. A regular procedure stores information for for Trace, TraceLnr, and other debugging related commands. Finally and maybe most importantly, regular procs support structured exception handling (Try/Catch, On Error, and unwinding).
Inspecting a procedure’s disassembly requires knowledge to identify the three main parts of a procedure; the entry code, the actual code, and the exit code, In the previous post we discussed how we can recognize these parts in Naked procedures, now we’ll see how to identify these parts in a regular procedure. The test() procedure is changed a little to demonstrate the use of local dynamic variables:
test(2, 6) Proc test(x As Int, y As Int) Local sStr As String, result As Int result = x \ y sStr = Str(result) Print sStr EndProc
This procedure declares a local dynamic variable of type String. Before the procedure exits the memory allocated for the string-data has to be released. We’ll see how this will become part of the exit code.
In a regular procedure all dynamic types are destroyed automatically before the procedure returns. (Starting with version 2.5 local arrays and hash tables are destructed correctly as well).
After selecting ‘Proc Disassembly’ the result is displayed in the Debug Output window:
-------- Disassembly -----------------------------------
1 Proc test(x As Int, y As Int) (Lines=7)
03C002D0: 6A 02 push 2
03C002D2: B8 63 00 00 00 mov eax,0x00000063
03C002D7: FF 15 3C 1A 4D 00 scall INITPROC ; Ocx: $1802775D
03C002DD: E8 5A 00 00 00 call 0x03C0033C
03C002E2: FF 55 B4 call dpt -76[ebp] ; @Tron
03C002E5: 8B 45 14 mov eax,dpt 20[ebp]
03C002E8: 99 cdq
03C002E9: F7 7D 18 idiv dpt 24[ebp]
03C002EC: 89 43 78 mov dpt 120[ebx],eax
03C002EF: FF 55 B4 call dpt -76[ebp] ; @Tron
03C002F2: 50 push eax
03C002F3: FF 15 60 1B 4D 00 scall STRSTRI ; Ocx: $1806AC50
03C002F9: 50 push eax
03C002FA: 8D 43 7C lea eax,124[ebx]
03C002FD: 50 push eax
03C002FE: FF 15 C0 1D 4D 00 scall STOSTRSV ; Ocx: $18067F73
03C00304: FF 55 B4 call dpt -76[ebp] ; @Tron
03C00307: 6A FF push -1
03C00309: 8B 43 7C mov eax,dpt 124[ebx]
03C0030C: FF 15 08 24 4D 00 scall PRSEXPCR ; Ocx: $18043D3F
03C00312: 5A pop edx
03C00313: FF 55 B4 call dpt -76[ebp] ; @Tron
03C00316: 8B 4D F0 mov ecx,dpt -16[ebp]
03C00319: 64 89 0D 00 00 00 00 mov dpt fs:[0x00000000],ecx
03C00320: 8D 4B 7C lea ecx,124[ebx]
03C00323: FF 15 CC 25 4D 00 scall CLEARSTR ; Ocx: $1807BA06
03C00329: 8B E5 mov esp,ebp
03C0032B: 5D pop ebp
03C0032C: 5B pop ebx
03C0032D: 5F pop edi
03C0032E: 5E pop esi
03C0032F: C2 08 00 ret 8
03C00332: 51 push ecx
03C00333: 8D 4B 7C lea ecx,124[ebx]
03C00336: FF 15 CC 25 4D 00 scall CLEARSTR ; Ocx: $1807BA06
03C0033C: C3 ret
03C0033D: 90 nop
03C0033E: EB F2 jmp short 0x03C00332
It’s immediately clear that the disassembly differs greatly from the Naked attributed procedure discussed in the previous post. The first thing we need to identify is the entry code where the stackframe is established.
The entry code
Part of the procedure’s entry code, where the stack is prepared, is located in the INITPROC library function, which takes two arguments. As we will see, in GB arguments to library functions are often passed via eax and the stack. Here, the first argument is passed through the stack and specifies an encoded value used to reserve and initialize stack space for local variables. The second argument is stored in eax and specifies the offset to an unwind (termination) handler. INITPROC is a general function that is responsible for setting up a stack for a regular GB procedure, preparing it for structured exception handling and for use with Tron/Trace. Therefor, the stack needs an additional of 80 bytes for an ‘Extended Information Block’ .
After INITPROC has returned the stack for test() has been setup as shown if the figure below.
These four lines constitute the entry code of the proc:
03C002D0: push 2
03C002D2: mov eax,0x00000063
03C002D7: scall INITPROC ; Ocx: $1802775D
03C002DD: call 0x03C0033C
If a procedure contains local variables the first argument tells INITPROC the number of stack-bytes to reserve and initialize. This happens in the same way as we saw in the Naked procedure. The stack bytes are reserved and initialized through a series of push eax instructions, determined by the encoded value of the argument. (The value is a compiler encoded number and does not specify the actually number of pushes that are inserted. In this case the value is coincidentally 2.)
The second argument of INITPROC, passed via eax, is an offset value used by INITPROC to calculate the address of the unwind code stored at virtual address $03C00332.
Without local variables the compiler inserts a call to INITPROC0 instead of INITPROC. INITPROC0 takes one argument only, the offset to the unwind-handler, and omits the code to prepare the procedure’s stack for use of local variables.
The unwind-termination code is only executed in case of an unhandled exception in the current proc, that is an exception that isn’t caught by a Try/Catch handler. (To be discussed in a coming post.)
The fourth and last line of the entry code calls $03C0033C and returns immediately. Why? I have no idea …
When the program is compiled to EXE, the calls to INITPROC or INITPROC0 are replaced by calls to INITPROCEXE and INITPROCEXE0 respectively. These library functions produce smaller extended stack information blocks (68 bytes), because EXEs don’t need the Tron/Trace support. In addition, the mysterious call just below INITPROC is removed.
The actual code
The code that represents the actual code starts at the fifth line at $03C002E2. The actual code starts with a call to the tron-handler:
03C002E2: call dpt -76[ebp] ; @Tron
The address of the tron-handler is stored on the stack in the extended information block. If there is no tron procedure present the call immediately returns and nothing happens. Because calls to a tron procedure occur before a code-statement is executed we use that information to identify and examine the actual code. The first executable statement is result = x \ y, so its assembly code is found just below the first call to the tron-handler. Note that the Dim statement doesn’t produce executable code, declaration statements only introduce variables to the compiler.
We can now inspect the code for the division of x by y and the storage of the result in a local variable. Identifying the parameters is a bit more complicated now.
03C002E5: mov eax,dpt 20[ebp] ; eax = x
03C002E8: cdq ; clear flag
03C002E9: idiv dpt 24[ebp] ; eax idiv y
03C002EC: mov dpt 120[ebx],eax ; store eax in result
The x-parameter is accessed using 20[ebp] and y-parameter through 24[ebp]. This means that the stackframe has moved 8 bytes compared to the Naked attributed procedure. Exactly the number of bytes required to save esi and edi so that they can be used for Register Int types.
The local variable result is access though the value in ebx at 120[ebx]. As you can see the compiler generated code for an integer division (idiv).
The next statement assigns result to sStr: sStr = Str(result) Again, the statement is preceded by a call to the tron-handler. We can easily identify the code:
03C002EF: call dpt -76[ebp] ; @Tron
03C002F2: push eax ; result of division
03C002F3: scall STRSTRI ; integer to temp string
03C002F9: push eax ; address of temp string
03C002FA: lea eax,124[ebx] ; address of string variable
03C002FD: push eax
03C002FE: scall STOSTRSV ; assign to variable
The instruction push eax passes the result of the division, which is still in eax, to STRSTRI. The integer argument is converted to string and STRSTRI returns (in eax) a pointer to a temporary string. Both the temporary string and the local string variable sStr at 124[ebx] are passed to STOSTRSV to assign (attach) the temporary string to the variable sStr, which makes it a permanent string.
Finally, the code prints the contents of sStr to the window: Print sStr
03C00304: call dpt -76[ebp] ; @Tron
03C00307: push -1 ; True, print CRLF
03C00309: mov eax,dpt 124[ebx] ; address of stringdata
03C0030C: scall PRSEXPCR ; print to window
03C00312: pop edx ; fix the stack
The PRSEXPCR shows how GB optimizes library function calls. The calling convention of this function and many more is GB-specific, one argument is pushed on the stack and one is passed via eax. Passing arguments via a register is very common and is always faster than using the stack. VC++ uses ecx and edx to pass the first arguments to a __fastcall function and Borland uses eax, ecx, and edx with its fast function calls. Many GB library functions use eax and the stack, however there are also examples of GB library functions that use the VC++ __fastcall convention and use ecx and edx for argument passing. We’ll see this with local variable destruction functions in the exit code.
The function PRSEXPCR uses the cdecl convention and doesn’t cleanup the stack so it has to be corrected by popping the one argument.
The exit code
The EndProc statement is preceded by a call to the tron-handler as well:
03C00313: call dpt -76[ebp] ; @Tron
03C00316: mov ecx,dpt -16[ebp] ; get saved ptr to prev SEH
03C00319: mov dpt fs:[0x00000000],ecx ; remove us from SEH-list
03C00320: lea ecx,124[ebx] ; address string variable
03C00323: scall CLEARSTR ; Ocx: $1807BA06
03C00329: mov esp,ebp ; restore the stack
03C0032B: pop ebp
03C0032C: pop ebx
03C0032D: pop edi
03C0032E: pop esi
03C0032F: ret 8 ; pop 2 parameters of 4 bytes
The first two lines remove the structured exception record from the thread’s SEH-linked list. The record was inserted when INITPROC created the ‘Extended Information Block’. Then, before leaving the procedure, the dynamic string has to be destroyed. The address of the string variable is passed in the ecx register to CLEARSTR which frees the allocated string memory.
The compiler inserts destruction code for all local variables with a dynamic datatype: String, Variant, Object (all COM objects), array and hash table. Before GFA-BASIC version 2.5 the destruction code for an array was only inserted if the proc contained at least one other local variable of type String, Variant or Object. Often, this required the addition of a dummy local string variable so the compiler was forced to generate the array’s destruction code. For hash tables the situation was even worse; a local hash wasn’t destructed at all. This has been fixed in update version 2.5.
Finally, the disassembly shows some more code below the the procedure’s return instruction.
03C00332: push ecx
03C00333: lea ecx,124[ebx]
03C00336: scall CLEARSTR ; Ocx: $1807BA06
03C0033C: ret
03C0033D: nop
03C0033E: jmp short 0x03C00332
some more code that is actually data
Below the procedure’s return instruction the local variables destruction code is replicated. With a normal execution of the procedure, without exceptions, this code is never executed. It is only called by the OS when the structured exception handler tries to recover from an error and starts unwinding. Most importantly, the destruction code for dynamic types within the normal flow of the procedure must be replicated here.The rest of the disassembly contains information for Tron/Trace. Although these bytes represent data, the disassembler tries to produce assembly code. Actually, everything below the second ret instruction is data.
Conclusion
Regular procedures are separated into four parts: the entry code, actual code, exit code, and unwind code. The construction of the entry code is relayed to INITPROC(0). The statements in the actual code are preceded by calls to the tron-handler and they can be used to identify the statement lines. Library functions use a wide variety of calling conventions, it sometimes requires some puzzling to identify the arguments.
When you start analyzing procedure disassemblies you will encounter a variety of the same sort of code. The information presented in this and the previous post should help you in interpreting it.