Source: Practical Binary Analysis
Due to the default nature of how Linux dynamic linking works, two sections help dynamic resolution of the function calls in some shared libraries named after .plt and .got.
By default during the compile time lazy binding is opted by various compilers and some symbols are kept to be relocated later. Which are relocated when the program is run and encounters a unresolved symbol then the corresponding PLT stub is called.
For example, as seen in the above diagram when the program encounters some unresolved function which in our case is puts
, then the program is redirected to corresponding PLT section.
First instruction in PLT section just jumps to the address in GOT which does nothing fancy but redirect the code flow to the next instruction in the PLT section. The second instruction pushed 0x0 on the stack which just work as some identifier. The next instruction then redirect the flow to default stub in the PLT section.
The default stub pushes another identifier onto the stack [taken from got],identifying the executable then jumps to the dynamic linker for resolving the address for puts. Once the call is returned from linker then the entry in the .got.plt is updated so whenever the system encounter the call to puts then the address can be taken from .got.plt headers.
It would be simple to update the address into the .plt then why use .got at all? As .plt is an executable area so if some binary have a vulnerability then it would be too easy for an attacker to modify the code if these areas (.text and .plt) were writable. But as .got is a data section it's okay for someone to write to this section as compared to the code section.
This extra layer of redirection allows us to avoid writing to code sections.