Assembly Language & Procedure Calls

an assembler

If assembly language is the readable text and machine code is the raw bits the processor eats, something has to translate one into the other. That translator is the assembler. Think of it as a very literal clerk: you hand it a list of instructions written in words and numbers, and it stamps out the exact bit pattern for each one, in order, with no creativity and no second-guessing.

The assembler's job is mostly mechanical. For each line it looks up the opcode (the bit code for add, lw, beq, and so on), packs the register numbers and any immediate value into the instruction's fields, and writes out the resulting machine word. It also keeps a symbol table: when it sees a label like loop:, it records the address of that spot, so that later a branch to loop can be filled in with the right number. Most assemblers make two passes over the file: a first pass to discover where every label lands, and a second pass to emit the bytes now that all addresses are known.

The output is usually not a runnable program yet but an object file: machine code plus a table of which symbols it defines and which it still needs from elsewhere. The linker stitches object files together afterwards. An assembler is much simpler than a compiler because it does almost no rearranging or optimizing; it faithfully reflects what you wrote. That faithfulness is exactly why people use assembly when they need to control the machine to the bit.

On the first pass the assembler reads loop: ... beq t0, zero, loop and notes that loop sits at address 0x100. On the second pass it computes the branch offset (target minus current address) and bakes that number into the beq instruction so the jump lands exactly on loop.

Two passes: discover all label addresses first, then emit machine code with the right offsets.

An assembler translates within one ISA, almost line for line; a compiler translates a whole different language (like C) and is free to reorganize. Don't conflate the two.