I see a lot about source codes being leaked and I’m wondering how it that you could make something like an exact replica of Super Mario Bros without the source code or how you can’t take the finished product and run it back through the compilation software?
4+4 is 8 But so is 6+2 And 7+1
You can’t guess which two numbers I started with knowing just the answer
Code is the same, just with much bigger numbers and more of them
I would say that it’s more like 4+4=8 but the original could have been (1+1+1+1)+(3+1) or (2+2)+(1+2+1) etc.
Basically it’s the same thing but if you really want to understand the code and modify it in any meaningful way you have to know how it was intended and not just the results.
My point being that decompiling does give you something similar to the original. It’s not just a guess that gives you random code with the correct result, but it could be very different from the source code.
The reason is that the compiler does a lot of things to make it more efficient but that just means that while 1+1+1+1 can be efficiently written as 4, there still is a good reason for 1+1+1+1 from a logical sense. For example, if you’re counting something, it would make sense to say 1+1+1+1. But if you’re looking at a specific value, maybe it makes more sense to just say 4.
The long answer involves a lot of technical jargon, but the short answer is that the compilation process turns high level source code into something that the machine can read, and that process usually drops a lot of unneeded data and does some low-level optimization to make things more efficient during actual processing.
One can use a decompiler to take that machine code and attempt to turn it back into something human readable, but will usually be missing data on variable names, function calls, comments, etc. and include compiler-added optimizations which makes it nearly impossible to reconstruct the original code
It’s sort of the code equivalent of putting a sentence into Google translate and then immediately translating it back to the original. You often end up with differences in word choice that give you a good general idea of intent, but it’s impossible to know exactly which words were in the original sentence.
Thank you, sorry to push further but my understanding is that computers deal with binary so every language is compiled to machine code, which I took as binary.
So if the language has elements being removed and the machine doesn’t need them shouldn’t you get back out exactly what is needed to do the task? Like if you compiled some code and then uncompiled it you would get the most efficient version of it because the computer took what it needed, discarded the rest and gave it back to you?
It depends on the specifics of how the language is compiled. I’ll use C# as an example since that’s what I’m currently working with, but the process is different between all of them.
C#, when compiled, actually gets compressed down to what is known as an intermediate language (MSIL for C# specifically). This intermediate file is basically a set of genericized instructions that are not linked to any specific CPU. This is useful because different CPUs require different instructions.
Then, when the program is run, a second compiler known as the JIT (just-in-time) compiler takes the intermediate commands and translates them into something directly relevant to the CPU being used.
When we decompile a C# dll, we’re really converting from the intermediate language (generic CPU-agnostic instructions) and translating it back into source code.
To your second point, you are correct that the decompiled version will be more efficient from a processing perspective, but that efficiency comes at the direct cost of being able to easily understand what is happening at a human level. :)
Could I trouble you to go deeper? I’m think I’m getting it but if we were to say uncompile GTA V or Super Mario Bros, could we make changes and figure it out from there or would it be complete nonsense with no way points to jump in at and get a grip on what is being done.
On a side note I was told once that everything is 1s and 0s and as a result that someone could type a picture of you if they got the order right. This could be why I’m so wrong in my understanding given I’m now assuming this was bullshit.
The main issue is that to make code human-readable, we include a lot of conventions that computers don’t need. We use specific formatting, name conventions, code structure, comments, etc. to help someone look at the code and understand its function.
Let’s say I write code, and I have a function named ‘findUserName’ that takes a variable ‘text’ and checks it against a global variable ‘userName’, to see if the user name is contained in the text, and returns ‘true’ if so. If I compile and decompile that, the result will be (for example) a function named ‘function_002’ that takes a variable ‘var_local_000’ and checks it against ‘var_global_115’. Also, my comments will be gone, and finding where the function was called from will be difficult. Yes, you could look at that code and figure out that it’s comparing the contents of two variables, but you wouldn’t know that var_global_115 is a username, so you’d have to go find where that variable was set and try to puzzle out where it was coming from, and follow that rabbit hole backwards until you eventually find a request for user input which you’d have to use context clues to determine the purpose of. You also wouldn’t have the context around what ‘var_local_000’ represented unless you found where the function was called, and followed a similar line backwards to find the origin of that variable.
It’s not that the code you get back from a decompiler is incorrect or inefficient, it’s that it’s very much not human-readable without a lot of extra investigatory work.
This might change now relatively fast, now that large language models can process code, you could give the function to LLM to rename the function. Iterating over the code and rename all functions and variables.
This won’t of course reproduce exact code, but it makes one really heavy part of reconstruction to human readable much lighter.
The implicit assumption with decompiling code is that the goal is either to inspect how the code works, or to try compiling for a different machine. I’ll try to explain why the latter is quite difficult.
As you said, compilation to machine code only keeps the details needed for the CPU to accomplish what was instructed. And indeed, that is supposed to be efficient to run on that CPU, by reason of being targeted exactly for that CPU. But when decompiling, the resulting code will reflect the specificity to that same CPU. If you then try to compile that code for a different CPU, it will likely work, but will likely be inefficient because the second CPU’s unique advantages won’t be leveraged.
To use an example, consider how someone might divide two large numbers. Person A learned long division in school, and so takes each number and breaks it down into a series of smaller multiplications and subtractions. Person B learned to do division using a calculator, which just involves entering the two numbers and requesting that they be divided.
Trying to do division by blindly giving Person B that series of multiplications and subtractions to do on the calculator is extremely inefficient because Person B knows how to do division easily. But Person B is following Person A’s methods, without knowing that the whole point of this exercise is to just divide the two original numbers. Compilation loses context and intent, which cannot be recovered from decompilation, for non-trivial programs.
Here is an example why source code is useful when it provides context: https://en.m.wikipedia.org/wiki/Fast_inverse_square_root#Overview_of_the_code . Very few people would be able to figure out how this works from just the machine code.
follow up, would it be easier to read this context-less source code or stay at assembly? If for example you’d like to modify a closed source app
if you compiled some code and then uncompiled it you would get the most efficient version of it … ?
Sorta, an optimizing compiler will always trim dead code which isn’t needed, but it will also do things that are more efficient but make the code harder to understand like unrolling loops. e.g. you might have some code that says “for numbers 1-100 call some function” the compiler can look at this and say “let’s just go ahead and insert 100 calls to that function with the specific number” so instead of a small loop you’ll see a big block of function calls almost the same.
Other optimizations will similarly obfuscate the original programmers intent, and thinks like assertions are meant to be optimized out in production code so those won’t appear in the de-compiled version of the sources.
I actually work on a C++ compiler… I think I should weigh in. The general consensus here that things are lossy is correct but perhaps non-obvious if you’re not familiar with the domain.
When you compile a program you’re taking the source, turning into a graph that represents every aspect of the program, and then generating some kind of IR that then gets turned into machine code.
You lose things like code comments because the machine doesn’t care about the comments right off the bat.
Then you lose local variable and function parameter names because the machine doesn’t care about those things.
Then you lose your class structure … because the machine really just cares about the total size of the thing it’s passing around. You can recover some of this information by looking at the functions but it’s not always going to be straight forward because not every constructor initializes everything and things like unions add further complexity … and not every memory allocation uses a constructor. You won’t get any names of any data members/fields though because … again the machine doesn’t care.
So what you’re left with is basically the mangled names of functions and what you can derive from how instructions access memory.
The mangled names normally tell you a lot, the namespace, the class (if any), and the argument count and types. Of course that’s not guaranteed either, it’s just because that’s how we come up with unique stable names for the various things in your program. It could function with a bunch of UUIDs if you setup a table on the compilers side to associate everything.
But wait! There’s more! The optimizer can do some really wild things in the name of speed… Including combining functions. Those constructors? Gone, now they’re just some more operations in the function bodies. That function you wrote to help improve readability of your code? Gone. That function you wrote to deduplicate code? Gone. That eloquent recursive logic you wrote? Gone, now it’s the moral equivalent of a giant mess of goto statements. That template code that makes use of dozens of instantiated functions? Those functions are gone now too; instead it’s all the instantiated logic puked out into one giant function. That piece of logic computing a value? Well the compiler figured out it’s always 27, so the logic to compute it? Gone.
Now all of that stuff doesn’t happen every time, particularly not all of those things are always possible optimizations or good optimizations … But you can see how incredibly difficult it is to reconstruct a program once it’s been compiled and gone through optimization. There’s a very low chance if you do reconstruct it, that it will look anything like what you started with.
Just wait until you see the crazy optimizers for embedded systems. They take the complete code of a system into consideration, and, in a number of compile passes, reuses code snippets from app, libraries, and OS layer to create one big tangled mess that is hard to follow even if you have the source code…
Isn’t that still the same exact process as a normal compiler except in the case of embedded systems your OS is like a couple kilobytes large and just compiled along with the rest of your code?
As in, are those “crazy optimizations” not just standard compiler techniques, except applied to the entire OS+applications?
The main difference is that when you compile a program for Windows, Linux etc., you have an operating system and kernel with their exposed functions/interfaces so even in a compiled program it’s pretty easy to find the function calls for opening a file, moving a window, etc. (as long as the developer doesn’t add specific steps hiding these calls). But in an embedded system, it’s one large mess without any interfaces apart from those directly on the hardware level.
In a way, yes. But it really creates a mess when the linker starts sharing code between your code of which you have sources, and then jumps in the middle of system code for which you don’t have sources. And a pain in the whatever to debug.
You can. It’s called decompiling. Problem is you lose all the human friendly metadata that was in the original source code, meaning comments, variable names, certain code structures are lost forever because it was deleted in the compilation process. There are tools to help you reintroduce that stuff by going through the variables and trying to make sense out of what they were for but it’s super tedious. With new ai tech that can certainly be improved with AI guessing what they were for but you’ll never get the original meta data back.
Also if the code was run through an optimizer (which all modern games should be) the code is even harder to make sense of as it doesn’t necessarily have the same structure and the same variables as the original code
This is also very similar to if they ran the source code through an obfuscation tool. Some people do this with chrome extensions. Since they need to give you the source code for it to work on your machine they just change the variables to a, b, c, d and route things though unneeded functions so you don’t know why anything is happening.