Last modified: Sun Jul 19 16:03:43 UTC+0200 2026 © A. Tarpai
Intel ENTER/LEAVE and the Base Pointer (BP)
The story
The stack is used to store arguments and local variables for a function/procedure, a mechanism used in high-level languages such as C or Pascal.
But the 8086 and legacy MODR/M addressing modes lack indexing by SP directly to access elements on the stack.
The BP register (stack base pointer) was made to operate on the stack to access parameters [BP + offs] and locals [BP - offs] with compact code bytes: BP-based memory access imply the Stack Segment Register, f. ex.:
8B 45 08 MOV AX, [BP + 8]
So right after entering a procedure, a common practice is to save old BP (PUSH BP) and then store SP into BP (BP=SP). BP gets the value of SP by:
PUSH BP MOV BP, SP SUB SP, n <--- optional locals
On function return BP and the stack can be restored by:
MOV SP, BP POP BP RET
This was so common that later cpu-s (186/286+) implemented ENTER/LEAVE – a CPU support for a typical C-like stack frame:
STACK
| |
+----------+
| | ^
| | |
| args | | eg. [BP + 8]
| | |
+----------+
| RET |
+----------+
| BP prev | <--- BP
+----------+
| | |
| | | eg. [BP - 12]
| locals | |
| | v
| |
| |
+----------+
| |
| |
BP points to the previous BP pushed and remains FIX during function execution.
RET is the return address and can be of different size (see eg. NASM %stacksize Directive flat, flat64, large and small). It affects the offsets used to access function arguments pushed onto stack.
ENTER/LEAVE opcodes
186/286 introduced two new opcodes C8/C9.
ENTER localsize, Level:
ENTER: C8 imm16 imm8
C8 00 00 04 <-- eg. ENTER 0, 4
Level is nesting level (0 to 31); L > 0 for nested procedure support - see below.
LEAVE is a one-byte opcode instruction and equivalent to:
LEAVE: C9 MOV SP, BP POP BP
Handy to save codebytes.
ENTER L=0
When the nesting level is zero.
ENTER 0, 0
C8 00 00 00 ENTER 0, 0 Equivalent to: 55 PUSH BP 8B EC MOV BP, SP
Note the longer code.
ENTER N, 0
C8 nn nn 00 ENTER N, 0 Equivalent to PUSH BP MOV BP, SP SUB SP, N <--- max 64K locals
ENTER operation L=0
STACK STACK STACK STACK
| | | | | | | |
+----------+ +----------+ +----------+ +----------+
| | | | | | | |
| | | | | | | |
| args | | args | | args | | args |
| | | | | | | |
+----------+ +----------+ +----------+ +----------+
| RET | <--- SP | RET | | RET | | RET |
+----------+ +----------+ +----------+ +----------+
| | | BP prev | <--- SP | BP prev | <--- SP, BP | BP prev | <--- BP
| | +----------+ +----------+ +----------+
| | | | | | | |
| | | | | | | |
| | | | | | | locals |
| | | | | | | |
| | | | | | | |
| | | | | | | | <--- SP
| | | | | | +----------+
| | | | | | | |
| | | | | | | |
caller pushed PUSH BP MOV BP, SP SUB SP, N
parameters and | | |
issued the CALL |____________ ENTER 0, 0 ___________| |
instruction | |
|_________________________ ENTER N, 0 __________________________|
LEAVE operation
STACK STACK STACK
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
| | | | | |
| args | | args | | args |
| | | | | |
+----------+ +----------+ +----------+
| RET | | RET | | RET | <--- SP
+----------+ +----------+ +----------+
| BP prev | <--- BP | BP prev | <--- SP, BP | |
+----------+ +----------+ | |
| | | | | |
| | | | | |
| locals | | | | |
| | | | | |
| | | | | |
| | | | | |
+----------+ | | | |
| | | | | |
| | | | | |
Function is ready MOV SP, BP POP BP
to return: | |
|_____________ LEAVE _________________|
and issue RET instruction
ENTER L > 0
Implemented for high-level language support of NESTED PROCEDURES, like Pascal:
procedure X;
procedure Y;
begin
{ ... } <-- Y has its own local vars plus X local vars. Needs two BP, stack base pointers
end
begin
{ ... }
end
Nested calls and the Base Pointer (BP)
For nested calls, pushing BP creates a chain of stack frame pointers. Example. 5 levels of calls or nested procedures. PROC (3) pushes BP of PROC (2) and so on, BP pushed by PROC (0) is don't care. Parameters (P) are above BP, locals (L) are below BP.
LEVEL
PROC (0)
PROC (1)
PROC (2)
PROC (3)
PROC (4)
STACK PROC
| |
| P (0) |
| BP old | <---+ <-- BP (0)
| L (0) | |
| | |
| P (1) | | "ENTER"
+---> | BP (0) | ----+ <-- BP (1) PUSH BP
| | L (1) | BP = SP
| | |
| | | creates a chain of stack frame pointers
| | P (2) | towards higher level procedures
+---- | BP (1) | <---+ <-- BP (2)
| L (2) | | "LEAVE"
| | | SP = BP
| | | POP BP
| | |
| | |
| P (3) | |
+---> | BP (2) | ----+ <-- BP (3)
| | L (3) |
| | |
| | |
| | P (4) |
+---- | BP (3) | <-- BP (4)
| L (4) | actual
| |
| |
The current proc could use double- triple- etc. indirection of BP to access higher level local variables, only that (multiple) pointer de-referencing has performance issues.
But for recursive calls this is quite problematic: we have no idea of the recursive-call-depth based on BP.
It would be nicer to have an array of BP-s for each level of procedure calls – and that's exactly what ENTER L>0 does.
Debugging ENTER localsize, level
Used VC++ 2005.
Code for 5 levels of ENTER:
__declspec(naked) static int enter()
{
__asm {
enter 0,0
enter 0,1
enter 0,2
enter 0,3
enter 0,4
}
}
track ESP and EBP, then dump stack content:
STACK
| |
0019FA08: | .. |
0019FA04: | .. | <--- ESP = 0019FA04 (EBP = 0019FF18)
| |
C8 00 00 00 enter 0,0 | |
0019FA00: | 0019FF18 | <--- ESP = EBP = 0019FA00
C8 00 00 01 enter 0,1 | |
0019F9FC: | 0019FA00 | <--- EBP = 0019F9FC
0019F9F8: | 0019F9FC | <--- ESP = 0019F9F8
C8 00 00 02 enter 0,2 | |
0019F9F4: | 0019F9FC | <--- EBP = 0019F9F4
0019F9F0: | 0019F9FC |
0019F9EC: | 0019F9F4 | <--- ESP = 0019F9EC
| |
C8 00 00 03 enter 0,3 | |
0019F9E8: | 0019F9F4 | <--- EBP = 0019F9E8
0019F9E4: | 0019F9FC |
0019F9E0: | 0019F9F4 |
0019F9DC: | 0019F9E8 | <--- ESP = 0019F9DC
| |
C8 00 00 04 enter 0,4 | |
0019F9D8: | 0019F9E8 | <--- EBP = 0019F9D8
0019F9D4: | 0019F9FC |
0019F9D0: | 0019F9F4 |
0019F9CC: | 0019F9E8 |
0019F9C8: | 0019F9D8 | <--- ESP = 0019F9C8
| |
0019F9C4: | .. |
0019F9C0: | .. |
| |
ENTER operation L>0
Something like this happens on each enter for L > 0:
- push old BP
- set new BP to here
- copy L-1 x BP-s
- push current BP
- optionally pull the stack for N bytes
Note L=0 is for comparison: push ebp only.
enter 0,0 enter 0,1 enter 0,2 enter 0,3 enter 40,4 ENTER L=0 ENTER L=1 ENTER L=2 ENTER L=3 ENTER L=4 | | | | | | | | | | +--------------+ +--------------+ +--------------+ +--------------+ +--------------+ | EBP PREV | | EBP PREV | | EBP PREV | | EBP PREV | | EBP PREV | <-- EBP +--------------+ +--------------+ +--------------+ +--------------+ +--------------+ | | | EBP L1 | -> | EBP L1 | -> | EBP L1 | -> | EBP L1 | | | +--------------+ +--------------+ +--------------+ +--------------+ | | | | | EBP L2 | -> | EBP L2 | -> | EBP L2 | | | | | +--------------+ +--------------+ +--------------+ | | | | | | | EBP L3 | -> | EBP L3 | | | | | | | +--------------+ +--------------+ | | | | | | | | | EBP L4 | | | | | | | | | +--------------+ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 40 bytes | | | | | | | | | | | <-- ESP | | | | | | | | +--------------+ | | | | | | | | | | | | | | | | | | | |
- every ENTER pushes previous EBP
- when L>0 the last push is the current EBP (this is for possible copy for the next level, i.e. to support nested procedures, the compiler should start from L=1)
- when L>1 the previous EBP-array is copied into the stack frame
ENTER solves the recursive-call unwind problem
Imagine the last L=4 function is recursive, then:
enter 0,3 enter 40,4 enter 40,4 enter 40,4
ENTER L=3 ENTER L=4 ENTER L=4 ENTER L=4
| | | | | | | |
+--------------+ +--------------+ +--------------+ +--------------+
| EBP PREV | | EBP PREV | | EBP PREV | | EBP PREV |
+--------------+ +--------------+ +--------------+ +--------------+
-> | EBP L1 | -> | EBP L1 | -> | EBP L1 | -> | EBP L1 |
+--------------+ +--------------+ +--------------+ +--------------+
-> | EBP L2 | -> | EBP L2 | -> | EBP L2 | -> | EBP L2 |
+--------------+ +--------------+ +--------------+ +--------------+
| EBP L3 | -> | EBP L3 | -> | EBP L3 | -> | EBP L3 |
+--------------+ +--------------+ +--------------+ +--------------+
| | | EBP L4 | | EBP L4' | | EBP L4'' |
| | +--------------+ +--------------+ +--------------+
| | | | | | | |
| | | | | | | |
| | | 40 bytes | | 40 bytes | | 40 bytes |
| | | | | | | |
| | +--------------+ +--------------+ +--------------+
| | | | | | | |
| | | | | | | |
Easy to unwind the whole recursive-call series by accessing EBP L3 at any depth.
386 ENTER/LEAVE
Same opcodes i.e. for ENTER the max local size is still 64K – and Level is still 0..31.
For 16-bit emulation both honors operand-size and the 66h prefix:
- ENTER: dword or word push with EBP-HI lost(!)
- LEAVE: dword or word pop with EBP-HI unchanged
Because of the implicit PUSH/POP-operations, the B-bit is also in effect (changing SP/ESP and addressing by SP/ESP).
386 ENTER
All push-operation is affected by the B-bit and operand-size.
- push old ebp
- copy and push ebp-array (L>1)
- push my ebp (L>0)
Copying the BP-array mirrors exactly the push-operation (based on B-bit, operand-size) but using eBP and read-operation instead of write:
- Operand-size determines WORD or DWORD copy
- B-bit determines pre-decrement EBP or BP and addressing by [SS : EBP] or [SS : BP] (with EBP-HI zero)
B=1 B=0
operand-size = 32 dec EBP by 4 dec BP by 4
D=1 or D=0 and 66h read DWORD [SS:EBP] read DWORD [SS:BP]
| |
v v
dec ESP by 4 dec SP by 4
write DWORD [SS:ESP] write DWORD [SS:SP]
operand-size = 16 dec EBP by 2 dec BP by 2
D=0 or D=1 and 66h read WORD [SS:EBP] read WORD [SS:BP]
| |
v v
dec ESP by 2 dec SP by 2
write WORD [SS:ESP] write WORD [SS:SP]
386 LEAVE
LEAVE:
- mov esp, ebp (B-bit)
- pop ebp (operand-size)
386 LEAVE
B=1 B=0
1. MOV eSP, eBP
ESP = EBP SP = BP
2. POP eBP
operand-size = 32 EBP = read DWORD [SS:ESP] EBP = read DWORD [SS:SP]
D=1 or D=0 and 66h inc ESP by 4 inc SP by 4
operand-size = 16 BP = read WORD [SS:ESP] BP = read WORD [SS:SP]
D=0 or D=1 and 66h inc ESP by 2 inc SP by 2
ENTER emulation
Equivalent assembly (NASM) for example enter 40, 4:
[BITS 16] [BITS 32]
55 push bp 55 push ebp
FF76FE push word [bp-2] FF75FC push dword [ebp-4]
FF76FC push word [bp-4] FF75F8 push dword [ebp-8]
FF76FA push word [bp-6] FF75F4 push dword [ebp-12]
89E5 mov bp,sp 89E5 mov ebp,esp
83C506 add bp,6 83C50C add ebp,12
55 push bp 55 push ebp
83EC28 sub sp,40 83EC28 sub esp,40
L=4:
- L>1 so push L-1 = 3 frame pointers from above
- L>0 so push my own ebp too last
- cannot use temp so ebp is adjusted before push: add ebp,
(L-1)*4orADD BP, (L-1)*2