Prev TOP NEXT HOME
What about subroutines?
I started with a simple COM program because I actually think they are easier to create than subroutines to be called from high level languages, but maybe its really the latter you are interested in. Here, I think you should get comfortable with the assembler FIRST with little exercises like the one above and also another one which I will finish up with.
Next you are ready to look at the interface information for your particular
language. You usually find this in some sort of an appendix. For example, the
BASIC manual has Appendix C on Machine Language Subroutines. The PASCAL manual
buries the information a little more deeply: the interface to a separately compiled
routine can be found in the Chapter on Procedures
and Functions, in a subsection called Internal Calling Conventions.
Each language is slightly different, but here are what I think are some common issues in subroutine construction.
1. NEAR versus FAR? Most of the time, your language will probably call your assembler routine as a FAR routine. In this case, you need to make sure the assembler will generate the right kind of return. You do this with a PROC...ENDP statement pair. The PROC statement is probably a good idea for a NEAR routine too even though it is not strictly required:
FAR linkage: | NEAR linkage:
ARBITRARY SEGMENT | SPECIFIC SEGMENT PUBLIC
PUBLIC THENAME | PUBLIC THENAME
ASSUME CS:ARBITRARY | ASSUME CS:SPECIFIC,DS:SPECIFIC
THENAME PROC FAR | ASSUME ES:SPECIFIC,SS:SPECIFIC
..... code and data | THENAME PROC NEAR
THENAME ENDP | ..... code and data ....
ARBITRARY ENDS | THENAME ENDP
END | SPECIFIC ENDS
With FAR linkage, it doesn't really matter what you call the segment. you must declare the name by which you will be called in a PUBLIC pseudo-op and also show that it is a FAR procedure. Only CS will be initialized to your segment when you are called. Generally, the other segment registers will continue to point to the caller's segments.
With NEAR linkage, you are executing in the same segment as the caller. Therefore, you must give the segment a specific name as instructed by the language manual. However, you may be able to count on all segment registers pointing to your own segment (sometimes the situation can be more complicated but I cannot really go into all of the details). You should be aware that the code you write will not be the only thing in the segment and will be physically relocated within the segment by the linker. However, all OFFSET references will be relocated and will be correct at execution time.
2. Parameters passed on the stack. Usually, high level languages pass parameters to subroutines by pushing words onto the stack prior to calling you. What may differ from language to language is the nature of what is pushed (OFFSET only or OFFSET and SEGMENT) and the order in which it is pushed (left to right, right to left within the CALL state ment). However, you will need to study the examples to figure out how to retrieve the parameters from the stack. A useful fact to exploit is the fact that a reference involving the BP register defaults to a reference to the stack segment. So, the following strategy can work:
DW 3 DUP(?) ;Saved BP and return address
ARG3 DW ?
ARG2 DW ?
ARG1 DW ?
PUSH BP ;save BP register
MOV BP,SP ;Use BP to address stack
MOV ...,[BP].ARG2 ;retrieve second argument
This example uses something called a structure, which is only available in the large assembler; furthermore, it uses it without allocating it, which is not a well-documented option. However, I find the above approach generally pleasing. The STRUC is like a DSECT in that it establishes labels as being offset a certain distance from an arbitrary point; these labels are then used in the body of code by beginning them with a period; the construction ".ARG2" means, basically, " + (ARG2-ARGS)."
What you are doing here is using BP to address the stack, accounting for the word where you saved the caller's BP and also for the two words which were pushed by the CALL instruction.
3. How big is the stack? BASIC only gives you an eight word stack to play with. On the other hand, it doesn't require you to save any registers except the segment registers. Other languages give you a liberal stack, which makes things a lot easier. If you have to create a new stack segment for yourself, the easiest thing is to place the stack at the end of your program and:
CLI ;suppress interrupts while changing the stack
MOV SSAVE,SS ;save old SS in local storage (old SP
; already saved in BP)
MOV SP,CS ;switch
MOV SS,SP ;the
MOV SP,OFFSET STACKTOP ;stack
Later, you can reverse these steps before returning to the caller. At the end of your program, you place the stack itself:
DW 128 DUP(?) ;stack of 128 words (liberal)
STACKTOP LABEL WORD
4. Make sure you save and restore those registers required by the caller.
5. Be sure to get the right kind of addressibility. In the FAR call example, only CS addresses your segment. If you are careful with your ASSUME statements the assembler will keep track of this fact and generate CS prefixes when you make data references; however, you might want to do something like
MOV AX,CS ;get current segment address
MOV DS,AX ;To DS
Be sure you keep your ASSUMEs in synch with reality.