Leer el contador del programa directamente
¿Se puede leer el contador del programa en las CPU Intel directamente (es decir, sin "trucos") en modo kernel o en algún otro modo?
No, no se puede acceder directamente a EIP/IP, pero en el código dependiente de la posición es una constante de tiempo de enlace, por lo que puede usar un símbolo cercano (o distante) como inmediato.
mov eax, nearby_label ; in position-dependent code
nearby_label:
Para obtener EIP o IP en código de 32 bits independiente de la posición:
call _here
_here: pop eax
; eax now holds the PC.
En CPU más nuevas que Pentium Pro (o probablemente PIII), call rel32
rel32=0 está en mayúsculas especiales para no afectar la pila de predictores de direcciones de retorno . Entonces, esto es eficiente y compacto en x86 moderno, y es lo que clang usa para código de 32 bits independiente de la posición.
En las CPU Pentium Pro antiguas de 32 bits, esto desequilibraría la pila de predictores de llamada/retorno, por lo que es preferible llamar a una función que realmente regrese, para evitar predicciones erróneas de bifurcación en aproximadamente 15 ret
instrucciones futuras en sus funciones principales. (A menos que no vaya a regresar, o tan raramente que no importe). Sin embargo, la pila de predictores de direcciones de retorno se recuperará.
get_retaddr_ppro:
mov eax, [esp]
ret ; keeps the return-address predictor stack balanced
; even on CPUs where call +0 isn't a no-op.
En modo x86-64, RIP se puede leer directamente utilizando un archivo RIP relativolea
.
default rel ; NASM directive: use RIP-relative by default
lea rax, [_here] ; RIP + 0
_here:
MASM o GNU .intel_syntax
:lea rax, [rip]
Sintaxis de AT&T: lea 0(%rip), %rax
Si necesita la dirección de una instrucción específica, normalmente algo como esto es suficiente:
thisone:
mov (e)ax,thisone
(Nota: en algunos ensambladores, esto puede hacer algo incorrecto y leer una palabra de [este], pero generalmente hay cierta sintaxis para lograr que el ensamblador haga lo correcto).
Si su código está cargado estáticamente en una dirección específica, el ensamblador ya conoce (si le indicó la dirección inicial correcta) las direcciones absolutas de todas las instrucciones. El código cargado dinámicamente, digamos como parte de una aplicación en cualquier sistema operativo moderno, obtendrá la dirección correcta gracias a la reubicación de direcciones realizada por el vinculador dinámico (siempre que el ensamblador sea lo suficientemente inteligente como para generar las tablas de reubicación, que normalmente lo son).
En x86-64 puedes hacer, por ejemplo:
lea rax,[rip] (48 8d 05 00 00 00 00)
No hay instrucciones para leer directamente el puntero de instrucciones (EIP) en x86. Puede obtener la dirección de la instrucción actual que se está ensamblando con un pequeño ensamblaje en línea:
// GCC inline assembler; for MSVC, syntax is different
uint32_t eip;
__asm__ __volatile__("movl $., %0", : "=r"(eip));
La .
directiva del ensamblador es reemplazada por la dirección de la instrucción actual del ensamblador. Tenga en cuenta que si incluye el fragmento anterior en una llamada de función, obtendrá la misma dirección (dentro de esa función) cada vez. Si desea una función C más utilizable, puede utilizar algún ensamblado no en línea:
// In a C header file:
uint32_t get_eip(void);
// In a separate assembly (.S) file:
.globl _get_eip
_get_eip:
mov 0(%esp), %eax
ret
Esto significa que cada vez que desea obtener el puntero de instrucción, es un poco menos eficiente ya que necesita una llamada de función adicional. Tenga en cuenta que hacerlo de esta manera no arruina la pila de direcciones de remitente (RAS). La pila de direcciones de retorno es una pila separada de direcciones de retorno que el procesador utiliza internamente para facilitar la predicción del objetivo de rama para las instrucciones RET.
Cada vez que tiene una instrucción CALL, el EIP actual se envía al RAS, y cada vez que tiene una instrucción RET, el RAS aparece y el valor superior se utiliza como predicción del objetivo de rama para esa instrucción. Si estropea el RAS (por ejemplo, al no hacer coincidir cada CALL con un RET, como en la solución de Cody ), obtendrá un montón de predicciones erróneas de ramas innecesarias, lo que ralentizará su programa. Este método no destruye el RAS, ya que tiene un par coincidente de instrucciones CALL y RET.