tutorial registers lenguaje language instruction ensamblador assembly x86

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.