-
-
Notifications
You must be signed in to change notification settings - Fork 54
[Z80.c] fix cycle count overwriting due to += operator #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
I should note that this bug causes the emulator to hang in an infinite loop on |
|
Have you checked that this really happens or is it a theory? Because I have not managed to reproduce the behavior you describe. In addition, The following test checks whether the cycle count stays at zero on instructions with the #include <Z80.h>
#include <stdio.h>
#include <string.h>
static zuint8 memory[65536];
static zuint8 cpu_read(void *context, zuint16 address)
{return memory[address];}
static void cpu_write(void *context, zuint16 address, zuint8 value)
{memory[address] = value;}
int main(int argc, char **argv)
{
Z80 cpu;
zusize cycles;
cpu.context = nullptr;
cpu.fetch_opcode = cpu_read;
cpu.nop = cpu_read;
cpu.fetch = cpu_read;
cpu.read = cpu_read;
cpu.write = cpu_write;
cpu.in = cpu_read;
cpu.out = cpu_write;
cpu.halt = nullptr;
cpu.ld_i_a = nullptr;
cpu.ld_r_a = nullptr;
cpu.reti = nullptr;
cpu.retn = nullptr;
cpu.hook = nullptr;
cpu.illegal = nullptr;
memset(memory, 0, 65536);
memory[0] = 0xFD;
memory[1] = 0x00; // FD nop
memory[2] = 0xDD;
memory[3] = 0xFD; // DD FD (prefix sequence)
memory[4] = 0xDD;
memory[5] = 0x45; // ld b,ixl
z80_power(&cpu, true);
cycles = z80_run(&cpu, 1);
printf("process prefix -- PC: %04X, cycles: %lu\n", Z80_PC(cpu), (zulong)cycles);
cycles = z80_run(&cpu, 1);
printf("process opcode -- PC: %04X, cycles: %lu\n", Z80_PC(cpu), (zulong)cycles);
cycles = z80_run(&cpu, 1);
printf("process prefix -- PC: %04X, cycles: %lu\n", Z80_PC(cpu), (zulong)cycles);
cycles = z80_run(&cpu, 1);
printf("replace prefix -- PC: %04X, cycles: %lu\n", Z80_PC(cpu), (zulong)cycles);
cycles = z80_run(&cpu, 1);
printf("replace prefix -- PC: %04X, cycles: %lu\n", Z80_PC(cpu), (zulong)cycles);
cycles = z80_run(&cpu, 1);
printf("process opcode -- PC: %04X, cycles: %lu\n", Z80_PC(cpu), (zulong)cycles);
return 0;
}
|
|
Thanks for the PR Glenn, However, according to the standard, the left-hand side (LHS) is evaluated only once. Therefore, the right-hand side (RHS) MUST be evaluated and executed BEFORE the load/modify/store sequence of the LHS occurs. ISO/IEC 9899:19906.3.16.2 Compound assignmentSemanticsA compound assignment of the form E1 op= E2 differs from the simple assignment expression E1 = E1 op (E2) only in that the lvalue E1 is evaluated only once. ISO/IEC 9899:20116.5.16.2 Compound assignmentSemanticsA compound assignment of the form E1 op= E2 is equivalent to the simple assignment expression E1 = E1 op (E2), except that the lvalue E1 is evaluated only once, and with respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation. If E1 has an atomic type, compound assignment is a read-modify-write operation with I'm sorry, but I don't think we can approve this PR. Also, your fix introduces undefined behavior because makes the code dependent on the compiler evaluation order, as Sofía pointed out. However, if you find a compiler that generates the bug you have explained, we will accept this PR with the appropriate modifications that do not introduce UB (e.g., use a variable to store the value returned by the function and then add to |
|
Yes it's a real problem that blocked me and actually occurs in my environment. However, the environment in this case is far outside your supported platforms: I'm using MSVC in C++/CLI fully managed mode, which also unfortunately requires that the entire compilation unit be treated as C++. Although it's possible that the C vs. C++ language-standard is making a difference, it seems more likely that the much more dramatic compilation of the entire For example, the target bytecode for this compilation is the ECMA CIL synthetic instruction set, which is a fundamentally stack-based virtual machine. So its possible that the C++/CLI spec allows for the (fetched value of the) left-hand side to be pushed onto the execution stack before considering anything about the RHS. I can plainly see in the debugger that--as you note--because |
|
I don't think your understanding of the language specs pertains to this case. When they talk about the LHS being evaluated only once, I think they mean the computation of the address. I don't think the specs you quoted discuss the case I submitted above, where the interior contents of the fetched value, and not the fetched value itself, is explicitly mutated by the RHS. The idea of The same problem happens in C#: Code: After running this, the value of Here's another example: Clearly, the LHS is evaluated before the RHS. The result is that I recommend that you fix |
In this discussion, The "single access" feature of |
|
Actually, my What happened was that last week I was compiling the z80.c as a .NET assembly using so-called C++/CLI mixed-mode, where the compilation of your In that situation, I didn't have the Although mixed-mode is a great, albeit aging, legacy technology, it's often the case with that the constant managed-to-native (and back) transitions can be quite a penalty, especially for this emulator, since the But I was soon amazed to discover that putting the whole But meanwhile now it's super-duper fast compared to the mixed-mode, because there are no more managed/native transitions. Thanks. |
|
I have asked for opinions on this matter and have been discussing it. At least, since C++17, it is guaranteed that: https://en.cppreference.com/w/cpp/language/eval_order:
Referenceshttps://en.wikipedia.org/wiki/Sequence_point#cite_note-9 However, the C standard is somewhat ambiguous: is there a sequence point between function argument evaluation and function call, but not between compound assignment and function call? C90
C11
According to ChatGPT and DeepSeek, there is no UB: Case evaluated: typedef struct {int counter;} S;
int do_something(S *s)
{
s->counter += 1;
return 4;
}
int main(int argc, char **argv)
{
S s;
s.counter = 0;
s.counter += do_something(&s);
return 0;
}
OK, that explains a lot, because I haven't seen this problem with compound assignment in my almost 3 decades writing C code. So, I propose you something: Fix the PR to not introduce UB and change #ifdef Z80_WITH_FORCED_EVALUATION_ORDER
zusize step_cycles;
#endif
/* ... */
#ifdef Z80_WITH_FORCED_EVALUATION_ORDER
step_cycles = insn_table[DATA[0] = FETCH_OPCODE(PC)](self);
self->cycles += step_cycles;
#else
self->cycles += insn_table[DATA[0] = FETCH_OPCODE(PC)](self);
#endifAlso, modify I think we could accept a PR that does that. But if you are going to do so don't rush, take your time and check that everything is ok before asking for the review. |
I wrote C since 1988, so I'd say the case is quite obscure. Seems so unusual to compute on a field inside a function, by reference, when the computation is already pending externally on the same field? I think the reason we haven't run across it is because it's a strange thing to do. I don't think it's the best practice to depend on this sketchy area, regardless of the language specs, because even when it works as you intended, it is doing an extra fetch, addition, and store on the main memory of That is, unless you don't intend for both additions to happen, which I asked earlier. This is perhaps the more serious problem with the code as it currently stands... whether it works or not, it's not clear what is supposed to happen. Either mutate the structure completely, including |
|
This emulator was written in x86 assembly before being ported to C. Originally, It may not look elegant, but from a certain point of view, it is; The goal isn't uniformity in doing everything the same way, but rather strategically using the most efficient and effective method in each specific case. |
|
It occurred to me that the inconsistent treatment by |
All right. Try it and if it works, I'll modify #if !defined(__DOXYGEN__) && defined(Z80_WITH_VOLATILE_CYCLES)
volatile
#endif
zusize cycles; |







This is one example of code where the attempt to increase the cycle count is incorrect due to the use of the
+=operator. (There may be others)There are opcode calls such as
dd_prefixandfd_prefixwhich internally increase the cycle count. Any such changes will be discarded/ignored, because the outer+=operator has already captured the prior value ofself->cycleslocally, meaning the addition will be based on that earlier value, and will not include any increase of theself->cyclesvalue that occurred inside the opcode.Assuming that both the explicit opcode increase of the cycle count and the returned value from the opcode function are both meant to be added, the fix shown in this PR is one way to solve the problem.
Legal notice (do not delete)
Contributors are required to assign copyright to the original author of the project so that he retains full ownership. This ensures that other entities can use the software more easily, as they only need to deal with a single copyright holder. It also provides the original author with the assurance that he can make decisions in the future without needing to consult or obtain consent from all contributors.
By submitting this pull request (PR), you agree to the following: