I suppose we're talking about x86 architecture.
You cannot have Self-Modifying Code in protected mode, which is used by most UNIX-based operating systems (and not only) that I'm aware of, because the code segments are always read-only. A loader does not control that -it is something that is being handled by the memory management subsystem of the kernel.
But even if you could "create the code for that table at load time" as you say, it would defy the whole purpose of shared libraries. This way, each process would have a "private" copy of the library's functions in its address space, effectively increasing its memory footprint -one of the reasons that shared libraries were created, was to address this issue.
The whole process you that you describe is quite complex, and it would cost more processing cycles than the PLT method that is used nowadays, and probably would introduce more, new & interesting security issues.
There’s one important piece of information missing from gdb
’s output: the pages’ permissions. (They’re shown on Solaris and FreeBSD, but not on Linux.) You can see those by looking at /proc/<pid>/maps
; the maps for your Protostar example show
$ cat /proc/.../maps
08048000-08049000 r-xp 00000000 00:0f 2925 /opt/protostar/bin/stack6
08049000-0804a000 rwxp 00000000 00:0f 2925 /opt/protostar/bin/stack6
b7e96000-b7e97000 rwxp 00000000 00:00 0
b7e97000-b7fd5000 r-xp 00000000 00:0f 759 /lib/libc-2.11.2.so
b7fd5000-b7fd6000 ---p 0013e000 00:0f 759 /lib/libc-2.11.2.so
b7fd6000-b7fd8000 r-xp 0013e000 00:0f 759 /lib/libc-2.11.2.so
b7fd8000-b7fd9000 rwxp 00140000 00:0f 759 /lib/libc-2.11.2.so
b7fd9000-b7fdc000 rwxp 00000000 00:00 0
b7fe0000-b7fe2000 rwxp 00000000 00:00 0
b7fe2000-b7fe3000 r-xp 00000000 00:00 0 [vdso]
b7fe3000-b7ffe000 r-xp 00000000 00:0f 741 /lib/ld-2.11.2.so
b7ffe000-b7fff000 r-xp 0001a000 00:0f 741 /lib/ld-2.11.2.so
b7fff000-b8000000 rwxp 0001b000 00:0f 741 /lib/ld-2.11.2.so
bffeb000-c0000000 rwxp 00000000 00:0f 0 [stack]
(The Protostar example runs in a VM which is easy to hack, presumably to make the exercises tractable: there’s no NX protection, and no ASLR.)
You’ll see above that what appears to be repeated mappings in gdb
actually corresponds to different mappings with different permissions. The text segment is mapped read-only and executable; the data segment is mapped read-only; BSS and the heap are mapped read-write. Ideally, the data segment, BSS and heap are not executable, but this example lacks NX support so they are executable. Each shared library gets its own mapping for its text segment, data segment and BSS. The fourth mapping is a non-readable, non-writable, non-executable segment typically used to guard against buffer overflows (although given the age of the kernel and C library used here this might be something different).
The offset, when given, indicates the offset of the data within the file, which doesn’t necessarily have much to do with its position in the address space. When loaded, this is subject to alignment constraints; for example, libc-2.11.2.so
’s program headers specify two “LOAD” headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x13d2f4 0x13d2f4 R E 0x1000
LOAD 0x13e1cc 0x0013f1cc 0x0013f1cc 0x027b0 0x0577c RW 0x1000
(Use readelf -l
to see this.)
These can result in multiple mappings at the same offset, with different virtual addresses, if the sections mapped to the segments have different protection flags. In stack6
’s case:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x00604 0x00604 R E 0x1000
LOAD 0x000604 0x08049604 0x08049604 0x00114 0x00128 RW 0x1000
(This also explains the small size shown by proc info mappings
for stack6
: each header requests less than 4KiB, with a 4KiB alignment, so it gets two 4KiB mappings with the same offset at different addresses.)
Blank mappings correspond to anonymous mappings; see man 5 proc
for details. You’d need to break on mmap
in gdb
to determine what they correspond to.
You can’t see the kernel mappings (apart from the legacy vsyscall
on some architectures) because they don’t matter from the process’s perspective (they’re inaccessible).
I don’t know of a better gdb
option, I always use /proc/$$/maps
.
See How programs get run: ELF binaries for details of the ELF format as read by the kernel, and how it maps to memory allocations; it has pointers to lots more reference material.
Best Answer
The following is a really good reference: http://www.ibm.com/developerworks/linux/library/l-dynamic-libraries/. It contains a bibliography at the end of a variety of different references at different levels. If you want to know every gory detail you can go straight to the source: http://www.akkadia.org/drepper/dsohowto.pdf. (Ulrich Drepper wrote the Linux dynamic linker.)
You can get a really good overview of all the sections in your executable by running a command like "objdump -h myexe" or "readelf -S myexe".
The .interp section contains the name of the dynamic loader that will be used to dynamically link the symbols in this object. The .dynamic section is a distillation of the program header that is formatted to be easy for the dynamic loader to read. (So it has pointers to all the other sections.)
The .got (Global Offset Table) and .plt (Procedure Linkage Table) are the two main structures that are manipulated by the dynamic linker. The .got is an indirection table for variables and the .plt is an indirection table for functions. Each executable or library (which are called "shared objects") has its own .got and .plt and these are tables of the symbols referenced by that shared object that are actually contained in some other shared object.
The .dynsyn contains all the information about the symbols in your shared object (both the ones you define and the external ones you need to reference.) The .dynsyn doesn't contain the actual symbol names. Those are contained in .dynstr and .dynsyn has pointers into .dynstr. .gnu.hash is a hash table used for quick lookup of symbols by name. It also contains only pointers (pointers into .dynstr, and pointers used for making bucket chains.)
When your shared object dereferences some symbol "foo" the dynamic linker has to go look up "foo" in all the dynamic objects you are linked against to figure out which one contains the "foo" you are looking for (and then what the relative address of "foo" is inside that shared object.) The dynamic linker does this by searching the .gnu.hash section of all the linked shared objects (or the .hash section for old shared objects that don't have a .gnu.hash section.) Once it finds the correct address in the linked shared object it puts it in the .got or .plt of your shared object.