r/computerscience 2d ago

General Does Replacing Instructions that Do Nothing with NOPs Change Anything?

Suppose in the executable, instructions N to N+6 do some load instructions from memory, then N+7 to N+30 are some integer operations like add, subtract, multiply, bitwise operations and so on. the results are not stored to memory. Then Instruction N+31 to N+37 loads new values in the registers and none of the results of the previous calculations do anything. A different version of the executable has instructions N to N+30 all be "NOP" and N+31 to N+37 are the same as before. Does anything change? My inspiration for this is the game Mario 64 had been dissected and if you look at the assembly of the NTCS version, there are plenty of instructions that do some calculations that aren't used and some weird things like loading from the same address to the same register when the register's value could not possibly be changed. At the same time these instructions couldn't outright be removed or the alignment of all the JMPs would be thrown off. My friend said perhaps some NOPs might be done with a lower power consumption than some of the multiplication instructions, but otherwise if the later instructions don't use the results of the extraneous instructions and are unaffected by their flags nothing should change.

9 Upvotes

13 comments sorted by

21

u/nuclear_splines PhD, Data Science 2d ago

Well, it depends on what you mean by "change anything".

  • If you perform a calculation and throw the results away, that should be equivalent to not performing the calculation in abstract logic land

  • If you read a value from memory it may be placed in cache, or could be kept around in cache for longer, even if the value isn't used. This could incidentally change the performance of future instructions

  • Depending on the hardware architecture, some instructions may take much longer to run than others. If the rest of the program is timing-sensitive, replacing 'unused' instructions with NOPs could upset that timing

  • Changing the instructions will of course change the bytes of the executable and its hash, which could theoretically impact behavior (such as copy protection checking whether the program has been altered)

1

u/ShadowGuyinRealLife 2d ago

Thanks for answering. I guess I wasn't too specific

*If you perform a calculation and throw the results away, that should be equivalent to not performing the calculation in abstract logic land

This is what I meant by "do nothing" instructions, just preforming calculations that are thrown away.

*Depending on the hardware architecture, some instructions may take much longer to run than others. If the rest of the program is timing-sensitive, replacing 'unused' instructions with NOPs could upset that timing

Thanks. I was sort of looking for side effects like this. The only thing my friend could think of was power consumption. And I was like "hmmm... surely depending on the hardware there are others right?"

*Changing the instructions will of course change the bytes of the executable and its hash, which could theoretically impact behavior (such as copy protection checking whether the program has been altered)

In this case the instructions of the executable are being both used as instructions, and as data for something that reads it (in this case something that tries to read the bytes of the executable and its hash). The thought in my head was parts of the executable acting just as instructions. So you're right anything that reads the executable as data will be altered.

Since I was mainly looking for side effects while executing the calculations themselves, if I knew of this possibility but not of the others, I probably would have asked "what if the calculations that are thrown out are replaced by NOPs and anything that reads the instructions in the executable as data altered to take in consideration the alteration while keeping the executable the same size." But as asked, you did have an answer so thank you.

I just noticed Reddit does very weird things with quotes.

6

u/nuclear_splines PhD, Data Science 2d ago

what if the calculations that are thrown out are replaced by NOPs and anything that reads the instructions in the executable as data altered to take in consideration the alteration while keeping the executable the same size.

As an aside, this is exactly what malware analysts and pirates do. Malware often hashes regions of its own text segment to see whether they've been altered (such as with software breakpoints or instructions NOPed out) and changes its behavior when under observation. So the analyst needs to identify those checks and NOP them out, too. Likewise, commercial software may check for alterations as part of copy protection or anti-cheat, so the pirates play a cat and mouse game identifying those checks and rewriting them.

2

u/Jonny0Than 23h ago

Btw, the cache aspect of this is exactly how the spectre attack works. The instructions don’t even need to be executed in order to affect the cache.  I don’t know anything about the N64 CPU so I can’t say if that would be relevant here.

The question reminded me of this article:  https://randomascii.wordpress.com/2018/01/07/finding-a-cpu-design-bug-in-the-xbox-360/

2

u/ShadowGuyinRealLife 21h ago

I mean that's not too relevant when the N64 was designed to run one process... the game you put in.

