How to get the GOT address from a PLT stub using GDB

3 days ago 2
1 November 2025

by Rafael Beirigo

  1. Overview
  2. Source code for the test program
  3. Dynamic analysis with gdb
  4. Summary

When we

  1. Use functions from shared libraries, like the puts,
  2. Opt for dynamic linking, and
  3. Opt for lazy binding,

the object code for puts is not included in the binary, but instead is linked at runtime. The linker adds a placeholder that will be patched at runtime with the real address of puts. That address is obtained by the dynamic linker from the shared library libc.so. But this is only done after the first call to puts (thus the lazy binding).

Moreover, when the program calls puts, it does so via a “trampoline”, in the form of a PLT stub. This stub is a short piece of code (3 instructions only) that runs everytime puts is called.

The first instruction jumps to the address currently in the placeholder (GOT slot). When the program starts, this address is the address of the next (second) instruction of the stub. See the illustration below.

[ main ]--. [ puts@plt ] [ puts in libc.so ] | +----------------+ +-------------------------------------------+ '-->| jmp *GOT[puts] |---. | push %r14 | | push <index> |<--' | push %r13 | | jmp dyn linker |---. | push %r12 | +----------------+ | | mov %rdi,%r12 | | | push %rbp | | | push %rbx | [ dynamic linker ]<-----' | sub $0x10,%rsp | | call 0x7ffff7dec110 <*ABS*+0x9f1b0@plt> | | ... | +-------------------------------------------+

The second instruction pushes an identifier for the dynamic linker, and the third jumps to run the dynamic linker itself.

The dynamic linker uses that identifier to fill the GOT slot with the real address of puts in libc.so. Then the program jumps to puts, which is executed, and the program resumes normal execution.

The next time puts is called, the first instruction jumps to the address in the GOT slot, which is the real address of puts. This runs puts, and resumes normal execution, and avoids further unnecessary calls to the resolver. See the illustration below.

[ main ]--. [ puts@plt ] [ puts in libc.so ] | +----------------+ +-------------------------------------------+ '-->| jmp *GOT[puts] |----->| push %r14 | | push <index> | | push %r13 | | jmp dyn linker | | push %r12 | +----------------+ | mov %rdi,%r12 | | push %rbp | | push %rbx | [ dynamic linker ] | sub $0x10,%rsp | | call 0x7ffff7dec110 <*ABS*+0x9f1b0@plt> | | ... | +-------------------------------------------+

Now let’s see it in action.

Here is the program we’ll use:

#include <stdio.h> int main() { puts("Hello, World!"); return 0; }

We compile it:

gcc -o hello hello.c

And examine with gdb:

gdb ./hello

We need to disassemble main to get the address of puts’ PLT stub. In order to get the adresses, we run the program. But first we add a breakpoint in main:

(gdb) break main Breakpoint 1 at 0x113d

Then run the program:

