A nifty trick to get a subroutine to repeat exactly once, without looping
Posted: Sun Oct 03, 2021 8:35 pm
Here's a pretty handy trick I came up with for debugging, although I doubt I'm the first person to think of this. Let's say you have a routine you're testing, that's supposed to loop multiple times. It works fine on the first go-round but bugs out on the rest. You want to quickly test what would happen if it looped exactly once. As an example, I'll show a loop I'm testing out on 8086 Assembly:
First, I'll comment out the loop overhead so that it runs once and terminates.
Here's the output of that.
Next, I will call the "repeatOnce" routine at the top of this one.
And here is the output:
This repeatOnce routine will result in everything after it, up to the next "ret", repeat exactly once. How does it do this? I'll show you:
And that's all there is to it. This works because when you call a subroutine, the top of the stack is guaranteed to have the return address on it. So if the first instruction is a pop, you'll get the return address. Next, that return address is pushed onto the stack twice. The "ret" in repeatOnce will bring execution to the point just after it was called. And since we pushed it twice, the outer routine's ret will take us there again. After that, the top of the stack contains the return address of the outer function, which will take us back to just after the outer function was called. It's hard to explain but it works.
However, there are times when this will not work. Here's an example of that:
This will result in a crash, because the stack is unbalanced. The stack is unbalanced because while bx and cx are pushed once each, they are getting popped twice. This means the return address of foo has been taken off the stack before the return can happen. The original example doesn't have this problem because the repeatOnce function is not nested between stack operations.
Code: Select all
Draw_VGA_Tilemap:
;no RLE
;INPUT:
;DS:SI = TILEMAP SOURCE
mov dx,0 ;reset drawing cursors to top left of screen
Draw_VGA_Tilemap_NextRow:
Draw_VGA_Tilemap_NextColumn:
lodsb ;get the next tile index
push si
push bx
mov ah,0
mov bx,offset TilemapLookupTable
;in a later build pull this from ram, to allow for more
;lookup tables than just one.
call XLATW
;returns the offset of the tile's bitmap data in ax
mov si,ax
call LocateVRAM ;adjust DI according to DX.
lodsb
mov cl,al
lodsb
mov ch,al
mov word ptr [ds:PixelPlotX],cx
call DrawBitmap_VGA_Palette0
pop bx
pop si
inc dh
cmp dh,40
jb Draw_VGA_Tilemap_NextColumn
mov dh,0
inc dl
cmp dl,0Dh
jb Draw_VGA_Tilemap_NextRow
done_Draw_VGA_Tilemap:
ret
Code: Select all
Draw_VGA_Tilemap:
;no RLE
;INPUT:
;DS:SI = TILEMAP SOURCE
mov dx,0 ;reset drawing cursors to top left of screen
Draw_VGA_Tilemap_NextRow:
Draw_VGA_Tilemap_NextColumn:
; call repeatOnce
lodsb ;get the next tile index
push si
push bx
mov ah,0
mov bx,offset TilemapLookupTable
;in a later build pull this from ram, to allow for more
;lookup tables than just one.
call XLATW
;returns the offset of the tile's bitmap data in ax
mov si,ax
call LocateVRAM ;adjust DI according to DX.
lodsb
mov cl,al
lodsb
mov ch,al
mov word ptr [ds:PixelPlotX],cx
call DrawBitmap_VGA_Palette0
pop bx
pop si
inc dh
; cmp dh,40
; jb Draw_VGA_Tilemap_NextColumn
; mov dh,0
; inc dl
; cmp dl,0Dh
; jb Draw_VGA_Tilemap_NextRow
done_Draw_VGA_Tilemap:
ret
Code: Select all
Draw_VGA_Tilemap:
;no RLE
;INPUT:
;DS:SI = TILEMAP SOURCE
mov dx,0 ;reset drawing cursors to top left of screen
Draw_VGA_Tilemap_NextRow:
Draw_VGA_Tilemap_NextColumn:
call repeatOnce
lodsb ;get the next tile index
push si
push bx
mov ah,0
mov bx,offset TilemapLookupTable
;in a later build pull this from ram, to allow for more
;lookup tables than just one.
call XLATW
;returns the offset of the tile's bitmap data in ax
mov si,ax
call LocateVRAM ;adjust DI according to DX.
lodsb
mov cl,al
lodsb
mov ch,al
mov word ptr [ds:PixelPlotX],cx
call DrawBitmap_VGA_Palette0
pop bx
pop si
inc dh
; cmp dh,40
; jb Draw_VGA_Tilemap_NextColumn
; mov dh,0
; inc dl
; cmp dl,0Dh
; jb Draw_VGA_Tilemap_NextRow
done_Draw_VGA_Tilemap:
ret
Code: Select all
repeatOnce:
pop ax
push ax
push ax
ret
However, there are times when this will not work. Here's an example of that:
Code: Select all
foo:
push bx
push cx
call repeatOnce
;do stuff
pop cx
pop bx
ret