Overview of Ret and Retf Instructions
In 8086 assembly language, <span>call</span> and <span>ret</span> instructions are crucial for implementing subroutine (procedure) calls. They achieve control flow transfer by modifying the IP or simultaneously modifying both CS and IP, utilizing the stack to save and restore the return address.
Principle of the Ret Instruction
<span>ret</span> instruction is used for near transfer, popping the return address from the stack into the IP register, thus returning from the subroutine to the calling point.
Steps for CPU Executing the Ret Instruction:
- (IP) = ((SS)*16 + (SP)); Pop the return address offset from the top of the stack into IP
- (SP) = (SP) + 2; Increment the stack pointer by 2
In assembly syntax, this is equivalent to:
pop IP
Principle of the Retf Instruction
<span>retf</span> instruction is used for far transfer, popping the return address into both IP and CS registers, enabling cross-segment subroutine returns.
Steps for CPU Executing the Retf Instruction:
- (IP) = ((SS)*16 + (SP)); Pop the return address offset from the top of the stack into IP
- (SP) = (SP) + 2; Increment the stack pointer by 2
- (CS) = ((SS)*16 + (SP)); Pop the return address segment from the top of the stack into CS
- (SP) = (SP) + 2; Increment the stack pointer by 2
In assembly syntax, this is equivalent to:
pop IP
pop CS
Code Examples
Example 1: Basic Usage of the Ret Instruction
; Example 1: Basic usage of the ret instruction
assume cs:code, ss:stack
stack segment
db 16 dup(0) ; 16 bytes of stack space
stack ends
code segment
start:
mov ax, stack
mov ss, ax ; Set stack segment
mov sp, 16 ; Set stack pointer
mov ax, 1000h ; Simulate some operations
call my_proc ; Call subroutine
mov ax, 4C00h ; End program
int 21h
my_proc proc
; Subroutine begins
mov bx, ax ; Use passed parameter
add bx, 100h ; Perform some processing
ret ; Return to calling point
my_proc endp
code ends
end start
Example 2: Usage of the Retf Instruction
; Example 2: Usage of the retf instruction (cross-segment return)
assume cs:code, ss:stack
stack segment
dw 2 dup(0) ; Reserve space for return address
stack ends
code segment
start:
mov ax, stack
mov ss, ax ; Set stack segment
mov sp, 4 ; Set stack pointer
; Push return address onto stack (simulate far call)
mov ax, cs ; Current code segment
push ax ; Push CS first
mov ax, offset after_call ; Offset of return address
push ax ; Then push IP
jmp far_proc ; Jump to far procedure (simulate far call)
after_call:
mov ax, 4C00h ; End program
int 21h
far_proc:
; Far procedure begins
mov ax, 0FFFFh ; Perform some processing
retf ; Far return, popping IP and CS from stack
code ends
end start
Example 3: Using Call and Ret Together
; Example 3: Using call and ret to implement a subroutine
assume cs:code, ss:stack
stack segment
db 64 dup(0) ; 64 bytes of stack space
stack ends
code segment
start:
mov ax, stack
mov ss, ax ; Set stack segment
mov sp, 64 ; Set stack pointer
mov ax, 10h ; Set parameter
mov bx, 20h ; Set parameter
call add_values ; Call addition subroutine
; Result is now stored in CX (10h + 20h = 30h)
mov ax, 4C00h ; End program
int 21h
; Addition subroutine
; Input: AX, BX
; Output: CX = AX + BX
add_values proc
push ax ; Save registers
push bx
mov cx, ax ; Store first parameter in CX
add cx, bx ; Add second parameter
pop bx ; Restore registers
pop ax
ret ; Return to calling point
add_values endp
code ends
end start
Example 4: Nested Calls
; Example 4: Nested call example
assume cs:code, ss:stack
stack segment
db 128 dup(0) ; 128 bytes of stack space
stack ends
code segment
start:
mov ax, stack
mov ss, ax ; Set stack segment
mov sp, 128 ; Set stack pointer
mov ax, 5 ; Set parameter
call factorial ; Call factorial function
; Result is now stored in AX (5!)
mov ax, 4C00h ; End program
int 21h
; Factorial function
; Input: AX = n
; Output: AX = n!
factorial proc
cmp ax, 1 ; Check base case
jle base_case
push ax ; Save current n value
dec ax ; n-1
call factorial ; Recursive call
pop bx ; Restore n value
mul bx ; AX = AX * BX (factorial(n-1) * n)
ret
base_case:
mov ax, 1 ; 0! = 1, 1! = 1
ret
factorial endp
code ends
end start
Principle of the Call Instruction
Although this chapter mainly discusses ret and retf, for better understanding, we will briefly introduce the principle of the call instruction.
Operations of the Call Instruction:
-
Near Call:
- Push the current IP onto the stack
- Jump to the target address
Far Call:
- Push the current CS and IP onto the stack
- Jump to the target address (modifying both CS and IP)
Cooperation of Call and Ret:
; Cooperation of call and ret
assume cs:code, ss:stack
stack segment
db 32 dup(0)
stack ends
code segment
start:
mov ax, stack
mov ss, ax
mov sp, 32
call my_procedure ; Call procedure
; Continue executing here after procedure returns
mov ax, 4C00h
int 21h
my_procedure proc
; Procedure begins
; Some processing code...
ret ; Return to calling point
my_procedure endp
code ends
end start
Detailed Stack Operations
Understanding the call and ret instructions is key to understanding how they operate on the stack:
-
Call Instruction:
- Near call: push IP
- Far call: push CS, push IP
Ret Instruction:
- Near return: pop IP
- Far return: pop IP, pop CS
Considerations
-
Stack Balance: Ensure each call has a corresponding ret to maintain stack balance.
-
Register Preservation: In subroutines, if registers used by the caller need to be modified, they should be pushed to save, and popped to restore at the end.
-
Parameter Passing: Parameters can be passed to subroutines via registers or the stack.
-
Return Value Storage: Agree on the register for storing return values (usually AX).
-
Recursive Calls: Be mindful of stack depth to avoid stack overflow.
Practical Applications
The call and ret instructions are fundamental for implementing structured programming, allowing code to be modular, enhancing code reusability and maintainability. By using these instructions appropriately, clear program structures can be created to implement complex algorithms and functionalities.
Conclusion
The ret and retf instructions are key for returning from subroutines, manipulating the stack to restore return addresses and achieve control flow transfer. Understanding how these instructions work is crucial for writing correct assembly programs, especially when implementing function calls, recursive algorithms, and modular code.
By effectively using the cooperation of call and ret, clear and modular program structures can be created, improving code reusability and maintainability. Mastering these instructions is an important step towards becoming a proficient assembly language programmer.