I realized that, instead of a configuration file, I could configure the executable instead! So, any changes in configuration meant the editor would patch its own exe file!
This marvelous technique came to an end when attempts to stop malware got folded into the operating system.
They just chucked the old system for a portable version of it, but until this last release, they still had to option of doing it the old school way.
Lisp Machines though.. updating the operating system was by loading bunch of compiled files that replaced currently loaded functions in memory.
Then again, Lisp Machines where very proud of self modification — the CADR had a fun feature where it could modify the next instruction depending on things…
How the world has changed.
The description is a bit confusing because it can both dump only the warmed up interpreter image or the whole process, I think.
"With the program undump, you can use `core' to reconstitute a preloaded executable, which does not need to read a `.fmt' file to get started. Although preloaded executables save startup time, they have a big disadvantage: neither the disk space to store them nor their code segments (at runtime) can be shared. Therefore, if both tex and latex are running, twice as much memory will be consumed, to the general detriment of performance."
https://mirror.gutenberg-asso.fr/tex.loria.fr/texlive-htmldo...
The overhead of cache flushing means some old school techniques are no longer viable, like modifying a constant in the next instruction. However it is still interesting to write machine code snippets once and execute them many times, like the nested function trampolines. I had a case where I had RGB masks like R=0x00ff0000 etc (loaded at startup once) and wanted to convert 0x00rrggbb to match the mask (so no-op in the common case but not always) which could have involved setting the shift amounts in a series of shift instructions.
The Linux kernel uses self-modification to change branches depending on whether certain features are on. For example when a user-mode process starts tracing a certain function, it adds code to the beginning of that function to trace the call, otherwise it pads that space with a no-op. JIT compilers also make good use of knowing whether a class has any subclasses, which is statically unknowable in Java but dynamically knowable.
- Static calls: like a call to a global function pointer, except instead of loading a function pointer and doing an indirect call, the code is patched to do a direct call to the destination
- Static keys: like an if statement testing a global boolean, except instead of loading a boolean and doing a conditional branch, the code is patched to do either an unconditional branch or a nop
- Runtime constants: like a load of a global variable, except instead of loading, the value is patched directly into the code
- Alternatives: selects one of multiple possible instruction sequences depending on (usually) whether the CPU supports specific instructions
It's really fascinating to see the kind of fun efficient stuff you can do when you have that level of low-level control. Not just code patching but things like RCU as well.
> However it is still interesting to write machine code snippets once and execute them many times, like the nested function trampolines.
I slightly disagree on this though. In my experience writing code with Clang blocks (which don't use trampolines), they're often useful for code organization even if the callback will only be called once. Therefore, even ignoring security issues, I think GCC choosing a design that required cache flushing was a mistake - certainly in retrospect (as cache flushing has become more expensive over the years), but perhaps even at the time. I did some research, and trampolines were introduced in GCC 2.0, which already included mprotect calls and/or cache flushes on some of the architectures it supported, such as MIPS. However, this was a relatively new development, and on most of the supported architectures it didn't do either of those things. But on MIPS it would do an mprotect every single time a trampoline was created, which can't have been fast.
BTW you can do all of this cool stuff in user mode on Linux too (but not on OpenBSD) - you just have to opt in to executable stack and/or writable .text. I could have written the dynamic shift instruction generator I mentioned, but I didn't want to spend the effort, but I imagined having a language with actual support for something like that (like static keys for variables).
Your program is either front-end stalled by uop count or instruction cache latency, or back-end bound by memory bandwidth or ALU throughput or a serial dependency chain. It doesn't matter which is the bottleneck for this case, because inlining constants improves most of the above!
When the back end is the bottleneck, the front end stalls and vice versa, though I'm not sure how all processors report it.
There is also JIT (like in regexp engines), but it's different story.
> Only a limited number of indirect targets that cross a 64MB aligned boundary relative to the branch address can be tracked in the indirect target predictor. Software should limit the number of indirect branch targets that cross such a boundary.
And one way doing this is to replace the indirect branch with a direct branch, which supports a 32-bit signed displacement.
I remember once learning of a runtime environment that would inline class functions. For example they wrote an OS, and if you had an object of a SATA hard drive class, it would copy the function code and inline the drive ID. I don't remember how well it worked for them.
A related idea is the "tracing JIT". You know how you expect a JIT to translate one function at a time? A tracing JIT doesn't - it follows the program logic wherever it goes, through whatever control flow, and compiles all of it until it decides to stop. The most well known implementation is probably LuaJIT.
That trampoline needs to live somewhere. Since the function is inherently noncallable after the stack returns, and C programmers hate it when their compiler sneaks in extra malloc calls under the hood, the compiler decides to stick the trampoline on the stack instead of heap-allocating it.
But now the stack needs to be executable.
D has the notion of a "delegate", which is a (function pointer) and (context pointer) pair. This is incredibly useful, because delegates can:
1. call nested functions that need a pointer to the stack frame of the nestee function
2. call member functions that need `this` pointer
3. call lambdas
4. call COM member functions
The neato thing about this is the ABI for delegates is all the same, so a function that gets a delegate parameter will work with any of 1..4. It's one of the most used features of D.
But because ISA was mentioned, x86 does indeed even have native support for this: https://devblogs.microsoft.com/oldnewthing/20231211-00/?p=10... These instructions are not too useful though and I do not think anybody uses them.
Supporting closures, more or less, requires design decisions that are equivalent to choosing a specific layout for objects in an object-oriented language. C, as it is, makes none of these assumptions and you can translate a lot of different language ABIs into some C code (that may be clumsy). Keeping the abstraction that function pointers = pointers to entry points for functions, well, that’s frustrating for C programmers writing C programs, but extremely useful for interoperability.