HALICERY

free-time coding, hardware dev, articles

Top
Home 8042 Blogs About
Home IntelEssential 16/32-bit Instructions ENTER

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:

  1. push old BP
  2. set new BP to here
  3. copy L-1 x BP-s
  4. push current BP
  5. 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
|              |    |              |    |              |    |              |    +--------------+
|              |    |              |    |              |    |              |    |              |
|              |    |              |    |              |    |              |    |              |
  1. every ENTER pushes previous EBP
  2. 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)
  3. 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:

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.

  1. push old ebp
  2. copy and push ebp-array (L>1)
  3. 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:

                          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:

  1. mov esp, ebp (B-bit)
  2. 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: