The following information is provided courtesy of Kenneth G. Hamilton, 12-Oct-1998.
This section is intended to assist the experienced assembly language programmer
in writing subprograms that can be called by LF95-compiled Fortran code. The examples
that follow were processed by Microsoft MASM v6.11a, although any recent assembler
will likely suffice. In addition to this information, you should also have on hand
appropriate documentation for your assembler. The examples in this write-up can
be found in subdirectories
Each sample program can be compiled and linked by using the GEN.BAT file that accompanies it.
When it compiles Fortran source code, LF95 shifts the names of subroutines and functions into lower case letters, and attaches an underscore symbol (_) both before and after each name. As an example, suppose that an LF95 program calls subroutine RAINBOW If that routine is written in assembly language, then it must have an entry point called _rainbow_ on a PROC or LABEL statement, and that name must declared to be a PUBLIC symbol.
LF95 requires that subroutines and functions preserve the value of the EBX, ESI, and EDI registers. If any of these registers are used in an assembly-language routine, they can be saved by pushing them upon entry, and then popping them before returning.
LF95 passes numeric and logical arguments by pushing their addresses onto the stack, from right to left. Each address is a four-byte quantity, so that, upon entry to a subprocedure the first argument's address is located at ESP+4, the second at ESP+8, the third at ESP+12, and so on. (The ESP register itself contains the address that control will return to in the calling routine, upon subprogram termination.)
Generally, the best procedure (and this is what LF95 itself does) is to push EBP onto the stack, and then move the contents of the ESP register into EBP. This is often known as the `preamble' of the routine. The arguments can then be accessed using EBP instead of ESP, and any additional pushing or popping will not result in any confusion about where the argument addresses are. Since pushing EBP onto the stack changes the stack pointer by four bytes, the first argument's address will be in EBP+8, the second argument's in EBP+12, the third's in EBP+16, and these offsets from EBP will not be altered by any local activity involving the ESP register.
For CHARACTER-valued arguments, the length of the string must also be passed. This is done by treating the lengths of the CHARACTER arguments as though they were extra parameters following the normal visible ones, and passing them by value. The term `by value' in this context means that the actual length is pushed, rather then the address of the length. These length parameters are treated as though they were to the right of the actual parameters in the call, and so they are actually pushed first, and are at higher offsets relative to EBP.
It is often easiest to learn a programming method by studying examples, and so we will now show and examine several cases in which a Fortran program calls an assembly language subprogram.
First, the following main program (ADDMAIN) passes two INTEGER variables to a Fortran subroutine (FORADD), where they are added, with their sum being returned as a third variable.
PROGRAM ADDMAIN
integer :: i,j,k,l
i = 17
j = 24
call foradd(i,j,k)
print *, 'i,j,k=',i,j,k
i = 52
j = 16
call asmadd(i,j,l)
print *, 'i,j,l=',i,j,l
stop
end
SUBROUTINE FORADD(II,JJ,KK)
kk = ii+jj
return
end
You should note that ADDMAIN also calls a second subroutine, ASMADD. Here it is:
TITLE ASMADD
.386
.MODEL FLAT
;
_ACODE SEGMENT PARA USE32 PUBLIC 'CODE'
ASSUME CS:_ACODE
PUBLIC _asmadd_ ; Entry point name
_asmadd_ PROC NEAR ; Start of procedure
push ebp ; Save EBP
mov ebp,esp ; Will use EBP for args
push ebx ; Must save EBX
mov eax,[ebp+8] ; 1st arg addr
mov ecx,[ebp+12] ; 2nd arg addr
mov edx,[ebp+16] ; 3rd arg addr
mov ebx,[eax] ; 1st arg value
mov eax,[ecx] ; 2nd arg value
add eax,ebx ; Form I+J
mov [edx],eax ; Store into K
pop ebx ; Restore saved EBX
mov esp,ebp ; Restore stack pointer
pop ebp ; Restore base pointer
ret ; Return to caller
_asmadd_ ENDP ; End of procedure
_ACODE ENDS ; End of segment
END
ASMADD is the assembly-language translation of FORADD: it also takes three variables, adds the first two, and returns the result in the third one. Examining ASMADD, we can see that once the preamble is completed, the addresses of the arguments are accessible to the assembly-language routine in EBP+8, EBP+12, and EBP+16. Since the EBX register is used in the processing, its contents must be preserved by being pushed onto the stack before it is clobbered, and popped off later.
LF95 assumes that the caller will fix the stack, i.e., remove the argument address pointers. As a result, the return to the calling routine is accomplished by means of a simple RET instruction.
Now, let us examine a case in which a subroutine contains some local data. The main program MULMAIN calls two subroutines, FORMUL (written in Fortran), and ASMMUL written in assembly language. Both FORMUL and ASMMUL do the same thing: multiply the first argument by 7, add 3, and then return the result as the second argument. This is the Fortran part:
PROGRAM MULMAIN
integer :: i,j,k,l
i = 5
call formul(i,j)
print *, 'i,j=',i,j
k = 3
call asmmul(k,l)
print *, 'k,l=',k,l
stop
end
SUBROUTINE FORMUL(II,JJ)
jj = 7*ii + 3
return
end
Here is the assembly-language subroutine ASMMUL, with two constants m1 and m2 stored in a local data area.
TITLE ASMMUL
.386
.MODEL FLAT
;
_ACODE SEGMENT PARA USE32 PUBLIC 'CODE'
ASSUME CS:_ACODE, DS:_ADATA
PUBLIC _asmmul_ ; Entry point name
_asmmul_ PROC NEAR ; Start of procedure
push ebp ; Save base pointer
mov ebp,esp ; Save stack pointer
mov eax,[ebp+8] ; 1st arg addr
mov eax,[eax] ; 1st arg EAX=I
mov ecx, m1 ; 7 into ECX
mul ecx ; 7*I is in EAX
add eax, m2 ; 7*I+3 is in EAX
mov edx,[ebp+12] ; 2nd arg addr
mov [edx],eax ; Store in 2nd arg (J)
mov esp,ebp ; Restore stack pointer
pop ebp ; Restore base pointer
ret
_asmmul_ ENDP
_ACODE ENDS
;
_ADATA SEGMENT PARA USE32 PUBLIC 'DATA'
m1 dd 7
m2 dd 3
_ADATA ENDS
;
END
The two variables are initialized to values of 7 and 3, and are not altered. Quantities stored in this manner could be changed during the course of computation, if required. Alternatively, this routine could have been written with the constants 7 and 3 being coded as immediate data in the MOV and ADD instructions that use them.
Floating point arithmetic is also possible in an assembly language routine that is called from an LF95 program. Here is an example of a main program (FLTMAIN) that calls two functionally-identical subroutines, FORFLT and ASMFLT, which are written in Fortran and assembly language, respectively.
PROGRAM FLTMAIN
real :: x, y, z
x = 3.0
y = 8.5
call forflt(x,y,z)
print 20, x,y,z
20 format (' x,y,z=',3F10.4)
x = 4.5
y = 7.1
call asmflt(x,y,z)
print 20, x,y,z
stop
end
SUBROUTINE FORFLT(XX,YY,ZZ)
zz = 3.1*xx + yy + 7.6
return
end
This is the assembly language routine, and we can see that REAL variables are also passed as addresses, located in EBP+8, EBP+12, EBP+16, etc.
TITLE ASMFLT
.386
.MODEL FLAT
;
_ACODE SEGMENT PARA USE32 PUBLIC 'CODE'
ASSUME CS:_ACODE, DS:_ADATA
PUBLIC _asmflt_ ; Entry point name
_asmflt_ PROC NEAR ; Start of procedure
push ebp ; Save base pointer
mov ebp,esp ; Save stack pointer
mov eax,[ebp+8] ; Addr X
mov ecx,[ebp+12] ; Addr Y
mov edx,[ebp+16] ; Addr Z
fld dword ptr d1 ; Load 3.1
fmul dword ptr [eax] ; 3.1*X
fadd dword ptr [ecx] ; 3.1*X+Y
fadd dword ptr d2 ; 3.1*X+Y+7.6
fstp dword ptr [edx] ; Store into Z
mov esp,ebp ; Restore stack pointer
pop ebp ; Restore base pointer
ret
_asmflt_ ENDP
_ACODE ENDS
;
_ADATA SEGMENT PARA USE32 PUBLIC 'DATA'
d1 dd 3.1
d2 dd 7.6
_ADATA ENDS
;
END
In assembly language, it is necessary to access the values of the variables using the keywords DWORD PTR for REAL(KIND=4) and QWORD PTR for REAL(KIND=8) variables.
If it is necessary for an assembly language subroutine to access the contents of a COMMON block, then we must find the starting address of that block.
The starting address of a named COMMON is put in a global variable; the name of that variable is composed by converting the COMMON block's name to lower case letters, and then attaching an underscore before and after the name. Thus, the starting address of a COMMON block that is named ZOOM can be found in the global variable _zoom_ . The starting address of blank COMMON is placed in the global variable __BLNK__. (Note that there are two underscore symbols both before and after the word ``BLNK.'')
In the following example, both blank COMMON and COMMON/RRR/ are passed to a Fortran subroutine (FORCOM) and its assembly language equivalent (ASMCOM), where some minor calculations are performed.
PROGRAM CMNMAIN
common i,j,k
common /rrr/ x,y,z
i = 4; j = 17; k = 0
x = 1.6; y = 3.7; z = 0.0
call forcom
print 10, i,j,k
10 format (' i,j,k=',3I6)
print 20, x,y,z
20 format (' x,y,z=',3F10.4)
i = 4; j = 17; k = 0
x = 1.6; y = 3.7; z = 0.0
call asmcom
print 10, i,j,k
print 20, x,y,z
stop
end
SUBROUTINE FORCOM
common i,j,k
common /rrr/ x,y,z
k = 5*i + j
z = x*y
return
end
This is ASMCOM, the assembly language subroutine that manipulates variables in the two COMMON blocks.
TITLE ASMCOM
.386
.MODEL FLAT
;
BLNKCOM STRUCT
i dword ?
j dword ?
k dword ?
BLNKCOM ENDS
;
EXTERN __BLNK__:BLNKCOM
;
RRRCOM STRUCT
x real4 ?
y real4 ?
z real4 ?
RRRCOM ENDS
;
EXTRN _rrr_:RRRCOM
;
_ACODE SEGMENT PARA USE32 PUBLIC 'CODE'
ASSUME CS:_ACODE, DS:_ADATA
PUBLIC _asmcom_ ; Entry point name
_asmcom_ PROC NEAR ; Start of procedure
push ebp ; Save EBP
mov ebp,esp ; use EBP for args
mov eax, dword ptr __BLNK__.i ; Get I
mov ecx, m1 ; Load 5
mul ecx ; Form 5*I
add eax, dword ptr __BLNK__.j ; 5*I+J
mov dword ptr __BLNK__.k,eax ; Store into K
fld dword ptr _rrr_.x ; Load X
fmul dword ptr _rrr_.y ; Form X*Y
fstp dword ptr _rrr_.z ; Z=X*Y
mov esp,ebp ; Restore stack pointer
pop ebp ; Restore base pointer
ret ; Return to caller
_asmcom_ ENDP ; End of procedure
_ACODE ENDS ; End of segment
;
_ADATA SEGMENT PARA USE32 PUBLIC 'DATA'
m1 dd 5
_ADATA ENDS
;
END
The starting addresses of the COMMON blocks are obtained by using EXTERN directives to connect to the global values. The individual variables within a COMMON block can then be accessed as STRUCTs that are written so as to match the layout of the Fortran code's COMMON declarations. Each COMMON block must consist of a STRUCT definition, plus an EXTERN declaration to connect it to the global data object.
Type CHARACTER variables are passed to subroutines as two arguments: the starting address of the string, and the string's length. The two arguments are not, however, pushed consecutively onto the stack. Rather, the address pointer is pushed in the usual order, and then after all arguments have been passed, the lengths of any CHARACTER arguments are passed by value.
Here is an example of a main program (CHRMAIN), that calls a Fortran subroutine (FORCAPS), and its assembly language equivalent (ASMCAPS). Both FORCAPS and ASMCAPS take two CHARACTER arguments; the first argument is converted into all upper case letters, and then returned in the second argument.
PROGRAM CHRMAIN
character (len=20) :: line1, line2, line3
line1 = 'This is a message'
line2 = 'zzzzzzzzzzzzzzzzzzzz'
line3 = 'aaaaaaaaaaaaaaaaaaaa'
call forcaps(line1,line2)
print 20, line1
print 20, line2
20 format (1X,A)
call asmcaps(line1,line3)
print 20, line1
print 20, line3
stop
end
SUBROUTINE FORCAPS(L1,L2)
character*(*) :: l1, l2
n = len(l1) ! Converts all
do i=1,n ! chars to caps
ic = ichar(l1(i:i))
if (ic.ge.97 .and. ic.le.122) ic = ic-32
l2(i:i) = char(ic)
enddo
return
end
This is the assembly language string capitalization routine.
TITLE ASMCAPS
.386
.MODEL FLAT
;
_ACODE SEGMENT PARA USE32 PUBLIC 'CODE'
ASSUME CS:_ACODE
PUBLIC _asmcaps_ ; Entry point name
_asmcaps_ PROC NEAR ; Start of procedure
push ebp ; Save EBP
mov ebp,esp ; Will use EBP for args
push esi ; Must preserve ESI
push edi ; Must preserve EDI
;
mov esi,[ebp+8] ; 1st string addr (L1)
mov edi,[ebp+12] ; 2nd string addr (L2)
mov ecx,[ebp+16] ; 1st string length
cmp ecx, 0 ; Length nonpositive?
jle Exit ; Yes, so return
;
Looper: mov al, [esi] ; Get char from L1
cmp al, 97 ; Below "a"?
jl PutIt ; Yes, so no conversion
cmp al, 122 ; Above "z"?
jg PutIt ; Yes, so no conversion
sub al, 32 ; Change LC to UC
PutIt: mov [edi], al ; Store
inc esi ; Point to next char
inc edi ; Point to next target
loop Looper ; Loop until done
;
Exit: pop edi ; Restore saved EDI
pop esi ; Restore saved ESI
mov esp,ebp ; Restore stack pointer
pop ebp ; Restore base pointer
ret ; Return to caller
_asmcaps_ ENDP ; End of procedure
_ACODE ENDS ; End of segment
END
Note that the starting addresses of the arguments are stored in EBP+8 and EBP+12, while the lengths of the two CHARACTER variables are in EBP+16 and EBP+20. In this code, we do not make use of the length of the second string, assuming it to be equal to that of the first one.
Since we use the ESI and EDI registers in this subroutine, we save their previous values on the stack and restore them before returning.
The methods for passing arguments and COMMON blocks to a FUNCTION are identical to those described above for a SUBROUTINE. The only difference in the calling sequence is that a FUNCTION returns a value, and the method that is used to send the result back to the calling routine depends upon the data type of that value.
INTEGER-valued FUNCTIONs return values using CPU registers, so that the return value for one-byte, two-byte, and four-byte functions are returned in AL, AX, and EAX, respectively.
Four-byte and eight-byte REAL FUNCTIONs use the top of the floating-point unit stack, ST(0) for return of values. The only difference in the assembly language access of these variable types is that the former require DWORD PTR, while the latter use QWORD PTR when loading to and storing from the FPU. These conventions are summarized in the table titled FUNCTION Return Mechanisms.
|
Function Type |
Kind No. |
Location of Return Value |
|---|---|---|
|
INTEGER |
1 |
AL |
|
INTEGER |
2 |
AX |
|
INTEGER |
4 |
EAX |
|
LOGICAL |
1 |
AL |
|
LOGICAL |
4 |
EAX |
|
REAL |
4 |
ST(0) |
|
REAL |
8 |
ST(0) |
|
COMPLEX |
4 |
Address on stack |
|
COMPLEX |
8 |
Address on stack |
|
CHARACTER |
all |
Address & length on stack |
When an LF95 program calls a COMPLEX-valued function, it first pushes the argument addresses onto the stack, and then also pushes the address of a place where the function should store its return value. Thus, after the function preamble (where the contents of ESP are stored into EBP), EBP+8 will contain the address of the return buffer, and the normal argument pointers will start at EBP+12.
Here is an example of a program that passes a COMPLEX variable to a COMPLEX-valued Fortran function CXFFUN that returns two times its argument.
PROGRAM CXMAIN
complex :: a, b, c, cxffun, cxafun
a = (1.0,2.0)
b = cxffun(a)
c = cxafun(a)
print *, 'a=',a
print *, 'b=',b
print *, 'c=',c
stop
end
FUNCTION CXFFUN(A)
complex :: a, cxffun
cxffun = a+a
return
end
The above program also calls a COMPLEX-valued assembly language function CXAFUN, that performs exactly the same operation as CXFFUN, i.e., it returns double the argument.
TITLE CXAFUN
.386
.MODEL FLAT
;
_ACODE SEGMENT PARA USE32 PUBLIC 'CODE'
ASSUME CS:_ACODE
PUBLIC _cxafun_ ; Entry point name
_cxafun_ PROC NEAR ; Start of procedure
push ebp ; Save EBP
mov ebp,esp ; Will use EBP for args
;
mov eax, [ebp+12] ; Argument address
fld dword ptr [eax] ; Get real part
fadd dword ptr [eax] ; Double it
fld dword ptr [eax+4] ; Get imag part
fadd dword ptr [eax+4] ; Double it
;
mov eax, [ebp+8] ; Return buffer address
fstp dword ptr [eax+4] ; Store imag part
fstp dword ptr [eax] ; Store real part
;
mov esp,ebp ; Restore stack pointer
pop ebp ; Restore base pointer
ret ; Return to caller
_cxafun_ ENDP ; End of procedure
_ACODE ENDS ; End of segment
END
Looking at this function, we can see that the single argument's address is stored in EBP+12. That is the address of the real part of the argument, with the imaginary part being stored four bytes higher in memory.
Both parts of the argument are copied into the FPU and doubled. The results are then stored into the return buffer, whose address is found at EBP+8. That is, of course, the address of the real part and the imaginary component is stored four bytes higher.
A somewhat more complicated mechanism is used for CHARACTER-valued functions. After the argument information has been pushed on the stack, they are followed by the length and starting address of the memory buffer that will accept the result. As a consequence, the return buffer's address can be found in EBP+8, and its length in EBP+12. The address of the first argument is then moved up to EBP+16, and any other arguments follow in the usual manner.
Here is a Fortran main program that sets a CHARACTER variable equal to the string ``Hello,'' and then calls a Fortran function (FFUN) that returns a capitalized form of the string. The program then calls an assembly language function (AFUN) that returns a decapitalized version.
PROGRAM CHMAIN
character*20 a, b, c, ffun, afun
a = 'Hello'
b = ffun(a)
c = afun(b)
print 20, a, b, c
20 format (' a = ',A/' b = ',A/' c = ',A)
stop
end
CHARACTER*20 FUNCTION FFUN(A)
character*(*) a
n = len(a)
do i=1,n
ic = ichar(a(i:i))
if (ic.ge.97 .and. ic.le.122) ic = ic-32
ffun(i:i) = char(ic)
enddo
return
end
This is the CHARACTER-valued assembly language function that is used by the program above:
TITLE AFUN
.386
.MODEL FLAT
;
_ACODE SEGMENT PARA USE32 PUBLIC 'CODE'
ASSUME CS:_ACODE
PUBLIC _afun_ ; Entry point name
_afun_ PROC NEAR ; Start of procedure
push ebp ; Save EBP
mov ebp,esp ; Will use EBP for args
push esi
push edi
;
mov edx, [ebp+12]; Length of return buffer
mov eax, [ebp+20]; Length of argument
cmp edx, eax ; Which is smaller?
jg L10 ; Return buffer
mov ecx, edx ; Get arg length
jmp L20
L10: mov ecx, eax ; Get ret buf length
L20: cmp ecx, 0 ; Length nonpositive?
jle L90 ; Yes, so return
;
mov esi, [ebp+16]; Addr of argument
mov edi, [ebp+8] ; Addr of ret buf
L30: mov al, [esi] ; Get char from L1
cmp al, 65 ; Below "A"?
jl L40 ; Yes, so no conversion
cmp al, 90 ; Above "Z"?
jg L40 ; Yes, so no conversion
add al, 32 ; Change UC to LC
L40: mov [edi], al ; Store
inc esi ; Point to next char
inc edi ; Point to next target
loop L30 ; Loop until done
;
L90: pop edi ; Restore saved EDI
pop esi
mov esp,ebp ; Restore stack pointer
pop ebp ; Restore base pointer
ret ; Return to caller
_afun_ ENDP ; End of procedure
_ACODE ENDS ; End of segment
END
The sole argument is passed with its starting address in EBP+16, and its length in EBP+20 --- remember that if there are several arguments, then the CHARACTER lengths follow the entire list of addresses. The return buffer, the place where the function should store its return value is communicated by its starting address (in EBP+8) and length (in EBP+12).