Showing posts with label elf. Show all posts
Showing posts with label elf. Show all posts

Tuesday, March 12, 2013

Understanding ELF using readelf and objdump, Linux, 2006, thanks to LinuxForums.org

What is ELF? ELF (Executable and Linking Format) is file formatthat defines how an object file is composed and organized. With thisinformation, your kernel and the binary loader know how to loadthe file, where to look for the code, where to look the initializeddata, which shared library that needs to be loaded and so on.

First of all, you should know about different kind of ELF object:

  • Relocatable file: an object file that holds code and data suitable for linking with other object files to create an executable or a shared object file. In other word, you can say that relocatable file is a foundation for creating executables and libraries.
    This is kind of file you get if you compile a source code like this:
    $ gcc -c test.cThat will produce test.o, which is a relocatable file.
    Kernel module (either suffixed with .o or .ko) is also a form of relocatable file.
  • Executable file: object file that holds a program suitable for execution. Yes, that means, your XMMS mp3 player, your vcd software player, even your text editor are all ELF executable files.
    This is also a familiar file if you compile a program:
    $ gcc -o test test.c
    After you make sure the executable bit of "test" is enabled, you can execute it. The question is, what about shell script? Shell script is NOT ELF executable, but the interpreter IS.
  • Shared object file: This file holds code and data suitable for linking in two contexts:
    1. The link editor may process it with other relocatable and shared shared object file to create another object file.
    2. The dynamic linker combines it with an executable file and other shared objects to create a process image.
    In simple words, these are the files that you usually see with suffix .so (normally located inside /usr/lib on most Linux installation).
Is there any other way to detect the ELF type? Yes there is. In every ELF object, there is a file header that explains what kind file it is. Assuming you have installed binutils package, you can use readelf to read this header. For example (command results are shortened to show related fields only):
$ readelf -h /bin/lsType: EXEC (Executable file)$ readelf -h /usr/lib/crt1.oType: REL (Relocatable file)$ readelf -h /lib/libc-2.3.2.soType: DYN (Shared object file)"File" command works too for object file identification, but I won't discuss itfurther. Let's focus on readelf and objdump, since we will use both of them.
To make us easier to study ELF, you can use the following simpleC program:
/* test.c */#includeint global_data = 4;int global_data_2;int main(int argc, char **argv){ int local_data = 3; printf("Hello Worldn"); printf("global_data = %dn", global_data); printf("global_data_2 = %dn", global_data_2); printf("local_data = %dn", local_data); return (0);}And compile it:
$ gcc -o test test.c
A. Examining ELF header.The produced binary will be our examination target. Let's start with the content of the ELF header:
$ readelf -h testELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x80482c0 Start of program headers: 52 (bytes into file) Start of section headers: 2060 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 7 Size of section headers: 40 (bytes) Number of section headers: 28 Section header string table index: 25What does this header tell us?
  • This executable is created for Intel x86 32 bit architecture ("machine" and "class" fields).
  • When executed, program will start running from virtual address 0x80482c0 (see entry point address). The "0x" prefix here means it is a hexadecimal number. This address doesn't point to our main() procedure, but to a procedure named _start. Never felt you had created such thing? Of course you don't. _start procedure is created by the linker whose purpose is to initialize your program.
  • This program has a total of 28 sections and 7 segments.
What is section? Section is an area in the object file that contains information which is useful for linking: program's code, program's data (variables, array, string), relocation information and other. So, in each area, several information is grouped and it has a distinct meaning: code section only hold code, data section only holds initialized or non-initialized data, etc. Section Header Table (SHT) tells us exactly what sections the ELF object has, but at least by looking on "Number of section headers" field above, you can tell that "test" contains 28 sections.
If section has meaning for the binary, our Linux kernel doesn't see it the same way. The Linux kernel prepares several VMA (virtual memory area) that contains virtually contigous page frames. Inside these VMA, one or more sections are mapped. Each VMA in this case represents an ELF segment. How the kernel knows which section goes to which segment? This is the function of Program Header Table(PHT).

