ELF Linker and Loader

Logan Chien

From Source Code to Execution

C compiler

gcc converts a C source file into an ELF object file (*.o)

gcc -c source.c -o source.o

Also known as relocatable files.

Static Library

ar combines several object files into a static library (*.a)

ar rcs libexample.a a.o b.o c.o

It is essentially an archive file with a symbol table.

Extract an object with ar x:

ar x libexample.a b.o

Linker

ld combines several object files and/or static libraries into an executable or a shared library (*.so).

ld -o a.out \
    --dynamic-linker /lib64/ld-linux-x86-64.so.2 \
    /usr/lib/x86_64-linux-gnu/crt1.o \
    /usr/lib/x86_64-linux-gnu/crti.o \
    source.o \
    /usr/lib/x86_64-linux-gnu/crtn.o

Also known as static linker or link editor

Linker

Linking object files with gcc (instead of ld) is easier.

gcc -o a.out source.o

gcc adds default linker flags and default crt*.o files.

gcc passes the options starts with -Wl to the linker.

gcc -Wl,-soname,libfoo.so ...
ld -soname libfoo.so ...

(comma are replaced by spaces)

Loader

A loader loads an executable into memory and relocates the fixups.

A loader can be bootloader (e.g. GRUB) or OS kernel (e.g. Linux and FreeBSD).

ELF shared objects will need a dynamic linker.

ELF File Format

Overview

ELF file format supports two scenarios:

(1) object files for static linkers

(2) binaries for loaders

Sections and Segments

An ELF file may have several sections and segments.

Compilers generate sections.

Linkers combine sections into segments.

Loaders load segments to memory.

Sections are recorded in section headers.

Segments are recorded in program headers.

ELF Header

struct Elf64_Ehdr {
    unsigned char e_ident[EI_NIDENT];  // magic, class, endianness
    Elf64_Half    e_type;     // relocatable | executable | shared objects
    Elf64_Half    e_machine;  // machine architecture
    Elf64_Word    e_version;
    Elf64_Addr    e_entry;    // start address
    Elf64_Off     e_phoff;    // offset to program headers
    Elf64_Off     e_shoff;    // offset to section headers
    Elf64_Word    e_flags;
    Elf64_Half    e_ehsize;
    Elf64_Half    e_phentsize;
    Elf64_Half    e_phnum;
    Elf64_Half    e_shentsize;
    Elf64_Half    e_shnum;
    Elf64_Half    e_shstrndx;
};

Dump ELF header with readelf -h

Section Header

struct Elf64_Shdr {
    Elf64_Word  sh_name;    // Section name
    Elf64_Word  sh_type;    // Section type
    Elf64_Xword sh_flags;   // WRITE | ALLOC | EXECINSTR
    Elf64_Addr  sh_addr;    // Section virtual address
    Elf64_Off   sh_offset;  // Section file offset
    Elf64_Xword sh_size     // Section size in bytes
    Elf64_Word  sh_link
    Elf64_Word  sh_info;
    Elf64_Xword sh_addralign;
    Elf64_Xword sh_entsize;
};

Dump section headers with readelf -S or objdump -h.

Program Header

struct Elf64_Phdr {
    Elf64_Word  p_type;    // PT_LOAD | PT_DYNAMIC | PT_INTERP | PT_PHDR
    Elf64_Word  p_flags;   // PF_R | PF_W | PF_X
    Elf64_Off   p_offset   // Segment file offset
    Elf64_Addr  p_vaddr;   // Segment virtual address
    Elf64_Addr  p_paddr;   // Segment physical address
    Elf64_Xword p_filesz;  // Segment size in file
    Elf64_Xword p_memsz;   // Segment size in memory
    Elf64_Xword p_align;
};

Dump program headers with readelf -l.

Relocatable files do not have program headers.

Header Example 1/4

static int bss_variable;

static int local_variable = 42;

static int local_function() {
    local_variable++;
    bss_variable++;
    return bss_variable + local_variable;
}

extern int printf(const char *fmt, ...);

int main() {
    printf("%d\n", local_function());
}

