assembly - registers - ¿Qué significa `rep ret`?
lenguaje ensamblador x86 (3)
Estaba probando un código en Visual Studio 2008 y noté security_cookie
. Puedo entender el sentido, pero no entiendo cuál es el propósito de esta instrucción.
rep ret /* REP to avoid AMD branch prediction penalty */
Por supuesto que puedo entender el comentario :) pero ¿qué está haciendo exactamente este prefijo en contexto con el ret
y qué pasa si ecx
es? = 0? Aparentemente, el recuento de bucles de ecx
se ignora cuando lo depuro, lo cual es de esperar.
El código donde encontré esto estaba aquí (inyectado por el compilador para seguridad):
void __declspec(naked) __fastcall __security_check_cookie(UINT_PTR cookie)
{
/* x86 version written in asm to preserve all regs */
__asm {
cmp ecx, __security_cookie
jne failure
rep ret /* REP to avoid AMD branch prediction penalty */
failure:
jmp __report_gsfailure
}
}
Aparentemente, algunos predictores de rama de los procesadores AMD se comportan mal cuando el objetivo de una rama o fallthrough es una instrucción ret
, y agregar el prefijo rep
evita esto.
En cuanto al significado de rep ret
, no hay mención de esta secuencia de instrucciones en Intel Instruction Set Reference , y la documentación de rep
no está siendo muy útil:
El comportamiento del prefijo REP no está definido cuando se usa con instrucciones que no son cadenas.
Esto significa al menos que el rep
no tiene que comportarse de manera repetitiva.
Ahora, desde la referencia del conjunto de instrucciones AMD (1.2.6 Prefijados de repetición):
Los prefijos solo deben usarse con tales instrucciones de cadena.
En general, los prefijos de repetición solo deben usarse en las instrucciones de cadena enumeradas en las tablas 1-6, 1-7 y 1-8 arriba [que no contienen ret].
Así que realmente parece un comportamiento indefinido, pero se puede suponer que, en la práctica, los procesadores simplemente ignoran los prefijos de rep
en las instrucciones ret
.
Como señala la respuesta de Trillian, AMD K8 y K10 tienen un problema con la predicción de bifurcación cuando ret
es un blanco de bifurcación, o sigue una bifurcación condicional.
La guía de optimización de AMD para K10 (Barcelona) recomienda ret 0
3 bytes en esos casos, que saca cero bytes de la pila y también regresa. Esa versión es significativamente peor que la rep ret
en Intel. Irónicamente, también es peor que la rep ret
en procesadores AMD posteriores (Bulldozer y otros). Por lo tanto, es bueno que nadie haya cambiado a usar ret 0
según la actualización de la guía de optimización Family 10 de AMD.
Los manuales del procesador advierten que los procesadores futuros podrían interpretar de manera diferente una combinación de un prefijo y una instrucción que no modifica. Eso es cierto en teoría, pero nadie va a hacer una CPU que no pueda ejecutar muchos binarios existentes.
gcc todavía usa rep ret
por defecto (sin -mtune=intel
, o -march=haswell
o algo así). Entonces, la mayoría de los binarios de Linux tienen un repz ret
en algún lado.
gcc probablemente dejará de usar rep ret
en unos años, una vez que K10 esté totalmente obsoleto. Después de otros 5 o 10 años, casi todos los binarios se compilarán con un gcc más nuevo que ese. Otros 15 años después de eso, un fabricante de CPU podría pensar en readaptar la secuencia de bytes f3 c3
como (parte de) una instrucción diferente.
Todavía habrá binarios heredados de código cerrado que utilicen rep ret
que no tienen compilaciones más recientes disponibles, y que alguien necesita seguir funcionando. Por lo tanto, cualquiera que sea la nueva característica f3 c3 != rep ret
es parte de necesitaría ser deshabilitada (por ejemplo, con una configuración BIOS), y hacer que esa configuración realmente cambie el comportamiento instrucción-decodificador para reconocer f3 c3
como rep ret
. Si esa compatibilidad hacia atrás para los binarios heredados no es posible (porque no se puede hacer de manera eficiente en términos de potencia y transistores), IDK qué tipo de marco de tiempo verías. Mucho más de 15 años, a menos que fuera una CPU solo para una parte del mercado.
Por lo tanto, es seguro usar rep ret
, porque todos los demás ya lo están haciendo. Usar ret 0
es una mala idea. En el nuevo código, puede ser una buena idea usar rep ret
por otros años. Es probable que todavía no haya muchas CPUs AMD PhenomII, pero son lo suficientemente lentas sin problemas extra de dirección de retorno o w / e el problema es.
El costo es bastante pequeño No termina tomando espacio extra en la mayoría de los casos, porque de todos modos es seguido por relleno nop
. Sin embargo, en los casos en los que da lugar a un relleno adicional, será el peor de los casos en que se necesita 15B de relleno para alcanzar el próximo límite de 16B. gcc solo puede alinearse en 8B en ese caso. (con .p2align 4,,10;
para alinear a 16B si tomará 10 o menos bytes de nop, luego un .p2align 3
para alinear siempre a .p2align 3
Use gcc -S -o-
para producir una salida de gcc -S -o-
a stdout para ver cuándo hace esto.)
Entonces, si estimamos que uno en 16 rep ret
termina creando un relleno adicional donde un ret
hubiera golpeado la alineación deseada, y que el relleno extra vaya a un límite de 8B, esto significa que cada rep
tiene un costo promedio de 8 * 1 / 16 = medio byte
rep ret
no se usa con la suficiente frecuencia para agregar mucho a nada. Por ejemplo, Firefox con todas las bibliotecas que ha mapeado solo tiene ~ 9k instancias de rep ret
. Eso es aproximadamente 4k bytes, en muchos archivos. (Y menos memoria RAM que eso, ya que muchas de esas funciones en bibliotecas dinámicas nunca se llaman).
# disassemble every shared object mapped by a process.
ffproc=/proc/$(pgrep firefox)/
objdump -d "$ffproc/exe" $(sudo ls -l "$ffproc"/map_files/ |
awk ''//.so/ {print $NF}'' | sort -u) |
grep ''repz ret'' -c
objdump: ''(deleted)'': No such file # I forgot to restart firefox after the libexpat security update
9649
Eso cuenta con rep ret
en todas las funciones en todas las bibliotecas que firefox ha mapeado, no solo con las funciones que llama. Esto es algo relevante, porque una densidad de código más baja en todas las funciones significa que sus llamadas se distribuyen en más páginas de memoria. ITLB y L2-TLB solo tienen un número limitado de entradas. La densidad local es importante para L1I $ (y uop-cache de Intel). De todos modos, rep ret
tiene un impacto muy pequeño.
Me llevó un minuto pensar en una razón por la cual /proc/<pid>/map_files/
no es accesible para el propietario del proceso, pero /proc/<pid>/maps
es. Si un UID = proceso raíz (p. Ej., Desde un binario suid-root) mmap(2)
sa 0666 archivo que está en un directorio 0700, entonces hace setuid(nobody)
, cualquiera ejecutando ese binario podría eludir la restricción de acceso impuesta por la falta de x for other
permiso en el directorio.
Hay un blog completo llamado así después de esta instrucción. Y la primera publicación describe el motivo: http://repzret.org/p/repzret/
Básicamente, hubo un problema en el pronosticador de ramas de AMD cuando un ret
byte siguió inmediatamente un salto condicional como en el código que citó (y algunas otras situaciones), y la solución fue agregar el prefijo de rep
, que es ignorado por CPU pero corrige la penalización del predictor.