ARM64 v8 PLT Stub Behavior and Dynamic Linking Overview
The issue at hand revolves around the inability to directly obtain the real address of a function (overwriteFunc
) in an ARM64 v8 architecture environment. When attempting to print the function address using &overwriteFunc
, the address returned corresponds to a Procedure Linkage Table (PLT) stub rather than the actual function address. The PLT stub is a small piece of code that facilitates dynamic linking, which is a mechanism used to resolve function addresses at runtime rather than at compile time. This behavior is typical in systems that use shared libraries or dynamically linked executables.
The disassembly of the PLT stub for overwriteFunc
reveals the following instructions:
0x0000000000423a90 <+0>: adrp x16, 0x513000
0x0000000000423a94 <+4>: ldr x17, [x16,#1640]
0x0000000000423a98 <+8>: add x16, x16, #0x668
0x0000000000423a9c <+12>: br x17
The adrp
instruction computes the base address of a 4KB page, and the ldr
instruction loads the real function address from a memory location into register x17
. The br
instruction then branches to the address contained in x17
. This two-step branching process is a fundamental aspect of how dynamic linking works on ARM64 v8.
The PLT is used to delay the resolution of function addresses until the function is actually called. This is particularly useful in systems where the same shared library might be used by multiple processes, allowing the library to be loaded at different addresses in each process. The PLT stub ensures that the correct address is resolved at runtime, which is why the address obtained via &overwriteFunc
points to the PLT stub rather than the actual function.
Dynamic Linking Mechanism and PLT Stub Initialization
The core of the issue lies in the dynamic linking mechanism employed by the ARM64 v8 architecture. When a program is compiled with dynamic linking, the compiler generates PLT stubs for functions that are defined in shared libraries. These stubs are small pieces of code that facilitate the resolution of the actual function addresses at runtime.
The PLT stub for overwriteFunc
is initialized by the dynamic linker (also known as the loader) when the program is executed. The dynamic linker is responsible for loading the shared libraries into memory and resolving the addresses of the functions defined in those libraries. The PLT stub contains instructions that load the real function address from the Global Offset Table (GOT) and then branch to that address.
The GOT is a table that stores the addresses of global variables and functions that are defined in shared libraries. When a function is called for the first time, the PLT stub loads the address of the function from the GOT and then branches to that address. If the address has not yet been resolved, the dynamic linker will resolve it and update the GOT accordingly. This mechanism allows the program to defer the resolution of function addresses until they are actually needed, which can improve startup time and reduce memory usage.
In the case of overwriteFunc
, the PLT stub loads the address of the function from the GOT into register x17
and then branches to that address. This is why the address obtained via &overwriteFunc
points to the PLT stub rather than the actual function. The actual function address is stored in the GOT, and the PLT stub is responsible for loading that address and branching to it.
Resolving Real Function Addresses in C++ on ARM64 v8
To obtain the real address of overwriteFunc
in C++ on an ARM64 v8 system, it is necessary to understand the underlying mechanisms of dynamic linking and the role of the PLT and GOT. One approach is to directly access the GOT to retrieve the real function address. However, this approach is highly platform-dependent and requires a deep understanding of the system’s memory layout and the specific implementation of the dynamic linker.
Another approach is to use the dladdr
function, which is part of the GNU C Library (glibc). The dladdr
function can be used to obtain information about a shared object that contains a given address. This function can be used to resolve the real address of a function by passing the address of the PLT stub to dladdr
. The function will return a Dl_info
structure that contains information about the shared object and the real address of the function.
Here is an example of how to use dladdr
to resolve the real address of overwriteFunc
:
#include <dlfcn.h>
#include <iostream>
void overwriteFunc(int, long, long) {
// Function implementation
}
int main() {
Dl_info info;
if (dladdr((void*)&overwriteFunc, &info)) {
std::cout << "Real address of overwriteFunc: " << info.dli_saddr << std::endl;
} else {
std::cerr << "Failed to resolve address of overwriteFunc" << std::endl;
}
return 0;
}
In this example, the dladdr
function is used to obtain information about the shared object that contains the address of overwriteFunc
. The Dl_info
structure contains the real address of the function in the dli_saddr
field. This approach is more portable and does not require direct manipulation of the GOT or PLT.
If modifying the compiler options is preferred, it is possible to link the program statically, which will eliminate the need for dynamic linking and PLT stubs. Static linking resolves all function addresses at compile time, so the address obtained via &overwriteFunc
will be the real function address. However, static linking can increase the size of the executable and may not be suitable for all applications.
To link the program statically, the -static
flag can be passed to the linker:
g++ -o my_program my_program.cpp -static
This will produce an executable that is statically linked, and the address of overwriteFunc
will be resolved at compile time rather than at runtime.
In summary, the issue of obtaining the real function address on ARM64 v8 is rooted in the dynamic linking mechanism and the use of PLT stubs. By understanding the role of the PLT and GOT, and by using tools like dladdr
or modifying compiler options, it is possible to resolve the real address of a function in a C++ program on ARM64 v8.