Contents Up << >>

Program Flow Instructions

All of the previous instructions execute sequentially; that is, when one instruction finishes, the next instruction is taken from the very next memory location. This is the default operation for the instruction pointer, IP---after each byte of instruction is fetched, the IP is incremented in preparation for the next fetch. The program flow instructions provide the facilities to modify the course of execution, allowing conditional execution (by jumping over parts of the code if certain conditions are met) and looping (by jumping backwards in the code).

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.