Condiciones en Ensamblador
SOLUCIONARIO
Condiciones en Ensamblador
2025-02-09
Por
Occam's Razor

    .intel_syntax noprefix  
    .global _start
    .extern puts
    .extern exit
    .extern scanf
    .extern printf
    .extern stderr

    .equ N, 42  # Constante 
    .text
_start:
    lea  rdi, fmt1
    lea  rsi, msg   
    call printf     # Imprime mensaje
    
    lea  rdi, fmt2
    lea  rsi, v
    call scanf      # Lee número

    mov  r11d, [v]
    cmp  r11d, 0
    jle  error  # Menor o igual que zero -> Error
    
    mov  r12d, N
    cmp  r11d, r12d
    je   bien        # igual a N? -> Hemos terminado
    jl   menor 

    sub  r11d, r12d
    mov  [h], r11d
    lea  rdi, menor_str
    call puts
    jmp  muestra_pista
menor:
    sub  r12d, r11d
    mov  [h], r12d
    lea  rdi, mayor_str
    call puts
muestra_pista:  
    mov  r11d, [h]
    cmp  r11d, 5
    jl   pista_te_quemas
    cmp  r11d, 10
    jl   pista_caliente
    cmp  r11d, 15
    jl   pista_templado
    lea  rdi, frio_str
    jmp  pista
pista_te_quemas:
    lea  rdi, te_quemas_str
    jmp  pista
pista_caliente:
    lea  rdi, caliente_str
    jmp  pista
pista_templado:
    lea  rdi, templado_str
pista:  
    call printf
    jmp  fin
bien:
    lea  rdi, bien_msg
    call puts
    jmp  fin
error:  
    mov  rdi, stderr
    lea  rsi, err
    call fprintf
fin:    
    call exit

    .section .rodata
msg:            .asciz "Adivina en que número estoy pensando ? "
err:            .asciz "Solo números positivos\n"
fmt1:           .asciz "%s"
fmt2:           .asciz "%d"
bien_msg:       .asciz "Bien Hecho!"
mayor_str:      .asciz "Sigue Probando!!\nEl número es MAYOR"
menor_str:      .asciz "Sigue Probando!!\nEl número es MENOR"
te_quemas_str:  .asciz "TE QUEMAS!!\n"
caliente_str:   .asciz "CALIENTE!\n"
templado_str:   .asciz "TEMPLADO\n"
frio_str:       .asciz "FRIO\n" 
    .section .data
h:  .word 0
v:  .word 0

En ensamblador no tenemos ningún tipo de construcción de alto nivel como bloques if... else, operadores ternarios, switch o valores booleanos, si bien, como es lógico, podemos simular toda esa funcionalidad.

La mayoría de ensambladores nos permiten definir constantes utilizando la pseudo instrucción equ. La sintaxis puede variar de un ensamblador a otro, pero es bastante común que esa sea la forma estándar de definir una constantes.

El concepto de tipo no existe y por tanto no existen los valores booleanos, los valores que obtenemos como resultado de una operación lógica y que pueden contener valores 0 y 1, o VERDADEOR y FALSO. Depende de nosotros como queramos representar, pero la forma más habitual es utilizar el valor 1 para TRUE/VERDADERO y 0 para FALSE/FALSO, puesto que facilitan la implementación de las condiciones. Veamos como.

La mayoría de procesadores disponen de una serie de flags, banderas o indicadores (como queráis llamarlos) que se activan dependiendo de ciertas operaciones. Algunos de los más comunes son el flag Zero que se activa cuando el resultado de una operación es cero, o el flag Sign que se activa según el signo del resultado de una operación.

SABÍAS QUE

No todos los procesadores tienen flags que nos indican el resultado de las operaciones aritméticas y lógicas. Los procesadores intel utilizan flags para este propósito. Otros procesadores como MIPS o RISC-V no usan flags y solo ofrecen saltos condicionales en los que debemos indicar explícitamente los registros que queremos comparar y el tipo de comparación

La forma más habitual para activar los flags y así poder comparar datos, es utilizar la instrucción CMP que permite comparar dos valores. Esta instrucción básicamente resta los dos valores que le pasamos, pero sin almacenar el resultado. Por ejemplo, la expresión:

if (a == b) puts ("Iguales"); else puts ("Diferentes");

Se traduce en:

    mov rax, [a]
    mov rbx, [b]
    cmp rax, rbx
    je iguales     # Si a=b cmp pone el flag Zero a 1
    # Este es el caso else a!=b
    lea rdi, diferentes_str
    jmp cont
