Cuando depuras un programa compilado, examinas un volcado de memoria o corriges un examen, a menudo solo tienes palabras hexadecimales. Pasar de ahí a ensamblador legible exige algo más que decodificar cada palabra por separado: hay que reconstruir las etiquetas y reconocer las pseudoinstrucciones. Lo hacemos con una rutina de ocho instrucciones escrita para esta guía; cada palabra se ha generado y comprobado con el decodificador del sitio.
Paso 1: decodificar cada palabra
Partimos de ocho palabras consecutivas que empiezan en la dirección 0x00. Aplicando el método de la guía de decodificación a cada una se obtiene:
| Dirección | Palabra | Formato | Decodificada |
|---|---|---|---|
| 0x00 | 00000293 | I | addi t0, zero, 0 |
| 0x04 | 00050A63 | B | beq a0, zero, 20 |
| 0x08 | 00157313 | I | andi t1, a0, 1 |
| 0x0C | 006282B3 | R | add t0, t0, t1 |
| 0x10 | 00155513 | I | srli a0, a0, 1 |
| 0x14 | FF1FF06F | J | jal zero, -16 |
| 0x18 | 00028513 | I | addi a0, t0, 0 |
| 0x1C | 00008067 | I | jalr zero, 0(ra) |
Esto ya es ensamblador correcto, pero cuesta leerlo: los saltos aparecen como desplazamientos numéricos y hay instrucciones con registros «raros», como sumar cero o saltar guardando el retorno en zero.
Paso 2: convertir desplazamientos en etiquetas
Los desplazamientos de beq y jal son relativos a la dirección de la propia instrucción. El beq de 0x04 salta +20, es decir, a 0x04 + 20 = 0x18. El jal de 0x14 salta −16, a 0x14 − 16 = 0x04. Esas dos direcciones merecen un nombre: llamamos loop a 0x04 (el destino del salto hacia atrás) y done a 0x18.
Paso 3: reconocer las pseudoinstrucciones
Las pseudoinstrucciones son abreviaturas que el ensamblador traduce a instrucciones reales. Al leer código máquina hay que hacer el camino inverso:
addi rd, zero, immesli rd, imm, yaddi rd, rs, 0esmv rd, rs.beq rs, zero, destinoesbeqz rs, destino.jal zero, destinoesj destino: un salto que no guarda dirección de retorno.jalr zero, 0(ra)esret: vuelve a la dirección guardada en ra.
| Dirección | Instrucción base | Forma legible |
|---|---|---|
| 0x00 | addi t0, zero, 0 | li t0, 0 |
| 0x04 | beq a0, zero, 20 → 0x18 | beqz a0, done |
| 0x08 | andi t1, a0, 1 | andi t1, a0, 1 |
| 0x0C | add t0, t0, t1 | add t0, t0, t1 |
| 0x10 | srli a0, a0, 1 | srli a0, a0, 1 |
| 0x14 | jal zero, -16 → 0x04 | j loop |
| 0x18 | addi a0, t0, 0 | mv a0, t0 |
| 0x1C | jalr zero, 0(ra) | ret |
Paso 4: entender qué hace
popcount: # a0 = número de entrada li t0, 0 # t0 = contador de unos loop: beqz a0, done # sin bits pendientes: terminar andi t1, a0, 1 # t1 = bit más bajo de a0 add t0, t0, t1 # sumar ese bit al contador srli a0, a0, 1 # descartar el bit más bajo j loop done: mv a0, t0 # devolver el contador en a0 ret
Con nombres y comentarios la intención aparece sola. En cada vuelta, andi t1, a0, 1 aísla el bit más bajo, se suma al contador y srli desplaza a0 a la derecha. Cuando a0 llega a 0 ya no quedan unos. La rutina cuenta cuántos bits valen 1 en a0 (en inglés, population count) y devuelve el resultado en a0, siguiendo la convención de llamada: argumento y resultado en a0, dirección de retorno en ra.
Paso 5: comprobarlo ejecutándolo
Para ejecutarla hace falta un programa que la llame. Este carga 13 (0b1101) en a0, llama a la rutina con jal y termina:
li a0, 13 jal ra, popcount j end popcount: li t0, 0 loop: beqz a0, done andi t1, a0, 1 add t0, t0, t1 srli a0, a0, 1 j loop done: mv a0, t0 ret end: nop
En el depurador el programa ejecuta 28 instrucciones y termina con a0 = 3, porque 1101 tiene tres unos. Prueba otros valores, por ejemplo 255 (ocho unos) o 0 (el bucle no se ejecuta ninguna vez).
Resumen del procedimiento
- Decodifica cada palabra y anota su dirección.
- Calcula el destino de cada salto (dirección + desplazamiento) y ponle una etiqueta.
- Sustituye las instrucciones base por sus pseudoinstrucciones cuando encajen.
- Busca la estructura: bucles (saltos hacia atrás), condiciones (saltos hacia delante) y llamadas (jal/ret).
- Comprueba tu interpretación ejecutando el código con valores conocidos.