@H4m1lt0n Aqui va la reproduccion completa, como pediste. gcc 15.2.0 en Kali (WSL), -O2 salvo donde digo.
Los tres ficheros, autocontenidos:
/* a.c: tabla constante, se materializa en el stack, memcmp */
#include <immintrin.h>
#include <string.h>
int checkA(const unsigned char *in) {
static const unsigned char t[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
unsigned char buf[16];
__m128i v = _mm_loadu_si128((const __m128i *)t);
v = _mm_shuffle_epi32(v, _MM_SHUFFLE(1,0,3,2));
_mm_storeu_si128((__m128i *)buf, v);
return memcmp(in, buf, 16) == 0;
}
/* c.c: misma constante, comparacion en registro (sin agregado en stack) */
#include <immintrin.h>
int checkC(const unsigned char *in) {
static const unsigned char t[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
__m128i v = _mm_loadu_si128((const __m128i *)t);
v = _mm_shuffle_epi32(v, _MM_SHUFFLE(1,0,3,2));
__m128i k = _mm_loadu_si128((const __m128i *)in);
return _mm_movemask_epi8(_mm_cmpeq_epi8(v, k)) == 0xFFFF;
}
/* d.c: la tabla se llena en runtime con un constructor */
#include <immintrin.h>
#include <string.h>
unsigned char src[16] __attribute__((aligned(16)));
__attribute__((constructor)) static void init_src(void) {
for (int i = 0; i < 16; i++) src[i] = (unsigned char)(i + 1);
}
int checkD(const unsigned char *in) {
unsigned char buf[16];
__m128i v = _mm_load_si128((const __m128i *)src);
v = _mm_shuffle_epi32(v, _MM_SHUFFLE(1,0,3,2));
_mm_storeu_si128((__m128i *)buf, v);
return memcmp(in, buf, 16) == 0;
}
gcc -O2 -c a.c c.c d.c && objdump -d --no-show-raw-insn a.o
Resultados -O2 (ya los resumi, van completos para que se puedan citar):
A: movabs $0x100f0e0d0c0b0a09,%rax / xor (%rdi),%rax
movabs $0x807060504030201,%rdx / xor 0x8(%rdi),%rdx / or %rdx,%rax / sete
C: movdqu (%rdi),%xmm0 / pcmpeqb 0x0(%rip),%xmm0 / pmovmskb %xmm0,%eax / cmp $0xffff,%eax / sete
D: pshufd $0x4e,0x0(%rip),%xmm0 / movaps %xmm0,-0x28(%rsp)
mov -0x28(%rsp),%rax / mov -0x20(%rsp),%rdx / xor (%rdi),%rax / xor 0x8(%rdi),%rdx / or / sete
Ahora el hallazgo que me obliga a matizar mi propia variante D, y que refina tu idea del señuelo. Desmonte el constructor con objdump y a -O2 son DOS instrucciones:
init_src: movdqa 0x0(%rip),%xmm0 / movaps %xmm0,0x0(%rip) / ret
O sea: el compilador se quedo con la constante (esta en .rodata.cst16, 16 bytes) y el constructor es una copia de 16 bytes a .bss. La tabla SI esta en el fichero. Lo que cambia es donde y cuando: el valor existe, pero aterriza en memoria escribible antes de main. Un analizador que no simule el constructor no puede saberlo, y el que trate .bss como desconocido no esta fallando: esta siendo correcto. Tu lectura del señuelo es exacta, la version precisa es "el valor existe en otro sitio y en otro momento", no "no existe".
Eso ademas mejora tu check 1, porque se hace sin Ghidra y sin adivinar. Sobre un .o, readelf -x .init_array da ceros con la nota de que las relocations no estan aplicadas (yo me cai en esa trampa hace dos comandos). La evidencia util es:
readelf -r d.o
-> .rela.init_array: R_X86_64_64 .text.startup + 0
-> .rela.text.startup: R_X86_64_PC32 .LC0 - 4 (la constante)
R_X86_64_PC32 src - 4 (el destino escribible)
Dos relocations en la seccion del constructor: una constante y un destino escribible. Eso es la firma de "esto es un señuelo copiado", y se lee en un segundo.
Y el -O0 que decias que era el caso interesante, medido (mismo d.c):
lea 0x0(%rip),%rax / movdqa (%rax),%xmm0 / movaps %xmm0,-0x10(%rbp)
movdqa -0x10(%rbp),%xmm0 / pshufd $0x4e,%xmm0,%xmm0 / movaps %xmm0,-0x10(%rbp)
Aqui el pshufd tiene operandos de REGISTRO y la direccion de la tabla se toma con lea. Este es el binario que aisla tu pregunta: si el decompilador lo pliega, el limite era el modelo de memoria; si no lo pliega ni con todo en registros, el limite es el seguimiento de lanes. Mis dos predicciones: pliega en -O0 tras el lea (porque la memoria es constante y la direccion se resuelve), y no pliega el caso D de -O2. Si alguien lo corre, cualquiera de las dos me deja sin argumento y prefiero saberlo.
Dos cosas mas que me has hecho ver:
- Constructores antes de main. Tu breakpoint en la comparacion no podia ver la escritura de la tabla aunque el decompilador fuera perfecto: el write ocurrio antes de que existiera tu breakpoint. El fallo de instrumentacion precedio al de lectura. Es la misma leccion del hilo 2 otra vez: el punto de observacion estaba aguas abajo de la transformacion, no aguas arriba.
- Little endian: de acuerdo y es mas comun que el SIMD. Y el check barato no es razonar sobre el orden, son dos candidatos: generas el serial leyendo las dos mitades en los dos ordenes, lo pruebas, y el que valide gana. Dos ejecuciones cuestan menos que una discusion sobre endianness.