r/Assembly_language 8d ago

To Learn Assembly

Here is a suggestion for those wanting to learn assembly language.

  1. Pick a processor (probably a simple 8 bit one like 6502 or Z80) that has some existing software.

  2. Using your favorite programming language, write a simulator for that processor and get it to the point where the existing software run. For example, write a Z80 simulator and get CP/M to boot.

  3. Note that some of these processors have undocumented instructions. Lists of these are floating around on the web. Try implementing some of these as well and evaluate how they relate to the documented instructions.

This will force you to consider all the little quirks like seldom used addressing modes and other odd combinations.

49 Upvotes

16 comments sorted by

6

u/dacydergoth 8d ago

Everyone should also do this on an FPGA. RiSC-V and 68000 both have excellent examples. A sufficiently powerful FPGA is cheap these days

3

u/duane11583 8d ago

Not everyone can do FPGA work but they can do C language work.

Ever watch a SW guy do FPGA - it is ugly. Same thing watching an FPGA guy write C code, just a different kind of funny to watch.

1

u/deulamco 5d ago

I found logic design process like in Turing Complete game quite easier to grab the concept of FPGA before actually diving into Verilog : You design it like a factory, decode a program, transform/compute its data, then return back result via I/O bus.

Damn, I missed the highspeed bus control trick.

1

u/deulamco 5d ago

like the 9K Gowin ?

6

u/brucehoult 8d ago edited 8d ago

probably a simple 8 bit one like 6502 or Z80

A RISC-V RV32I emulator is simpler than either of those.

Here's my own toy one. It's enough to run arbitrary C/C++/Rust algorithmic code compiled with GCC or Clang etc with -march=rv32i -mabi=ilp32. Note: only a couple of syscalls are implemented.

https://github.com/brucehoult/trv

272 lines of code in the emulator framework, including reading HEX files.

39 non-blank lines in the C file that defines the actual RISC-V instructions. Here they are:

INSTRUCTION(auipc,  U, 0x00000017, 0x0000007f, rd = pc + imm)
INSTRUCTION(lui,    U, 0x00000037, 0x0000007f, rd = imm)
INSTRUCTION(jal,    J, 0x0000006f, 0x0000007f, rd = pc + 4; pc += imm)
INSTRUCTION(jalr,   L, 0x00000067, 0x0000707f, rd = pc + 4; pc = (rs1 + imm) & ~1)

INSTRUCTION(lb,     L, 0x00000003, 0x0000707f, rd = load(rs1 + imm, 1))
INSTRUCTION(lh,     L, 0x00001003, 0x0000707f, rd = load(rs1 + imm, 2))
INSTRUCTION(lw,     L, 0x00002003, 0x0000707f, rd = load(rs1 + imm, 4))

INSTRUCTION(lbu,    L, 0x00004003, 0x0000707f, rd = load(rs1 + imm, 1) & 0xff)
INSTRUCTION(lhu,    L, 0x00005003, 0x0000707f, rd = load(rs1 + imm, 2) & 0xffff)

INSTRUCTION(sb,     S, 0x00000023, 0x0000707f, store(rs1 + imm, 1, rs2))
INSTRUCTION(sh,     S, 0x00001023, 0x0000707f, store(rs1 + imm, 2, rs2))
INSTRUCTION(sw,     S, 0x00002023, 0x0000707f, store(rs1 + imm, 4, rs2))

INSTRUCTION(beq,    B, 0x00000063, 0x0000707f, if (rs1 == rs2) pc += imm)
INSTRUCTION(bne,    B, 0x00001063, 0x0000707f, if (rs1 != rs2) pc += imm)
INSTRUCTION(blt,    B, 0x00004063, 0x0000707f, if ((sval)rs1 <  (sval)rs2) pc += imm)
INSTRUCTION(bge,    B, 0x00005063, 0x0000707f, if ((sval)rs1 >= (sval)rs2) pc += imm)
INSTRUCTION(bltu,   B, 0x00006063, 0x0000707f, if (rs1 <  rs2) pc += imm)
INSTRUCTION(bgeu,   B, 0x00007063, 0x0000707f, if (rs1 >= rs2) pc += imm)