(dead image link)

Figure 1. ELF structure in two different point of view.
{mospagebreak title=Examining Section Header Table(SHT)}B. Examining Section Header Table(SHT).Let's see what kind of sections that exist inside our program (output is shortened):
$ readelf -S testThere are 28 section headers, starting at offset 0x80c:
Section Headers:[Nr] Name Type Addr Off Size ES Flg Lk Inf Al........[ 4] .dynsym DYNSYM 08048174 000174 000060 10 A 5 1 4........[11] .plt PROGBITS 08048290 000290 000030 04 AX 0 0 4[12] .text PROGBITS 080482c0 0002c0 0001d0 00 AX 0 0 4........[20] .got PROGBITS 080495d8 0005d8 000004 04 WA 0 0 4[21] .got.plt PROGBITS 080495dc 0005dc 000014 04 WA 0 0 4........[22] .data PROGBITS 080495f0 0005f0 000010 00 WA 0 0 4[23] .bss NOBITS 08049600 000600 000008 00 WA 0 0 4........[26] .symtab SYMTAB 00000000 000c6c 000480 10 27 2c 4.........text section is a place where the compiler put executablescode. As the consequence, this section is marked as executable ("X" onFlg field). In this section, you will see the machine codes of ourmain() procedure:
$ objdump -d -j .text test-d tells objdump to diassembly the machine code and -j tellsobjdump to focus on specific section only (in this case, .text section)
08048370 :....... 8048397: 83 ec 08 sub $0x8,%esp 804839a: ff 35 fc 95 04 08 pushl 0x80495fc 80483a0: 68 c1 84 04 08 push $0x80484c1 80483a5: e8 06 ff ff ff call 80482b0 80483aa: 83 c4 10 add $0x10,%esp 80483ad: 83 ec 08 sub $0x8,%esp 80483b0: ff 35 04 96 04 08 pushl 0x8049604 80483b6: 68 d3 84 04 08 push $0x80484d3 80483bb: e8 f0 fe ff ff call 80482b0 ........data section hold all the initialized variable inside theprogram which doesn't live inside the stack. "Initialized" here meansit is given an initial value like we did on "global_data". How about"local_data"? No, local_data's value isn't in .data since it liveson process's stack.
Here is what objdump found about .data section:
$ objdump -d -j .data test.....080495fc : 80495fc: 04 00 00 00 .........One thing that we can conclude so far is that objdump kindly does address-to-symbol transformation for us. Without looking into symbol table, we know that 0x08049424 is the address of global_data. There, we clearly see that it is initialized with 4. Please note that common executables installed by most Linux distribution has been striped out, thus there is no entry in its symbol table. It makes objdump difficult to interpret the addresses.
And what is .bss? BSS (Block Started by Symbol) is a section where all unitialized variables are mapped. You might think "everything surely has an initial value". True, in Linux case, all unitialized variables are set as zero, that's why .bss section is just bunch of zeroes. For character type variables, that means null character. Knowing this fact, we know that global_data_2 is assigned 0 on runtime:
$ objdump -d -j .bss test-lagiDisassembly of section .bss:.....08049604 : 8049604: 00 00 00 00 .........Previously, we mentioned a bit about symbol table. This table is useful to find the correlation between a symbol name (non external function, variable) and an address. Using -s, readelf will decode the symbol table for you:
$ readelf -s ./testSymbol table '.dynsym' contains 6 entries: Num: Value Size Type Bind Vis Ndx Name..... 2: 00000000 57 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.0 (2).....Symbol table '.symtab' contains 72 entries: Num: Value Size Type Bind Vis Ndx Name..... 49: 080495fc 4 OBJECT GLOBAL DEFAULT 22 global_data..... 55: 08048370 109 FUNC GLOBAL DEFAULT 12 main..... 59: 00000000 57 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.0..... 61: 08049604 4 OBJECT GLOBAL DEFAULT 23 global_data_2....."Value" denotes the address of the symbol. For example, if an instruction refers to this address (e.g: pushl 0x80495fc), that means it refers to global_data. Printf() is treated differently, since it is a symbol that refers to an external function. Remember that printf is defined in glibc, not inside our program. Later, I will explain how our program calls printf.
C. Examining Program Header Table(PHT).Like I explained previously, segment is the way operating system "sees" our program. Thus, let's see how will our program be segmented:
$ readelf -l test.....There are 7 program headers, starting at offset 52Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align[00] PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4[01] INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1[02] LOAD 0x000000 0x08048000 0x08048000 0x004fc 0x004fc R E 0x1000[03] LOAD 0x0004fc 0x080494fc 0x080494fc 0x00104 0x0010c RW 0x1000[04] DYNAMIC 0x000510 0x08049510 0x08049510 0x000c8 0x000c8 RW 0x4[05] NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4[06] STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag 06Note:I add numbers on the left of each PHT entries to make the reader easier to study the section to segment mapping.The mapping is quite straightforward. For example, inside segment number 02, there are 15 sections mapped. .text section is mapped in this segment. Its flags are R and E, which means it is Readable and Executable. If you see W in segment's flag, that means it is writable.
By looking on "VirtAddr" column, we can discover the virtual start address of each segment. Back to the segment number #2, the start address is 0x08048000. Later in this section, we will discover that this address isn't the real address of the segment on memory. You can ignore the PhysAddr, because in Linux always operate in protected mode (on Intel/AMD 32 bit and 64 bit) thus virtual address is the thing that matters.
Segment has many types, but let's focus on two types:
  • LOAD: The segment's content is loaded from the executable file. "Offset" denotes the offset of the file where the kernel should start reading the file's content. "FileSiz" tells us how many bytes must be read from the file.
    For example,segment #2 is actually the content of the file starting from offset 0 to 4fc (offset+filesiz). To speed up the execution, the file's content is read on demand, thus it is only read from the disk if it is referenced at runtime.
  • STACK : The segment is stack area. Interesting to see that all the fields except "Flg" and "Align" are given 0. Is it an error? No, it is valid. It is the kernel's job to decide where the stack segment starts from and how big it is. Remember that on Intel compatible processor, stack grows downward (address is decremented each time a value is pushed).
