Nested procedures: are they actually supported or not?

Started by MazeGen, January 09, 2005, 08:45:45 PM

Previous topic - Next topic

Ratch

MazeGen,
QuoteWe've discussed here nesting in context of definying one procedure inside another procedure (pure compile-time problem). You are speaking about different context of nesting procedures

     PROCS/subroutines have to be defined and CALLed.  My method can define a subroutine at anywhere and CALL to anywhere within a .CODE segment.

QuoteFor some reasons, when I started using assembly language, I never used any HL syntax or macros, because I simply loved the most lowest level of coding.
Now, after some years of coding in assembly, I use a lot of macros, HL syntax and so on, simply because my code is much more READABLE, and the assembler emits no additional code, because I know how to use them. So my point of view is opposite to you: I feel my code is obfuscated when I don't use them (push/call versus invoke; long identifier names in procedures because of its global visibility versus short private names in procedures defined using PROC; many repetitive lines of code versus simple macros, which expands this code; and so on and so on.)

     This is not a high level code issue.  PROCs and subroutines are not a high level item.  Spaghetti code can still flourish with PROCS or subroutines.  I am not against high level, and sometimes even use the built in features of MASM supporting high level constructs when they emit code to my liking.  I also use INVOKE, which is neither a high level construct or macro.  Neither am I against MACROs.  I've written plenty of them and use them all the time.  My gripe is the inplementation of subroutines using the PROC method.

QuoteAs far as I know, AMD does recommend to use LEAVE to release the stack frame. If you don't like LEAVE, you can code your own epilogue code.

     I concede you are correct.  IF you are going to use stack frames with EBP, then LEAVE is for you.  Now, note what else AMD says about stack frames.
QuoteThis (LEAVE) is optimal in cases where the use of a frame pointer is
desired. For highest performance code, do not use a frame
pointer at all. Function arguments and local variables should be
accessed directly through ESP, thus freeing up EBP for use as a
general purpose register and reducing register pressure.

     As AMD recommends above, I reference args and vars directly through ESP.

QuotePROC nad ENDP directives are still worth for me as they naturally delimit the procedure and procedures defined with them have private identifiers.

     Subroutines can be defined just fine with indentation and white space.  The fields within a STRUC are private too.  Just a matter of choice.  Ratch

MazeGen

Quote from: RatchI am not against high level, and sometimes even use the built in features of MASM supporting high level constructs when they emit code to my liking.  I also use INVOKE, which is neither a high level construct or macro.  Neither am I against MACROs.

It seems I've acted too rashly - when I've read that PROC, ENDP etc are garbage features, I've thought you don't like anything else than mnemonics ;-)

Quote from: RatchSo at the beginning of each subroutine I build a STRUC of what my stack will look like after it is "loaded".  Then I simply do a STRUC reference for each local variable or parameter I need.  If I am within a nested subroutine, and need something from the stack of a previous subroutine, I include the STRUC of the previous subroutine within the STRUC of my present subroutine.  Basically I am using a static method with the assembler instead of a dynamic method with the INTEL cpu.

You take interesting way.

Quote from: RatchI reference args and vars directly through ESP

How did you tackle with the ESP changes after each PUSH/POP? Do you use some trick, or do you simply adjust some variable manually?

Ratch

MazeGen,

QuoteIt seems I've acted too rashly - when I've read that PROC, ENDP etc are garbage features, I've thought you don't like anything else than mnemonics ;-)

     Try it, you'll like it.

QuoteYou take interesting way.

     It is a methodical way of implementing a "frameless" stack.

QuoteHow did you tackle with the ESP changes after each PUSH/POP? Do you use some trick, or do you simply adjust some variable manually?

     Below is an example.  Notice that I adjust the PUSH parameter to reflect what was pushed before.  Also since I do not need the EBP to keep track of the stack, I usually set it to zero.  Since zero is often used in comparison instructions and parameters, having this value in a register shortens the instruction length.  For instance, PUSH 0 is two bytes while PUSH EBP is only one byte.  As you can see from the example, having the stack represented by a STRUC is like looking at a map.  If I need to call still another subroutine, I encapsulate the calling STRUC within the CALLed subrouinte's STRUC and continue.  Thus you can nest subroutines to any level and access the params of any previous level.  It also helps to use EQUs to cut down on the typing.  Questons or comments are welcome.  Ratch

MAIN:

; lots and lots of skipped code

INVOKIT PopFileRead,pstrFileName,hwndEdit ; subroutine called with EBP=0

;more skipped code



