"The advantage of using relative mode over direct mode is that relative addressing is a code which is position-independent, i.e. it can be loaded anywhere in memory without the need to adjust any addresses."

I can't understand this part. why we need to adjust addresses in direct mode but not in the relative mode.

and please give a brief explanation of this part:

"Also, relative addressing is particularly useful in connection with jumps, because typical jumps are to nearby instructions."

• If you have code which references part of itself (or co-packaged data) by absolute address, and you instead load all of that into a different memory address, how could it still work? For part 2, what does it cost to store an address? An offset? – Chris Stratton Oct 17 '18 at 17:56

One reason why PC-relative jumps are advantageous is that they require fewer bits. A relative offset might be just 8 or 10 bits while a full, absolute address might be 32 bits. So, relative jumps take less memory in the instruction code. Since typical jumps are nearby, using relative jumps also makes the code smaller in addition to the advantage of relocatability.

Also, the offset for relative jumps can be computed at compile time, while the address for an absolute (direct) jump needs to be computed at link time. This makes building code with relative jumps a little faster.

Pretend I was your neighbour, who always lives one floor above yours. Even if you move, I will move after to a floor above yours. This means you'll easily find me all the time by walking a short distance, because you know my address relative to yours.

However, if I were to move away to somewhere random, the only way you could find me is by knowing my new absolute address. And every time I moved again, you would need to be updated on that address.

In this metaphor, you are a jump instruction that wants to execute a code block (me). "Moving" means code being reutilized or recompiled. Hope this helps.

PC-relative addressing allows position-independent code, which was historically important before virtual memory was common in microprocessors.

Consider how we often expect computer software to behave:

• Programs must exist in memory (ROM or RAM) in order to be executed by the processor. However, memory is expensive, so we often buy less memory and instead store programs on cheaper bulk storage devices (floppy disks, hard drives, FLASH drives, network storage), loading them into RAM as needed.

• Even if the cost of ROM and RAM is not an issue, the processor has a memory address space limited by the architecture. The space needed by all possible programs exceeds the limit of the address space.

• Many computer systems allow software to be added at a later time. This might be through compiling/assembling a new program on the system, or transferring software from another system using a bulk storage device. Purchased software is an example of the latter.

• At the time a computer system is built, we might not know which programs the user intends to run, or the order that the user will run them.

Some systems that have a dedicated purpose (e.g. a microwave oven, a spacecraft guidance computer) can be carefully designed to avoid all of the above issues. However, a general-purpose computer is going to have some (perhaps all) of the above issues. Clearly these issues mean that there are not enough addresses to assign every possible instruction of every possible program a fixed address (called "absolute addressing"). There are several ways to get around these issues:

1. No multitasking. Only one program runs on the system at a time, with complete control of how memory is used, until the program exits. Absolute addressing is okay here, because there are no other programs to compete for address space. This was acceptable for early computers, but today we expect computers to be able to multitask.

2. If physical memory (ROM and RAM) is inadequate, we can swap portions of the code from bulk storage to memory using an overlay. This is tricky to program, and is slow because we have to wait each time new code is loaded.

3. On the other hand, we can design a system with more ROM or RAM than will fit into the processor's address space, and then switch between parts of it with memory banking. This is also tricky to program, and the computers that used it were more expensive than those that did not.

4. Some processor architectures allow the value in a register to be used as the address for a jump or subroutine call instruction (register indirect). This means we can put our code anywhere in the address space, storing the start of our code in some register or well-defined location. When we need to make a jump or call, we add an offset to the start address of the code, and then jump or call to that result. It is a minimal solution, but is tedious to program, creates a larger program, and is fairly slow.

5. Once a program is loaded, the offset between different points in the code will be the same, even if the program has been relocated. Therefore, many architectures have an addressing mode relative to the program counter (PC-relative). Small offsets might even be possible to encode inside the instruction word, reducing code size. Because the processor is doing the hard work, it is easier to code and much faster than method #4 above. Most architectures use PC-relative instructions for conditional branches (because most branches tend to be small offsets), and many architectures also offer PC-relative conditional jumps and subroutine calls.

6. Another method is to have dedicated registers that are used as a base address for memory access. The code can be placed anywhere, and the base register is set to the start of the code. A jump or call instruction then specifies an offset, which the processor automatically adds to the base register. Other registers can specify the base address of the stack, data heap, and so on, making relocation of both code and data easy. This is the segmented memory model used in the 8086/8088 microprocessors of early PCs.

7. As processors evolved, they gained memory management units. These check each memory access, translating the logical address that a program sees into a physical address in actual memory. They can also load code and data from bulk storage, swap out data to bulk storage when necessary, and even prevent programs from accessing various parts of memory. This allows a program to be written as if it is the only program in memory, and not have to worry about other programs. As far as individual programs are concerned, we are back to the memory model of #1 above!

Two big Linux userland advantages of position independent code

• shared libraries can get loaded to any position in memory at runtime, e.g. through mmap

They must therefore necessarily be position independent.

It is not possible to fix the load address, otherwise there would be virtual memory conflicts between different shared libraries.

• if the main executable is position independent, the Linux kernel can also load it at random locations in memory which makes certain kinds of exploits harder.

GCC enables it with gcc -fPIE and that has become the default on Ubuntu at some point, including at least 18.04.

If you write assembly manually and try to link it with -fPIE link fails.