Courious to see the real layout of process segment? We can use /proc//maps file to reveal it. is the PID of the process we want to observe. Before we move on, we have a small problem here. Our test program runs so fast that it ends before we can even dump the related /proc entry. I use gdb to solve this. You can use another trick such as inserting sleep() before it calls return().
In a console (or a terminal emulator such as xterm) do:
$ gdb test(gdb) b mainBreakpoint 1 at 0x8048376(gdb) rBreakpoint 1, 0x08048376 in main ()Hold right here, open another console and find out the PID of program "test". If you want the quick way, type:
$ cat /proc/`pgrep test`/mapsYou will see an output like below (you might get different output):

[1] 0039d000-003b2000 r-xp 00000000 16:41 1080084 /lib/ld-2.3.3.so[2] 003b2000-003b3000 r--p 00014000 16:41 1080084 /lib/ld-2.3.3.so[3] 003b3000-003b4000 rw-p 00015000 16:41 1080084 /lib/ld-2.3.3.so[4] 003b6000-004cb000 r-xp 00000000 16:41 1080085 /lib/tls/libc-2.3.3.so[5] 004cb000-004cd000 r--p 00115000 16:41 1080085 /lib/tls/libc-2.3.3.so[6] 004cd000-004cf000 rw-p 00117000 16:41 1080085 /lib/tls/libc-2.3.3.so[7] 004cf000-004d1000 rw-p 004cf000 00:00 0[8] 08048000-08049000 r-xp 00000000 16:06 66970 /tmp/test[9] 08049000-0804a000 rw-p 00000000 16:06 66970 /tmp/test[10] b7fec000-b7fed000 rw-p b7fec000 00:00 0[11] bffeb000-c0000000 rw-p bffeb000 00:00 0[12] ffffe000-fffff000 ---p 00000000 00:00 0
Note: I add number on each line as reference.Back to gdb, type:
(gdb) qSo, in total, we see 12 segment (also known as Virtual Memory Area--VMA). Focus on the first and the last field. First field denotes VMA address range, while last field shows the backing file. Do you see the similarity between VMA #8 and segment #02 listed in PHT? The difference is, SHT said it is ended on 0x080484fc, but on VMA #8, we see that it ends on 0x08049000. Same thing happens between VMA #9 and segment #03; SHT said it starts at 0x080494fc, while the VMA starts at 0x0804900.
There are several facts we must observe:
  1. Even though the VMA started on different address, the related sections are still mapped on exact virtual address.
  2. The kernel allocate memory on per page basis and the page size is 4KB. Thus, every page address is actually a multiple of 4KB e.g: 0x1000, 0x2000 and so on. So, for the first page of VMA #9, the page's address is 0x0804900. Or technically speaking, the address of the segment is rounded down (aligned) to the nearest page boundary.
