I don't think the author of the book refers to the difference between the logical and physical addresses. The author refers to the difference between static and dynamic binding. In static binding, the addresses (both physical and logical) don't change during execution. In dynamic binding, the addresses can change during program execution (not the position of the program but the address referenced by the instructions). There is really no requirement or logic which says that, for some linking types, the physical addresses must be the same as the logical addresses or that they must be different.
Actually, a user mode program doesn't know anything about physical addresses. The OS really manages the whole thing without any requirement on where a logical address is translated. The memory allocator of the OS is the only one responsible to determine where a logical address is translated. It determines that by some algorithm such as the buddy algorithm. It selects a portion of physical memory and builds the page tables so that logical address translations arrive to that portion.
User mode programs really only care about logical addresses. They don't know anything about where they translate to. I think the author simply uses syntactic turns that make its phrases unclear. I think the author actually wishes to introduce the difference between static and dynamic binding and categorize the 3 binding types as either static or dynamic.
For binding/linking (I'll use linking for the rest of the answer because I think it refers to the same concept), there is mostly data linking and function linking. Your data is either static/global or local. For functions, they are either defined directly in the code or they are defined outside of it in a library which can itself be either dynamic or static. Functions can also be virtual. I'll give some examples of those using Linux and gcc on the x86 architecture so you can see an actual implementation.
Data is either static/global or local. Static/global data is simply placed in the data segment of the executable. This data is accessed using either PC-relative addressing or using absolute addresses. PC-relative doesn't care about the start address and makes the code position independent. GCC defaults to using PC-relative addressing unless you specify a large code model. PC-relative addressing is limited to a 32 bits offset from the code because of machine code limitations (the instruction only has 4 bytes for the offset). If your code is too large (some portion of it may be further away than a signed 4 bytes offset or 2GB), PC-relative doesn't work and you must use a large code model. For example, look at the following code:
int global = 0;
int main(){
global = 3;
}
It compiles to (without optimization):
main:
push rbp
mov rbp, rsp
mov DWORD PTR global[rip], 3
mov eax, 0
pop rbp
ret
Here, the compiler leaves an unresolved symbol for the linker. The linker (the one you call yourself when building your program), sees that unresolved symbol in the object of the program and resolves the offset from PC to put in the mov DWORD PTR global[rip], 3
instruction.
Local data is data found within a function without the static keyword. It is the most common type of data in a program because static/global data is often avoided to provide code modularity and manageability. Local data is accessed with an offset from the stack base pointer register. For example, take the following code:
int main(){
int local = 0;
local = 3;
}
It compiles to (again without optimization):
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 0
mov DWORD PTR [rbp-4], 3
mov eax, 0
pop rbp
ret
The code normally does 3 things:
- It pushes RBP;
- It moves RSP in RBP;
- It decrements RSP of the space required for the function.
In this case, the third element wasn't needed. The compiler optimized it away due to the simplicity of the program even though optimization is not enabled. Those 3 steps basically build what is called a stack frame. The stack frame is per function and every function has its own. It means that every function will do these 3 steps unless some of them can be optimized away.
Local data will thus be accessed by using a negative offset from RBP as you can see in the mov DWORD PTR [rbp-4], 0
and mov DWORD PTR [rbp-4], 3
instructions. Since RBP is found below the stack frame, the data within it is accessed using a negative offset from it. This whole concept of local data and its usage of the stack is often simply called automatic storage. Automatic storage is determined only by the compiler (the offsets from RBP, the amount of bytes to decrement RSP and the optimizations to do or not do).
For function linking, there are several cases. Functions can either be defined within the code you write or in a library (like the standard library) outside your program. They can also be virtual which is also a special case. For functions, dynamic vs static binding is really only determined by the virtual
keyword and is not found in C (only in C++).
The functions you define yourself within your program are called with a relative call
instruction. If your function is within the same object file as where it is called, the compiler simply calculates the relative offset right now. If the function is found in another object, the compiler leaves a symbol for the linker. The linker will thus combine all this code together (the different object files) and determine the resulting offsets to place in the call
instructions.
The functions you call outside your program are often found within a dynamic library (like it is the case for the C/C++ standard library implementations). When you call a function in a dynamic library, the compiler again leaves a symbol for the linker. The compiler passes several libraries (like the standard libraries such as libc or libstdc++) by default to the linker. The linker will thus find those unresolved symbols in those dynamic libraries. The linker will leave them unresolved for the dynamic linker to resolve them at load time of the program. The dynamic linker will thus place the dynamic libraries in the logical address space somewhere and resolve the resulting offsets in the code of your program.
Shared libraries can be positioned anywhere in the logical address space but the x64 call
instruction can actually contain a 64 bits immediate. It can thus call anywhere the function was resolved to be in the logical address space.
As to runtime linking, it is really only determined by the virtual
keyword in C++. For example, look at this code:
class A {
void f() { }
};
class B: public A {
void f() { }
};
void g(A& arg) {
arg.f();
}
int main() {
B x;
g(x);
}
The code will compile to:
A::f():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
g(A&):
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call A::f()
nop
leave
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
lea rax, [rbp-1]
mov rdi, rax
call g(A&)
mov eax, 0
leave
ret
Here, we have a class B that inherited the class A. A programmer would like that, when it calls the f() function, the right function is called based on the type of the object referenced. If the programmer calls f() with a reference to an object of type A, it would like that A::f() is called and vice-versa. This is called polymorphism. You have 2 definitions of the same function and would like that the right one is called based on the type of object referenced. To achieve this, you must make A::f() virtual. In the g() function above (looking at the assembly), A::f() is blindly called without looking at the type of the object referenced. The type of the object referenced cannot be determined before runtime because which object calls the g() function is not known yet. The type of the object referenced must be determined at runtime (during program execution). This is called runtime linking (execution-time address binding).
If you change the code to the following (simply adding the virtual keyword on A::f()):
class A {
public:
virtual void f() { }
};
class B: public A {
public:
void f() { }
};
void g(A& arg) {
arg.f();
}
int main() {
B x;
g(x);
}
The compiled g() function changes to:
g(A&):
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call rdx
nop
leave
ret
The g() function will determine which f() function to call based on the type of the object referenced instead of only on the type of the reference in the definition of g().