When you debug a compiled program, inspect a memory dump or mark an exam, you often only have hexadecimal words. Going from there to readable assembly takes more than decoding each word on its own: you have to rebuild the labels and recognize the pseudo-instructions. We do it with an eight-instruction routine written for this guide; every word was generated and checked with the site’s decoder.
Step 1: decode every word
We start from eight consecutive words beginning at address 0x00. Applying the method from the decoding guide to each of them gives:
| Address | Word | Format | Decoded |
|---|---|---|---|
| 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) |
This is already correct assembly, but it is hard to read: branches appear as numeric offsets and some instructions use “odd” registers, such as adding zero or jumping while saving the return address into zero.
Step 2: turn offsets into labels
The offsets of beq and jal are relative to the address of the instruction itself. The beq at 0x04 jumps +20, i.e. to 0x04 + 20 = 0x18. The jal at 0x14 jumps −16, to 0x14 − 16 = 0x04. Those two addresses deserve a name: we call 0x04 loop (the target of the backward jump) and 0x18 done.
Step 3: recognize the pseudo-instructions
Pseudo-instructions are shorthands the assembler turns into real instructions. When reading machine code you go the other way:
addi rd, zero, immisli rd, imm, andaddi rd, rs, 0ismv rd, rs.beq rs, zero, targetisbeqz rs, target.jal zero, targetisj target: a jump that saves no return address.jalr zero, 0(ra)isret: it goes back to the address saved in ra.
| Address | Base instruction | Readable form |
|---|---|---|
| 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 |
Step 4: understand what it does
popcount: # a0 = input number li t0, 0 # t0 = count of ones loop: beqz a0, done # no bits left: finish andi t1, a0, 1 # t1 = lowest bit of a0 add t0, t0, t1 # add that bit to the count srli a0, a0, 1 # drop the lowest bit j loop done: mv a0, t0 # return the count in a0 ret
With names and comments the intent becomes obvious. On each iteration, andi t1, a0, 1 isolates the lowest bit, it is added to the counter and srli shifts a0 right. When a0 reaches 0 there are no ones left. The routine counts how many bits of a0 are 1 (the population count) and returns the result in a0, following the calling convention: argument and result in a0, return address in ra.
Step 5: check it by running it
To run it you need a program that calls it. This one loads 13 (0b1101) into a0, calls the routine with jal and stops:
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
In the debugger the program executes 28 instructions and ends with a0 = 3, because 1101 has three ones. Try other values, for example 255 (eight ones) or 0 (the loop body never runs).
Summary of the procedure
- Decode every word and note its address.
- Compute each branch target (address + offset) and give it a label.
- Replace base instructions with their pseudo-instructions where they fit.
- Look for structure: loops (backward branches), conditions (forward branches) and calls (jal/ret).
- Check your reading by running the code with known values.