Addressing modes y operandos de memoria
Los operandos de memoria son donde el assembly deja de parecer "matemática de
registros" y empieza a parecer programas reales: arrays, structs, slots de stack,
globals y punteros. En x86-64, un operando de memoria no es un nombre de variable; es
una expresión de dirección. La CPU computa una effective address (dirección
efectiva), y después la instrucción decide si carga desde esa dirección, guarda en ella
o solo la computa con lea. Leer esa forma entre corchetes es el puente entre la
sintaxis de punteros de C y las instrucciones de máquina.
El reset:
[rdi + 8*rsi + 16]no es "el valor". Es una expresión de dirección. La instrucción alrededor decide si se leen bytes, se escriben bytes o no se toca memoria.
Cómo funciona realmente
La fórmula común de direcciones de memoria x86-64 es:
base + index * scale + displacement
En sintaxis Intel eso aparece dentro de corchetes:
qword ptr [rdi + 8*rsi + 16]
Leelo así:
| Pieza | Significado |
|---|---|
rdi |
registro base: normalmente un puntero |
rsi |
registro índice: muchas veces un índice de array |
8 |
escala: tamaño de elemento para long / valores pointer-sized |
16 |
displacement: offset fijo en bytes |
qword ptr |
tamaño de operando: ocho bytes |
La escala puede ser 1, 2, 4 u 8. El displacement es una constante signed. No todos los
operandos usan todas las piezas: [rdi], [rdi + 8], [rdi + 8*rsi] y
[rip + symbol] son formas normales. La expresión de dirección computa bytes, no
elementos; el compilador multiplica los índices de array por el tamaño del elemento.
Un acceso real a array
El artefacto ejecutable de esta nota vive en
examples/assembly-and-compiler-output/addressing-modes-and-memory-operands/.
long touch_neighbors(long *xs, long i, long bias) {
long current = xs[i];
long ahead = xs[i + 2];
long updated = current + bias;
xs[i + 1] = updated;
return updated + ahead;
}
En esta máquina, gcc es Apple clang 15 apuntando a Mach-O x86-64. Este es el cuerpo
relevante de la función copiado de la salida real de:
gcc -S -O2 -masm=intel demo.c -o demo.s
_touch_neighbors: ## @touch_neighbors
.cfi_startproc
## %bb.0:
push rbp
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp
.cfi_def_cfa_register rbp
mov rax, rdx
add rax, qword ptr [rdi + 8*rsi]
mov qword ptr [rdi + 8*rsi + 8], rax
add rax, qword ptr [rdi + 8*rsi + 16]
pop rbp
ret
.cfi_endproc
Mapeá primero los argumentos: xs está en rdi, i en rsi, bias en rdx, y el
valor de retorno sale en rax.
Ahora decodificá los operandos de memoria:
mov rax, rdxcopiabiasal registro de retorno/acumulador.add rax, qword ptr [rdi + 8*rsi]cargaxs[i]y lo suma abias.8*rsiexiste porquesizeof(long) == 8en este target.mov qword ptr [rdi + 8*rsi + 8], raxguardaupdatedenxs[i + 1]. El+ 8extra es unlongmás.add rax, qword ptr [rdi + 8*rsi + 16]cargaxs[i + 2]y lo suma al valor de retorno. El+ 16extra son dos elementoslong.
La distinción importante: add rax, [memory] lee memoria; mov [memory], rax escribe
memoria. La expresión entre corchetes es solo la dirección. La instrucción aporta la
acción.
Qué puede ser un operando de memoria
x86-64 es flexible, pero no arbitrario. Muchas instrucciones pueden usar un operando de memoria:
mov rax, qword ptr [rdi] ; load
mov qword ptr [rdi], rax ; store
add rax, qword ptr [rdi + 8*rsi] ; load y suma
sub qword ptr [rdi], rax ; read-modify-write de memoria
Pero la mayoría de las instrucciones no permite dos operandos de memoria. La CPU normalmente no puede hacer esto en una sola instrucción:
mov qword ptr [rdi], qword ptr [rsi] ; no es la forma normal de x86-64
Cargás a un registro y después guardás:
mov rax, qword ptr [rsi]
mov qword ptr [rdi], rax
Esta es una razón por la que los registros son el centro de la máquina. La memoria es donde los datos viven más tiempo; los registros son donde la mayoría del cómputo se vuelve posible.
lea usa addressing sin memoria
La sintaxis de addressing mode también alimenta a lea:
lea rax, [rdi + 8*rsi + 16]
Eso computa la dirección numérica rdi + 8*rsi + 16 y la escribe en rax. No lee
desde memoria en esa dirección. En términos de C, lea está más cerca de computar
&xs[i + 2] que de leer xs[i + 2].
Los compiladores lo usan de dos maneras:
- Formación de punteros. Computar una dirección que se va a usar después.
- Aritmética barata. Computar formas como
x + 4*y + 16sin tocar flags.
Ese doble uso es por qué tenés que leer la instrucción, no solo los corchetes.
Corchetes dentro de mov o add normalmente significan memoria. Corchetes dentro de
lea significan aritmética sobre una expresión de dirección.
Apéndice ARM64
El mismo demo.c se cross-compiló en esta máquina con:
clang -S -O2 -arch arm64 demo.c -o demo.arm64.s
Cuerpo relevante de la función:
_touch_neighbors: ; @touch_neighbors
.cfi_startproc
; %bb.0:
add x8, x0, x1, lsl #3
ldr x9, [x8]
ldr x10, [x8, #16]
add x9, x9, x2
add x0, x9, x10
str x9, [x8, #8]
ret
.cfi_endproc
AArch64 es más explícito. x0 es xs, x1 es i y x2 es bias. La primera
instrucción computa un puntero base para &xs[i]: x8 = x0 + (x1 << 3). Los loads y
el store después usan offsets inmediatos chicos: [x8] para xs[i], [x8, #8] para
xs[i + 1] y [x8, #16] para xs[i + 2].
El contraste es la lección. x86-64 plegó base + index*8 + displacement directo dentro
de operandos de memoria. ARM64 primero computó la base indexada y después usó
instrucciones load/store. Mismo C, vocabulario de addressing distinto.
Modos de falla y trade-offs
- La sintaxis de dirección no dereferencia por sí sola.
[rdi + 8]dentro demov rax, ...carga; dentro delea rax, ...no. - Los offsets son bytes.
+ 16significa dieciséis bytes, no dieciséis elementos. Paralong, eso son dos elementos en esta plataforma. - Los operandos de memoria esconden latencia.
add rax, [addr]parece aritmética, pero puede esperar a cache o RAM.addregistro-a-registro yaddcon memoria no tienen el mismo costo. - Read-modify-write es una actualización real de memoria. Instrucciones como
add [addr], raxleen y escriben memoria. Eso importa para aliasing, sharing y concurrencia. - La mayoría de las instrucciones tiene un operando de memoria. Si fuente y destino viven en memoria, esperá un registro temporal.
- C no chequea bounds. Si
iestá mal, la expresión de dirección igual se computa. Undefined behavior (comportamiento indefinido) significa que el compilador no te debe un trap.
En la práctica
- Traducí acceso a array a bytes.
xs[i + 2]se vuelve basexs, índicei, escalasizeof *xs, displacement2 * sizeof *xs. - Nombrá las piezas. Al leer
[rdi + 8*rsi + 16], decí "puntero base, índice escalado, offset fijo" antes de preocuparte por la instrucción alrededor. - Mirá operandos de memoria en hot code. Un loop con muchos operandos
[base + index*scale]puede estar limitado por loads/stores, aunque la aritmética parezca chica. - Conectalo con punteros. La aritmética de punteros en C es por elementos; el addressing de máquina es por bytes. El compilador traduce entre aritmética de punteros y stride y estas expresiones de dirección.
- Separá
leaen tu cabeza.leausa sintaxis de dirección para producir una dirección entera; no es un load.
Conecta con: El core instruction set: mov, aritmética, lea · Registros x86-64 y el register file · Qué es realmente un puntero · Aritmética de punteros y stride · Layout de structs: alineación y padding
Fuentes
- Intel 64 and IA-32 Architectures Software Developer's Manual, Vol. 1 y Vol. 2 — addressing modes x86-64, operandos de memoria y semántica de instrucciones. https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
- Felix Cloutier — x86 and amd64 instruction reference — lookup rápido de formas de instrucción que aceptan operandos de memoria y semántica de
lea. https://www.felixcloutier.com/x86/ - Bryant & O'Hallaron — Computer Systems: A Programmer's Perspective (CS:APP), cap. 3 — representación a nivel máquina de arrays, punteros y referencias de memoria. https://csapp.cs.cmu.edu/
- System V AMD64 ABI — roles de registros usados para mapear argumentos antes de leer los operandos de memoria. https://gitlab.com/x86-psABIs/x86-64-ABI
- Ed Jorgensen — x86-64 Assembly Language Programming with Ubuntu — ejemplos accesibles de operandos de memoria, arrays y addressing modes. https://open.umn.edu/opentextbooks/textbooks/x86-64-assembly-language-programming-with-ubuntu
- Arm Architecture Reference Manual + Apple ARM64 docs — formas de addressing load/store ARM64 y detalles de ABI de plataforma para la salida del apéndice. https://developer.arm.com/documentation/ddi0487/latest · https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms