GOT, `__stubs` and Linking
This is a version of the Procedure Link Tables article I wrote for x86_64, but for macOS and aarch64.
Lets start with a simple program
#include<stdio.h>
int moorthySir(int k) {
return k - 1;
}
int blarty = 46;
extern int malarchy(int);
extern int martyMcBoe;
int main() {
int a = martyMcBoe;
a += blarty;
printf("%d\n", a);
int mal = malarchy(683);
printf("%d\n", mal);
printf("%d\n", moorthySir(346));
return 0;
}
We have a small helper shared
library
int martyMcBoe = 895;
int malarchy(int b) {
return b * 4;
}
gcc -c -fpic helper.c
gcc -shared -o libhelper.so helper.o
Our main
depends on functionality from libhelper.so
(a global variable and a function). What problems are we solving when linking them together ?
- main.c might be compiled with other
.o
(object files) to create the final executable. If we rely on an hardcoded address forblarty
, when we create the final executables, this address will be invalid. This is because other.o
or libraries will also have statically initialized data and in the final executable, we will have only one.data
section that concatenates all the.data
sections of the object files. - We declare that we depend or need a global variable
martyMcBoe
and a fnmalarychy
(andprintf
). We don’t know which other object file or shared library will provide it, just that we need anint
martyMcBoe and aint -> int fn
namedmalarchy
.
So how do we figure out the right addresses to load our global variables from, or the right addresses to jump to ?
Enter relocations
.
Now let us look at relocations in the file.
objdump --reloc main.o
main.o: file format mach-o arm64
RELOCATION RECORDS FOR [__text]:
OFFSET TYPE VALUE
00000000000000b8 ARM64_RELOC_BRANCH26 _printf
00000000000000a0 ARM64_RELOC_BRANCH26 _moorthySir
0000000000000098 ARM64_RELOC_BRANCH26 _printf
0000000000000078 ARM64_RELOC_BRANCH26 _malarchy
0000000000000070 ARM64_RELOC_BRANCH26 _printf
0000000000000068 ARM64_RELOC_PAGEOFF12 l_.str
0000000000000064 ARM64_RELOC_PAGE21 l_.str
0000000000000044 ARM64_RELOC_PAGEOFF12 _blarty
0000000000000040 ARM64_RELOC_PAGE21 _blarty
0000000000000034 ARM64_RELOC_GOT_LOAD_PAGEOFF12 _martyMcBoe
0000000000000030 ARM64_RELOC_GOT_LOAD_PAGE21 _martyMcBoe
The offset
of a relocation depends on what kind of file we are looking at. In an object file, the offset
is an offset into the _text
section of the file and indicates what must be changed.
For example ARM64_RELOC_BRANCH26
_printf indicates that at an offset 70 into the file, we should do a relocation so that the code there can call the fn printf.
We can see that at 70 offset into _text
, there is indeed a bl
instructions there (similar to call/jmp in x86)
[Side Note] Relocations in macOS for global variables is a little tricky, which is why you will see 2 relocations for each global variable, such as _martyMcBoe
(in macOS, all names of C fns or variables is prefixed with an `_)
There is also another problem with aarch64. All instructions (including operands) in aarch64 are 32 bits wide. How do you then jump to another 64-bit address ?
This is how the linker does this on aarch64:
- Provide a 26-bit (+/- 128 MB) offset relative to the current instruction pointer (PC) as arg. to the
bl
instruction. - control jumps to that addres. There, we load the actual address of
printf
from a special location. - Once the actual address of
printf
is in a register, we then use thebr
instruction to jump to the 64-bit address in the register
Lets now compile our object file into an executable with the shared library
gcc -L. -g -o test main.c -lhelper
test
is the name of our executable. We can verify that all the relocations we needed are patched
100003f88: 08 00 00 94 bl 0x100003fa8 <_printf+0x100003fa8>
100003f8c: e0 13 40 b9 ldr w0, [sp, #16]
100003f90: fd 7b 42 a9 ldp x29, x30, [sp, #32]
100003f94: ff c3 00 91 add sp, sp, #48
100003f98: c0 03 5f d6 ret
Our executable first trampolines
into a __stubs
section. From there it knows how to call into printf
- Jump into the the trampoline in
__stubs
. - Load the address of
printf
from theGOT
(Global Offset Table) intox16
. - Jump to
printf
.
Disassembly of section __TEXT,__stubs:
0000000100003f9c <__stubs>:
100003f9c: 10 00 00 b0 adrp x16, 0x100004000 <__stubs+0x4>
100003fa0: 10 02 40 f9 ldr x16, [x16]
100003fa4: 00 02 1f d6 br x16
100003fa8: 10 00 00 b0 adrp x16, 0x100004000 <__stubs+0x10>
100003fac: 10 0a 40 f9 ldr x16, [x16, #16]
100003fb0: 00 02 1f d6 br x16
We can verify that the GOT
is at the address 0x100004000
objdump -s -j __got test
test: file format mach-o arm64
Contents of section __DATA_CONST,__got:
100004000 00000000 00001080 01000000 00001080 ................
100004010 02000000 00000080 ........
The dynamic linker knows which shared libraries (like libc
) our executable needs and loads the shared libraries into memory and maps the address of fns we need into the GOT.
Running this program in the debugger, we see that from __stubs
, we can jump into printf
without knowing the exact address of printf
test`printf:
-> 0x100003fb0 <+8>: br x16
0x100003fb4: .long 0x000a6425 ; unknown opcode
0x100003fb8: udf #0x1
0x100003fbc: udf #0x1c
Target 0: (test) stopped.
(lldb) register read x16
x16 = 0x000000018c7351d0 libsystem_c.dylib`printf
(lldb) disassemble
test`printf:
0x100003fa8 <+0>: adrp x16, 1
0x100003fac <+4>: ldr x16, [x16, #0x10]
-> 0x100003fb0 <+8>: br x16
(lldb) image lookup -n printf
1 match found in /usr/lib/system/libsystem_c.dylib:
Address: libsystem_c.dylib[0x00000001802bd1d0] (libsystem_c.dylib.__TEXT.__text + 192556)
Summary: libsystem_c.dylib`printf