ARM: registro de enlace y puntero de marco
Estoy tratando de entender cómo funcionan el registro de enlace y el puntero de marco en ARM. He estado en un par de sitios y quería confirmar lo que entendí.
Supongamos que tengo el siguiente código:
int foo(void)
{
// ..
bar();
// (A)
// ..
}
int bar(void)
{
// (B)
int b1;
// ..
// (C)
baz();
// (D)
}
int baz(void)
{
// (E)
int a;
int b;
// (F)
}
y llamo foo(). ¿El registro de enlace contendría la dirección del código en el punto (A) y el puntero de marco contendría la dirección del código en el punto (B)? ¿Y el puntero de la pila podría estar en cualquier lugar dentro de la barra(), después de que se hayan declarado todos los locales?
Algunas convenciones de llamada de registros dependen de la ABI (Interfaz binaria de aplicación). Se FP
requiere en el estándar APCS y no en el AAPCS más nuevo (2003). Para AAPCS (GCC 5.0+), FP
no es necesario utilizarlo, pero ciertamente se puede utilizar; La información de depuración está anotada con el uso del puntero de marco y pila para rastrear la pila y desenredar el código con AAPCS . Si una función es static
, un compilador realmente no tiene que cumplir ninguna convención.
Generalmente todos los registros ARM son de propósito general . El lr
(registro de enlace, también R14) y pc
(el contador de programa también R15) son especiales y están consagrados en el conjunto de instrucciones. Tienes razón en que lr
apuntaría a A . Los pc
y lr
están relacionados. Uno es "dónde estás" y el otro es "dónde estabas". Son el aspecto del código de una función.
Normalmente, tenemos sp
(puntero de pila, R13) y fp
( puntero de marco , R11). Estos dos también están relacionados. Este
diseño de Microsoft hace un buen trabajo al describir las cosas. La pila se utiliza para almacenar datos temporales o locales en su función. Cualquier variable en foo()
y bar()
se almacena aquí, en la pila o en los registros disponibles. Realiza un fp
seguimiento de las variables de una función a otra. Es un marco o ventana de imagen en la pila para esa función. La ABI define un diseño de este marco . Normalmente, lr
el compilador guarda aquí los registros y otros en segundo plano, así como el valor anterior de fp
. Esto crea una lista vinculada de marcos de pila y, si lo desea, puede rastrearla hasta main()
. La raíz es fp
, que apunta a un marco de pila (como a struct
) con una variable en struct
el anterior fp
. Puedes ir avanzando en la lista hasta el final fp
que normalmente es NULL
.
Entonces es sp
donde está la pila y fp
es donde estaba la pila, muy parecido a pc
y lr
. Cada antiguo lr
(registro de enlace) se almacena en el antiguo fp
(puntero de marco). Los sp
y fp
son un aspecto de datos de las funciones.
Tu punto B es el activo pc
y sp
. El punto A es en realidad el fp
y lr
; a menos que llame a otra función y luego el compilador podría prepararse para configurar el fp
para que apunte a los datos en B .
A continuación se muestra un ensamblador ARM que podría demostrar cómo funciona todo esto. Esto será diferente dependiendo de cómo optimice el compilador, pero debería dar una idea.
; Prologue - setup
mov ip, sp ; get a copy of sp.
stmdb sp!, {fp, ip, lr, pc} ; Save the frame on the stack. See Addendum
sub fp, ip, #4 ; Set the new frame pointer.
...
; Maybe other functions called here.
; Older caller return lr
stored in stack frame.
bl baz
...
; Epilogue - return
ldm sp, {fp, sp, lr} ; restore stack, frame pointer and old link.
... ; maybe more stuff here.
bx lr ; return.
Así es como foo()
se vería. Si no llama bar()
, entonces el compilador realiza una optimización de hoja y no necesita guardar el marco ; sólo bx lr
se necesita el. Lo más probable es que sea esta la razón por la que los ejemplos web le confunden. No siempre es lo mismo.
La comida para llevar debería ser,
pc
y son registros de códigoslr
relacionados . Uno es "Dónde estás", el otro es "Dónde estabas".sp
y son registros de datos localesfp
relacionados . Uno es "Dónde están los datos locales", el otro es "Dónde están los últimos datos locales".- El trabajo en conjunto junto con el paso de parámetros para crear maquinaria funcional .
- Es difícil describir un caso general porque queremos que los compiladores sean lo más rápidos posible, por eso utilizan todos los trucos que pueden.
Estos conceptos son genéricos para todas las CPU y lenguajes compilados, aunque los detalles pueden variar. El uso del registro de enlace y el puntero de marco son parte de la función prólogo y epílogo, y si entendiste todo, sabrás cómo funciona un desbordamiento de pila en un ARM.
Ver también: Convención de llamadas ARM .
Artículo de MSDN sobre la pila ARM
Universidad de Cambridge Descripción general de APCS
Blog de seguimiento de la pila ARM
Enlace ABI de Apple
El diseño básico del marco es,
- fp[-0] save
pc
, donde almacenamos este fotograma. - fp[-1] guardado
lr
, la dirección de devolución de esta función. - fp[-2] anterior
sp
, antes de que esta función coma la pila. - fp[-3] anterior
fp
, el último fotograma de la pila . - muchos registros opcionales...
Un ABI puede usar otros valores, pero los anteriores son típicos para la mayoría de las configuraciones. Los índices anteriores son para valores de 32 bits, ya que todos los registros ARM son de 32 bits. Si está centrado en bytes, multiplique por cuatro. La trama también está alineada con al menos cuatro bytes.
Anexo: Esto no es un error en el ensamblador; es normal. Hay una explicación en la pregunta de los prólogos generados por ARM .