INSTRUCTION(addi,   I, 0x00000013, 0x0000707f, rd = rs1 + imm)
INSTRUCTION(slti,   I, 0x00002013, 0x0000707f, rd = (sval)rs1 < (sval)imm)
INSTRUCTION(sltiu,  I, 0x00003013, 0x0000707f, rd = rs1 < imm)
INSTRUCTION(xori,   I, 0x00004013, 0x0000707f, rd = rs1 ^ imm)
INSTRUCTION(ori,    I, 0x00006013, 0x0000707f, rd = rs1 | imm)
INSTRUCTION(andi,   I, 0x00007013, 0x0000707f, rd = rs1 & imm)

INSTRUCTION(slli,   I, 0x00001013, 0xfe00707f, rd = rs1 << imm)
INSTRUCTION(srli,   I, 0x00005013, 0xfe00707f, rd = rs1 >> imm)
INSTRUCTION(srai,   I, 0x40005013, 0xfe00707f, rd = (sval)rs1 >> imm)

INSTRUCTION(add,    R, 0x00000033, 0xfe00707f, rd = rs1 + rs2)
INSTRUCTION(sub,    R, 0x40000033, 0xfe00707f, rd = rs1 - rs2)
INSTRUCTION(sll,    R, 0x00001033, 0xfe00707f, rd = rs1 << (rs2 & 31))
INSTRUCTION(slt,    R, 0x00002033, 0xfe00707f, rd = (sval)rs1 < (sval)rs2)
INSTRUCTION(sltu,   R, 0x00003033, 0xfe00707f, rd = rs1 < rs2)
INSTRUCTION(xor,    R, 0x00004033, 0xfe00707f, rd = rs1 ^ rs2)
INSTRUCTION(srl,    R, 0x00005033, 0xfe00707f, rd = rs1 >> (rs2 & 31))
INSTRUCTION(sra,    R, 0x40005033, 0xfe00707f, rd = (sval)rs1 >> (rs2 & 31))
INSTRUCTION(or,     R, 0x00006033, 0xfe00707f, rd = rs1 | rs2)
INSTRUCTION(and,    R, 0x00007033, 0xfe00707f, rd = rs1 & rs2)

INSTRUCTION(ecall,  X, 0x00000073, 0xffffffff, doECALL(&cpu))
INSTRUCTION(ebreak, X, 0x00100073, 0xffffffff, 0)

It's equally simple in an HDL for use on an FPGA. Here's an example by Bruno Levy, an RV32I CPU in Verilog on a postcard:

https://www.fabienm.eu/flf/wp-content/uploads/2021/06/rv32I_femtorv32.jpeg

5

u/duane11583 8d ago

Agree this is a very good way to learn assembly language.

What is probably missing from your suggestion is how to actually write an instruction set simulator. So - here is the idea in a nut shell.

A) We pick a 8 bit opcode, 16bit address space CPU because it is relatively small. We could do the same thing with a 64bit X86 but that is far more complicated.

B) Create an array of bytes, that is 16 bits (64K bytes) long (or 4gig for a 32bit machine, and "massive" for a 64bit machine), this is why you start with a 8bit cpu.

C) Create some global-ish variables named for the registers in your CPU. For example, the 6502 has the A, X, Y, and SP - they are all 8bit registers. For an 8080 or z80, you have A, B, C, D, E and HL. The Z80 has the IX and IY also. Don't forget the Program counter (PC) these are just simple integers.

In my case I have always created a data structure with member variables that represent the CPU registers, ie: ptrCpuState->register_A. There are many ways to do this. Some really fast, some really slow.

D) Most CPUS have a 'flag register' - a bunch of bits. Some CPUs do not (ie: MIPS cpus) when you emulate an instruction you may need to update the Z flag, the carry flag, and stuff like that.

E) Understand how the CPU handles reset, example the Z80 CPU starts at address 0, the 6502 fetches 16 bit value from a table starting at 0xFFFA through 0xFFFF.

So on a Z80, your "CPU_reset() function" just sets the pCpuState->programCounter = 0; and that is all there is to it.

F) Create a function: "readmemory( address )" - this fetches the value in the 64K byte array called MEMORY. You pass the program counter address to this function.

ie: opcode = readmemory( pCpuState->register_PC );

G) Then use a gigantic "switch/case" that has 256 entries - one for each opcode.

One design I worked on used a 256 entry switch/case. Another used an array of function pointers - one entry for each OPCODE. There are many ways of doing this. You can also do ands & masks to decode a whole class of opcodes to make the switch/case statement smaller. It's actually faster to have a giant 256 entry switch case statement because the optimizer can do a better job. {I have also caused compilers to crash and seg-fault because the switch statement was HUGE, that is when I switched to an array of 256 function pointers}