Last, which one is the stack? That is VMA #11. Usually, the kernel allocate several pages dynamically and map to the highest virtual address possible in user space to form stack area. Simply speaking, each process address space is divided into two part (this assume Intel compatible 32 bit processor): user space and kernel space. User space is in 0x00000000-0xc0000000 range, while kernel space starts on 0xc0000000 onwards.
So, it is clear that stack is assigned address range near the 0xc0000000 boundary. The end address is static, while the start address is changing according to how many values are stored on stack.{mospagebreak title=How a function is referenced?}
D. How a function is referenced?If a program calls a function that resides within its own executable, all it has to do is simple: just call the procedure. But what happens if it calls something like printf() that is defined inside glibc shared library?
Here, I won't discuss deeply about how the dynamic linker really works, but I focus on how the calling mechanism is implemented inside the executable itself. With this assumption in mind, let's continue.
When a program wants to call a function, it actually does following flow:
  1. It made a jump to relevant entry in PLT (Procedure Linkage Table).
  2. In PLT, there is another jump to an address mentioned in related entry in GOT (Global Offset Table).
  3. If this is the first the function is called, follow step #4. If this isn't, follow step #5.
  4. The related GOT entry contains an address that points back to next instruction in PLT. Program will jump to this address and then calls the dynamic linker to resolve the function's address. If the function is found, its address is put in related GOT entry and then the function itself is executed.
    So, another time the function is called, GOT already holds its address and PLT can jump directly to the address. This procedure is called lazy binding; all external symbols are not resolved until the time it is really needed (in this case, when a function is called). Jump to step #6.
  5. Jump to the address mentioned in GOT. It is the address of the function thus PLT is no longer used.
  6. Execution of the function is finished. Jump back to the next instruction in the main program.
