The unconditional jump instruction, JMP, causes IP (and sometimes CS) to be modified so that the next instruction is fetched from the location given in the operand (the target). Here are the valid forms:
JMP SHORT imm8
JMP imm16
JMP imm16:imm16
JMP r/m16
JMP FAR mem32
The short version saves space when the target of the jump is within a
few dozen instructions forward or backward; the assembler computes the
difference between the new address and the next address sequentially,
and just stores this difference as one (signed) byte. The second (and
most common) version allows a jump to any location in the current code
segment, while the third allows a jump to any location in memory by also
specifying an immediate value to be loaded into CS. The fourth version
will take the target address from a register or memory location; since
this address is only 16 bits, the target has to be within the segment.
Finally, the far version fetches both the offset and the segment from
four consecutive bytes in memory (compare to the LDS and
LES instructions; JMP FAR mem32 could have been called
"LCS IP, mem32'').The conditional jump instructions, Jcc, where cc is one of the condition codes listed earlier (E, NE, ...), perform a short jump if the condition is true, based on the current contents of the status flags. For example, the code sample that was given in the discussion of LODSB, to convert a string to lower-case, used the JA and JB instructions; these made their jump if the result of the previous comparison found that the current character was above 'Z' or below 'A'. Since a conditional jump can only be to a nearby target, it is sometimes necessary to combine conditional and unconditional jumps as follows:
JNLE NoJLE
JMP target
NoJLE:
This will have the same effect as JLE target, except there is no
restriction on how far away the target may be (within the code segment).There are two specialized versions of conditional jump that are particularly useful when executing a loop a fixed number of times. The looping statements
LOOP imm8
LOOPE imm8
LOOPNE imm8
(as usual, the synonyms LOOPZ and LOOPNZ are also
available) are very similar to the REP, REPE, and
REPNE prefixes from the string instructions. The LOOP
instruction decrements CX and makes a short jump if the count has not
reached zero. The LOOPE instruction adds the condition that it
will only take the jump if the Zero flag is set (usually indicating that
the last comparison had equal operands); the LOOPNE will only
take the jump if the Zero flag is clear. The string operation
REP MOVSB, for example, could have been performed with
Repeat MOVSB
LOOP Repeat
(except this would have been considerably slower, since it requires
repeatedly fetching and decoding the two instructions instead of just
fetching and decoding the single REP MOVSB instruction once).After looping or repetitive string operations, it is occasionally necessary to test whether the count register reached zero (to check whether the loop ran for the full count or whether it exited early because the Zero flag changed). The instruction
JCXZ imm8
serves exactly this purpose; it takes a short jump if the CX register
contains zero. It is short for performing CMP CX, 0 followed by
JZ imm8.All of the above branching instructions are variations on the infamous GOTO statement; they cause a permanent change in the course of execution. To perform an operation more like a function or subroutine call, where the flow of control will eventually return to pick up with the next instruction, the 8086 provides two mechanisms: CALL/RET and INT/IRET.
The CALL instruction offers a similar range of addressing modes to the JMP instruction, except there is no "short'' call:
CALL imm16
CALL imm16:imm16
CALL r/m16
CALL FAR mem32
A call is the same as a jump, except the instruction pointer is first
pushed onto the stack (in the second and fourth versions, which include
a new segment, the current CS register is also pushed).To reverse the effect of a CALL, when the subroutine is done it should execute a RET or RETF instruction; this pops the return address off of the stack and back into IP (and RETF also pops the saved value of CS, to return from a far call). After the return, the next instruction that will be fetched will be from the next location after the CALL. There is an optional 16-bit immediate operand that may be specified with a return instruction; this value is added to the stack pointer after popping off the return address, to recover however many bytes had been pushed onto the stack with parameters before the call. For example, here is one way to implement a subroutine to print a character, where the calling code first pushes the character (as the low byte of a word, since there is no option to push a single byte) before making the call:
PutChar PUSH BP ;Save current values of registers that we'll modify
PUSH AX
PUSH DX
MOV BP, SP ;Copy stack pointer to BP
MOV AH, 2 ;DOS function code for printing a character
MOV DL, [BP + 8] ;Fetch character parameter from stack
;Stack contains (from tos) DX, AX, BP, return address, and parameter
INT 21h ;Call DOS function
POP DX ;Restore modified registers
POP AX
POP BP
RET 2 ;Return and pop 2 byte parameter
For completeness, here is what a typical call might look like (in fact,
this is a complete routine to print a NUL-terminated string, assuming
that the string starts at DS:SI):
NextCh LODSB ;Load next character into AL
CMP AL, 0
JE Done ;Quit if NUL
PUSH AX ;Set up parameter for call
CALL PutChar
JMP NextCh ;Continue with next character
Done:
This is just one of several common conventions for passing parameters to
subroutines; even more common is to just specify that, for example, the
character will be passed directly in the DL register.The other function-call-like mechanism is the interrupt. We have been using this all along to call the standard DOS services, such as printing a character or a '$'-terminated string. The INT instruction behaves much like the CALL FAR instruction except for two things: it pushes the FLAGS register before pushing CS and IP (the idea is that an interrupt should be able to completely restore the state of the processor when it is finished, since this is also the mechanism used for handling hardware interrupts from the rest of the system---they can happen at any time, independent of what the processor might be working on, and they should occur as transparently to the current process as possible), and it gets the target address from a standard table of interrupt handler vectors kept at the bottom of memory. When the processor executes INT n, where n is an 8-bit immediate value, it fetches a far pointer (that is, a 4-byte combination of segment and offset) from the memory address 0000:4n; this is the target address for the interrupt call. For example, the address of the DOS interrupt handler, the routine called when INT 21h is executed, is stored at locations 0000:0084 through 0000:0087; the first two bytes give the offset, to load into IP, and the second two bytes give the segment, to load into CS.
To return from an interrupt handler, the IRET instruction is used. It pops the IP, CS, and FLAGS registers, which causes the state of the machine to return to where it left off when the interrupt occurred.