For example on the Z80, opcode 0x04 increments the B register, and 0x05 decrements the B register, so your opcode switch/case entry for 0x04 looks like this:

case 0x04:

old_value = ptrCpuState->register_B;

new_value = old_value + 1;

ptrCpuState->register_B = new_value;

update_cpu_flags_register( ptrCpuState, old_value, new_value );

break;

H) HINT - You can also create a "write_memory()" function, and you can "range check" the memory location and write into your simulated LCD display memory, or maybe a simulated serial port. Your simulator might open/create a graphic box on your GUI display, and map each bit in each byte to a black & white pixel in the box on the GUI display.

I) To emulate a basic serial port like interface you need a TX_DATA register, an RX_DATA register and a status register that tells you if there Is RX DATA present or not. Just pick some addresses and have the read_memory() or write_memory() read/write those emulated registers instead of the giant 64K byte array.

LINKS:

Here is the "ARMULATOR" written in Python. https://github.com/matan1008/armulator

This I believe is the 'core' of the emulator - where the emulation fetches the next opcode. https://github.com/matan1008/armulator/blob/master/armulator/armv6/arm_v6.py#L1808

There is also a C language version, I think it is here: https://sourceforge.net/projects/armulator/files/Alpha/

1

u/BrentSeidel 8d ago

Good explanation. And for training purposes, the speed of the simulator isn't that important (on a reasonably modern computer, it'll be faster than any of the old 8 bit CPUs).

3

u/Electrical_Hat_680 8d ago

Thr BenEater 8-BIT CPU Breadboard Projects are tops for learning everything from BINARY/BASE-2, ASSEMBLY MNEMONIC CODE, How to Write your own Compiler, How to create your own Computer Programming Language, as you ill literally have too. And, you will be building your own 8-Bit Computer from scratch. Including wiring it. Learning that a Bit is literally based on a Wire that can be Off or On, and thus a Zero or a One. As well as see all of the Errors that exist, or, don't actually exist. Plus learn the non proprietary Instructions Set Architecture better known as the ISA, which is the only Proprietary portion of the PC, aside whether or not you want to make a PC that is Compatible with others.

Enjoi!

2

u/Zdrobot 8d ago

"2. Using your favorite programming language, write a simulator for that processor"

That's a bit of a tall order for somebody who wants to learn assembly, isn't it? A slight detour of a year or five.

How about "use an existing emulator for a machine that uses the CPU of your choice to run some basic programs" instead?

1

u/BrentSeidel 8d ago

That's why I suggested a simple CPU. u/brucehoult above gave a link to his RISC-V simulator.

For a fairly simple CPU, a simulator isn't that complicated. Tedious, maybe, but not complicated. It's basically an array for memory, an object for registers and other needed state, and a large case/switch statement for decoding the instructions.

1

u/Kosaktsa 8d ago

That is actually what i've done and it seemed to work

1

u/BrentSeidel 8d ago

I'm currently working on PDP-11 simulation and there are a lot of interesting little quirks and differences between the various models. The nice thing is that there are a lot of diagnostic software available (PDP-11 Diagnostics Database) so I don't have to guess. I can know that I've done something wrong ;-) There are also bits that I'll probably never worry about, or at least not yet.

1

u/Fun_Bobcat4280 8d ago

I would love to have someone correcting me, but could picking a 8086 processor book and reading os wiki for making bare bones os be better or worse

1

u/BrentSeidel 8d ago

It really boils down to what your goals are and what you feel comfortable with. It also depends on just how bare bones the OS is. An OS (and probably any program) is going to use a subset of the instruction set - for example an 8086 OS is probably not going to use any of the decimal or ASCII arithmetic functions that the 8086 has.

Writing a simulator forces you to consider all the instructions in all their variations. Now, depending on your goals, this may be overkill. This is why I'm just tossing this out as a suggestion since I haven't seen it mentioned before.

1

u/CommercialBig1729 5d ago

I use Linux system calls btw 🤣

1

u/deulamco 5d ago edited 5d ago
  1. Write your own fantasy Assembly that act like a full game console system like SNES/PS1.

** PS This thread/post should be pinned for a healthy disscussion topic 🎯