Skipping instructions
Skipping instructions are opcode fragments that combine with one or more subsequent opcodes. The subsequent opcodes can be used with a different entrypoint than the prepended skipping instruction. Using a skipping instruction instead of an unconditional short jump can save code space, be faster, and set up incidental state such as NC
(No Carry).
My examples are all for 16-bit Real/Virtual 86 Mode, but a lot of these techniques can be used similarly in 16-bit Protected Mode, or 32- or 64-bit modes.
Quoting from my ACEGALS guide:
11: Skipping instructions
The constants __TEST_IMM8, __TEST_IMM16, and __TEST_OFS16_IMM8 are defined to the respective byte strings for these instructions. They can be used to skip subsequent instructions that fit into the following 1, 2, or 3 bytes. However, note that they modify the flags register, including always setting NC. The 16-bit offset plus 16-bit immediate test instruction is not included for these purposes because it might access a word at offset 0FFFFh in a segment. Also, the __TEST_OFS16_IMM8 as provided should only be used in 86M, to avoid accessing data beyond a segment limit. After the db instruction using one of these constants, a parenthetical remark should list which instructions are skipped.
The 86 Mode defines in lmacros1.mac 323cc150061e (2021-08-29 21:45:54 +0200):
%define __TEST_IMM8 0A8h ; changes flags, NC
%define __TEST_IMM16 0A9h ; changes flags, NC
; Longer NOPs require two bytes, like a short jump does.
; However they execute faster than unconditional jumps.
; This one reads random data in the stack segment.
; (Search for better ones.)
%define __TEST_OFS16_IMM8 0F6h,86h ; changes flags, NC
The 0F6h,86h
opcode in 16-bit modes is a test byte [bp + disp16], imm8
instruction. I believe I am not using this one anywhere actually. (A stack memory access might actually be slower than an unconditional short jump, in fact.)
0A8h
is the opcode for test al, imm8
in any mode. The 0A9h
opcode changes to an instruction of the form test eax, imm32
in 32- and 64-bit modes.
Two use cases in ldosboot boot32.asm 07f4ba0ef8cd (2021-09-10 22:45:32 +0200):
First, chain two different entrypoints for a common function which both need to initialise a byte-sized register. The mov al, X
instructions take 2 bytes each, so __TEST_IMM16
can be used to skip one such instruction. (This pattern can be repeated if there are more than two entrypoints.)
error_fsiboot:
mov al,'I'
db __TEST_IMM16 ; (skip mov)
read_sector.err:
mov al, 'R' ; Disk 'R'ead error
error:
Second, a certain entrypoint that needs two bytes worth of additional teardown but can otherwise be shared with the fallthrough case of a later code part.
mov bx, [VAR(para_per_sector)]
sub word [VAR(paras_left)], bx
jbe @F ; read enough -->
loop @BB
pop bx
pop cx
call clust_next
jnc next_load_cluster
inc ax
inc ax
test al, 8 ; set in 0FFF_FFF8h--0FFF_FFFFh,
; clear in 0, 1, and 0FFF_FFF7h
jz fsiboot_error_badchain
db __TEST_IMM16
@@:
pop bx
pop cx
call check_enough
jmp near word [VAR(fsiboot_table.success)]
Here's a use case in inicomp lz4.asm 4d568330924c (2021-09-03 16:59:42 +0200) where we depend on the test al, X
instruction clearing the Carry Flag:
.success:
db __TEST_IMM8 ; (NC)
.error:
stc
retn
Further, here's a very similar use of a skipping instruction in DOSLFN Version 0.41c (11/2012). Instead of test ax, imm16
they're using mov cx, imm16
which has no effect on the status flags but clobbers the cx
register instead. (Opcode 0B9h
is mov ecx, imm32
in non-16-bit modes, and writes to the full ecx
or rcx
register.)
;THROW-Geschichten... [english: THROW stories...]
SetErr18:
mov al,18
db 0B9h ;mov cx,nnnn
SetErr5:
mov al,5
db 0B9h ;mov cx,nnnn
SetErr3:
mov al,3
db 0B9h ;mov cx,nnnn
SetErr2:
mov al,2
SetError:
Finally, the FAT12 boot loader released on 2002-11-26 as fatboot.zip/fat12.asm by Chris Giese (which I based my FAT12, FAT16, and FAT32 loaders on) uses cmp ax, imm16
as a skipping instruction in its error handler. This is similar to my lDOS boot error handlers but cmp
leaves an indeterminate Carry Flag state rather than always setting up No Carry. Also note the comment referring to "Microsoft's Color Computer BASIC":
mov al,'F' ; file not found; display blinking 'F'
; 'hide' the next 2-byte instruction by converting it to CMP AX,NNNN
; I learned this trick from Microsoft's Color Computer BASIC :)
db 3Dh
disk_error:
mov al,'R' ; disk read error; display blinking 'R'
error: