The first thing you need is something like this file. This is the instruction database for x86 processors as used by the NASM assembler (which I helped write, although not the parts that actually translate instructions). Lets pick an arbitrary line from the database:
ADD rm32,imm8 [mi: hle o32 83 /0 ib,s] 386,LOCK
What this means is that it describes the instruction
ADD. There are multiple variants of this instruction, and the specific one that is being described here is the variant that takes either a 32-bit register or memory address and adds an immediate 8-bit value (i.e. a constant directly included in the instruction). An example assembly instruction that would use this version is this:
add eax, 42
Now, you need to take your text input and parse it into individual instructions and operands. For the instruction above, this would probably result in a structure that contains the instruction,
ADD, and an array of operands (a reference to the register
EAX and the value
42). Once you have this structure, you run through the instruction database and find the line that matches both the instruction name and the types of the operands. If you don't find a match, that's an error that needs to be presented to the user ("illegal combination of opcode and operands" or similar is the usual text).
Once we've got the line from the database, we look at the third column, which for this instruction is:
[mi: hle o32 83 /0 ib,s]
This is a set of instructions that describe how to generate the machine code instruction that's required:
mi is a descriptiuon of the operands: one a
modr/m (register or memory) operand (which means we'll need to append a
modr/m byte to the end of the instruction, which we'll come to later) and one an immediate instruction (which will be used in the description of the instruction).
- Next is
hle. This identifies how we handle the "lock" prefix. We haven't used "lock", so we ignore it.
- Next is
o32. This tells us that if we're assembling code for a 16-bit output format, the instruction needs an operand-size override prefix. If we were producing 16-bit output, we'd produce the prefix now (
0x66), but I'll assume we aren't and carry on.
- Next is
83. This is a literal byte in hexadecimal. We output it.
/0. This specifies some extra bits that we will need in the modr/m bytem, and causes us to generate it. The
modr/m byte is used to encode registers or indirect memory references. We have a single such operand, a register. The register has a number, which is specified in another data file:
eax REG_EAX reg32 0
We check that
reg32 agrees with the required size of the instruction from the original database (it does). The
0 is the register's number. A
modr/m byte is a data structure specified by the processor, that looks like this:
(most significant bit)
2 bits mod - 00 => indirect, e.g. [eax]
01 => indirect plus byte offset
10 => indirect plus word offset
11 => register
3 bits reg - identifies register
3 bits rm - identifies second register or additional data
(least significant bit)
Because we are working with a register, the
mod field is
reg field is the number of the register we're using,
- Because there's only one register in this instruction, we need to fill in the
rm field with something. That's what the extra data specified in
/0 was for, so we put that in the
modr/m byte is therefore
0xC0. We output this.
- Next is
ib,s. This specifies a signed immediate byte. We look at the operands and note we have an immediate value available. We convert it to a signed byte and output it (
The complete assembled instruction is therefore:
0x83 0xC0 0x2A. Send it to your output module, along with a note that none of the bytes constitute memory references (the output module may need to know if they do).
Repeat for every instruction. Keep track of labels so you know what to insert when they're referenced. Add facilities for macros and directives that get passed to your object file output modules. And this is basically how an assembler works.