Direccionamiento de memoria y marcos de pila en x86-64
Seguir direcciones efectivas, alineación, crecimiento de la pila y marcos de función
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
| Momento | rsp | Significado de [rsp] | Estado guardado |
|---|---|---|---|
| Entrada | base | dirección de retorno | ninguno aún |
| Tras prólogo | base - N | zona local/save | enumerar |
Antes de nested call | verificar ABI | zona outgoing | proteger valores vivos |
| Tras epílogo | valor de entrada | dirección de retorno | todo 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.