This document explains the full pipeline: writing C code → compiling to MIPS assembly → assembling to machine code, with binary and hexadecimal representation. We use a C program that prompts the user to enter a temperature in Fahrenheit, converts it to Celsius, and prints the result.
Table of Contents
- Overview of the Pipeline
- Step 1: Write C Code
- Step 2: Compile C to MIPS Assembly
- Step 3: Understand the Generated Assembly
- Step 4: Assemble to Machine Code
- Step 5: Binary and Hex for Key Instructions
- Summary
1. Overview of the Pipeline
High-level languages like C must be translated into something the CPU can execute:
C source code MIPS assembly Machine code (binary/hex)
(.c file) → (.s file) → (object file / executable)
│ │ │
[Compiler] [Assembler] [Linker optional]
(e.g. gcc -S) (e.g. as, MARS) Produces final
or MIPS encodes each bytes CPU runs
cross-compiler instruction
- Compiler: Translates C into assembly language for the target CPU (MIPS). For our program, that means turning the Fahrenheit prompt,
scanf, the formula ( (F-32)\times 5/9 ), and the "Celsius is ..." print into a sequence of MIPS instructions and syscalls. - Assembler: Translates assembly into machine code (32-bit instructions). Each line of assembly (e.g.
addi $t0, $t0, -32) becomes one or more 32-bit words. - Machine code is what the CPU executes; it is often shown in hex (e.g.
0x2108FFE0) or binary (32 bits per instruction).
2. Step 1: Write C Code
File: fahrenheit_to_celsius.c
#include <stdio.h>
int main() {
int fahrenheit;
int celsius;
// 1) Prompt user to input Fahrenheit
printf("Enter temperature in Fahrenheit: ");
scanf("%d", &fahrenheit);
// 2) Convert Fahrenheit to Celsius
// Formula: C = (F - 32) * 5 / 9
celsius = (fahrenheit - 32) * 5 / 9;
// 3) Print the Celsius result
printf("Celsius is %d\n", celsius);
return 0;
}
What this program does (in detail):
| Part of the program | Purpose |
|---|---|
int fahrenheit; int celsius; |
Declares two integer variables: one for the user's input (Fahrenheit) and one for the computed result (Celsius). |
printf("Enter temperature in Fahrenheit: "); |
Prints a prompt string so the user knows what to type. |
scanf("%d", &fahrenheit); |
Reads a single integer from standard input and stores it at the address of fahrenheit. The %d means "decimal integer"; &fahrenheit passes the memory address so scanf can write the value there. |
celsius = (fahrenheit - 32) * 5 / 9; |
Conversion formula: Subtract 32 from Fahrenheit, multiply by 5, then integer-divide by 9. This is the standard formula ( C = \frac{5}{9}(F - 32) ). Integer division truncates toward zero (e.g. 10/9 = 1). |
printf("Celsius is %d\n", celsius); |
Prints the literal text Celsius is, then the value of celsius, then a newline. So the output looks like: Celsius is 25. |
return 0; |
Returns 0 from main, which by convention means "success" to the operating system. |
Why the formula works: The Fahrenheit and Celsius scales agree at -40° and relate linearly. The formula ( C = \frac{5}{9}(F - 32) ) comes from the fact that 0°C = 32°F and a 1°C change equals a ( \frac{9}{5} )°F change. So we subtract 32 (shift the origin) then scale by ( \frac{5}{9} ).
The compiler must generate code that: calls library routines for printf and scanf, passes format strings and addresses, performs subtraction, multiplication, and division, and returns 0. Below we use an equivalent MIPS program that does the same I/O and conversion using MARS syscalls, so the logic stays clear and we can show machine-code encoding.
3. Step 2: Compile C to MIPS Assembly
What the compiler does
- Parsing: Builds a syntax tree from your C code (e.g. the assignment
celsius = (fahrenheit - 32) * 5 / 9is parsed as an expression with subtraction, multiplication, and division). - Semantic analysis: Resolves types and checks correctness (e.g.
scanfneeds the address offahrenheit; the operands of*and/are integers). - Code generation: Produces MIPS assembly: code to call the C library for
printfandscanf, and a sequence of arithmetic instructions (subtract 32, multiply by 5, divide by 9) and stores for the result.
For a program that uses printf and scanf, the generated assembly is long (library call setup, stack frames, etc.). So for this document we use a hand-written MIPS equivalent that behaves the same way but uses MARS syscalls. The pipeline (C → assembly → machine code) and the way instructions are encoded are unchanged.
How to get MIPS assembly from this C program
Using a MIPS cross-compiler (e.g. on Linux):
mips-linux-gnu-gcc -S -o fahrenheit_to_celsius.s fahrenheit_to_celsius.c -fno-pic -march=mips32r2
Using MARS: You can type (or paste) the MIPS assembly from the next section into MARS, assemble it, and run it. MARS will show the generated machine code in the Text Segment.
4. Step 3: Understand the Generated Assembly
Below is a MIPS program equivalent to the C program: it (1) prompts the user to enter Fahrenheit, (2) reads the integer, (3) converts Fahrenheit to Celsius using ( C = (F - 32) \times 5 / 9 ), and (4) prints Celsius is {result}. It uses MARS syscalls:
- Syscall 4: Print string (address in
$a0). - Syscall 5: Read integer (result in
$v0). - Syscall 1: Print integer (value in
$a0).
Variables fahrenheit and celsius are stored in the .data segment.
MIPS assembly equivalent to the C program
.data
prompt: .asciiz "Enter temperature in Fahrenheit: "
result_msg: .asciiz "Celsius is "
newline: .asciiz "\n"
fahrenheit: .word 0
celsius: .word 0
.text
.globl main
main:
# 1) printf("Enter temperature in Fahrenheit: ");
li $v0, 4
la $a0, prompt
syscall
# 2) scanf("%d", &fahrenheit);
li $v0, 5
syscall
sw $v0, fahrenheit
# 3) Convert: celsius = (fahrenheit - 32) * 5 / 9
lw $t0, fahrenheit # $t0 = F
addi $t0, $t0, -32 # $t0 = F - 32
li $t1, 5 # $t1 = 5
mul $t2, $t0, $t1 # $t2 = (F - 32) * 5
li $t3, 9 # $t3 = 9
div $t2, $t3 # lo = quotient, hi = remainder
mflo $t4 # $t4 = (F-32)*5/9 = Celsius
sw $t4, celsius # store result
# 4) printf("Celsius is %d\n", celsius);
li $v0, 4
la $a0, result_msg
syscall
li $v0, 1
lw $a0, celsius
syscall
li $v0, 4
la $a0, newline
syscall
# return 0;
li $v0, 0
jr $ra
What each part does (detailed)
Data section (.data):
| Label | Directive | Meaning |
|---|---|---|
prompt |
.asciiz "Enter temperature in Fahrenheit: " |
Null-terminated string for the input prompt. The assembler stores each character as a byte plus a final \0. |
result_msg |
.asciiz "Celsius is " |
The literal text printed before the number. |
newline |
.asciiz "\n" |
A single newline character (ASCII 10) so the output ends with a line break. |
fahrenheit |
.word 0 |
Reserves one word (4 bytes) for the user's input. Initial value 0; overwritten by sw after syscall 5. |
celsius |
.word 0 |
Reserves one word for the computed Celsius value. |
Text section (.text) – main logic:
| Instruction(s) | C equivalent | Detailed comment |
|---|---|---|
li $v0, 4 then la $a0, prompt then syscall |
printf("Enter temperature in Fahrenheit: "); |
li loads the immediate value 4 into $v0 (syscall 4 = print string). la loads the address of the label prompt into $a0 (syscall 4 expects the string address in $a0). syscall transfers control to the kernel/simulator, which reads $v0 and $a0 and prints the string. |
li $v0, 5 then syscall then sw $v0, fahrenheit |
scanf("%d", &fahrenheit); |
Syscall 5 reads one integer from the user and places it in $v0. sw (store word) writes that value from $v0 into the memory address of fahrenheit so we can use it for the conversion. |
lw $t0, fahrenheit |
Load fahrenheit |
lw (load word) reads the 32-bit value at the address of fahrenheit into register $t0. |
addi $t0, $t0, -32 |
fahrenheit - 32 |
addi adds the 16-bit signed immediate (-32) to $t0 and writes the result back to $t0. So $t0 now holds ( F - 32 ). MIPS has no “sub immediate”; subtracting 32 is done by adding -32. |
li $t1, 5 then mul $t2, $t0, $t1 |
(F-32) * 5 |
li sets $t1 = 5. mul (multiply) computes $t0 * $t1 and places the low 32 bits of the product in $t2. So $t2 = (F - 32) * 5. |
li $t3, 9 then div $t2, $t3 |
Divide by 9 | div divides $t2 by $t3. The quotient goes into the special register lo, the remainder into hi. We need the quotient for integer division. |
mflo $t4 |
Get quotient | mflo (move from lo) copies the value in lo (the quotient) into $t4. So $t4 = (F-32)*5/9 = Celsius. |
sw $t4, celsius |
celsius = ... |
Store the value in $t4 into the memory location labeled celsius. |
li $v0, 4 then la $a0, result_msg then syscall |
printf("Celsius is "); |
Print the string "Celsius is " using syscall 4. |
li $v0, 1 then lw $a0, celsius then syscall |
printf("%d", celsius); |
Syscall 1 prints the integer in $a0. We load celsius into $a0 and then invoke the syscall. |
li $v0, 4 then la $a0, newline then syscall |
printf("\n"); |
Print a newline so the output ends with a line break. |
li $v0, 0 then jr $ra |
return 0; |
Set return value to 0 in $v0 and jr (jump register) to the address in $ra, i.e. return to the caller (e.g. the runtime startup). |
Pseudo-instructions and syscalls (detailed):
li(load immediate): The assembler expands it to a single instruction that can put a constant into a register, e.g. addi$rd, $zero, imm(add immediate: register = 0 + imm). For large immediates it may use lui + ori.la(load address): Puts the 32-bit address of a label into a register. The assembler typically uses lui (load upper immediate) to set the top 16 bits and ori (or immediate) to set the lower 16 bits, since MIPS I-type instructions only have a 16-bit immediate field.syscall: Special instruction that raises a trap. The operating system or simulator looks at$v0to decide which service to run (4 = print string, 5 = read integer, 1 = print integer). Other arguments are passed in$a0,$a1, etc., as documented for each syscall.mul: In MIPS32, mul is a real instruction (e.g. SPECIAL2 opcode, mul funct). It computesrd = rs * rt(low 32 bits of the product). Some assemblers also support it as a pseudo-instruction expanding to mult + mflo.div: divrs, rtputs quotient inloand remainder inhi. mflo / mfhi move those values into a general-purpose register.
We will encode several of these instructions into binary and hex next.
5. Step 4: Assemble to Machine Code
What the assembler does
- Reads the assembly source.
- Parses labels, mnemonics, and operands.
- Resolves labels to addresses.
- Encodes each instruction into a 32-bit word (R-type, I-type, or J-type).
- Outputs object code or a listing with addresses and machine code.
MIPS instruction formats (brief)
| Format | Used for | Fields |
|---|---|---|
| R-type | Register arithmetic (add, sub, …), jr, syscall | op (6) | rs (5) | rt (5) | rd (5) | shamt (5) | funct (6) |
| I-type | Immediate (addi, li), load/store (lw, sw) | op (6) | rs (5) | rt (5) | immediate (16) |
| J-type | Jump (j, jal) | op (6) | target (26) |
6. Step 5: Binary and Hex for Key Instructions
We encode a representative subset of the instructions from the program above. MIPS is typically shown big-endian: leftmost bits are most significant (e.g. opcode at bits 31–26).
Register numbers used
| Register | Number | 5-bit binary |
|---|---|---|
| $zero ($0) | 0 | 00000 |
| $v0 ($2) | 2 | 00010 |
| $a0 ($4) | 4 | 00100 |
| $t0 ($8) | 8 | 01000 |
| $t1 ($9) | 9 | 01001 |
| $t2 ($10) | 10 | 01010 |
| $t3 ($11) | 11 | 01011 |
| $t4 ($12) | 12 | 01100 |
| $ra ($31) | 31 | 11111 |
Instruction 1: li $v0, 4 (pseudo-instruction → addi $v0, $zero, 4)
- Format: I-type
- Meaning: Load immediate 4 into
$v0(syscall number for “print string”).
I-type: op | rs | rt | immediate
- op = 001001 (9) → addi
- rs = 00000 ($zero)
- rt = 00010 ($v0)
- immediate = 4 = 0x0004
Binary:
001001 00000 00010 0000000000000100
op rs rt immediate
Hex: 0x20020004
Instruction 2: lw $t0, fahrenheit
- Format: I-type
-
Meaning: Load word at the address of
fahrenheitinto$t0(the value the user entered). -
op = 100011 (35) → lw
- rs = base register (e.g. $gp = 28). The assembler resolves the label
fahrenheitto an address; often it is lwrt, offset(base)with a small offset from$gp. - rt = 01000 ($t0) — destination register.
- immediate = 16-bit offset (e.g. 0 if
fahrenheitis the first data word).
Binary (example: lw $t0, 0($gp)):
100011 11100 01000 0000000000000000
lw $gp $t0 offset 0
Hex: 0x8F880000
Note: The exact offset depends on where the assembler places fahrenheit in the data segment. In MARS you can see the generated encoding in the Text Segment.
Instruction 3: addi $t0, $t0, -32 (F - 32)
- Format: I-type
- Meaning: Subtract 32 from
$t0by adding -32. After this,$t0holds ( F - 32 ), the first part of the Celsius formula.
I-type: op | rs | rt | immediate
- op = 001001 (9) → addi
- rs = 01000 ($t0) — first operand (current value of
$t0). - rt = 01000 ($t0) — destination (result written back to
$t0). - immediate = -32. In 16-bit two’s complement: -32 = 0xFFE0 (binary: 1111 1111 1110 0000).
Binary:
001001 01000 01000 1111111111100000
op rs rt immediate (-32)
Hex: 0x2108FFE0
Why add -32 instead of “sub 32”? MIPS has no “subtract immediate” instruction. The ISA provides addi with a signed 16-bit immediate, so the compiler/assembler uses addi with a negative immediate to implement subtraction of a constant.
Instruction 4: mul $t2, $t0, $t1 ((F-32) * 5)
- Format: R-type (MIPS32 mul uses the SPECIAL2 opcode).
- Meaning: Multiply
$t0by$t1and put the low 32 bits of the product in$t2. Here$t0 = F - 32and$t1 = 5, so$t2 = (F - 32) * 5.
R-type (SPECIAL2): op | rs | rt | rd | shamt | funct
- op = 011100 (28) → SPECIAL2 (MIPS32 Release 2).
- rs = 01000 ($t0).
- rt = 01001 ($t1).
- rd = 01010 ($t2).
- shamt = 00000 (not used by mul).
- funct = 000010 (2) → mul.
Binary:
011100 01000 01001 01010 00000 000010
op rs rt rd shamt funct
0111 0001 0000 1001 0101 0000 0000 0010
Hex: 0x71095002
Note: Some assemblers implement mul as a pseudo-instruction using mult (rs, rt) followed by mflo rd. Then the machine code would be two instructions. MARS and MIPS32 can emit the single mul encoding above.
Instruction 5: div $t2, $t3 (divide by 9)
- Format: R-type
- Meaning: Divide
$t2by$t3. The quotient is placed in the special register lo, the remainder in hi. We use the quotient for integer division by 9.
R-type: op | rs | rt | rd | shamt | funct
- op = 000000 (SPECIAL).
- rs = 01010 ($t2) — dividend.
- rt = 01011 ($t3) — divisor (9).
- rd = 00000 (unused for div).
- shamt = 00000.
- funct = 011010 (26) → div.
Binary:
000000 01010 01011 00000 00000 011010
op rs rt (unused) funct
Hex: 0x014B001A
Instruction 6: mflo $t4 (get Celsius quotient)
- Format: R-type
- Meaning: Move the value in lo (the quotient from the previous div) into
$t4. So$t4= (F-32)*5/9 = Celsius.
R-type: op | rs | rt | rd | shamt | funct
- op = 000000, rs = 00000, rt = 00000 (unused).
- rd = 01100 ($t4) — destination.
- shamt = 00000, funct = 010010 (18) → mflo.
Binary:
000000 00000 00000 01100 00000 010010
0000 0000 0000 0000 0110 0000 0001 0010 Hex: 0x00006012
Instruction 7: sw $t4, celsius
- Format: I-type
-
Meaning: Store the word in
$t4(the computed Celsius value) into the memory address of the label celsius. -
op = 101011 (43) → sw
- rs = base (e.g. $gp), rt = 01100 ($t4), immediate = offset of
celsius(e.g. 4 iffahrenheitis at 0 andcelsiusat 4).
Binary (example: sw $t4, 4($gp)):
101011 11100 01100 0000000000000100
101011 11100 01100 000 0000 0000 00100
Hex: 0xAF8C0004
Instruction 8: syscall
- Format: R-type (SPECIAL)
-
Meaning: Trap to the kernel/simulator. The kernel reads
$v0to select the service (4 = print string, 5 = read integer, 1 = print integer) and uses$a0, etc., as arguments. -
op = 000000, rs = 00000, rt = 00000, rd = 00000, shamt = 00000, funct = 001100 (12) → syscall
Binary:
000000 00000 00000 00000 00000 001100
Hex: 0x0000000C
Instruction 9: jr $ra
- Format: R-type
-
Meaning: Jump to the address in
$ra(return address). Used to return frommainto the caller (e.g. startup code). -
op = 000000, rs = 11111 ($ra), rt = 00000, rd = 00000, shamt = 00000, funct = 001000 (8) → jr
Binary:
000000 11111 00000 00000 00000 001000
Hex: 0x03E00008
Instruction 10: li $v0, 0 (return 0)
-
Pseudo-instruction: → addi $v0, $zero, 0.
-
op = 001001, rs = 00000 ($zero), rt = 00010 ($v0), immediate = 0
Binary:
001001 00000 00010 0000000000000000
Hex: 0x20020000
Summary table: binary and hex (Fahrenheit to Celsius program)
| # | Instruction | Binary (32 bits) | Hex |
|---|---|---|---|
| 1 | li $v0, 4 (addi $v0, $zero, 4) | 0010 0000 0000 0010 0000 0000 0000 0100 | 0x20020004 |
| 2 | lw $t0, fahrenheit (e.g. 0($gp)) | 1000 1111 1000 1000 0000 0000 0000 0000 | 0x8F880000 |
| 3 | addi $t0, $t0, -32 | 0010 0001 0000 1000 1111 1111 1110 0000 | 0x2108FFE0 |
| 4 | mul $t2, $t0, $t1 | 0111 0001 0000 0101 0100 0000 0000 0010 | 0x71054002 |
| 5 | div $t2, $t3 | 0000 0001 0100 1011 0000 0000 0001 1010 | 0x014B001A |
| 6 | mflo $t4 | 0000 0000 0000 0000 0110 0000 0001 0010 | 0x00006012 |
| 7 | sw $t4, celsius (e.g. 4($gp)) | 1010 1111 1000 1100 0000 0000 0000 0100 | 0xAF8C0004 |
| 8 | syscall | 0000 0000 0000 0000 0000 0000 0000 1100 | 0x0000000C |
| 9 | jr $ra | 0000 0011 1110 0000 0000 0000 0000 1000 | 0x03E00008 |
| 10 | li $v0, 0 | 0010 0000 0000 0010 0000 0000 0000 0000 | 0x20020000 |
Note: The exact encoding of lw/sw for labels fahrenheit and celsius depends on where the assembler places them (e.g. relative to $gp or with lui/ori). The table assumes fahrenheit at offset 0 and celsius at offset 4 from the same base. In MARS you can check the generated machine code in the Text Segment after assembling.
7. Summary
| Step | Input | Output | Tool / Action |
|---|---|---|---|
| 1 | Algorithm / requirements | C source (.c) |
You write C: prompt for Fahrenheit, read integer, compute ( C = (F-32)\times 5/9 ), print "Celsius is {result}", return 0. |
| 2 | C source | MIPS assembly (.s) |
Compiler or hand-written equivalent using syscalls (4 = print string, 5 = read int, 1 = print int). |
| 3 | Assembly | Understanding | Map each instruction to the C step: prompt, scanf, load, addi -32, mul by 5, div by 9, mflo, store, print string, print int, newline, return. |
| 4 | Assembly | Machine code (binary/hex) | Assembler encodes each instruction into 32-bit words (R-type, I-type, or J-type). |
| 5 | Machine code | Execution | CPU (or MARS) fetches, decodes, and executes each instruction. |
Concepts (detailed):
- Compiler: C → assembly. One C expression like
celsius = (fahrenheit - 32) * 5 / 9becomes many instructions: loadfahrenheit, addi -32, li 5, mul, li 9, div, mflo, sw tocelsius. - Assembler: Assembly → machine code. Each instruction becomes one 32-bit word (or two for pseudo-instructions like la). The assembler resolves labels to addresses and fills in immediates and offsets.
- R-type: op=0 (or 011100 for mul), then rs, rt, rd, shamt, funct. Used for add, sub, mul, div, mflo, jr, syscall.
- I-type: op, rs, rt, 16-bit signed immediate. Used for addi (including negative for subtract-immediate), li, lw, sw.
- Division: div puts quotient in lo and remainder in hi; mflo moves lo into a general-purpose register so we can use the quotient (Celsius) in the rest of the program.
To see the actual machine code for your program: assemble the MIPS source in MARS and view the Text Segment, or compile the C file with a MIPS toolchain and inspect the object file with objdump -d fahrenheit_to_celsius.o.
This pipeline—C → assembly → machine code (binary/hex)—is the same for any C program; only the instructions, operands, and addresses change. The Fahrenheit-to-Celsius example illustrates how I/O (syscalls), arithmetic (addi, mul, div, mflo), and memory (lw, sw) are all encoded as 32-bit MIPS instructions.