As always, looking inside the executable is the best way to explain it. If you do:
$ objdump -d -j .text testYou will see the following code fragment:
.....08048370 :..... 804838f: e8 1c ff ff ff call 80482b0What we have on 0x80482b0 is:
080482b0 : 80482b0: ff 25 ec 95 04 08 jmp *0x80495ec 80482b6: 68 08 00 00 00 push $0x8 80482bb: e9 d0 ff ff ff jmp 8048290 <_init x18="">As you see, the jump on 0x80482b0 is indirect jump ('*' in front of the address). So, to see where it will jump, we must peek into 0x80482b0. The guesses are, either this address is in .got section or in .got.plt. Looking back in SHT, it is clear that we must check .got.plt. I use readelf to do hexadecimal dump because it does number reordering for us:
$ readelf -x 21 testHex dump of section '.got.plt': 0x080495dc 080482a6 00000000 00000000 08049510 ................ 0x080495ec 080482b6 ....(Note: first column is virtual address. The data in this address is described at the 5th column, not the second one! So, from right to left, the address is in ascending order.)Bingo! We have "080482b6" here. In other word, we go back to PLT and there we eventually jump another address. This is where the work of the dynamic linker is started, so we will skip it. Assuming the dynamic linker has finished its magic work, the related GOT entry now holds the address of printf().
E. Alternative tool to inspect ELF structure.Besides counting on readelf and objdump, there is another tool called Beye. This is actually a file viewer but it is capable to parse the ELF structure.. You can grab the source from http://beye.sourceforge.net and compile it by yourself. Usually Beyeis included in hacking oriented Linux Live CD such as Phlak. Refer to the website and the packaged documents on how to compile and install Beye.
I personally like Beyebecause it offers curses based GUI display. Navigation between sections, checking ELF header, listing symbols and other tasks are now just a matter of pressing certain keyboard shortcut and you're done.
For example, you can list the symbols and directly jump to the symbol's address. Here, we try to jump to main(). First execute Beye:
$ beye testPress Ctrl+A followed by F7 to view symbol table. To avoid wasting time traversing the table, press again F7 to open "Find string" menu. Type "main" and press Enter. Once the highlighted entry is what you're looking for, simply press Enter and Beye will jump to the address of main(). Don't forget to switch to the disassembler mode (press F2 to select it) so you can see the high level interpretation of the opcodes.

(another dead image link)

Figure 2. Beye lists all symbols
Since we usually refer to virtual address, not file offset, it is better to switch to virtual address view. Press Ctrl+C followed by F6 and select "Local". Now, what you see in the leftmost column is the virtual address.
ConclusionThis article is just an overview on how to study ELF structure. Using readelf and objdump, you are ready to take your first journey. If needed, tool like Beye can help you to explore the binary internal faster. Use any arsenal you have, be creative and practice it regularly, then soon you can master the technique. Happy exploring.
Further reading
Rate This Article: poorexcellent 


Understanding ELF using readelf and objdump Linux article

'via Blog this'

Tuesday, July 10, 2012

The ELF Object File Format by Dissection | Linux Journal

The ELF Object File Format by Dissection | Linux Journal:


The ELF Object File Format by Dissection

 in
The Executable and Linking Format has been a popular topic lately, as people ask why the kernel configuration script asks whether or not to configure loading ELF executables. Since ELF will eventually be the common object file format for Linux binaries, it is appropriate to document it a bit. Last month, Eric introduced us to ELF, and this month, he gives us a guided tour of real ELF files.
Last month, we reached a point where were beginning to dissect some real ELF files. For this, I will use the readelfutility which I wrote when I was first trying to understand the ELF format itself. Later on, it became a valuable tool for debugging the linker as I added support for ELF. The sources to readelf should be on tsx-11.mit.edu in pub/linux/packages/GCC/src or in pub/linux/BETA/ibcs2.
Let us start with a very simple program—the hello world program we used last month.
largo% cat hello.c
main()
{
        printf("Hello World\n");
}
largo% gcc-elf -c hello.c
On my laptop, the gcc-elf command invokes the ELF version of gcc—once ELF becomes the default format, you will be able to use the regular gcc command which produces the ELF file hello.o. Each ELF file starts with a header (struct elfhdr in /usr/include/linux/elf.h), and the readelf utility can display the contents of all of the fields:
largo% readelf -h hello.o
ELF magic:7f 45 4c 46 01 01 01 00 00 00 00 00
        00 00 00 00