Header Example 2/4

gcc -c header.c && readelf -S header.o

[ 1] .text             PROGBITS         0000000000000000  00000040
     000000000000005a  0000000000000000  AX       0     0     1
[ 2] .rela.text        RELA             0000000000000000  000002e8
     00000000000000c0  0000000000000018   I      10     1     8
[ 3] .data             PROGBITS         0000000000000000  0000009c
     0000000000000004  0000000000000000  WA       0     0     4
[ 4] .bss              NOBITS           0000000000000000  000000a0
     0000000000000004  0000000000000000  WA       0     0     4
[ 5] .rodata           PROGBITS         0000000000000000  000000a0
     0000000000000004  0000000000000000   A       0     0     1
[10] .symtab           SYMTAB           0000000000000000  00000128
     0000000000000168  0000000000000018          11    12     8
[11] .strtab           STRTAB           0000000000000000  00000290
     0000000000000057  0000000000000000           0     0     1

Header Example 3/4

gcc -o header header.o && readelf -l header

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000001f8 0x00000000000001f8  R E    0x8
  INTERP         0x0000000000000238 0x0000000000000238 0x0000000000000238
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000008a8 0x00000000000008a8  R E    0x200000
  LOAD           0x0000000000000db8 0x0000000000200db8 0x0000000000200db8
                 0x000000000000025c 0x0000000000000268  RW     0x200000
  DYNAMIC        0x0000000000000dc8 0x0000000000200dc8 0x0000000000200dc8
                 0x00000000000001f0 0x00000000000001f0  RW     0x8

Header Example 4/4

gcc -o header header.o && readelf -l header

Section to Segment mapping:
 Segment Sections...
  00
  01     .interp
  02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr
         .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got
         .text .fini .rodata .eh_frame_hdr .eh_frame
  03     .init_array .fini_array .dynamic .got .data .bss
  04     .dynamic
  05     .note.ABI-tag .note.gnu.build-id
  06     .eh_frame_hdr
  07
  08     .init_array .fini_array .dynamic .got

Some sections are not mapped to fragments (e.g. .symtab and .strtab)

Symbols

Symbols are the names of functions, variables, or sections.

Defined symbols are defined in the ELF file and have section index.

Undefined symbols are not in the ELF file and must be resolved by linkers. Usually undefined symbols are referred by relocation entries.

Symbols

struct Elf64_Sym {
    Elf64_Word    st_name;   // Symbol name
    unsigned char st_info;   // Symbol type and binding
    unsigned char st_other;  // Symbol visibility
    Elf64_Half    st_shndx;  // Section index where the symbol resides
    Elf64_Addr    st_value;  // Symbol value (e.g. offset)
    Elf64_Xword   st_size;
};

Dump .symtab with readelf -s, objdump -t, and nm

Dump .dynsym with readelf -s, objdump -T, and nm -D.

Symbol Binding 1/3

Local symbols are only visible to itself (static functions or variables)

Global symbols are visible to other object files.

Weak symbols are similar to global symbols except that multiple definitions are accepted and undefined references are resolved to NULL.

Symbol Binding 2/3

static int local_variable = 42;

int global_variable = 43;

extern int undef_global_variable;

extern void undef_global_function(int x);

static void local_function() {
    undef_global_function(local_variable);
    undef_global_function(global_variable);
    undef_global_function(undef_global_variable);
}

void global_function() {
    local_function();
}

Symbol Binding 3/3

void weak_func() __attribute__((weak)) { }

void __attribute__((weak)) undef_weak_func();

inline void inline_func() { }

void use_weak_functions() {
    weak_func();

    if (undef_weak_func) {
        undef_weak_func();
    }

    inline_func();
}

template <typename T>
void cpp_template_function() { }

void use_template() {
    cpp_template_function<int>();
    cpp_template_function<char>();
}

Relocations 1/3

struct Elf64_Rel {
    Elf64_Addr r_offset;    /* Location to apply relocation */
    Elf64_Xword r_info;     /* Symbol index and relocation type */
}