;_____PopFileRead Subroutine____________________________________________________

STKPFR        STRUC            ;tailor the STRUCT according to the subroutine
EBXsave      DWORD ?          ;\
EDIsave      DWORD ?          ; >saved registers
ADR1 = $
iUniTest     DWORD ?          ; \
dwBytesRead  DWORD ?          ;  \
iFileLength  DWORD ?          ;   >local variables for subroutine
pBuffer      DWORD ?          ;  /
pText        DWORD ?          ; /
pConv        DWORD ?          ;/
ADR2 = $
return       DWORD ?          ;return address
ADR3 = $
hwndEdit     DWORD ?          ;\
pstrFileName DWORD ?          ; >pushed parameters
ADR4 = $
return1      DWORD ?          ;\
hwnd         DWORD ?          ; \
msg          DWORD ?          ;  >You don't have to add these unless you are in message
wParam       DWORD ?          ; /     processing, and need to reference these params from the
lParam       DWORD ?          ;/      CALLBACK routine. They are already PUSHed on the stack
STKPFR ENDS

S$1 EQU ESP.STKPFR             ;use this to save some typing

PopFileRead:                   ;PopFileRead--hwndEdit,pstrFileName--enter this subroutine with EBP=0
SUB ESP,ADR2-ADR1             ;make local variables space
RPUSHIT EBX,EDI               ;save those registers

                               ;Open the file
;                              ;***NOTICE HOW WE HAVE TO ADJUST FOR 6 PUSHES AHEAD OF THIS PARAMETER
;                               ^
;                              / \
;                             /   \
;                            /     \
;                           /       \
;                          /         \
;                         /           \
;                        /             \
;                       /               \
;                      /                 \
;                     /                   \
;                    /                     \
;                   /                       \      NOTICE BELOW THAT A PUSH EBP IS A ONE BYTE
;                  /                         \     INSTRUCTION AND PUSH 0 IS TWO BYTES!
                  ;_____________^_____________\
INVOKE CreateFile,[S$1.pstrFileName+6*DWORD],GENERIC_READ,FILE_SHARE_READ\
                  ,EBP,OPEN_EXISTING,EBP,EBP
.IF EAX==INVALID_HANDLE_VALUE
   XOR EAX,EAX                 ;return false
   JMP PFR$RET1
.ENDIF

; skipped code



INVOKE LocalAlloc,LMEM_FIXED,EAX
MOV [S$1.pBuffer],EAX
                               ;Read file and put terminating zeros at end.
LEA EAX,[S$1.dwBytesRead]
INVOKE ReadFile,EBX,[S1$.pBuffer+3*DWORD],[S1$.iFileLength+2*DWORD],EAX,EBP
                               ;***NOTICE THE STACK COMPENSATION IN THE INVOKE ABOVE

; skipped code

MOV ECX,[S$1.iFileLength]     ;typical example of a local variable reference

;skipped code

                               ;***NOTICE THE STACK COMPENSATION IN THE INVOKE BELOW
INVOKE IsTextUnicode,[S$1.pBuffer+2*DWORD],[S$1.iFileLength+1*DWORD],EAX ;EAX=&iUniTest

TEST EAX,EAX
.IF !ZERO?                    ;is the file Unicode?
   MOV ECX,[S$1.pBuffer]
   SUB [S$1.iFileLength],2
   INC ECX
   INC ECX
   LEA EDX,[EBP+4]             ;EBP=0,EDX=4
   MOV [S$1.pText],ECX   

   .IF D[S$1.iUniTest] & IS_TEXT_UNICODE_REVERSE_SIGNATURE ;
     MOV ECX,[S$1.iFileLength]

     ;skipped code

   .ENDIF
.ENDIF

;skipped code

INVOKE LocalFree,[S$1.pConv]
MOV EAX,ESP                   ;RETURN ESP=TRUE = NONZERO

PFR$RET1:
POPIT EBX,EDI                 ;RESTORE THOSE REGGIES
ADD ESP,ADR2-ADR1             ;RECOVER LOCAL VARIABLE SPACE
RET ADR4-ADR3                 ;see STRUC STKPFR ABOVE

END MAIN


Randall Hyde

Although versions of MASM have allowed you to "nest" procedures (without prolog generating code), such nesting rarely had the intended effect (say, as we've come to expect in a high level language). For example,


xyz proc
     <some code>
abc proc
    <some code>
abc endp
    <some more code>
xyz endp

The xyz proc falls directly into abc. It does not jump around abc as you would expect in a block-structured HLL. This is why you can't have stack frames in nested procedures -- it really messes things up.

If you want block-structured assembly code, use HLA :-)
cheers,
Randy Hyde