iguales:
    lea rdi, iguales_str
cont:
    call puts

Aunque a nivel de código fuente tenemos que escribir mucho más, el código que se genera finalmente suele ser más corto que para la expresión equivalente en un lenguaje de alto nivel.

El flag Z o Zero es uno de los más sencillos de usar y es por ello que si elegimos 0 como valor falso y cualquier otro valor como verdadero, podemos comparar valores “boleanos” usando el flag Z.

Por ejemplo, en C encontramos habitualmente expresiones como:

if (a) goto es_verdadero; else goto es_falso;

Que se traducen directamente en esto:

  cmp BYTE [a], 0
  jz  es_falso
  ; Caso es verdadero
es_falso:
  ; Caso es falso

Un operador ternario tendría una forma muy similar, pero habilitando alguna forma de devolver el valor.

De la misma forma un switch se puede implementar como una secuencia de ifs:

switch (ax) {
    case 0: 
      bx = 0;
      break;
    case 1:
      bx = 1
    case 2:
      bx = 2;
      break;
    default
      bx = 3;
}

Que en ensamblador sería algo como esto:

    cmp ax, 0
    je  caso1
    cmp ax, 1
    je  caso1
    cmp ax, 2
    je  caso2
    jmp default
caso0:  
    mov bx, 0
    jmp fin     # break
caso1:
    mov bx, 1   # caso sin break... la ejecución continua
caso2:
    mov bx, 2
    jmp fin     # break
default:
    mov bx, 3
fin :
    # Continua

En ese fragmento de código podéis ver porque si nos olvidamos el break (jmp fin realmente) el programa sigue a través de los distintos casos del switch.

En el programa de ejemplo podríamos haber usado la función abs de la librería estándar para calcular el valor absoluto de la pista, sin embargo, en este caso tan sencillo, puesto que ya habíamos hecho la comparación, ya sabemos que número es mayor y simplemente invertimos la resta dependiendo del caso:

    mov  r11d, [v]
    (...)
    mov  r12d, N
    cmp  r11d, r12d
    je   bien        # V == N? -> Hemos terminado
    jl   menor       # V < N vamos a menor

    sub  r11d, r12d # Restamos V-N para que el resultado sea poisitvo
    mov  [h], r11d
    (...)
menor:
    sub  r12d, r11d   # Restamos N -V para que el resultado sea positivo
    mov  [h], r12d

Una forma alternativa de implementar esto sería:

  mov r11d. [v]
  mov r12d, N
  sub r11d, r12d
  mov [h], r11d   # Tenemos que almacenarlo ya que es modificado
                  # por un puts intermedio
  (...)
  mov r11d, [h]
  mov r12d, r11d
  neg r11d
  cmovl r11d, r12d

En este caso usamos la instrucción cmovl que se ejecuta dependiendo de los valores de los flags. El sufijo l significa Less Than o Menor que. Si el valor h almacenado en r11d fuera negativo, al negarlo con NEG pasaría a ser positivo y por lo tanto cmovl no se ejecutaría. Por el contrario, si el valor en r11d fuera positivo, al negarlo pasaría a ser negativo y cmovl se ejecutaría restaurando el valor original (positivo) almacenado en r12d temporalmente.

SABÍAS QUE

La instrucción CMOVcc fue introducida en la familia de procesadores P6 (Pentium Pro, Pentium II/III), por lo que no puede usarse con procesadores anteriores como el i386, i486 o los procesadores Pentium P5 (los Pentium normales).

Como podéis ver, en ensamblador tenemos una gran flexibilidad para hacer las cosas de diferentes formas, si bien esa característica es una de las cosas que hacen la programación en ensamblador complicada. Además, ahora también podéis ver porque ciertas cosas en el lenguaje C son como son. Por ejemplo el considerar valores 0 como falsos y cualquier otro valor como verdadero, o porque es necesario poner break en cada caso del switch.

Resumen

  • No soporta if ... [elsif] ...[else] ...
  • No Soporta operador terciario
  • No soporta booleanos: ==0 -> FALSO y !=0 VERDADERO
  • No soporta switch
  • Soporta constantes si bien la sintaxis depende del ensamblador usado.

SOBRE Occam's Razor
Somos una revista libre que intenta explicar de forma sencilla conceptos tecnológicos y científicos de los que no podemos escapar. Al final, todo es más fácil de lo que parece!

 
Tu publicidad aquí :)