struct Elf64_Rela {
    Elf64_Addr r_offset;    /* Location to apply relocation */
    Elf64_Xword r_info;     /* Symbol index and relocation type */
    Elf64_Sxword r_addend;  /* Addend */
};

Dump .rel and .rela with readelf -r or objdump -r

Dump .rel.dyn, .rela.dyn, with readelf -r or objdump -R

Relocations 2/3

static int local_variable = 42;

int global_variable = 43;

extern int undef_global_variable;

extern void undef_global_function(int x);

static void local_function() {
    undef_global_function(local_variable);
    undef_global_function(global_variable);
    undef_global_function(undef_global_variable);
}

void global_function() {
    local_function();
}

gcc -c symbol_binding.c && readelf -r symbol_binding.o

Relocations 3/3

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
0000000000000006 R_X86_64_PC32     .data-0x0000000000000004
000000000000000d R_X86_64_PLT32    undef_global_function-0x0000000000000004
0000000000000013 R_X86_64_PC32     global_variable-0x0000000000000004
000000000000001a R_X86_64_PLT32    undef_global_function-0x0000000000000004
0000000000000020 R_X86_64_PC32     undef_global_variable-0x0000000000000004
0000000000000027 R_X86_64_PLT32    undef_global_function-0x0000000000000004
0000000000000000 <local_function>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax
   a:	89 c7                	mov    %eax,%edi
   c:	e8 00 00 00 00       	callq  11 <local_function+0x11>
  11:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax
  17:	89 c7                	mov    %eax,%edi
  19:	e8 00 00 00 00       	callq  1e <local_function+0x1e>
  1e:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax
  24:	89 c7                	mov    %eax,%edi
  26:	e8 00 00 00 00       	callq  2b <local_function+0x2b>
  2b:	90                   	nop
  2c:	5d                   	pop    %rbp
  2d:	c3                   	retq

Link Executables

Link Static Libraries 2/3

gcc -L. main.o -lfoo -lbar

-L options are library search paths for static linker.

-l specifies the static libraris or shared libraries to be linked.

Link Static Libraries 2/3

gcc -L. main.o -lfoo -lbar

The order of static libraries is important. Linkers will search for undefined symbols from subsequent libraries.

Link errors will be raised if libbar.a depends on symbols defined in libfoo.a in this example.

Link Static Libraries 3/3

Linkers will only extract the object files which define the referenced symbols from archive files.

gcc *.o -L. \
    -Wl,--whole-archive
    -lfoo -lbar
    -Wl,--no-whole-archive

If a static library is specified between -Wl,--whole-archive and -Wl,--no-whole-archive, all object files will be extracted and included in the linked binary.

Linker script

OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu")
SECTIONS
{
  . = SEGMENT_START("text-segment", 0) + SIZEOF_HEADERS;
  .interp         : { *(.interp) }
  .rela.dyn       : { *(.rela.text .rela.text.*) }
  .text           : { *(.text .text.*) }
  . = DATA_SEGMENT_ALIGN (CONSTANT(MAXPAGESIZE), CONSTANT(COMMONPAGESIZE));
  .dynamic        : { *(.dynamic) }
  .data           : { *(.data .data.*) }
  .bss            : { *(.bss .bss.*) }
  _end = .; PROVIDE (end = .);
  . = DATA_SEGMENT_END (.);
}

Dump the linker script with gcc -fuse-ld=bfd -Wl,-t -Wl,--verbose.

Use your own linker script with -Wl,-T,[script-path].

Tricks to Save Spaces

Add -ffunction-sections and -fdata-sections to CFLAGS so that C/C++ compiler generates one section for each symbol.

And then, add -Wl,--gc-sections to LDFLAGS so that static linker will discard all unreferenced sections.

C++ Constructors and Destructors 1/3

C++ requires the global variables must be constructed before running main() and after running main the global variables must be destructed.

class Example {
public:
    Example() { fprintf(stderr, "construct %p\n", this); }
    ~Example() { fprintf(stderr, "destruct %p\n", this); }
};

Example a;
Example b;

C++ Constructors and Destructors 2/3

