On ELF, Part 2 - 29 minutes read
The Executable and Linkable Format, ELF, is too complex. I firmly feel it was created without critically thinking about the ramifications it would have on future tool chains. In this article, I show how ELF-like features can be safely retrofitted onto executable formats contemporary with ELF’s debut. Hopefully, future executable format authors will reconsider their needs more critically in the future before committing to something as complicated as ELF.
Before writing this installment, I had a realization. One of the big mistakes from Unix, besides the X Window System, was the linker. At the time, it must have seemed like a great idea! Put yourself in the original authors’ shoes: why should the kernel spend so much effort relocating a binary when that image will just appear at the same place in every process? So, why not just relocate any linked executable at that point before the kernel ever gets a chance to see it? The kernel can be made much simpler (just read in an opaque binary blob, then call a specified address), and as a nice side-effect, it’s faster to load the programs as well, which was probably noticeable on timesharing systems of that era.
But, as we’ve seen in the previous article, this can open a can of worms from the security point of view as well as constrain new features, such as the new hotness at about this time, component-oriented programming (what eventually led to CORBA and DCOM). It turns out that program relocation at load-time is a good thing.1 The design of ELF, in part, reflects the realization that mistakes were made in the past, and AT&T wanted to both move forward with new features while preserving at least some of their initial investment. (Aside: I, personally, have mixed feelings about this; but, this article is not about those specific feelings.)
So, why is such a big mistake? Bluntly, it’s a tool which, like ELF, fails to adhere to the Unix philosophy of doing one thing well. See, is responsible for at least two services:
While these two concepts are clearly related, they are most definitely not the same thing. We see that the Plan 9 compiler toolchain partially remedies this past oversight by keeping software in a binary representation of an assembly language listing for as long as possible before the very last step of producing the final binary executable.
Basically, there are two principles involved.
Principle of Linking. A linker’s job is to coalesce, merge, and perhaps even sort sections of equivalent type. If it helps you, think of it as a merge-sort for related code. A typical program might have hundreds of sections, which a linker might reduce to a smaller, perhaps still quite numerous, quantity. Ultimately, by the time you yield a final executable, it’s been massaged down to a much smaller set (typically, text, data, and BSS).
Principle of Loading. A loader’s job is to unmarshall the concrete program representation in memory from an off-line representation. This prepares a program for execution. In the case of dynamic linking, a loader may perform some basic relocations, but it never merges sections of comparable types. It just lays them out in the address space somehow. This can be seen on any Linux installation by looking at a process’ memory map layout ( , which will show each dynamically loaded module’s text, data, BSS, and stack segments). I’m sure BSD and Plan 9 have similarly accessible methods of showing this information.
Where goes wrong, then, is that it attempts to both link and load in one step. The limitations of this approach do not become visible as long as
Violate any of these assumptions, and ’s approach breaks down hard. This is why requires so many horrible-looking arguments when linking code together: one set of options intends to put a choke-hold on from building an executable or shared object when all you want is a relocatable module, while another set is often to do just the opposite.
I mention this only because I want this article to focus exclusively on program loading. That is, nothing I write in this article is intended to relate to linking at all. The formats below may or may not make suitable targets for linkers. In fact, there’s a great chance that they won’t at all. Plan 9 accepts this. There’s no reason why the progeny of Unix can’t either.
So, with the full understanding that we’re talking exclusively about loading programs, let us now tear ELF a new hole in its address space by illustrating viable alternatives to it.
In the previous article, I had stated that the format was one of the earliest and most portable of executable formats around. To illustrate this point, let’s compare Unix’s original format to an OS where you might not have expected it to show up: Atari TOS and GEMDOS, the two components that make up the Atari ST/TT’s operating system.
After the file header, you’ll find the actual executable artifacts:
That’s it: there are no tables containing pointers to segments, there are no complex graphs to traverse to get at some piece of information, there exists no reason to invest more than a handful of printed pages to describe the entire structure. Despite this minimalism, the format is surprisingly versatile. It supported TOS applications in a single address space, it supported MultiTOS applications in a single address space, and it later supported MultiTOS applications in separate address spaces as well. Clearly, we see that is quite adept at handling both DOS-like and Unix-like environments with equal facility.
Note that some details differ between the two headers. That’s perfectly OK. Exact layout doesn’t matter, what matters are the concepts supported. It gives the loader just enough information to allocate a chunk of memory for code, a chunk of memory for data and BSS, to locate the relevant data in the files and load them into the allocated memory, and then apply fixups to resolve broken references. The binaries were self-contained and self-consistent otherwise: it was not possible to legally create an file which referred to an undefined symbol. Intra-segment relocations (for instance, where a symbol in module A’s segment refers to a symbol in the segment in module B) are resolved by the linker prior to emitting the final , often relying upon PC-relative or register-relative addressing. Absolute references (e.g., with JSR or JMP instructions, or when storage in was referenced from a location in ) were resolved using the bundled fixups. The reason for this is that there was little to no guarantee that and would remain adjacent in the computer’s address space, particularly for single-address space environments.
In my previous article, I’d stated that was intended to be loaded/mapped blindly as an opaque blob. This is especially evident in the original Unix header, as the “magic” field, used to identify executables from other file types, is literally a PDP-11 machine language instruction that jumps over the header. The Atari TOS program header continues this tradition.
OK, so now that we’ve seen how trivial is, how do we extend it to support dynamic linking? Pretend, for a moment, that you were in charge of adding dynamic linking to MultiTOS for its next release. How would you retrofit this loader format to do so? Here is how I would handle the task.
The first task would be to identify what additional support is required to handle the job.
First and foremost, a statically linked binary is expected to be self-contained. Dynamic linking throws that assumption out the door; therefore, we need a way for a module to identify its dependencies. So, we need to introduce a segment that lists library dependencies. Let’s not concern ourselves with precise layout at this point; we only need to know that it consumes space in the file, and thus, has a size field associated with it.
Second, we observe that dynamically linked modules may require symbols defined in the executable. It turns out has us covered here, as both PDP-11 and TOS versions of the file support a symbol table explicitly. Convenient! So out-bound definitions are taken care of; however, in-bound relocations are not. There are two ways to handle this: first, we can extend the existing symbol table definition to support both symbol definitions and symbol references, or we can introduce a separate symbol table segment just for in-bound references. Just to make things harder and for the sake of illustration, we’ll assume the latter. In the real-world, I’d probably shoot for the former.
To support in-bound symbol relocations, we will also need to extend our relocation records. Atari TOS uses a very simplistic model: every N bytes, add the base address of the appropriate segment to the 32-bit word at that current location. Again, the assumption is that references are so this is fine if all you’re doing is relocating a microcosm of code. For dynamic linking, however, we need to know not only where to perform the relocations, but also against what symbol. For this reason, I would replace individual bytes in the relocation segment with 32-bit words, with a layout along these lines:
This makes for a larger executable file, but is also substantially more flexible. The rules are as follows:
In both cases, we know if we’re making a code or data segment reference based both on the value of the referenced symbol, and on the relative offset we’re patching in the file.
So far as I’m aware, this is all that is required to make the format support dynamic linking. Now let’s lay this stuff out into something that makes the task of loading it easy.
Since the initial is a 68000 instruction that skips over the length of the program header, it follows that if we append our extra fields onto the program header, then we must adjust the branch offset as well. This implies we create a new magic cookie for which the loader can use to determine if the binary is dynamically linked or not.
Just in case some other file type uses this same magic value, we’re going to use an unused bit to identify a dynamically linked artifact. Let’s call it .
If is correct yet is not set, then we either have a corrupt program header, or the file is not actually a dynamically linked executable. Thus, we reject the file.
The following fields can appear immediately after the field:
Observe that loading the module’s code and data segments is meaningless if we can determine ahead of time that we cannot satisfy this module’s dependencies. For this reason, we need only insert our list of dependencies and symbol imports, in that order, ahead of the code segment. In this way, you can still compute the offsets of everything without having to provide explicit pointers. This keeps the size of the header down. We also take care to order the segments we’ll need in the order we’ll use them.
By doing things in this order, we minimize unnecessary seeking through the file.
Here, we get to the basic algorithm of loading and relocating a dynamic executable using this format.
You’ll notice a reference to a global context. This is some arbitrary data structure that serves as a global record-keeping device keeping track of resources allocated and/or files opened. In Unix, this would be the process itself. For something like TOS, this structure would need to exist separately. Either way, it’s required if we want to properly track resources in the event we return with an error. Otherwise, resources will be allocated but unable to be freed, since we wouldn’t have a long-standing reference to them. The context also serves as a rendezvous point for symbols, where they’re defined, and what value they equate to.
First, we start out at the top level: when loading a PRG, we dispatch to the appropriate loader based on values.
The legacy loader, as defined in GEMDOS, can be used to load the statically-linked executable, so I won’t repeat its pseudo-code here. Instead, let’s focus on the dynamic loader.
This pseudo-code is more or less representative of the actions that must be taken for someone to extend GEM’s PRG format to include dynamic linking abilities. Actual code is straight-forward, the code follows the layout of the data as found in the executable file, and should be easily maintained as future requirements dictate.
This pseudo-code does not handle the case where a shared library is already resident in memory. However, with a small change to the calling convention of the procedure, it should be possible to short-circuit the loading process for resident libraries.
The pseudo-code above assumes the same 680x0 processor family that drives the Atari ST/TT line. As written, it won’t scale to newer architectures. Many RISC processors requires multiple kinds of relocations to resolve references, since instructions rarely can pack a full word’s worth of bits in an instruction. For example, a RISC-V processor can only address +/- 2KiB from some base address. For a 32-bit address, then, you need to break the reference up across two instructions: a LUI instruction to load the upper 20 bits of the address, and an addition to contribute the lower 12 bits. Worse still, the addition is sign-extended, which must be accounted for in the upper portion of the address.
A 64-bit RISC-V implementation can’t even use this approach, for there is no way to embed the upper 32-bits of an address directly into the instruction stream. You end up loading addresses indirectly from a code or data word of memory, and that is what receives the relocation. Alternatively, if the value has lots of binary 0s in it, you can load a portion of the address in the low-order bits and then shift up appropriately. As you can see, resolving a 64-bit address on RISC-V can potentially be messy.
Assuming a 64-bit RISC-V target, the loader will need to understand how to perform a minimum of four different kinds of relocations:
This can be encoded easily enough in the relocation segment of the file. You just need to make the relocation record format known to the loader somehow, either implicitly through the magic cookie, or via additional header fields.
Some languages, like Modula-2 and C++, often require objects to be pre-initialized via “constructors” before the main program begins. In the case of Modula-2 and Oberon, the “constructor” is the global body of a module definition. In C++, D, Sather, et. al., these are provided through more explicit programming constructs on classes. Either way, they must be invoked for the main program to run correctly. After all, the executable might be written in plain-vanilla C, while the library you’re depending upon could very well be written in C++!
In a statically linked executable, this is not a problem, since the linker is given libraries which are capable of handling this issue. It has compile-time (at best) or link-time (at worst) knowledge of which routines to call and when. Thus, it can provide supporting code in the segment, and the day is saved.
For dynamically linked executables, however, it’s not as simple. Since the linker and loader both have no idea what languages modules are written in, the loader must support a means of calling constructors independently of the main program’s entry point. The ELF , , and related sections all exist to support languages which require global constructors to be called before invoking the main program.
We map this to the file by requiring initializers and finalizers to exist in the segment. Further, instead of providing a single routine to call, we express requirements as a vector of routines to call. These can be handled by extending the header once more with the following fields:
From the loader’s point of view, there’s nothing special about these vectors. The linker must provide zero or more such vector entries for each module it helps produce. The loader, then, must take on the responsibility of invoking them as it loads module dependencies.
First, I want to get this off my chest. There is no such thing as the Amiga Hunk Format. It’s just … Hunk Format. Remember, AmigaDOS is just a port of the Tripos operating system to run under the kernel. You can find more about Commodore-Amiga’s rebranding of Tripos at this Page Table article. I highly recommend it; it’s actually quite fascinating.
Now that that’s cleared up, let’s talk about hunks. Big hunks, heavy hunks, small hunks, hot hunks, however you prefer them to be. A hunk is nothing more than a self-identifying, often explicitly length-delimited, array of integers. Like so:
In the Amiga’s case, these words are always, always, 32-bits wide. That’s because (a.k.a. Tripos) was written in a 32-bit dialect of BCPL. In STS’s case, these words were always 16-bits wide. For a 64-bit RISC-V port, they could well be 64-bits wide. I think you get the idea.
A hunk formatted file, then, is just a file with an array of hunks in it. That’s it. You now understand the structure of a hunk formatted file.
You’ll notice that there’s no mention of text, data, or BSS segments. No relocations. No symbol definitions or imports. That’s because hunk format is a container format, just as MP3 is a container for audio, MP4 for different kinds of video, etc.
Next step, then, is to learn how and Tripos actually used it to store executable artifacts.
The Hunk Format executable is naturally multi-processor capable. Metacomco defined it for the Motorola 68000 processor originally, the details of which you can read in the Amiga Binary File Specification. Since then, however, the Amiga community moved to support the PowerPC processor architecture. Although modern versions of AmigaOS support ELF, originally, they simply adopted the Hunk Format to include PowerPC-specific hunks.
So, right out of the gate, we plainly see hunk format supports multiple processors. The fact that I used the basic concepts in a 16-bit OS of my own design also illustrates that hunk format is not constrained to just AmigaOS. So multi-processor and platform independence have never been unique selling points for ELF. And we haven’t even begun to look at the details of hunk format yet.
For the purposes of this article, however, I will once again concentrate just on the 68000 version of the file.
The large-scale structure of an Amiga executable looks more or less like this:
The layout of a hunk file is intended to be processed with an “event-driven” parser, which looks more or less like this skeleton pseudo-code:
As you can see, the very structure of a hunk file is intended to simplify the loader a great deal.
hunks identify the file as an executable as well as gives due warning to the loader on the number of segments to allocate and how big they are. But, hark, look what else it provides!
This hunk includes the names of shared library dependencies. The loader is responsible for opening (and loading if not already in memory) each specified library prior to loading the rest of the binary. In this way, transitive dependencies are satisfied.
Assuming this has been done successfully, the next step is to allocate an array of segment descriptors, whose size is specified by the hunk table size field. It then copies the libraries’ hunk descriptors into this image’s descriptor table. From the loader’s perspective, all your shared libraries have been included in this image’s hunk table as if the contents of those libraries came from this executable file. This greatly eases relocating hunks without having to traverse a complex graph of dependencies.
Once all the libraries have been opened and their segment tables merged into this executable’s segment table, then the fun part happens: the loader reads the remainder of the hunk to get at the list of hunk sizes. This gives the loader an opportunity to pre-allocate all the segments it needs to hold the binary in memory. These segments will take their place sequentially in the hunk table starting at the index, and continuing until the index, inclusive.
So, if we have a clock executable that includes (for sake of argument) and , then the loader’s segment table could well look a lot like this:
Gosh, this looks an awful lot like the information made accessible in Linux files, wouldn’t you agree?
The actual will look vaguely like this in the file:
After processing the header, the loader now has a complete description of a program image in memory. It just doesn’t have any of the specific details hammered out yet. For that, we need to load our segments with data.
hunks are for providing what would be considered or content in other formats. This is where your executable … well, code goes. Similarly, hunks provide statically allocated/initialized data that would fall in a section in other formats.
A BSS segment is specified in a similar manner; however, since its content is implicitly undefined, we only need to specify how big it is.2
Note that, just like ELF but unlike , you can have any number of code, data, or BSS segments. This was useful for earlier versions of AmigaOS, which did not automatically defragment free chunks of memory. As a result, there was an incentive to have many smaller code, data, or BSS segments as a means of working around the memory fragmentation that would inevitably build up like a layer of mold. You had to be careful though; too many small segments would actually exacerbate the problem, especially after you ran and quit an executable linked in this manner several times on a 256KB or 512KB Amiga! (Thankfully, I believe this issue was fixed with AmigaOS 2.0 or 3.0, so the incentive to coalesce like-typed sections became much stronger.)
Anyway, back to the hot hunks.
The very first code, data, or BSS hunk that the loader encounters in the file will obviously be used to load the very first hunk that is defined to belong to this file. Recall in the header that this was specified by the field. Each subsequent code or data hunk would fill subsequent hunk in the hunk table. Any code or data hunks that would exceed the table would be ignored.
Relocation hunks come in many varieties. I’m only going to illustrate one: .3 As with the GEMDOS file format, relocations are to 32-bit absolute addresses only. By the time you’re loading something into memory, all PC-relative or base address-relative offsets should have been resolved by the linker first. There also is a for PowerPC branch relocations as well.
The hunk looks like this on disk:
Basically, this is saying:
This happens until we reach the end of the list of relocations, identified by the final 0 value.
Remember that relocations always apply to the most recently loaded segment. This is why relocations for a segment always follows the segment to which it applies. You can have any number of different types of relocations following a code or data hunk.4
Also note that it’s perfectly OK to refer to a segment which hasn’t been loaded yet. Recall that while processing the , the loader knows how big each segment will be and can thus pre-allocate them. Obviously, you can also refer to segments which belong to shared libraries.
We’ve seen that Hunk Format executables share just about every major feature with ELF out of the box. It’s fully capable of supporting ASLR, and there’s nothing which prevents it from being used in either single- or multiple-address spaces. The only thing it doesn’t support is statically linked executables pre-designated for a specific load address. Further, it supports dynamic linking.
Just not the way AT&T specified with ELF.
See, there’s a frailty with how Tripos handles shared objects: the executable needs to know at link-time where the library’s assets are located (which segment and the offset within that segment). On its own, this places a hard dependency on a specific version of a library. Change the library to a newer version, and those assumptions could well break.
For this reason, AmigaOS does not support Tripos-style shared libraries. Instead, it uses its own library system which provides, which addresses this exact issue by using well-defined offsets on a jump table, itself relative to a kind of handle to the library. This lets specific addresses of various assets float within their respective binaries, with only the relative offsets being well-known at compile-time.
Exec’s jump table is exactly like ELF’s PLT (Procedure Linkage Table) segment, only instead of your program constructing it, it’s part of the library you’re trying to call. Any publicly available data is similarly made available relative to this library base pointer. This has several advantages over ELF:
Of course, it comes at a cost: you can’t just use to resolve a symbol on a library.
You can definitely get some great milage out of Tripos’ approach to shared libraries, despite the aforementioned limitations. Tripos, circa 1981, was successfully using them to make command-line tools as small as 512 bytes, while comparable tools in Linux, even with ELF’s shared objects, require closer to 20KB. Even accounting for binary size differences of 16-bit and 32-bit platforms, we’re still looking at an order of magnitude smaller executables for Tripos than we are seeing for Linux. Arguably, Tripos wins at shared libraries.
But, OK, we want the ability to use C++, and we want unique data segments per process. How do we get these?
Relieve the linker of detailed knowledge of dependencies. This is not a prerequisite for normal, day to day use of Tripos shared libraries. But, it is definitely a requirement if you want symbols defined in the executable to be usable by those libraries. There are two ways of resolving this issue. Either introduce a replacement hunk type for that implies the new semantics, or specify the use of negative hunk indices as a flag to the loader. For instance, you can declare bit 31 of field to be set for “module-relative” hunk indices or some such. That way, the loader can reference library hunks when that bit is clear, and module-relative hunks when set. This would apply to all hunk indices, including inside relocations. The probability of exceed two billion hunks is asymptotically zero on 32-bit systems, and particularly so on 64-bit systems, so this is a pretty safe extension.
Retain symbol imports and exports from the linker stage. This is a requirement that’s been discussed above under the GEMDOS section; it’s required if you want to allow libraries to bind to symbols you create in your executable.5 The loader of a module would need, then, not only to maintain hunk tables, but also symbol tables. This would enable, in turn, the next item.
Allocate new data segments on each successful load. Since libraries and executables are statically linked in Tripos, code hunks can freely reference data hunks, thus eliminating completely the need for GOTs. This implies that Tripos libraries (as with Exec-style libraries) maintain a shared body of data across all clients of the library. ELF mandates this cannot happen. By definition, each new program is in its own address space, so each library shares only its code; and at that, this code can appear anywhere in the process’ address space. All data hunks must be remapped for each address space, uniquely. To enable this to happen, the loader must recreate data and BSS hunks for each client use-case.
Page aligned code and data segments. This is not a hard requirement; don’t let anyone tell you otherwise. Even given an executable with non-aligned segments, you can still them into memory. You need only give special consideration to the leading and trailing pages, which may have to be loaded by copying from the file if their boundaries aren’t page aligned. Given a 4KB page size, this means that you end up transferring no more than 8KB of content. For larger binaries (which are everywhere on my Linux box, by the way), the overhead of this would be immeasurable, as the overwhelming bulk of binary content would be memory-mapped as normal. However, if this is such an important feature to have, then one could introduce two new hunk types:
The hunk allows for coarse-grained alignment control within the executable, while provides alignment control at individual word granularity.
Support for initializers and finalizers. This can be supported fairly easily using two new hunk types: and . As with my GEMDOS modifications above, these would contain vectors of initialization functions to call. Unlike GEMDOS, however, these would not occupy any code or data space. Rather, the loader would read in a set of hunk and offset pairs, intended to point at initialization and/or finalization functions, and call them sequentially in the order provided.
So far as I am aware, this is all that is required for Hunk Format to attain parity with ELF. Arguably, it is simpler to get Hunk Format working like ELF than it is for GEMDOS, particularly since the whole arrangement is built to be extensible via an event-driven loader.
As with the GEMDOS loader, these new semantics can be selected by altering which kind of header hunk appears at the head of the file. If you want the older-style, Tripos semantics, use . For the newer, ELF-like semantics, use (just inventing a name here) (for multiple address space support).
Though, honestly, considering Tripos libraries overwhelmingly superior memory savings over ELF-style, I’m not sure why you’d ever want to.
I’ve shown how one can build a dynamic loader for the format. It involves adding a few missing fields and pseudo-segments to the file, but it is not impossible, and it’s patently much simpler than ELF’s current file structure.
I’ve shown how Hunk Format executables already implements the vast majority of ELF’s more salient features. It should be noted that Hunk Format predates ELF by almost two decades. It is possible to retrofit ELF-like behavioral patterns onto Hunk Format, but it’s not clear to me that this delivers a technical win. The memory savings one gets with a Tripos-style shared library is rather significant, even after accounting for instruction set density differences between processors contemporary in 1978, 1988, and 1998.
The one thing I would consider unconditionally retrofitting onto Hunk Format loaders, though, is the use of negative indices to the hunk table to refer to module-local hunks. It will make life easier for anyone looking to independently upgrade libraries from their clients. Knowing that your module’s hunks starts at offset 6 in the hunk table (e.g., as in our clock example above) seems klunky to me, and to an extent breaches the encapsulation the loader’s intended to provide. I’ll need to research this further some day.
As well, I would continue to provide mezzanine documentation on conventions to help ameliorate versioning-related incompatibilities. This documentation would include, for instance, how to discover where a library’s jump table resides, etc. However, these things are complimentary to Tripos-style libraries, and do not replace them.
Personally, I think Unix System V Release 4 would have been a lot better off if they’d built on top of Hunk Format executables instead of relying on ELF. There’s definite memory-savings potential from not having everything and their grandmother embedding a symbol table, the lack of a (or at least a smaller) GOT means programs run faster since fewer indirection is happening all the time, and, allowing a library to keep shared state across all its clients is a really useful thing to have, enabling libraries to offer first-class, system-wide services. While not intending to serve as a complete replacement, there would be reduced pressure for the use of kernel modules, which are platform-specific and often with strange interface requirements, to serve these needs. This, in turn, would lead to a more modular and robust user-land experience without sacrificing the architectural benefits that a monolithic kernel can provide.
Anyway, there you have it: not just one; but, two technologies which existed at the time ELF was invented, which with a little bit of thought could have easily subsumed all the use-cases of ELF, and which didn’t require 186 pages of documentation to describe poorly. It’s regrettable that neither of these technologies had the weight of AT&T backing them.6
1: For a quite compelling argument against this point of view, please read this collection of quotes against dynamic linking in general, and a uniquely ELF-related concept of versioned symbols.
2: It is interesting that we must give a hunk size for , even though nothing actually follows it. The authors of Tripos’ loader could have just as easily declared to just be a null hunk, but instead chose an extraneous size word to follow. Please note, however, that the BSS size specified in the takes priority over the supplied size in the hunk.
3: Starting with AmigaOS 2.0, you can also use hunks to basically include the exact same information as above, but more compactly, using 16-bit words instead of 32-bit words. This is a space saving measure, since most segments for AmigaOS tend to be 64KiB or smaller.
4: It is technically illegal to place relocations after a BSS hunk. I’m not personally aware of any version of AmigaOS which will throw out such a binary, however. I suppose, if you’re crafty enough, you can use these relocations to pre-load pointers placed in BSS. I don’t recommend this, for obvious reasons; your program could well break on any revision of the operating system’s loader.
5: I think this is one of ELF’s worst features. A library should always be something that you depend upon; the library should never have to depend on you. When the latter happens, you really have a degenerate framework, and as we all know, frameworks are evil.
6: AT&T certainly did have control over the header format for their Unix distributions prior to their switch to COFF with Unix System V Release 1. However, looking at the Wikipedia article on COFF, I’m utterly bamboozled as to why AT&T even bothered with it, when even a little bit of research in the matter would have shown hunk format to have both existed and to have met most of their needs at the time. As I’ve illustrated above, adding a few different hunk types would have added the remaining semantics that it was missing. For some reason, this never crossed anyone’s mind. Why? I can only think that, like so many other computer-related companies at the time, AT&T Bell Labs suffered dearly from Not Invented Here Syndrome.
Source: Github.io
Powered by NewsAPI.org