That article is interesting. Reminds me of an earlier question I asked How Are Split Caches Handled with Thread Coordination? I find it strange that the xdcbt instruction is so bad. If the problem was just that it was sometimes unsafe then the solution would be to just be careful when prefetching, but the fact that a speculative executed xdcbt can cause crashes makes me wonder why they even bothered to implement it.

So if I'm understanding this right, using xdcbt is bad but even hypothetically using the flawed instruction and then throwing away the results without doing anything with them would also cause problems.

-2

u/SummitYourSister 1d ago

So you are asking about a class of instructions that both have no side effects and have side effects. Nonsense.

0

u/pconrad0 1d ago edited 1d ago

And... If the program has "undefined behavior" like a pointer referencing a place on the stack that represents an out of scope variable, like indexing into an array in C beyond the length of that array, then depending on how far the impacts go, and how much memory protection there is on the hardware ...

At some point, anything could happen.

Now if the program always had this "undefined behavior" you might argue it was never correct in the first place.

But, for bugs that are not exactly "heisenbugs", but something closely related, the bug might have predictably never happened before the behavior change.

Then, the contents of the stack get subtly rearranged as a result of downstream effects of caching changes, etc... suddenly the bug always happens.

In principle, a program where replacing "meaningless" instructions with NOPs changes the behavior is, and always has been, an "incorrect program".

But in the real world, there are lots of businesses and organizations relying, in mission critical ways, on programs that are "technically incorrect", but "correct just enough", that they are getting the job done without anyone realizing.

So do this at your own risk.

Edited to add: multi core architectures with shared memory, i.e. threads on different cores ... make this whole mess an order of magnitude more complicated.

The point is that there is a lot of daylight between the abstract model of computation, and the physical model of computation, plus a bleep ton of undefined behavior in C code.

That leaves a lot of room for things to go awry if the programmer, through neglect or malice (malware) steps into this territory and starts branching and updating state based on "undefined" values.

3

u/xcookiekiller 2d ago

I don't know if this is relevant for this exact case but I think it's worth looking into the topic of memory mapped I/O, it kinda breaks the assumption of no side effects of a read/write when the value isn't used again

3

u/InjAnnuity_1 1d ago

As mentioned elsewhere, reads (and writes) to specific hardware addresses may have side effects outside of the CPU and memory, i.e., to other specific hardware such as audio, video, or other memory-mapped devices.

Even if the results are thrown away, the instructions may have been chosen to run for a specific length of time. For example, if a function has two execution paths, it may help other hardware-specific code (or help the programmer!) if the execution time is guaranteed to be identical either way.

This can help with some old video hardware, especially if it shared memory with the main CPU. You wanted to make sure that video memory accesses were timed so that they didn't garble the image.

Radio Shack Model I was just such a machine.

2

u/esaule 2d ago

Hard to tell without seeing the exact code. It is possible that these instruction create side effects in control registers. Things like settting divide by zero, or pushing something on an accumulator implicitely.

1

u/binarycow 2d ago

NOPs often have different cycle timing.

1

u/soundman32 1d ago

Back in the c64 days, you could ask the video chip to interrupt the cpu when the display scan line hit a certain line onscreen. If you wanted to change the colour of the border half way down the screen, you could configure the interrupt and write to the border colour register and return.  The problem was that the interrupt took enough microseconds that the colour change happened part of the way through the left border, so you got a step.  The trick was to nop enough times (could be 2 or 3 i cant remember) that you delayed the processor until it was inside the border and then did the switch.  So, the right border was changed on the correct line 125 and the left border was changed on the line after and nobody noticed.  (Then you reset the colour back at line zero). 

1

u/Guvante 3h ago

NOPs serve a couple vital purposes. The biggest one is alignment, there are a ton of reasons you want an instruction to have a particular alignment and NOPs are the gold standard.

Note that NOP tends to refer to a collection of instructions that do nothing like multiply a register by 1.

Useless instructions is more complicated especially with an old game for a unique architecture.

Sometimes the compiler messes up for a bunch of complex reasons. For instance if you have a calculation and then branch on the result but the compiler realizes the branch isn't needed so elides it but doesn't (for a ton of potential reasons) remove the calculation.

Another factor can be helpers used to simplify programming that accidentally introduce extraneous work. The compiler can't always fix those up.

A final note these kinds of issues are really hard to fix because as you noted any change wrecks everything else in the program and more importantly these little mistakes aren't consistent so if you change anything else they might change or go away.