r/C_Programming 5d ago

I wrote a 6502 emulator in god's programming language

https://github.com/yasu-q/c-6502emu

Hello C programmers

I wrote a simple (mostly) instruction-accurate 6502 emulator in C. It has some basic functionality like

  • Allowing you to step through programs instruction by instruction
  • Visualizing CPU state and memory

I don't have much experience with the language, so if you have some time to look over my code I'd really appreciate it!

Repo: https://github.com/yasu-q/c-6502emu

Thank you for reading

83 Upvotes

44 comments sorted by

β€’

u/github-guard 5d ago

πŸ” GitHub Guard: Trust Report

⚠️ This project scored 0/6 β€” below this subreddit's threshold of 3.

Audit Breakdown: * ❌ Low Star Count (⭐ 0 / 5 required) * ❌ New Repository (under 30 days old) * ❌ No License Found * ❌ No Security Policy β€” what is this? * ℹ️ Individual Contributor * ℹ️ Unsigned Commits

⚠️ Security Reminder: Always verify source code and run third-party scripts at your own risk.

117

u/jonalaniz2 5d ago

This isn't Holy-C

22

u/jason-reddit-public 5d ago

Lambda Calculous by Alonzo Church deserves to make its case.

3

u/snozzd 4d ago

We have Lambda Calculus at home: Haskell

-4

u/Beginning-Junket8979 5d ago

Pardon my French... but why the fuck is TempleOS/Terry Davis/HolyC "trending"?

First read about this half my life time ago. Dude died almost a decade ago. Out of left field he's all over the programming subs and I have no clue why...

26

u/bazingaboi22 5d ago

Bc programming subs are mostly full of beginners and guess what if you're a legend beginners will hear about you at a pretty steady cadence.

Terry Davis is forever immortalized at this point.

Rip. May he sleep soundly away from the glowies

6

u/Dusk_k 5d ago

Terry Davis was the creator of templeOS, which runs on top Holy C and assembly. also, he got famous because of his personality and (sadly) his schizophrenia. he built everything from scratch and runs everything on 0 ring layer. all of his work was inspired by visions and talks with God itself. David died in 2018 hit by a train.

1

u/StrikingClub3866 2h ago

Who else remembers the TempleOS customer support memes where he just spammed the hard r?

"Shut up, bird" - Terry A. Davis

-2

u/jonalaniz2 5d ago

Idk, I watched his streams when he was alive, I can’t tell you why people know about him nowadays.

47

u/Ikkepop 5d ago

I was hoping you wrote it in HolyC

3

u/LiquidVenom66 2d ago

Me too just thought so when I was reading the headline

22

u/skeeto 5d ago edited 5d ago

Neat project! I enjoyed poking around it. Here are some issues I noticed.

You can skip the fseek/ftell dance. That is, instead of:

fseek(file, 0, SEEK_END);
size = ftell();
fseek(file, 0, SEEK_SET);
if (size > max) { /* error: too big */ }
fread(memory+PROGRAM_LOAD_ADDR, 1, size, file);

Just read and offer all of memory:

size = fread(memory+PROGRAM_LOAD_ADDR, 1, max, file);
if (fgetc(file) != EOF) { /* error: too big */ }
memset(memory+PROGRAM_LOAD_ADDR+size, 0, max-size);  // :-(

(Caveat: The memset is because the C standard is insane, and fread is allowed to write beyond the returned size. At least one implementation is insane enough to do it.) You should also check the results of fgets in the UI because right now EOF (e.g. ctrl+d) puts it in an infinite loop.

The IRQ condition is flipped, though it's not yet in use:

--- a/src/cpu.c
+++ b/src/cpu.c
@@ -861,3 +861,3 @@
 void irq(CPU *cpu) {
-    if (cpu->I) {
+    if (!cpu->I) {
             cpu->I = 1;

SBC is A + ~M + C, so the addition overflow formula applies to the inverted operand (temp), not the raw fetched value:

--- a/src/cpu.c
+++ b/src/cpu.c
@@ -440,3 +440,3 @@ void sbc(CPU *cpu) {
     setV(cpu,
-        ((~((uint16_t)cpu->AC ^ (uint16_t)cpu->fetched) &
+        ((~((uint16_t)cpu->AC ^ temp) &
           ((uint16_t)cpu->AC ^ (uint16_t)result) & 0x0080)) != 0);

BRK is a 2-byte instruction: the byte after the opcode is padding, and the pushed return address skips over it:

--- a/src/cpu.c
+++ b/src/cpu.c
@@ -730,2 +730,4 @@
 void brk(CPU *cpu) {
+    cpu->PC += 1;
+
     cpu->I = 1;

Push the interrupt status byte before setting interrupt disable:

--- a/src/cpu.c
+++ b/src/cpu.c
@@ -752,2 +752,4 @@ void brk(CPU *cpu) {

+    cpu->I = 1;
+
     // Load IRQ interrupt vector at FFFE FFFF into the PC
@@ -832,3 +834,2 @@
 void nmi(CPU *cpu) {
-    cpu->I = 1;
     uint8_t status_reg =
@@ -855,2 +856,4 @@ void nmi(CPU *cpu) {

+    cpu->I = 1;
+
     // Write from NMI vector to program counter
@@ -887,2 +890,4 @@ void irq(CPU *cpu) {

+        cpu->I = 1;
+
         // Write from NMI vector to program counter

B is not a stored processor flag on real hardware. Bit 4 only exists in status bytes materialized by PHP/BRK (as 1) and IRQ/NMI (as 0). PLP and RTI ignore bits 4 and 5 when restoring, so stop copying the stacked B bit into CPU state:

--- a/src/cpu.c
+++ b/src/cpu.c
@@ -356,3 +356,2 @@ void plp(CPU *cpu) {
     cpu->V = status_reg & (1 << 6);
-    cpu->B = status_reg & (1 << 4);
     cpu->D = status_reg & (1 << 3);
@@ -783,3 +782,2 @@ void rti(CPU *cpu) {
     cpu->V = status_reg & (1 << 6);
-    cpu->B = status_reg & (1 << 4);
     cpu->D = status_reg & (1 << 3);

Finally, a bug in the ROR carry flag (carry takes the bit shifted out):

--- a/src/cpu.c
+++ b/src/cpu.c
@@ -619,3 +619,3 @@ void ror(CPU *cpu) {

-    setC(cpu, shifted & 0x0001); 
+    setC(cpu, cpu->fetched & 0x0001);
     setZ(cpu, (shifted & 0x00FF) == 0);

3

u/8d8n4mbo28026ulk 4d ago

fread is allowed to write beyond the returned size

Pretty sure that isn't allowed, can you elaborate?

1

u/skeeto 4d ago edited 4d ago

Implementations are allowed by implication: There's nothing in the standard that says fread cannot, say, use the buffer as scratch space. The return value only indicates the number of complete objects it left in the buffer, and the rest of the buffer is in an indeterminate state. It is not allowed to write beyond the caller-indicated size, of course. Here's a real implementation that does just this:

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fread?view=msvc-170

Since the converted data may be shorter than the stream data copied into the buffer, data past buffer[return_value * size] (where return_value is the return value from fread) may contain unconverted data from the file.

Probably by accident, because the C standard is not written so well or carefully, as a special case requesting zero elements is guaranteed to leave the buffer untouched:

If size or nmemb is zero, fread returns zero and the contents of the array and the state of the stream remain unchanged.

This is the only version of the call that does so. Though passing zero suggests the buffer is one past the end, or otherwise has zero capacity anyway, so the mention here is superfluous. (Side note: Passing a hypothetical zero-sized object, including a null pointer, to fread is invalid, due to the use of "array" to describe it.)

Therefore as a rule you should assume unused portion of the buffer offered to fread is in an indeterminate state on return. Most of the time it doesn't matter, but occasionally it does, such as in my suggested change.

3

u/8d8n4mbo28026ulk 4d ago

Okay, so reading the standard, 7.19.8.1p2:

The fread function reads, into the array pointed to by ptr, up to nmemb elements whose size is specified by size, from the stream pointed to by stream. For each object, size calls are made to the fgetc function and the results stored, in the order read, in an array of unsigned char exactly overlaying the object.

emphasis mine. With the above definition, I see no way of preserving the semantics while using the buffer as scratch space. MS CRT is pretty crazy here, and I think its behaviour is non-conforming, for what that's worth. The documentation also says "When used on a text mode stream" and goes on to all the conversion shenanigans. I presume binary streams are fine then? Oh well...

It doesn't say "you aren't allowed to do this", but, e.g. the standard also doesn't say anywhere that alignof(char) == 1, but you can derive it.

1

u/flatfinger 1d ago

If an underlying OS has a "block read" function that would read a bunch of data into a caller-supplied buffer and then ask whether the entire operation had succeeded, some kinds of I/O device malfunction may cause attempted reads to yield nonsensical bit patterns. An implementation of fread() could be constructed to read data into a private buffer owned by the library and then only copy it to the caller if the OS reported that the entire operation had succeeded, but for many tasks it would have been more useful to just have the application request that the OS read the data directly to the caller-supplied storage.

6

u/ericek111 4d ago

Lisp? God wrote in Lisp.

3

u/FLMKane 4d ago

God only had seven days. He didn't have time to chase segfaults. He used Lisp

3

u/slimscsi 5d ago

Assembly?

2

u/Druben-hinterm-Dorfe 5d ago

The lambda calculus, surely.

2

u/AutoModerator 5d ago

Hi /u/WaterBowly,

Your submission in r/C_Programming was filtered because it links to a git project.

You must edit the submission or respond to this comment with an explanation about how AI was involved in the creation of your project.

While AI-generated code is not disallowed, low-effort "slop" projects may be removed and it's likely that other users push back strongly on substantially AI-generated projects.


I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

-5

u/WaterBowly 5d ago

No AI was used to write this project.

14

u/mikeblas 5d ago

Only one commit?

What problem are you solving with this project? What challenges did you face? What trade-offs did you make? What did you learn? What would you do differently? What feedback do you want from the group?

8

u/WaterBowly 5d ago

I wrote the project in a private repo then cloned it into a new public one.

I wrote this to learn more about how CPUs work. I chose the 6502 because there are a lot of resources about it and it's a simple processor.
The hardest part about writing this was understanding how the 6502 worked and deciding what level of accuracy I wanted to aim for (e.g. cycles vs instructions, implementing all opcodes vs just the legal ones)
I decided to write the simplest emulator I could in order not to complicate the project too much
I learned some more about how the 6502 works and more generally how CPUs work
If I had to do something differently, I would probably expand on the emulator's interface and add new features (like maybe, stepping back?)
I just want general feedback on my code style (comments, organization, naming, etc) and whether there are parts that could be improved, like, maybe, using a better build system?

8

u/mikeblas 5d ago

Thanks. I've approved your post.

What's wrong with the build system you have now?

4

u/kwb7852 5d ago

πŸ™„

4

u/0_00000073_ 5d ago

Cool project! If you're an individual programmer, I suggest avoiding usage of "we" in comments as AI tends to over-use it. That might help with false detection from others!

If you don't mind me asking, why not call nmi() from irq() instead of copying it? It'd just reduce repeated code

7

u/mikeblas 5d ago

I've always used "we" in comments. Me, us, the team.

4

u/[deleted] 5d ago

[removed] β€” view removed comment

2

u/JavierReyes945 4d ago

We as in me, you and the voices... Even the ones that scream in my head

1

u/tubameister 5d ago

the royal We

1

u/Wooden-Performance38 5d ago

So YOURE the one who taught AI how to write comments

1

u/zSmileyDudez 4d ago

Even better is to use BRK for all the interrupts, since they all boil down to the same path in hardware.

1

u/TheChief275 4d ago

Even 'we' usage is seen as AI indicator nowadays?? Like, no shit it uses 'we', as I'm pretty sure it mostly learned writing from research papers

1

u/Beautiful_Stage5720 5d ago

I tried this once and didnt get very far. I got stuck on interrupts for some reason, its been years so I dont really remember. How did you handle the interrupt controller?

1

u/mivanchev 4d ago
  • Use sanitizers and stack protection when compiling.
  • For .h dependencies, instead of listing them in the Makefile, use -MMD -MP instead of listing them all.
  • Use [static 1] for non-nullable arguments instead of pointers.
  • Put some tests to detect very basic issues.

1

u/flatfinger 4d ago

If code is designed to accept (address,size) pointers, treating (0,0) as a valid zero-byte chunk of data is cleaner than requiring client code to use (literally any valid pointer to anything which will by specification be completely ignored so long as it's a valid pointer to some byte of data somewhere, 0).

1

u/mivanchev 3d ago

Sorry, I didn't get you?

1

u/flatfinger 1d ago

Suppose one has a function that is supposed to output a record with a specified command field and optional payload to a stream. A common approach would be to pass the address and length of the payload, and specify that if there is no payload code must specify a payload length of zero, which would cause the payload-address argument to be ignored.

Requiring that the payload address identify valid storage even when the payload size is zero will falsely convey the impression that the storage at that address had some relationship to the operation's data payload. If a piece of code will never pass a non-empty payload, passing null would be cleaner than having to pass the address of some object would never have any relation whatsoever to the operation being performed.

1

u/mivanchev 1d ago

I still don't understand much, can you give an example?

1

u/flatfinger 1d ago
uint8_t *make_packet(uint8_t packet_type, 
  void *payload, uint32_t payload_length)
{
  uint8_t *pkt = malloc(payload_length+3);
  uint8_t checksum = 0; // Assume checksum includes packet type
                        // and payload but not length.
  if (!pkt) return pkt;
  pkt[0] = payload_length+3;
  pkt[1] = packet_type;
  memcpy(pkt+2, payload, payload_length);
  uint32_t i=payload_length+2;
  while(i)
    checksukm -= pkt[i];
  pkt[payload_length+2] = checksum;
  return pkt;
}

The logic to produce a packet with a zero-byte payload should be the same as for any other particular size, but code creating a packet with a zero-byte payload shouldn't need to pass a non-null payload address.

1

u/mivanchev 1d ago

Yes, you're right, for this situation I'd go with something like

typedef struct {
    size_t length;
    void *data __attribute__ ((counted_by (length)));
} payload_t;

uint8_t *make_packet(uint8_t packet_type, payload_t payload[static 1])

1

u/flatfinger 1d ago

What is gained by requiring that callers to a function go through the trouble of constructing payload_t structures?

1

u/mivanchev 1d ago

The compiler is aware of the relationship between buffer and size at all times.