GCC adds __attribute__((constructor)) and __attribute__((destructor)) for initializers and finalizers in C.

void __attribute__((constructor)) my_init() {
    puts("my_init");
}

void __attribute__((destructor)) my_fini() {
    puts("my_fini");
}

C++ Constructors and Destructors 3/3

GCC generates .init_array and .fini_array sections which include function pointers to constructors and destructors.

Linker merges .init_array and .fini_array from object files.

__libc_csu_init calls the functions in .init_array.

__libc_csu_fini calls the functions in .fini_array.

These sections will be mapped to DT_INIT_ARRAY and DT_FINI_ARRAY in shared objects.

ELF Shared Objects

Three Different Types 1/2

Compilers generates relocatable files for static linker

Static linker may generate static executable files, which are self-contained at run-time.

Static linker may generate shared object files, which must be linked by dynamic linker at run-time.

Shared object files includes both shared libraries and dynamically-linked executables, which depend on shared libraries.

Three Different Types 2/2

ELF type can be found in the header:

struct Elf64_Ehdr {
    unsigned char e_ident[EI_NIDENT];  // magic, class, endianness
    Elf64_Half    e_type;     // relocatable | executable | shared objects

And can be checked with readelf -h:

$ readelf -h /bin/bash | grep Type
  Type:                              EXEC (Executable file)

$ readelf -h /usr/bin/python | grep Type
  Type:                              DYN (Shared object file)

$ readelf -h /lib/x86_64-linux-gnu/libc.so.6 | grep Type
  Type:                              DYN (Shared object file)
			

Position Dependent Code 1/2

(non-PIC code)

Position Dependent Code 2/2

#include <stdio.h>

int global_int = 0;

void put_int(int x) {
  printf("%d\n", x);
}

int main() {
  put_int(global_int);
}
$ gcc test.c -fno-PIC -static -m32
$ objdump -d a.out | sed '/\<main\>/,$!d' | head -n 18
080488a1 <main>:
 80488a1:	8d 4c 24 04          	lea    0x4(%esp),%ecx
 80488a5:	83 e4 f0             	and    $0xfffffff0,%esp
 80488a8:	ff 71 fc             	pushl  -0x4(%ecx)
 80488ab:	55                   	push   %ebp
 80488ac:	89 e5                	mov    %esp,%ebp
 80488ae:	51                   	push   %ecx
 80488af:	83 ec 04             	sub    $0x4,%esp
 80488b2:	a1 9c 6f 0d 08       	mov    0x80d6f9c,%eax
 80488b7:	83 ec 0c             	sub    $0xc,%esp
 80488ba:	50                   	push   %eax
 80488bb:	e8 c5 ff ff ff       	call   8048885 <put_int>
 80488c0:	83 c4 10             	add    $0x10,%esp
 80488c3:	b8 00 00 00 00       	mov    $0x0,%eax
 80488c8:	8b 4d fc             	mov    -0x4(%ebp),%ecx
 80488cb:	c9                   	leave
 80488cc:	8d 61 fc             	lea    -0x4(%ecx),%esp
 80488cf:	c3                   	ret
$ objdump -t a.out | grep global_int
080d6f9c g     O .bss	00000004 global_int

Position Independent Code 1/4

Why do we need position-independent code?

