nearheap.asm
上传用户:super_houu
上传日期:2008-09-21
资源大小:4099k
文件大小:31k
- ;[]-----------------------------------------------------------------[]
- ;| NEARHEAP.ASM |
- ;[]-----------------------------------------------------------------[]
- ;
- ; Paradigm C/C++ Run-Time Library - Version 5.0
- ;
- ; Copyright (c) 1998 Paradigm Systems. All rights reserved.
- ; Portions Copyright (c) 1996 Borland International.
- ;
- ; $Revision: 10 $
- ; $Workfile: nearheap.asm $
- ;
- INCLUDE ServicesIncludeRULES.ASI
- INCLUDE ServicesIncludeC0.INC
- INCLUDE ServicesIncludeSTARTUP.INC
- LOCALS
- INCLUDE ServicesInclude_HEAP.INC
- ;IF LDATA EQ false
- ;-----------------------------------------------------------------------
- ; Memory Block Header (near heap)
- ;-----------------------------------------------------------------------
- ; Each block in the heap, whether allocated or free, has a header.
- ; For an allocated block, only the first two fields of the header are
- ; used. For a free block all eight bytes are used, thus the smallest
- ; possible block is the size of a free header.
- ;
- ; Field Description
- ; --------- ----------------------------------------------------------
- ; size total size, in bytes, of this block (+1 if the block is in use)
- ; prev_real pointer to the physically previous block in the heap
- ; prev_free pointer to the logically previous free block
- ; next_free pointer to the logically next free block
- ;
- ; Note that the size field is also used to indicate whether the block
- ; is allocated or free. A doubly-linked queue is maintained of the
- ; free blocks and it is important to know that ordering of the blocks
- ; in this queue is logical rather than physical. If there is only one
- ; free block on the heap prev_free and next_free point to itself.
- ;-----------------------------------------------------------------------
- Header STRUC
- bsize dw ?
- prev_real dw ?
- prev_free dw ?
- next_free dw ?
- ENDS
- ;-----------------------------------------------------------------------
- ; heapinfo structure (near heap)
- ;-----------------------------------------------------------------------
- ; Used by the heapwalk function.
- ; heapwalk accepts a pointer to a struct of this type.
- ; On entry, the pointer field contains the address of the previous
- ; memory block in the heap (NULL for the first call). The next block
- ; in the heap is then found and its address is stored in the structure
- ; along with its size, in bytes, and a 'used' flag.
- ;-----------------------------------------------------------------------
- HeapInfo STRUC
- hi_ptr dw ?
- hi_size dw ?
- hi_inuse dw ?
- ENDS
- UsedHeaderSize EQU 4
- FreeHeaderSize EQU 8
- DISINTERR = 1
- POOL STRUC
- head dw ?
- first dw ?
- last dw ?
- rover dw ?
- brklvl dw ?
- heaplimit dw ?
- ENDS
- ;-----------------------------------------------------------------------
- ; Only three variables are needed to efficiently manage the heap.
- ;-----------------------------------------------------------------------
- EXTRN __nearheap_start : DWORD
- EXTRN __stack_segment : DWORD
- _BSS SEGMENT WORD PUBLIC 'BSS'
- ; PUBLIC __first,__last,__rover
- ; PUBLIC ___brklvl, ___heaplimit
- PUBLIC __heapbase
- PUBLIC __mempool
- ALIGN 2
- ;__first dw ? ;pointer to the first block
- ;__last dw ? ;pointer to the last block
- ;__rover dw ? ;pointer to an arbitrary free block
- ;___brklvl dw ?
- ;___heaplimit dw ?
- DEFAULT_HEAP equ 0
- UI_HEAP equ 1
- MAX_POOLNUM equ 2
- __mempool POOL MAX_POOLNUM DUP(<>)
- __heapbase dw ?
- _BSS ENDS
- IF LPROG
- IF DISINTERR
- EXTRADISP equ 4 ; Allow for FAR returns when getting parms
- ELSE
- EXTRADISP equ 2 ; Allow for FAR returns when getting parms
- ENDIF
- ELSE
- IF DISINTERR
- EXTRADISP equ 2 ; Allow for FAR returns when getting parms
- ELSE
- EXTRADISP equ 0 ; Allow for FAR returns when getting parms
- ENDIF
- ENDIF
- DGROUP GROUP _DATA, _BSS
- ALIAS <_I49malloc> = <__clib_malloc>
- ALIAS <_I49realloc> = <__clib_realloc>
- ALIAS <_I49free> = <__clib_free>
- EXTRN ___brk:NEAR, ___sbrk:NEAR
- _TEXT SEGMENT PUBLIC 'CODE'
- ASSUME CS:_TEXT,DS:DGROUP
- ;-----------------------------------------------------------------------------
- ; C callable function to free a memory block
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the block to free (stack)
- ; Returns: void
- ;-----------------------------------------------------------------------------
- PUBLIC __clib_free
- __clib_free PROC DIST
- push si
- push di
- IF DISINTERR
- pushf
- cli
- ENDIF
- push bp
- mov bp,sp
- mov bx,[bp+(10+EXTRADISP)] ;bx = first parameter passed
- call CalHeapBase
- mov bx,[bp+(8+EXTRADISP)] ;bx = first parameter passed
-
- sub bx,UsedHeaderSize ;bx = the address of the block to free
- jc short @@AllDone ;skip if NULL
-
- ;cmp bx,[__last] ;is this the last block in the heap?
- mov di, __heapbase
- cmp bx,[di.last]
-
- je short @@LastBlock
- @@InnerBlock:
- call FreeInnerBlock ;free the inner block
- jmp short @@AllDone
- @@LastBlock:
- call FreeLastBlock ;free the last block
- @@AllDone:
- pop bp
- IF DISINTERR
- popf
- ENDIF
- pop di ;all done
- pop si
- ret
- __clib_free ENDP
- ;Input: pool ID (bx)
- CalHeapBase PROC NEAR
- push ax
- push dx
- mov ax, TYPE POOL
- mul bx
- add ax, OFFSET __mempool
- mov __heapbase, ax
- pop dx
- pop ax
- ret
- CalHeapBase ENDP
- ;-----------------------------------------------------------------------------
- ; Frees the last block on the heap
- ; free helper function
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the block (bx)
- ; Returns: void
- ;-----------------------------------------------------------------------------
- FreeLastBlock PROC NEAR
- ;cmp [__first],bx ;freeing the ONLY block?
- mov di,__heapbase
- cmp [di.first],bx
-
- je short @@KillHeap
- mov si,[bx.prev_real] ;si = next-to-last block
- test BYTE PTR [si.bsize],01h
- jz short @@PreviousBlockIsFree
- @@PreviousBlockIsUsed:
- ;mov __last,si
- mov di,__heapbase
- mov [di.last],si
-
- jmp short @@ResetBreak ;done!
- @@PreviousBlockIsFree:
- ;cmp si,[__first] ;is the previous block the
- mov di,__heapbase
- cmp si,[di.first]
-
- je short @@ResetHeap ;first block in the heap?
- mov bx,si ;remove the next-to-last block
- call PullFreeBlock ;from the free-block queue
- mov ax,[bx.prev_real]
- ;mov __last,ax
- mov di,__heapbase
- mov [di.last],ax
- jmp short @@ResetBreak ;done!
- @@ResetHeap:
- mov bx,si
- ;we are freeing the only block so reset the break level and kill the heap
- @@KillHeap:
- xor ax,ax
- ;mov __first,ax ;clear variables
- ;mov __last,ax
- ;mov __rover,ax
- mov di,__heapbase
- mov [di.first],ax
- mov [di.last],ax
- mov [di.rover],ax
- @@ResetBreak:
- push bx
- call ___brk ;reset the break level
- pop bx ;cleanup stack
- ret
- FreeLastBlock ENDP
- ;-----------------------------------------------------------------------------
- ; Frees an interior block from within the heap
- ; free helper function
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the block (bx)
- ; Returns: void
- ;-----------------------------------------------------------------------------
- FreeInnerBlock PROC NEAR
- dec WORD PTR [bx.bsize] ;mark the block as free
- ;cmp bx,[__first] ;first block?
- mov di,__heapbase
- cmp bx,[di.first]
-
- je short @@PreviousBlockIsUsed
- mov si,[bx.prev_real] ;get the previous block (si)
- mov ax,[si.bsize] ;is the previous block free?
- test al,01h
- jnz short @@PreviousBlockIsUsed
- ; join this block to the previous block
- @@PreviousBlockIsFree:
- add ax,[bx.bsize] ;add the size of this block to
- mov [si.bsize],ax ;the size of the previous block
- mov di,[bx.bsize] ;get the next block (di)
- add di,bx
- mov [di.prev_real],si ;adjust the prev_real pointer
- mov bx,si ;set up the current block
- jmp SHORT @@CheckNextBlock
- @@PreviousBlockIsUsed:
- call InsertFreeBlock ;add it to the free queue
- @@CheckNextBlock:
- mov di,[bx.bsize] ;get the next block (di)
- add di,bx
- mov ax,[di.bsize] ;is the next block free?
- test al,01h
- jz short JoinFreeBlocks ;join this block to the next
- @@AllDone:
- ret ;all done
- FreeInnerBlock ENDP
- ;-----------------------------------------------------------------------------
- ; Joins two physically adjacent free blocks together
- ; free helper function
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the lower block (bx)
- ; Pointer to the upper block (di)
- ; Size of the upper block, in bytes (ax)
- ; Returns: void
- ; Registers destroyed: ax si di
- ; This routine falls through to PullFreeBlock
- ;-----------------------------------------------------------------------------
- JoinFreeBlocks PROC NEAR
- add [bx.bsize],ax ;adjust the size of the lower block
- mov si,di ;si = the next block after di
- add si,ax
- mov [si.prev_real],bx ;adjust the link
- mov bx,di
- ;;;; jmp SHORT PullFreeBlock ;eliminate the upper block
- JoinFreeBlocks ENDP
- ;-----------------------------------------------------------------------------
- ; Removes a block from the free block queue
- ; free helper function
- ; malloc helper function
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the block (bx)
- ; Returns: void
- ;-----------------------------------------------------------------------------
- PullFreeBlock PROC NEAR
- mov di,[bx.next_free] ;di = the next free block
- cmp bx,di ;removing the last free block?
- je short @@NoFreeBlocks
- ;mov __rover,di
- mov si,__heapbase
- mov [si.rover],di
-
- mov si,[bx.prev_free] ;si = previous free block
- mov [di.prev_free],si ;adjust the links
- mov [si.next_free],di
- ret ;all done
- @@NoFreeBlocks:
- ;mov __rover,0
- mov di,__heapbase
- mov [di.rover],0
- ret ;all done
- PullFreeBlock ENDP
- ;-----------------------------------------------------------------------------
- ; Inserts a block into the free block queue
- ; free helper function
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the block (bx)
- ; Returns: void
- ;-----------------------------------------------------------------------------
- InsertFreeBlock PROC NEAR
- ;mov si,[__rover] ;si = rover pointer
-
- mov di,__heapbase
- mov si,[di.rover]
-
- or si,si ;no free blocks?
- jz short @@FirstFreeBlock
- @@AnotherFreeBlock:
- mov di,[si.next_free] ;di = free block after rover
- mov [si.next_free],bx ;adjust links
- mov [di.prev_free],bx
- mov [bx.next_free],di
- mov [bx.prev_free],si
- ret
- @@FirstFreeBlock:
- ;mov __rover,bx
- mov di,__heapbase
- mov [di.rover],bx
- mov [bx.prev_free],bx
- mov [bx.next_free],bx
- ret
- InsertFreeBlock ENDP
- ;-----------------------------------------------------------------------------
- ; C callable function to allocates a given number of bytes from the heap
- ;-----------------------------------------------------------------------------
- ; Args: Number of bytes requested (stack)
- ; Returns: Address of the first byte of user space available
- ; from the heap if successful (ax)
- ; NULL if failure (ax)
- ;-----------------------------------------------------------------------------
- PUBLIC __clib_malloc
- __clib_malloc PROC DIST
- push si ;should be on an odd address
- push di
- IF DISINTERR
- pushf
- cli
- ENDIF
- push bp
- mov bp,sp
- mov ax,[bp+(8+EXTRADISP)] ;bx = first parameter passed
- or ax,ax ;does he want zero bytes?
- jz short @@AllDone
- mov bx,[bp+(10+EXTRADISP)] ;bx = first parameter passed
- call CalHeapBase
- add ax,UsedHeaderSize+1 ;add the header size
- jc short @@NoCanDo ;was size too great?
- and ax,0fffeh ;force a word boundary
- cmp ax,FreeHeaderSize
- jae short @@BigEnough
- mov ax,FreeHeaderSize
- @@BigEnough:
- ; cmp [__first],0 ;do we have a heap yet?
- mov di,__heapbase
- cmp [di.first],0
- jz short @@BuildHeap
- ;mov bx,[__rover] ;bx = rover pointer
- mov di,__heapbase
- mov bx,[di.rover]
-
- or bx,bx ;are there any free blocks at all?
- jz short @@AddToHeap
- mov dx,bx ;dx = rover pointer
- @@SearchHeap:
- cmp [bx.bsize],ax ;big enough to use at all?
- jae short @@AllocateBlock
- @@TooSmall:
- mov bx,[bx.next_free] ;move to the next free block
- cmp bx,dx ;at the end of the list?
- jne short @@SearchHeap
- @@AddToHeap:
- call ExtendHeap
- jmp SHORT @@AllDone
- @@DivideFreeBlock:
- call AllocatePartialBlock
- jmp SHORT @@AllDone
- @@BuildHeap:
- call CreateHeap
- jmp SHORT @@AllDone
- @@NoCanDo:
- xor ax,ax
- jmp SHORT @@AllDone
- @@AllocateBlock:
- mov si,ax ;si = smallest divisible block size
- add si,FreeHeaderSize
- cmp [bx.bsize],si ;big enough to break up?
- jae short @@DivideFreeBlock
- call PullFreeBlock ;remove it from the free-block queue
- inc [bx.bsize] ;mark it as allocated
- mov ax,bx
- add ax,UsedHeaderSize
- @@AllDone:
- xor dx,dx
- or ax,ax ;are there any free blocks at all?
- jz short @@ReturnNULL
- mov dx,DGROUP
- @@ReturnNULL:
- pop bp
- IF DISINTERR
- popf
- ENDIF
- pop di ;all done
- pop si
- ret
- __clib_malloc ENDP
- ;-----------------------------------------------------------------------------
- ; Creates a heap from scratch
- ; malloc helper function
- ;-----------------------------------------------------------------------------
- ; Args: Number of bytes for the first block requested (ax)
- ; Returns: Address of the first byte of user space available
- ; from the heap if successful (ax)
- ; NULL if failure (ax)
- ;-----------------------------------------------------------------------------
- CreateHeap PROC NEAR
- push ax ;save the size
- xor ax,ax ;align the heap on word
- push ax
- push ax
- call ___sbrk ;retrieve the break level
- pop bx ;cleanup stack
- pop bx
- and ax,0001h
- jz short @@Aligned
- xor dx,dx
- push dx
- push ax
- call ___sbrk ;align the heap
- pop bx ;cleanup stack
- pop bx
- @@Aligned:
- pop ax ;retrieve and save the size
- push ax
- xor bx,bx ;convert size request from
- push bx ;unsigned int to signed long
- push ax
- call ___sbrk ;adjust the break level
- pop bx ;cleanup stack
- pop bx
- cmp ax,-1 ;failure?
- je short @@NoRoom
- mov bx,ax ;bx = new block
- ;mov __first,bx ;save pointers
- ;mov __last,bx
- mov di, __heapbase
- mov [di.first], bx
- mov [di.last],bx
-
- pop ax ;retrieve the size
- inc ax ;mark it as allocated
- mov [bx.bsize],ax
- add bx,UsedHeaderSize
- mov ax,bx
- ret
- @@NoRoom:
- pop bx ;clear the size from the stack
- xor ax,ax
- ret
- CreateHeap ENDP
- ;-----------------------------------------------------------------------------
- ; Attempts to extend the heap.
- ; malloc helper function
- ;-----------------------------------------------------------------------------
- ; Args: Number of bytes for the block requested (ax)
- ; Returns: Address of the first byte of user space available
- ; from the heap if successful (ax)
- ; NULL if failure (ax)
- ;-----------------------------------------------------------------------------
- ExtendHeap PROC NEAR
- push ax ;save the size
- xor bx,bx ;convert size request from
- push bx ;unsigned int to signed long
- push ax
- call ___sbrk ;adjust the break level
- pop bx ;cleanup stack
- pop bx
- cmp ax,-1 ;failure?
- je short @@NoRoom
- mov bx,ax ;bx = new block
- mov di, __heapbase
- ;mov ax,[__last] ;ax = next-to-the-last block
- mov ax,[di.last]
- mov [bx.prev_real],ax
- ;mov __last,bx ;update last-block pointer
- mov [di.last],bx
- pop ax ;retrieve the size
- inc ax ;mark it as allocated
- mov [bx.bsize],ax
- add bx,UsedHeaderSize
- mov ax,bx
- ret
- @@NoRoom: pop ax ;retrieve the size
- xor ax,ax
- ret
- ExtendHeap ENDP
- ;-----------------------------------------------------------------------------
- ; Divides a free block into two pieces.
- ; malloc helper function
- ;-----------------------------------------------------------------------------
- ; Args: Number of bytes for the block requested (ax)
- ; Pointer of the block to divide (bx)
- ; Returns: Address of the first byte of user space available
- ; from the heap (ax)
- ;-----------------------------------------------------------------------------
- AllocatePartialBlock PROC NEAR
- sub [bx.bsize],ax ;make room!
- mov si,bx ;si = new block address
- add si,[bx.bsize]
- mov di,si ;di = the block after the new block
- add di,ax
- inc ax
- mov [si.bsize],ax
- mov [si.prev_real],bx
- mov [di.prev_real],si
- add si,UsedHeaderSize
- mov ax,si
- ret
- AllocatePartialBlock ENDP
- ;-----------------------------------------------------------------------------
- ; Attempts to expand a block, relocating it if necessary
- ; realloc helper function
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the old block (bx)
- ; Size of the block (cx)
- ; Number of bytes requested (ax)
- ; Returns: Address of the first byte of user space available
- ; from the heap if successful (bx)
- ; NULL if failure (bx)
- ;-----------------------------------------------------------------------------
- ExpandBlock PROC NEAR
- mov bp,sp
- push bx ;[bp-2] = old block
- push ax ;[bp-4] = new size
- push cx ;[bp-6] = old block size
- push ax
- call __clib_malloc ;ax = data area of new block
- pop bx ;cleanup stack
- mov bx,ax
- or ax,ax
- jz short @@AllDone ;malloc failed
- @@MallocOK:
- push ds ;move the data to the new block
- pop es
- cld
- mov di,ax ;di = data area of new block
- mov si,[bp-2] ;si = old block
- mov cx,[si.bsize] ;cx = old block size
- add si,UsedHeaderSize ;si = data area of old block
- push si ;save for call to _free
- sub cx,UsedHeaderSize+1 ;cx = number of bytes in old data area
- @@MoveIt:
- shr cx,1 ;cx = number of words in data area
- rep
- movsw
- mov [bp-2],ax ;save data area of new block in scratch area
- call __clib_free
- pop bx ;cleanup stack
- mov bx,[bp-2]
- @@AllDone:
- add sp,6
- ret
- ExpandBlock ENDP
- ;-----------------------------------------------------------------------------
- ; Shrinks a block
- ; realloc helper function
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the block (bx)
- ; Size of the block (cx)
- ; Normalized number of bytes requested (dx)
- ; Returns: Address of the first byte of user space available
- ; from the heap if successful (bx)
- ;-----------------------------------------------------------------------------
- ShrinkBlock PROC NEAR
- mov ax,dx ;ax = requested block size
- add dx,FreeHeaderSize
- cmp dx,cx
- ja short @@AllDone
- mov dx,cx ;dx = old block size
- @@DivideTheBlock:
- ;cmp bx,[__last] ;last block in the heap?
- mov di, __heapbase
- cmp bx, [di.last]
-
- jne short @@InnerBlock
- @@LastBlock:
- mov [bx.bsize],ax
- inc [bx.bsize]
- add ax,bx
- push bx ;save the old block
- push ax
- call ___brk ;reset the break level
- pop bx ;cleanup stack
- pop bx ;restore old block
- jmp SHORT @@AllDone
- @@InnerBlock:
- mov di,bx
- add di,ax ;di = new (free) block
- mov [di.prev_real],bx
- sub dx,ax ;dx = size of new (free) block
- sub [bx.bsize],dx
- mov si,di ;si = next block after the new one
- add si,dx
- mov [si.prev_real],di ;adjust the link
- inc dx ;mark it as used
- mov [di.bsize],dx
- mov cx,bx ;save the old block
- mov bx,di
- call FreeInnerBlock
- mov bx,cx ;restore old block
- @@AllDone:
- add bx,UsedHeaderSize
- ret
- ShrinkBlock ENDP
- ;-----------------------------------------------------------------------------
- ; Attempts to reallocate a block
- ;-----------------------------------------------------------------------------
- ; Args: Pointer to the old block (stack)
- ; Number of bytes requested (stack)
- ; Returns: Address of the first byte of user space available
- ; from the heap if successful (ax)
- ; NULL if failure (ax)
- ;-----------------------------------------------------------------------------
- PUBLIC __clib_realloc
- __clib_realloc PROC DIST
- push si
- push di
- IF DISINTERR
- pushf
- cli
- ENDIF
- push bp
- mov bp,sp
- mov bx,[bp+(12+EXTRADISP)] ;bx = first parameter passed
- call CalHeapBase
-
- mov bx,[bp+(8+EXTRADISP)] ;bx = pointer to the block to realloc
- mov ax,[bp+(10+EXTRADISP)] ;ax = number of bytes requested
- or ax,ax ;does he really want 0 bytes???
- jz short @@FreeIt ;let's give him what he wants!
- or bx,bx ;did we get a NULL pointer?
- jz short @@MallocIt ;OK, try to malloc it
- sub bx,UsedHeaderSize ;make bx = start of block
- mov cx,[bx.bsize] ;cx = size of block
- dec cx
- mov dx,ax
- add dx,UsedHeaderSize+1 ;add the header size and
- and dx,0fffeh ;force a word boundary
- cmp dx,FreeHeaderSize
- jae short @@BigEnough
- mov dx,FreeHeaderSize
- @@BigEnough:
- cmp cx,dx
- jb short @@ExpandIt
- ja short @@ShrinkIt
- @@NoChange:
- add bx,UsedHeaderSize
- jmp SHORT @@Resized
- @@ShrinkIt:
- call ShrinkBlock
- jmp SHORT @@Resized
- @@ExpandIt:
- call ExpandBlock
- @@Resized:
- mov ax,bx
- jmp SHORT @@AllDone
- @@MallocIt:
- push ax
- call __clib_malloc
- jmp SHORT @@Cleanup
- @@FreeIt:
- push bx
- call __clib_free
- xor ax, ax
- @@Cleanup:
- pop bx ;cleanup stack
- @@AllDone:
- xor dx,dx
- or ax,ax ;are there any free blocks at all?
- jz short @@ReturnNULL
- mov dx,DGROUP
- @@ReturnNULL:
- pop bp ;all done
- IF DISINTERR
- popf
- ENDIF
- pop di
- pop si
- ret
- __clib_realloc ENDP
- ;
- ; This is the code which initializes the near heap
- ;
- public __I49InitHeapConstants
- __I49InitHeapConstants PROC DIST
- mov bx, DEFAULT_HEAP
- call CalHeapBase
-
- mov ax, OFFSET DGROUP:__nearheap_start
- ;mov ___brklvl,ax
- mov bx, __heapbase
- mov [bx.brklvl],ax
- mov [bx.head],ax
-
- mov ax, OFFSET DGROUP:__stack_segment
- ;mov ___heaplimit,ax
- mov [bx.heaplimit],ax
- ret
- __I49InitHeapConstants ENDP
- ENDS
-
- ;
- ; Near heap initializer. This entry gets placed in the table of startup code
- ; initializers to automatically initialize the heap.
- ;
- ;DefSeg _INIT_, word, public, INITDATA, IGROUP
- ;_INIT_ segment
- ; InitRec <MM_NEAR, PRI_DMM, offset _InitNearHeap, seg _InitNearHeap >
- ;_INIT_ ends
- ;ENDIF
- END