Type, machine, version = 1 3 1
Entry, phoff, shoff, flags = 0 0 440 0
ehsize, phentsize, phnum = 52 0 0
shentsize, shnum, shstrndx = 40 11 8
The ELF magic field is just a way of unambiguously identifying this as an ELF file. If a file does not contain those 16 bytes in the magic field, it is not an ELF file. The type, machine, and version fields identify this as an ET_REL file (i.e., an object file) for the i386. The ehsize field is just the sizeof(struct elfhdr).
Each ELF file contains a table that describes the sections within the file. The shnumfield indicates that there are 11 sections; the shoff field indicates that the section header table starts at byte offset 440 within the file. The shentsize field indicates that the entry for each section is 40 bytes long. All throughout ELF, the sizes of various structures are always explicitly stated. This allows for flexibility; the structures can be expanded as required for some hardware platforms and the standard ELF tools do not have to know about this to be able to make sense of the binary. Also, it allows room for future expansion of the structures by newer versions of the standard.
largo% readelf -S hello.o
There are 11 section headers, starting at offset 1b8:
[0]             NULL            00000000 00000 00000 00 / 0 0 0 0
[1] .text       PROGBITS        00000000 00040 00014 00 / 6 0 0 10
[2] .rel.text   REL             00000000 00370 00010 08 / 0 9 1 4
[3] .data       PROGBITS        00000000 00054 00000 00 / 3 0 0 4
[4] .bss        NOBITS          00000000 00054 00000 00 / 3 0 0 4
[5] .note       NOTE            00000000 00054 00014 00 / 0 0 0 1
[6] .rodata     PROGBITS        00000000 00068 0000d 00 / 2 0 0 1
[7] .comment    PROGBITS        00000000 00075 00012 00 / 0 0 0 1
[8] .shstrtab   STRTAB          00000000 00087 0004d 00 / 0 0 0 1
[9] .symtab     SYMTAB          00000000 000d4 000c0 10 / 0 a a 4
[a] .strtab     STRTAB          00000000 00194 00024 00 / 0 0 0 1
Listing 1. Section Table for hello.o
Each section header is just a struct ELF32_Shdr. You may notice that the name field is just a number—this is not a pointer, but an offset into the .shstrtab section (we can find the index of the .shstrtab section from the file header in the shstrndx field). Thus we find the name of each section at the specified offset within the .shstrtab section. Let us dump the section table for this file; see figure 1. You will notice sections for nearly everything which we have already discussed. Each section has an identifier which specifies what the section contains (in general, you should never have to actually know the name of a section or compare it to anything).
After the type, there is a series of numbers. The first of these is the address in virtual memory where this section should be loaded. Since this is a .o file, it is not intended to be loaded into virtual memory, and this field is not filled in. Next is the offset within the file of the section, and then is the size of the section. After this come a series of numbers—I won't parse these in detail for you, but they contain things like the required alignment of the section, a set of flags which indicate whether the section is read-only, writable, and/or executable.
The readelf program is capable of performing disassembly:
largo% readelf -i 1 hello.o
0x00000000  pushl       %ebp
0x00000001  movl        %esp,%ebp
0x00000003  pushl       $0x0
0x00000008  call        0x08007559
0x0000000d  addl        $4,%esp
0x00000010  movl        %ebp,%esp
0x00000012  popl        %ebp
0x00000013  ret
The .rel.text section contains the relocations for the .text section of the file, and we can display them as follows:
largo% readelf -r hello.o
Relocation section data:.rel.text (0x2 entries)
Tag: 00004 Value 00301 R_386_32    (0 )
Tag: 00009 Value 00b02 R_386_PC32  (0 printf)
This indicates that the .text section has two relocations. As expected, there is a relocation for printf indicating that we must patch the address of printf into offset 9 from the beginning of the .text section, which happens to be the operand of the call instruction. There is also a relocation so that we pass the correct address to printf.
Now let us see what happens when this file is linked into an executable. The section table now looks something like Listing 2.
The first thing you will notice is a lot more sections than were in the simple .o file. Much of this because this file requires the ELF shared library libc.so.1.
At this point I should mention the mechanics of what happens when you run an ELF program. The kernel looks through the binary and loads it into the user's virtual memory. If the application is linked to a shared library, the application will also contain the name of the dynamic linker that should be used. The kernel then transfers control to the dynamic linker, not to the application. The dynamic loader is responsibile for first initializing itself, loading the shared libraries into memory, resolving all remaining relocations, and then transferring control to the application.
Going back to our executable, the .interp section simply contains an ASCII string that is the name of the dynamic loader. Currently this will always be /lib/elf/ld-linux.so.1 (the dynamic loader itself is also an ELF shared library).
Next you will notice 3 sections, called .hash, .dynsym, and .dynstr. This is a minimal symbol table used by the dynamic linker when performing relocations. You will notice that these sections are mapped into virtual memory (the virtual address field is non-zero). At the very end of the image are the regular symbol and string tables, and these are not mapped into virtual memory by the loader. The .hash section is just a hash table that is used so that we can quickly locate a given symbol in the .dynsym section, thereby avoiding a linear search of the symbol table. A given symbol can typically be located in one or two tries through the use of the hash table.
The next section I want to mention is the .plt section. This contains the jump table that is used when we call functions in the shared library. By default the .plt entries are all initialized by the linker not to point to the correct target functions, but instead to point to the dynamic loader itself. Thus, the first time you call any given function, the dynamic loader looks up the function and fixes the target of the .plt so that the next time this .plt slot is used we call the correct function. After making this change, the dynamic loader calls the function itself.
This feature is known as lazy symbol binding. The idea is that if you have lots of shared libraries, it could take the dynamic loader lots of time to look up all of the functions to initialize all of the .plt slots, so it would be preferable to defer binding addresses to the functions until we actually need them. This turns out to be a big win if you only end up using a small fraction of the functions in a shared library. It is possible to instruct the dynamic loader to bind addresses to all of the .plt slots before transferring control to the application—this is done by setting the environment variable LD_BIND_NOW=1 before running the program. This turns out to be useful in some cases when you are debugging a program, for example. Also, I should point out that the .plt is in read-only memory. Thus the addresses used for the target of the jump are actually stored in the .got section. The .got also contains a set of pointers for all of the global variables that are used within a program that come from a shared library.
The .dynamic section contains some shorthand notes used by the dynamic loader. You will notice that the section table is not itself loaded into virtual memory, and in fact it would not be good for performance for the dynamic loader to have to try to parse it to figure out what needs to be done. The .dynamic section is essentially just a distilled version of the section header table that contains just what is needed for the dynamic loader to do its job.
You will notice that since the section header table is not loaded into memory, neither the kernel nor the dynamic loader will be able to use that table when loading files into memory. A shorthand table of program headers is added to provide a distilled version of the section table containing just the information required to load a file into memory. For the above file it looks something like:
largo% readelf -l hello
Elf file is Executable
Entry point 0x8000400
There are 5 program headers, starting at offset 34:
PHDR       0x00034 0x08000034 0x000a0 0x000a0 R E
Interp     0x000d4 0x080000d4 0x00017 0x00017 R
Requesting program interpreter \
[/lib/elf/ld-linux.so.1]
Load       0x00000 0x08000000 0x00515 0x00515 R E
Load       0x00518 0x08001518 0x000cc 0x000d4 RW
Dynamic    0x0054c 0x0800154c 0x00098 0x00098 RW
Shared library: [libc.so.4] 1
As you can see, the program header contains a pointer to the name of the dynamic loader, instructions on what portions of the file are to be loaded into virtual memory (and the virtual addresses they should be loaded to), the permissions of the segments of memory, and finally a pointer to the .dynamic section that the dynamic loader will need. Note that the list of required shared libraries is stored in the .dynamic section.
I will not pick apart an ELF shared library for you here—libraries look quite similar to ELF executables. If you are interested, you can get the readelf utility and pick apart your own libraries.
At the start of this article, I said one reason we were switching to ELF was that it was easier to build shared libraries with ELF. I will now demonstrate how. Consider two files:
largo% cat hello1.c
main()
{
        greet();
}
largo% cat english.c
greet()
{
        printf("Hello World\n");
}
The idea is that we want to build a shared library from english.c, and link hello1 against it. The commands to generate the shared library are:
largo% gcc-elf -fPIC -c english.c
largo% gcc-elf -shared -o libenglish.so english.o
That's all there is to it. Now we compile and link the hello1 program:
largo% gcc-elf -c hello1.c
largo% gcc-elf -o hello1 hello1.o -L. -lenglish
And finally we can run the program. Normally the dynamic loader only looks in certain locations for shared libraries, and the current directory is not one of the places it normally looks. Thus to run the program, you can use a command like:
largo% LD_LIBRARY_PATH=. ./hello1
Hello World
The environment variable LD_LIBRARY_PATH tells the dynamic loader to look in additional places for the shared libraries (this feature is disabled for setuid programs for security reasons).
To avoid having to specify LD_LIBRARY_PATH, you have several options. You could copy your shared library to /lib/elf, but you can also link your program in the following way:
largo% gcc-elf -o hello1 hello1.o\ /home/joe/libenglish.so
largo% ./hello1
Hello World
To build more complicated shared libraries, the procedure is not really that much different. Everything that you want to put into the shared library should be compiled with -fPIC; when you have compiled everything, you just link it all together with thegcc -shared command.
The procedure is so much simpler mainly because we bind addresses to functions at runtime. With a.out libraries, the addresses are bound at link time. This means that lots of special care must be taken to ensure that the .plt and .got have sufficient room for future expansion and that we keep the variables at the same addresses from one version of the library to the next. The tools for building a.out libraries help ensure all of this, but it makes the build procedure much more complicated.
ELF offers one further feature that is not easily available with a.out. The dlopen()function can be used to dynamically load a shared library into the user's memory, and you are then able to call the dynamic loader to find symbols within this shared library—in other words, you can call functions that are defined in these modules. In addition, the dynamic loader is used to resolve any undefined symbols within the module itself.
This may be easiest to explain with an example. Given the following source file:
#include 
main(int argc, char * argv[])
{
   void (*greeting)();
   void * module;
   if( argc > 2 ) exit(0);
   module = dlopen(argv[1], RTLD_LAZY);
  if(!module) exit(0);
   greeting = dlsym(module, "greet");
   if(greeting) {
     (*greeting)();
   }
  dlclose(module);
 }