  1. Security enhancement with address space layout randomization
  2. Different shared library virtual address w/o text segment relocations

Position Independent Code 2/4

Shared libraries and address space

Position Independent Code 3/4

$ gcc test.c -fPIC -m32 -shared
$ objdump -d a.out | sed '/\<main\>/,$!d' | head -n 18
0000054b <main>:
 54b:	8d 4c 24 04          	lea    0x4(%esp),%ecx
 54f:	83 e4 f0             	and    $0xfffffff0,%esp
 552:	ff 71 fc             	pushl  -0x4(%ecx)
 555:	55                   	push   %ebp
 556:	89 e5                	mov    %esp,%ebp
 558:	53                   	push   %ebx
 559:	51                   	push   %ecx
 55a:	e8 2a 00 00 00       	call   589 <__x86.get_pc_thunk.ax>
 55f:	05 a1 1a 00 00       	add    $0x1aa1,%eax      // COMPUTE GLOBAL OFFSET TABLE ADDRESS
 564:	8b 90 f0 ff ff ff    	mov    -0x10(%eax),%edx  // LOAD global_int ADDRESS FROM GOT
 56a:	8b 12                	mov    (%edx),%edx       // LOAD global_int
 56c:	83 ec 0c             	sub    $0xc,%esp
 56f:	52                   	push   %edx
 570:	89 c3                	mov    %eax,%ebx
 572:	e8 79 fe ff ff       	call   3f0 <put_int@plt>  // JUMP TO PLT
 577:	83 c4 10             	add    $0x10,%esp
 57a:	b8 00 00 00 00       	mov    $0x0,%eax
 57f:	8d 65 f8             	lea    -0x8(%ebp),%esp
 582:	59                   	pop    %ecx
 583:	5b                   	pop    %ebx
 584:	5d                   	pop    %ebp
 585:	8d 61 fc             	lea    -0x4(%ecx),%esp
 588:	c3                   	ret
$ objdump -R pic.out
DYNAMIC RELOCATION RECORDS
00001ff0 R_386_GLOB_DAT    global_int@@Base
		
$ objdump -t a.out  | grep global_int
0000200c g     O .bss	00000004              global_int

Position Independent Code 4/4

-fPIC vs. -fPIE

$ gcc test.c -fPIE -m32 -shared
$ objdump -d a.out | sed '/\<main\>/,$!d' | head -n 18
0000052b <main>:
 52b:	8d 4c 24 04          	lea    0x4(%esp),%ecx
 52f:	83 e4 f0             	and    $0xfffffff0,%esp
 532:	ff 71 fc             	pushl  -0x4(%ecx)
 535:	55                   	push   %ebp
 536:	89 e5                	mov    %esp,%ebp
 538:	51                   	push   %ecx
 539:	83 ec 04             	sub    $0x4,%esp
 53c:	e8 24 00 00 00       	call   565 <__x86.get_pc_thunk.ax>
 541:	05 bf 1a 00 00       	add    $0x1abf,%eax     // LOAD DATA SEGMENT ADDRESS
 546:	8b 80 18 00 00 00    	mov    0x18(%eax),%eax  // LOAD global_int
 54c:	83 ec 0c             	sub    $0xc,%esp
 54f:	50                   	push   %eax
 550:	e8 fc ff ff ff       	call   551 <main+0x26>  // RELATIVE BRANCH
 555:	83 c4 10             	add    $0x10,%esp
 558:	b8 00 00 00 00       	mov    $0x0,%eax
 55d:	8b 4d fc             	mov    -0x4(%ebp),%ecx
 560:	c9                   	leave
 561:	8d 61 fc             	lea    -0x4(%ecx),%esp
 564:	c3                   	ret

C Start Up 1/3

execve() triggers these tasks:

Kernel loads the main executable, reads its program header, and maps its segments.

Kernel loads and maps the ELF interpreter (RTLD) specified in PT_INTERP.

Kernel fills the end of data segments (bss sections) with zeros.

Kernel initializes the stack pointer and builds the auxilary vector.

Kernel sets the program counter to the start address of RTLD.

Return to user mode (context switch)

Auxilary Vector

Auxilary vector holds some kernel information that are essential to the user program.

AT_BASE is the base address where RTLD is loaded.

AT_PHDR is the address of the program headers of the main executable.

AT_ENTRY is the start address of the main executable.

Debug tricks: LD_SHOW_AUXV=1 command

C Start Up 2/3

RTLD bootstraps itself.

RTLD loads the shared libraries specified in DT_NEEDED entries recursively.

RTLD maps the segments of the shared libraries with mmap().

RTLD relocates the entries in DT_REL or DT_RELA.

C Start Up 3/3

RTLD runs the DT_INIT and DT_INIT_ARRAY from every loaded shared objects.

RTLD branches to AT_ENTRY (the start address of the main executable).

Program Header (Recap)

struct Elf64_Phdr {
    Elf64_Word  p_type;    /* PT_LOAD | PT_DYNAMIC | PT_INTERP | PT_PHDR */
    Elf64_Word  p_flags;   /* PF_R | PF_W | PF_X */
    Elf64_Off   p_offset   /* Segment file offset */
    Elf64_Addr  p_vaddr;   /* Segment virtual address */
    Elf64_Addr  p_paddr;   /* Segment physical address */
    Elf64_Xword p_filesz;  /* Segment size in file */
    Elf64_Xword p_memsz;   /* Segment size in memory */
    Elf64_Xword p_align;
};

PT_LOAD

PT_LOAD maps a segment from the ELF file into the memory.

p_flags is the protection mode PF_R, PF_W, PF_X.

If a segment does not have PF_W, then the segment will be mapped with MAP_SHARED and PROT_READ.

If p_memsz is greater than p_filesz, initialize them to zero. This is the case for the .bss section in the data segment.

PT_INTERP

PT_INTERP keeps the path to the ELF interpreter.

ELF interpreter is the dynamic linker that loads and links shared libraries.

(also known as rtld or ldso)

Similar to the shebang #!/bin/bash in shell scripts.

Set PT_INTERP with -Wl,-dynamic-linker,[name]

-Wl,-dynamic-linker,/lib/ld-linux.so.2

ANALOGY: ld-linux.so is the dynamic linker and x86_64-linux-gnu-ld.gold is the static linker.

PT_DYNAMIC

PT_DYNAMIC refers the .dynamic section.

It contains several information for the dynamic linker.

ANALOGY: Dynamic linker's PT_DYNAMIC is static linker's LDFLAGS.

struct Elf64_Dyn {
    Elf64_Sxword  d_tag;  /* Tag */
    union {
        Elf64_Xword d_val;
        Elf64_Addr  d_ptr;
    } d_un;
};

Dumped these entries with readelf -d.

PT_DYNAMIC

readelf -d /usr/bin/python

Dynamic section at offset 0x2fc370 contains 32 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x000000000000000c (INIT)               0x4cd70
 0x0000000000000019 (INIT_ARRAY)         0x4fb2b0
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x0000000000000005 (STRTAB)             0xc948
 0x0000000000000006 (SYMTAB)             0x2b80
 0x0000000000000007 (RELA)               0x14568
 0x0000000000000008 (RELASZ)             224424 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x0000000000000000 (NULL)               0x0

DT_SONAME

DT_SONAME defines the library name that should be recorded by the user.

Link a shared library with -Wl,-soname,[name] and a DT_SONAME entry will be created in the .dynamic section

$ readelf -d /lib/x86_64-linux-gnu/libz.so.1 | grep SONAME
0x000000000000000e (SONAME)             Library soname: [libz.so.1]

DT_NEEDED

DT_NEEDED specifies the shared libraries that the output depends on.

When a shared library is specified by -l, the static linker will read soname from DT_SONAME and emit a DT_NEEDED entry with the soname.

$ readelf -d /usr/bin/python | grep NEEDED
0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
0x0000000000000001 (NEEDED)             Shared library: [libutil.so.1]
0x0000000000000001 (NEEDED)             Shared library: [libz.so.1]
0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]