Randall Hyde

Quote from: Ratch on January 11, 2005, 03:04:58 AM
MazeGen,
     Your question gives me a opportunity to go into my periodic rant again.  ...<snipped>
     If you are interested, I could present an example.  But I don't want to go through the work unless it is desired and appreciated.  Ratch
You are confusing static or lexical nesting (of, say PROC directives) with dynamic nesting.

Static nesting is useful for information hiding purposes and encapsulation. If those terms don't mean anything (good) to you, then I can see why you don't like nested procedures.  The cool thing about statically nested procedures, though, is that you can easily access local variables in the outer procedure. That's not something you can easily do without a block-structured language.  And in some cases, the ability to do this is quite useful (I use this feature all the time, for example, in the HLA v2.0 source code).  Sure, you could make all your variables global, or pass pointers to your locals to other procedures, but that's inefficient and clumsy. 

Given that you're supposed to be able to do everything in assembly that can be done in a HLL, I find it amusing when people argue how you *shouldn't* do advanced things in assembly. :-)
Cheers,
Randy Hyde

Ratch

Randall Hyde,

QuoteYou are confusing static or lexical nesting (of, say PROC directives) with dynamic nesting.

     Au contraire, I do know the difference between static and dynamic nesting.  Procedures/subroutines need to be defined (static), and called (dynamic).  Static nesting doesn't mean too much in a assembler unless it tries to be a compiler with lots of tables keeping track of what variable is local or global.  And as you pointed out, MASM doesn't even prevent fall through execution.

QuoteStatic nesting is useful for information hiding purposes and encapsulation. If those terms don't mean anything (good) to you, then I can see why you don't like nested procedures.

     Information concealment and encapsulaton aren't necessary to keep me from writing bad code.

QuoteThe cool thing about statically nested procedures, though, is that you can easily access local variables in the outer procedure.

     I can reference anything pushed on the stack (local variables) using my method as explained in my previous example.

QuoteSure, you could make all your variables global, or pass pointers to your locals to other procedures, but that's inefficient and clumsy

     I use globalization sparingly and carefully.  I usually pass params to subroutines using the stack and registers when convenient and prudent.

QuoteGiven that you're supposed to be able to do everything in assembly that can be done in a HLL, I find it amusing when people argue how you *shouldn't* do advanced things in assembly. :-)

     I don't shirk from violating "structured programming principles" to speed up or shrink code, provided it is clear and documented.  Ratch


hutch--

I have tended to see procedures in the light of how convenient they are and constructing a stack frame ALA MASM proc does not worry me one iota as in most instances it does not effect the speed of the code.

When a procedure is short enough to be sensitive to stack entry and exit overhead, it is also small enough to inline which is clearly faster that procedures with no stack frame. I do bother from time to time to write procedures without a stack frame but it is to get EBP as a general purpose register, the speed gain is negligible but the extra register allows you to write code that is often a lot faster as you can avoid a range of memory load/store operations by doing so.
Download site for MASM32      New MASM Forum
https://masm32.com          https://masm32.com/board/index.php

Ratch

hutch--,
     To each his/her own choice.  Stack frames just aren't my way.  Ratch

MazeGen

Ratch,
it is interesting indeed, I've never seen similar method before.

But I still don't see how you handle the ESP changes after PUSH/POP somewhere inside the procedure:


PUSH EBX
...
MOV EAX,[S$1.pBuffer] ; address of S$1.pBuffer will be incorrect here
...
POP EBX


There is also at least one duplicity: Registers saved in the prologue code using RPUSHIT macro have to match appropriate items in the stack structure and tehrefore it may lead to bugs.

I, personally, like automatized method more than doing it all by hand with structures and macros. I hope new verison of pmacros will be finished soon.

rea

One way is that you already know how is the structure of the stack, if you see, there normally always be arguments, locals, more locals pushes that in the mean time will become arguments to other function pheraphs.


In that way, you can define deepness of a local, for example [nArguments][retaddr][nLocals][nPushes] with each push you update a extra variable and this one "know" the feep  (how many) pushes have you done, then you can reference a local function some like.....

for example, the deep of the arguments is like this:
nPushes*4+nLocals*4+4+argumentToAccess
or
deepOfPushes+deepOfLocals+4+argumentToAccess

And the deep of the locals is:

