← SEARCHABLE ARCHIVE
Assembly · 2026-08-15

Direccionamiento de memoria y marcos de pila en x86-64

Seguir direcciones efectivas, alineación, crecimiento de la pila y marcos de función

INTERACTIVE READING MODE →

Direccionamiento de memoria y marcos de pila en x86-64

Estos artículos del blog se han creado con fines didácticos. Los ejemplos usan sintaxis Intel. El layout exacto de pila depende de la ABI, el compilador, la optimización y la información de depuración.

En ensamblador, player.health puede aparecer como [rcx+12] y values[i] como [rdi+rsi*4]. Una variable local tampoco es una casilla permanente: es almacenamiento localizado respecto al rsp actual o a un frame pointer opcional.

Dirección efectiva

[base + index * scale + displacement]

scale puede ser 1, 2, 4 u 8; los demás términos son opcionales.

mov eax, [rcx]
mov eax, [rcx + 12]
mov eax, [rcx + rdx*4]
mov eax, [rcx + rdx*4 + 12]

La expresión entre corchetes se calcula como dirección y se accede con la anchura elegida. lea es la excepción: devuelve la dirección sin desreferenciarla.

lea rax, [rcx + 8]    ; dirección
mov rax, [rcx + 8]    ; valor almacenado allí

Los bits de un registro no llevan una etiqueta de tipo puntero. Durante la depuración conviene marcar cada valor como dirección o dato.

Arrays y estructuras

Para un array de int de cuatro bytes:

mov eax, dword ptr [rcx + rdx*4]

La dirección es base + index * sizeof(int). Una estructura de 12 bytes exige aritmética adicional, porque el scale de hardware solo admite 1, 2, 4 y 8.

lea rax, [rdx + rdx*2]
mov ecx, [rcx + rax*4 + 4]

El offset de un campo es un contrato binario, no un nombre. Orden, packing, herencia o ABI pueden cambiarlo.

static_assert(offsetof(Player, health) == 12);
static_assert(sizeof(Player) == 16);

RIP-relative y little endian

lea rdi, [rip + message]
mov eax, dword ptr [rip + counter]

Los datos se localizan respecto a la instrucción en vez de incrustar una dirección absoluta completa. El enlace dinámico puede añadir indirección mediante GOT/PLT de ELF o la tabla de importación de PE.

0x1122334455667788 se almacena en memoria x86-64 así:

dirección +0  +1  +2  +3  +4  +5  +6  +7
byte      88  77  66  55  44  33  22  11

El endian describe el orden de bytes de una representación multibyte; no invierte la aritmética.

La alineación tiene varios contratos

Muchas cargas y escrituras x86-64 toleran direcciones no alineadas, pero no siempre son correctas ni gratuitas.

  • Cruzar una cache line o página puede costar más.
  • Algunas formas SIMD requieren alineación.
  • La ABI exige alineación de pila en las llamadas.
  • Desreferenciar un puntero tipado mal alineado puede ser indefinido en C/C++.
  • La atomicidad depende de instrucción, anchura, alineación y límites.

La pila crece hacia direcciones menores

push rbx       ; rsp -= 8; [rsp] = rbx
pop rbx        ; rbx = [rsp]; rsp += 8

call guarda una dirección de retorno y transfiere el control; ret la recupera.

antes de call       RSP = S
entrada del callee  RSP = S - 8
después de ret      RSP = S

Aunque las ABI modernas pasan muchos argumentos en registros, la dirección de retorno sigue en la pila.

Marco de pila

push rbp
mov rbp, rsp
sub rsp, 32
; body
add rsp, 32
pop rbp
ret

Se conserva rbp, se crea una referencia estable, se reservan 32 bytes y se deshacen los pasos. El frame pointer es opcional: el código optimizado puede usar rbp como registro general o no crear marco.

Un marco puede contener arrays locales, spills, registros nonvolatile guardados, argumentos en pila, shadow space de Windows, padding, canary y almacenamiento dinámico. Por tanto [rsp+40] no es automáticamente “la quinta variable local”.

Funciones leaf y diferencias de ABI

add_three:
    lea eax, [rdi + rsi]
    add eax, edx
    ret

Una función leaf puede no usar la pila. En Windows x64 el caller reserva 32 bytes de shadow space. System V no usa esa regla, pero permite a ciertas funciones leaf de espacio de usuario una red zone de 128 bytes bajo rsp. Windows no ofrece esa red zone.

Leer un bucle como contrato de búfer

xor eax, eax
xor r8d, r8d
loop_begin:
    cmp r8, rdx
    jae loop_end
    movsxd r9, dword ptr [rcx + r8*4]
    add rax, r9
    inc r8
    jmp loop_begin
loop_end:
    ret

[rcx+r8*4] es la posible dirección de values[r8]; movsxd extiende un elemento signed de 32 bits. Si count > 0, el caller debe proporcionar al menos count*4 bytes legibles. ASM no puede deducir el tamaño de una asignación a partir de un puntero.

Hoja de depuración

MomentorspSignificado de [rsp]Estado guardado
Entradabasedirección de retornoninguno aún
Tras prólogobase - Nzona local/saveenumerar
Antes de nested callverificar ABIzona outgoingproteger valores vivos
Tras epílogovalor de entradadirección de retornotodo restaurado

La corrupción suele verse como un ret a una dirección ajena, registros nonvolatile del caller alterados, stack walking roto solo durante excepciones o fallos en una instrucción SIMD alineada.

Resumen

x86-64 forma direcciones como [base + index*scale + displacement]. El scale suele revelar el tamaño del elemento y el displacement el offset de campo. call coloca el retorno en la pila; una función reserva zonas locales, spills, conservación y llamadas solo cuando las necesita. Siga cada cambio de rsp en vez de memorizar un dibujo universal.

Fuentes oficiales