(gdb) run Starting program: /home/rafa/cybersec-dojo/_drafts/hello [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, 0x000055555555513d in main ()

We examine main to get puts’ PLT stub address. The symbol is aptly named puts@plt:

(gdb) disassemble main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp => 0x000055555555513d <+4>: lea 0xec0(%rip),%rax # 0x555555556004 0x0000555555555144 <+11>: mov %rax,%rdi 0x0000555555555147 <+14>: call 0x555555555030 <puts@plt> 0x000055555555514c <+19>: mov $0x0,%eax 0x0000555555555151 <+24>: pop %rbp 0x0000555555555152 <+25>: ret End of assembler dump.

We disassemble the stub. The first instruction is the jump to the address GOT points to.

(gdb) disassemble 0x555555555030 Dump of assembler code for function puts@plt: 0x0000555555555030 <+0>: jmp *0x2fca(%rip) # 0x555555558000 <[email protected]> 0x0000555555555036 <+6>: push $0x0 0x000055555555503b <+11>: jmp 0x555555555020 End of assembler dump.

We saw that the GOT’s address is 0x555555558000. To see the address it points to, we examine the contents of that memory address.

(gdb) x/gx 0x555555558000 0x555555558000 <[email protected]>: 0x0000555555555036

We see, that, in fact, this first time puts is being called, GOT points to the second instruction of puts’ PLT stub, puts@plt.

Let’s look at that address after puts has been called. We add a breakpoint right after the call to puts:

(gdb) break *0x000055555555514c Breakpoint 2 at 0x55555555514c

and continue execution.

(gdb) continue Continuing. Hello, World! Breakpoint 2, 0x000055555555514c in main ()

The program prints Hello, World!, showing that puts was in fact called. Now we examine the address GOT points to:

(gdb) x/gx 0x555555558000 0x555555558000 <[email protected]>: 0x00007ffff7e3d980

It changed. Let’s look at the code there:

(gdb) disassemble 0x00007ffff7e3d980 Dump of assembler code for function __GI__IO_puts: Address range 0x7ffff7e3d980 to 0x7ffff7e3db15: 0x00007ffff7e3d980 <+0>: push %r14 0x00007ffff7e3d982 <+2>: push %r13 0x00007ffff7e3d984 <+4>: push %r12 0x00007ffff7e3d986 <+6>: mov %rdi,%r12 0x00007ffff7e3d989 <+9>: push %rbp 0x00007ffff7e3d98a <+10>: push %rbx 0x00007ffff7e3d98b <+11>: sub $0x10,%rsp 0x00007ffff7e3d98f <+15>: call 0x7ffff7dec110 <*ABS*+0x9f1b0@plt> 0x00007ffff7e3d994 <+20>: mov 0x15b46d(%rip),%r13 # 0x7ffff7f98e08 0x00007ffff7e3d99b <+27>: mov %rax,%rbx 0x00007ffff7e3d99e <+30>: mov 0x0(%r13),%rbp 0x00007ffff7e3d9a2 <+34>: mov 0x0(%rbp),%eax 0x00007ffff7e3d9a5 <+37>: and $0x8000,%eax 0x00007ffff7e3d9aa <+42>: jne 0x7ffff7e3da00 <__GI__IO_puts+128> 0x00007ffff7e3d9ac <+44>: mov %fs:0x10,%r14 0x00007ffff7e3d9b5 <+53>: mov 0x88(%rbp),%rdx 0x00007ffff7e3d9bc <+60>: cmp %r14,0x8(%rdx) 0x00007ffff7e3d9c0 <+64>: je 0x7ffff7e3dab0 <__GI__IO_puts+304> 0x00007ffff7e3d9c6 <+70>: mov $0x1,%ecx 0x00007ffff7e3d9cb <+75>: lock cmpxchg %ecx,(%rdx) 0x00007ffff7e3d9cf <+79>: jne 0x7ffff7e3db00 <__GI__IO_puts+384> 0x00007ffff7e3d9d5 <+85>: mov 0x88(%rbp),%rdx 0x00007ffff7e3d9dc <+92>: mov 0x0(%r13),%rdi 0x00007ffff7e3d9e0 <+96>: mov %r14,0x8(%rdx) 0x00007ffff7e3d9e4 <+100>: mov 0xc0(%rdi),%eax 0x00007ffff7e3d9ea <+106>: addl $0x1,0x4(%rdx) 0x00007ffff7e3d9ee <+110>: test %eax,%eax 0x00007ffff7e3d9f0 <+112>: je 0x7ffff7e3da0d <__GI__IO_puts+141> 0x00007ffff7e3d9f2 <+114>: cmp $0xffffffff,%eax 0x00007ffff7e3d9f5 <+117>: je 0x7ffff7e3da17 <__GI__IO_puts+151> 0x00007ffff7e3d9f7 <+119>: mov $0xffffffff,%eax 0x00007ffff7e3d9fc <+124>: jmp 0x7ffff7e3da76 <__GI__IO_puts+246> 0x00007ffff7e3d9fe <+126>: xchg %ax,%ax 0x00007ffff7e3da00 <+128>: mov %rbp,%rdi 0x00007ffff7e3da03 <+131>: mov 0xc0(%rdi),%eax 0x00007ffff7e3da09 <+137>: test %eax,%eax 0x00007ffff7e3da0b <+139>: jne 0x7ffff7e3d9f2 <__GI__IO_puts+114> 0x00007ffff7e3da0d <+141>: movl $0xffffffff,0xc0(%rdi) 0x00007ffff7e3da17 <+151>: mov 0xd8(%rdi),%r14 0x00007ffff7e3da1e <+158>: lea 0x157fbb(%rip),%rdx # 0x7ffff7f959e0 <_IO_helper_jumps> 0x00007ffff7e3da25 <+165>: lea 0x158d1c(%rip),%rax # 0x7ffff7f96748 0x00007ffff7e3da2c <+172>: sub %rdx,%rax 0x00007ffff7e3da2f <+175>: mov %r14,%rcx 0x00007ffff7e3da32 <+178>: sub %rdx,%rcx 0x00007ffff7e3da35 <+181>: cmp %rax,%rcx 0x00007ffff7e3da38 <+184>: jae 0x7ffff7e3dac0 <__GI__IO_puts+320> 0x00007ffff7e3da3e <+190>: mov %rbx,%rdx 0x00007ffff7e3da41 <+193>: mov %r12,%rsi 0x00007ffff7e3da44 <+196>: call *0x38(%r14) 0x00007ffff7e3da48 <+200>: cmp %rax,%rbx 0x00007ffff7e3da4b <+203>: jne 0x7ffff7e3d9f7 <__GI__IO_puts+119> 0x00007ffff7e3da4d <+205>: mov 0x0(%r13),%rdi 0x00007ffff7e3da51 <+209>: mov 0x28(%rdi),%rax 0x00007ffff7e3da55 <+213>: cmp 0x30(%rdi),%rax 0x00007ffff7e3da59 <+217>: jae 0x7ffff7e3dad0 <__GI__IO_puts+336> 0x00007ffff7e3da5b <+219>: lea 0x1(%rax),%rdx 0x00007ffff7e3da5f <+223>: mov %rdx,0x28(%rdi) 0x00007ffff7e3da63 <+227>: movb $0xa,(%rax) 0x00007ffff7e3da66 <+230>: add $0x1,%rbx 0x00007ffff7e3da6a <+234>: mov $0x7fffffff,%eax 0x00007ffff7e3da6f <+239>: cmp %rax,%rbx 0x00007ffff7e3da72 <+242>: cmovbe %rbx,%rax 0x00007ffff7e3da76 <+246>: testl $0x8000,0x0(%rbp) 0x00007ffff7e3da7d <+253>: jne 0x7ffff7e3daa2 <__GI__IO_puts+290> 0x00007ffff7e3da7f <+255>: mov 0x88(%rbp),%rdi 0x00007ffff7e3da86 <+262>: mov 0x4(%rdi),%esi 0x00007ffff7e3da89 <+265>: lea -0x1(%rsi),%edx 0x00007ffff7e3da8c <+268>: mov %edx,0x4(%rdi) 0x00007ffff7e3da8f <+271>: test %edx,%edx 0x00007ffff7e3da91 <+273>: jne 0x7ffff7e3daa2 <__GI__IO_puts+290> 0x00007ffff7e3da93 <+275>: movq $0x0,0x8(%rdi) 0x00007ffff7e3da9b <+283>: xchg %edx,(%rdi) 0x00007ffff7e3da9d <+285>: cmp $0x1,%edx 0x00007ffff7e3daa0 <+288>: jg 0x7ffff7e3dae8 <__GI__IO_puts+360> 0x00007ffff7e3daa2 <+290>: add $0x10,%rsp 0x00007ffff7e3daa6 <+294>: pop %rbx 0x00007ffff7e3daa7 <+295>: pop %rbp 0x00007ffff7e3daa8 <+296>: pop %r12 0x00007ffff7e3daaa <+298>: pop %r13 0x00007ffff7e3daac <+300>: pop %r14 0x00007ffff7e3daae <+302>: ret 0x00007ffff7e3daaf <+303>: nop 0x00007ffff7e3dab0 <+304>: mov %rbp,%rdi 0x00007ffff7e3dab3 <+307>: jmp 0x7ffff7e3d9e4 <__GI__IO_puts+100> 0x00007ffff7e3dab8 <+312>: nopl 0x0(%rax,%rax,1) 0x00007ffff7e3dac0 <+320>: call 0x7ffff7e45c10 <_IO_vtable_check> 0x00007ffff7e3dac5 <+325>: mov 0x0(%r13),%rdi 0x00007ffff7e3dac9 <+329>: jmp 0x7ffff7e3da3e <__GI__IO_puts+190> 0x00007ffff7e3dace <+334>: xchg %ax,%ax 0x00007ffff7e3dad0 <+336>: mov $0xa,%esi 0x00007ffff7e3dad5 <+341>: call 0x7ffff7e48d00 <__GI___overflow> 0x00007ffff7e3dada <+346>: cmp $0xffffffff,%eax 0x00007ffff7e3dadd <+349>: jne 0x7ffff7e3da66 <__GI__IO_puts+230> 0x00007ffff7e3dadf <+351>: jmp 0x7ffff7e3d9f7 <__GI__IO_puts+119> 0x00007ffff7e3dae4 <+356>: nopl 0x0(%rax) 0x00007ffff7e3dae8 <+360>: mov %eax,0xc(%rsp) 0x00007ffff7e3daec <+364>: call 0x7ffff7e4c160 <__GI___lll_lock_wake_private> 0x00007ffff7e3daf1 <+369>: mov 0xc(%rsp),%eax 0x00007ffff7e3daf5 <+373>: jmp 0x7ffff7e3daa2 <__GI__IO_puts+290> 0x00007ffff7e3daf7 <+375>: nopw 0x0(%rax,%rax,1) 0x00007ffff7e3db00 <+384>: mov %rdx,%rdi 0x00007ffff7e3db03 <+387>: call 0x7ffff7e4c0b0 <__GI___lll_lock_wait_private> 0x00007ffff7e3db08 <+392>: jmp 0x7ffff7e3d9d5 <__GI__IO_puts+85> 0x00007ffff7e3db0d <+397>: mov %rax,%rbx 0x00007ffff7e3db10 <+400>: jmp 0x7ffff7dec7cc <__GI__IO_puts.cold> Address range 0x7ffff7dec7cc to 0x7ffff7dec801: 0x00007ffff7dec7cc <-332212>: testl $0x8000,0x0(%rbp) 0x00007ffff7dec7d3 <-332205>: jne 0x7ffff7dec7f9 <__GI__IO_puts-332167> 0x00007ffff7dec7d5 <-332203>: mov 0x88(%rbp),%rdi 0x00007ffff7dec7dc <-332196>: mov 0x4(%rdi),%eax 0x00007ffff7dec7df <-332193>: sub $0x1,%eax 0x00007ffff7dec7e2 <-332190>: mov %eax,0x4(%rdi) 0x00007ffff7dec7e5 <-332187>: jne 0x7ffff7dec7f9 <__GI__IO_puts-332167> 0x00007ffff7dec7e7 <-332185>: xor %edx,%edx 0x00007ffff7dec7e9 <-332183>: mov %rdx,0x8(%rdi) 0x00007ffff7dec7ed <-332179>: xchg %eax,(%rdi) 0x00007ffff7dec7ef <-332177>: sub $0x1,%eax 0x00007ffff7dec7f2 <-332174>: jle 0x7ffff7dec7f9 <__GI__IO_puts-332167> 0x00007ffff7dec7f4 <-332172>: call 0x7ffff7e4c160 <__GI___lll_lock_wake_private> 0x00007ffff7dec7f9 <-332167>: mov %rbx,%rdi 0x00007ffff7dec7fc <-332164>: call 0x7ffff7ded530 <_Unwind_Resume> End of assembler dump.

And it is in fact the code for puts!

  • GOT entries start pointing back into the PLT.
  • The dynamic resolver patches them with real libc addresses.
  • Subsequent calls jump directly to the resolved function.
tags: gdb - got - got/plt - dynamic-linking
Read Entire Article