Saltar a contenido

Operators part2

Operadores y sentencias (parte 2) – apuntes de reversing

  • Repaso clave: idiv (con signo) arma un operando de 64 bits usando EDX:EAX y deja cociente en EAX y resto en EDX. Necesita cdq para extender el signo (EDX = 0 si positivo, EDX = 0xFFFFFFFF si negativo). div (sin signo) no necesita cdq, pero sí EDX en cero; aun así cdq sirve porque deja EDX=0 en positivos.
  • Si omites cdq con números negativos, el divisor “ve” un valor positivo (EDX=0) y el cociente sale con signo incorrecto; el resto puede seguir coincidiendo, por eso a veces el bug pasa desapercibido.
  • Representación con signo: en 32 bits el rango va de 0x00000000 (0) a 0x7FFFFFFF (máx. positivo) y de 0x80000000 (primer negativo) a 0xFFFFFFFF (-1). En 64 bits lo mismo con prefijos de 8 bytes; los negativos decrecen desde 0x8000...000 hasta 0xFFFF...FFF.

Ejemplo 1: múltiplos de 3 desde -20 con idiv

for (int i = -20; i <= 0; ++i) {
    cout << i;
    if (i % 3 == 0) cout << " es multiplo de 3";
    else cout << " no es multiplo de 3";
    cout << endl;
}
- Aquí se muestra el porqué de cdq: al dividir negativos por 3, idiv usa EDX:EAX; cdq corrige el signo en EDX. Sin él, el cociente da mal (se interpreta como positivo). - Reversing: en IDA se ve el ajuste de stack sub rsp, 38h (shadow space + padding de alineación 16B) y el resto en EDX usado para decidir el mensaje.

Ejemplo 2: triángulo numérico (for anidado)

1
1 2
1 2 3
...
1 ... 20
for (int i = 1; i <= 20; ++i) {
    for (int j = 1; j <= i; ++j) cout << j;
    cout << endl;
}
- En asm: dos bucles con comparaciones y saltos; j se reinicia a 1 cada línea. El primer cout de cada línea usa RCX = std::cout, RDX = j. Stack frame: return address, shadow space (4 qwords), locales i y j, padding de alineación. - Nota: no todas las funciones usan shadow space para todos los argumentos; a veces solo guardan RCX.

Ejemplo 3: secuencia 1, 5, 3, 7, 5, 9… hasta 23 (do while)

int i = 1;
bool sumar = true, terminado = false;
do {
    cout << i;
    terminado = (i == 23);
    cout << (terminado ? "." : ",");

    i = sumar ? i + 4 : i - 2;
    sumar = !sumar;
} while (!terminado);
- Alterna +4 y -2 invirtiendo el flag sumar. El chequeo de fin se hace tras imprimir y el último 23 termina con punto.

Ejemplo 4: factorización prima con entrada repetida

int numero, factor = 2;
char resp[12];
do {
    cout << "Introduce un numero entero: ";
    cin >> numero;
    factor = 2;
    while (numero >= factor * factor) {
        if (numero % factor == 0) {
            cout << factor << " * ";
            numero /= factor;
            continue;
        }
        factor = (factor == 2) ? 3 : factor + 2; // 2,3,5,7,...
    }
    cout << numero << endl; // ultimo factor (primo)
    cout << "Descomponer otro numero? (s/n): ";
    cin >> resp;
} while (resp[0] == 's' || resp[0] == 'S');
- Estrategia: divide por 2 hasta que no sea divisible; luego prueba 3 y avanza solo por impares (factor+=2). Se detiene cuando factor*factor supera al número restante. - Reversing: se observa el uso de cdq+idiv para % y /, el continue salta la actualización del factor, y las comparaciones del carácter de respuesta ('s'/'S'). - Seguridad: el compilador inserta stack cookie al haber un buffer (resp[12]); se guarda en el prólogo y se verifica en el epílogo. El layout del frame queda: return address, shadow space, factor, numero, resp[12], padding de 4 bytes para alinear a 16, cookie, padding final para mantener rsp alineado antes de llamadas.

Tips de IDA aplicados

  • Activar nombres demanglados y “setup names” para abreviar std::. Ajustar el grafo para limpiar flechas y leer mejor el control flow.
  • El decompilado (F5) sincronizado ayuda a mapear líneas de cout/cin a varias instrucciones (RCX = std::cout, RDX = argumento), pero entender el ensamblador sigue siendo necesario cuando falten símbolos.