nPushes*n4+localToAccess
or
deepOfPushes+localToAccess

In that way you only need update one variable for each push and pop, also in that way you can have nested locals, you only do the clean of the nesteds locals...



That way have worked the majority of the times nice, dont remember exactly when I have problems ;).....

Ratch

MazeGen,

QuoteBut I still don't see how you handle the ESP changes after PUSH/POP somewhere inside the procedure:

     You COULD do it this way as shown in my example above when using INVOKE.


PUSH EBX
MOV EAX,[S$1.pBuffer+DWORD]
POP EBX


     But another way would be to define something like EBXSAV DWORD ? within the STRUC. Then code the following.


MOV [$$1.EBXSAV],EBX
MOV EAX,[S$1.pBuffer]        ;compensation not needed here this time
MOV EBX,[$$1.EBXSAV]


QuoteThere is also at least one duplicity: Registers saved in the prologue code using RPUSHIT macro have to match appropriate items in the stack structure and tehrefore it may lead to bugs.


RPUSHIT EBX,EBP,ESI,EDI
..........
POPIT EBX,EBP,ESI,EDI


    The RPUSHIT MACRO generates code to PUSH the parameters onto the stack in reverse order.  POPIT takes the params off the stack in normal order.  You can copy the params of RPUSHIT directly to POPIT and be assured the params will store and retrieve in the correct order.  Ratch

Ratch

rea,

     You don't have to do any compensation for changing the stack UNLESS you reference a parameter on the stack after changing the stack.  If you call a subroutine within another subroutine, define a new STRUC as follows.


SS$2 STRUC             ;structure for inner subroutine
LOCVAR1 DWORD ?
LOCVAR2 DWORD?
EBXSAV DWORD ?
RETURN DWORD ?
SS$1{}                ;previous defined structure from calling subroutine
SS$2 ENDS


     Then a parameter from the previous subroutine can be reference like MOV EDX,[ESP.SS$2.SS$1.EBXSAVE] .  EQUs can be used to shorten the code phrase.  Ratch

MazeGen

Ratch,

Quote from: Ratch
You COULD do it this way as shown in my example above when using INVOKE.

As far as you use it only in INVOKE, I agree it is usable.

Quote from: Ratch
PUSH EBX
MOV EAX,[S$1.pBuffer+DWORD]
POP EBX

That's why we tend to use only symbolic names - such hardcoding is awkward. Any time you add or remove some of PUSH-POP sequencies, you have to rewrite all references to S$1:


PUSH EBX
PUSH ECX
MOV EAX,[S$1.pBuffer+2*DWORD]
POP ECX
POP EBX


Quote from: Ratch
     But another way would be to define something like EBXSAV DWORD ? within the STRUC. Then code the following.


MOV [$$1.EBXSAV],EBX
MOV EAX,[S$1.pBuffer]        ;compensation not needed here this time
MOV EBX,[$$1.EBXSAV]

Similar. Any time you add or remove some of these substitutive sequencies, you have to rewrite the structure.

That is a big limitation of your method :tdown
I wonder how do you handle extensive procedures - it has to be very complicated.

Ratch

MazeGen,
QuoteSimilar. Any time you add or remove some of these substitutive sequencies, you have to rewrite the structure.That is a big limitation of your method

     Some might consider it a bother, but it is not a limitation.  You only have to edit the pertinent parts of the structure.  Not a big deal as far as I can see.  Likewise if you choose to compensate references to the stack directly after pushing and popping.   It just doesn't seem very onerous to me.  Not if one is always cognizant of what s/he and the stack are doing, which is what a programmer should be aware of anyway.

QuoteI wonder how do you handle extensive procedures - it has to be very complicated

     Not at all. It is very methodical.  Perhaps I don't understand what you mean by extensive. 

     My method is quite flexible.  For instance, I can SUB ESP,4*DWORD within Sub1 just before calling Sub2.  Sub2 can then fill in those 4 items of data according to its STRUC map and return to Sub1.  So then Sub1 has 4 items of data/params ready to go.  Can PROC-ENDP do that easily?  I don't know.  Ratch


rea

QuoteLikewise if you choose to compensate references to the stack directly after pushing and popping.   It just doesn't seem very onerous to me.

Yes, the programmer should be aware of what is in the stack and the locals, but the assembler is not able to know this :), you have one way, I like the way where I define the locals thinking that there can be not only locals+arguments+retaddr but +fastlocals(push/pop) that need modify a little push/pop or have a way for handle the variable that hold the "compensation" of the fastlocals.

:D.