you can compile, link, and run it (using the shared library english.so which was built earlier):
largo% gcc-elf -c hello2.c
largo% gcc-elf -o hello2 hello2.o -ldl
largo% ./hello2 ./libenglish.so
Hello World
To expand this example a little bit, you could generate other modules with greetings in other languages. Thus in theory, one could add multi-lingual support for some application merely by supplying a set of shared libraries that contain the language-specific portions of the application. In the above example, I showed how you can locate the address of a function within a shared library. But the dlsym() function will also return the address of data variables, so you could just as easily retrieve the address of a text string from the shared library.
As I prepare to close, I should mention some options to readelf which I have not demonstrated. readelf -s dumps the symbol tables and readelf -f dumps the .dynamic section.
Finally, I should mention something about the timetable. When we first got ELF to a point where it was usable (last September), we decided to spend a relatively long period of time testing it and shaking out all of the problems. Back then I felt that roughly 4-to-6 months would allow people to test it thoroughly, plus we wanted to give an opportunity for certain applications to be adapted for ELF (the most recent versions of insmod and Wine now support ELF, for example). As I write this, no firm date has been set for a public release, but it is possible that ELF will be public by the time you read this.
In these articles I have attempted to give you a guided introduction to the ELF file format. A lot of the material I have covered is not of much practical value to most users (unless you want to hack the linker), but my experience is that there are a lot of people who are curious about how it all works, and I hope that I have provided enough information to satisfy most people.
For more information about the ELF file format, you can obtain the ELF specifications from a number of sources—you can try ftp.intel.com in pub/tis/elf11g.zip. The specifications are also available in a printed format. See SYSTEM V Application Binary Interface (ISBN 0-13-100439-5) and SYSTEM V Application Binary Interface, Intel386 Architecture Processor Supplement (ISBN 0-13-104670-5).
Eric Youngdale has worked with Linux for over three years, and has been active in kernel development. He developed the current a.out Linux shared libraries before developing much of the new ELF support. He can be reached as eric@aib.com.

'via Blog this'