A nifty trick to get a subroutine to repeat exactly once, without looping

Made something in Assembly? Show it off, and tell us how it works...
Absolute beginners welcome... we want to see what you've done!
Forum rules
There is a special rule on this forum...
This forum is intended to offer equal amounts encouragement and constructive feedback...
Therefore, if you say 2 negative things about someones work, you must think of 2 or more equally positive things...

Eg: "Great first effort, the idea is absolutely fascinating... However I noticed a few bugs, and maybe the graphics could be improved..."

If you can't think of anything good to say, then don't say anything!

If we don't encourage newbie programmers they won't have the confidence and motivation to stick at it and become great programmers! *speaking from experience*
Post Reply
puppydrum64
Posts: 34
Joined: Thu Apr 22, 2021 9:30 pm

A nifty trick to get a subroutine to repeat exactly once, without looping

Post by puppydrum64 » 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:

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
First, I'll comment out the loop overhead so that it runs once and terminates.

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
Here's the output of that.
once.PNG
once.PNG (6.16 KiB) Viewed 3477 times
Next, I will call the "repeatOnce" routine at the top of this one.

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
And here is the output:
repeatonce.PNG
repeatonce.PNG (5.7 KiB) Viewed 3477 times
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:

Code: Select all

repeatOnce:
	pop ax
	push ax
	push ax
	ret
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:

Code: Select all

foo:
	push bx
	push cx
	call repeatOnce
	;do stuff
	pop cx
	pop bx
	ret
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.

Post Reply

Return to “Show and Tell”