ANALOGY: Dynamic linker's DT_NEEDED is static linker's -l.

DT_SONAME Purpose

Provide file-level shared library versioning.

$ ls -l /usr/lib/x86_64-linux-gnu/libxml2*
/usr/lib/x86_64-linux-gnu/libxml2.so -> libxml2.so.2.9.4
/usr/lib/x86_64-linux-gnu/libxml2.so.2 -> libxml2.so.2.9.4
/usr/lib/x86_64-linux-gnu/libxml2.so.2.9.4

DT_SONAME is libxml2.so.2 (without patch level).

If there are incompatible changes, change soname to libxml2.so.3 and re-link libxml2.so to libxml2.so.3.0.

As Needed

If a shared library -l[name] is specified between -Wl,--as-needed and -Wl,--no-as-needed, its soname won't be added to DT_NEEDED entries unless its symbols are referenced.

Linker discards unused shared libraries.

Unreferenced shared libraries with .init_array or .fini_array will be discarded as well.

DT_RUNPATH

DT_RPATH and DT_RUNPATH are library search paths for dynamic linker.

ANALOGY: Dynamic linker's DT_RUNPATH is static linker's -L.

Specify -Wl,-rpath,[path] in LDFLAGS.

A popular value is -Wl,-rpath,\${ORIGIN}/../lib.

DT_RUNPATH has a better run-time behavior.

Add -Wl,-enable-new-dtags to LDFLAGS to generate DT_RUNPATH.

Other Shared Lib Search Paths

Dynamic linker configuration files /etc/ld.so.conf* specify several default shared libraries search paths, e.g. /usr/lib/x86_64-linux-gnu.

Environment variable LD_LIBRARY_PATH may specify extra search paths (separated by :).

DT_SYMTAB and DT_REL

DT_SYMTAB points to the .dynsym section, which includes the undefined/defined symbols in an ELF shared object.

DT_RELA and DT_REL points to the .rela.dyn and .rel.dyn sections, which should be relocated by dynamic linker.

ANALOGY: Dynamic linker's DT_SYMTAB and DT_REL is static linker's .symtab and .rel*.

How Does RTLD Resolve Symbols? 1/2

DT_GNU_HASH (.gnu_hash section) is a hash table which maps symbol names to ELF symbol entries.

Multiple DT_GNU_HASH tables are linked together and become a link map.

Shared libraries loaded during C start up constitutes the global link map (sorted).

How Does RTLD Resolve Symbols? 2/2

  1. If the shared object has DT_SYMBOLIC (-Wl,-Bsymbolic), find the symbol defined in this shared object.
  2. Find the symbol in global link map
  3. Find the symbol defined in the shared object
  4. Find the symbol defined in the shared object specified by DT_NEEDED

Static Linker Symbol Checks

-Wl,--no-undefined emits errors when there are references to undefined symbols that can't be resolved.

-Wl,--no-allow-shlib-undefined emit errors when there are references (in the shared libraries specified as -lname) to undefined symbols.

Symbol visibility 1/4

How to control the exported symbols of a shared library?

LOCAL symbols are only visible within a file but GLOBAL symbols are exported by default.

HIDDEN symbols are GLOBAL symbols that can be referenced by other relocatable objects and the static linker will convert them to LOCAL symbol.

Add __attribute__((visibility("hidden"))) to the functions or variables that you would like to hide.

Compiler will set the visibility bits to HIDDEN in st_other field in Elf64_Sym.

Symbol Visibility 2/4

static int local_func() {
  return 1;
}

int __attribute__((visibility("hidden"))) hidden_func() {
  return 2;
}

int global_func() {
  return local_func() + hidden_func();
}

Symbol Visibility 3/4

gcc -c visibility.c && objdump -t visibility.o

visibility.o:     file format elf64-x86-64
0000000000000000 l     F .text	000000000000000b local_func
000000000000000b g     F .text	000000000000000b .hidden hidden_func
0000000000000016 g     F .text	0000000000000020 global_func

gcc -shared visibility.o && objdump -t a.out

a.out:     file format elf64-x86-64
00000000000005ca l     F .text	000000000000000b              local_func
00000000000005d5 l     F .text	000000000000000b              hidden_func
00000000000005e0 g     F .text	0000000000000020              global_func

Symbol visibility 4/4

To hide most symbols by default, you may specify -fvisibility=hidden.

You have to revert the effect with __attribute__((visibility("default"))).

Alternatively, you can hide inline function symbols with -fvisibility-inlines-hidden

Version script

Version script provides another way to control the exported symbols:

EXAMPLE_1_0 {
  global:
    foo;
    bar;
  local:
    *;
};

EXAMPLE_2_0 {
  global:
    example;
} EXAMPLE_1_0;

gcc -shared -Wl,--version-script,libexample.map example.c

THE END

Q & A