ELF binary analysis static vs dynamic. How does assembly code| instruction memory mapping changes?

3

./hello is a simple echo program in c.
according to objdump file-headers,

$ objdump -f ./hello

./hello:     file format elf32-i386
architecture: i386, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x00000430

./hello has start address 0x430

Now loading this binary in gdb.

(gdb) file ./hello
Reading symbols from ./hello...(no debugging symbols found)...done.
(gdb) x/x _start
0x430 <_start>: 0x895eed31
(gdb) break _start
Breakpoint 1 at 0x430
(gdb) run
Starting program: /1/vooks/cdac/ditiss/proj/binaries/temp/hello 

Breakpoint 1, 0x00400430 in _start ()
(gdb) x/x _start
0x400430 <_start>:  0x895eed31
(gdb) 

in above output before setting breakpoint or running the binary, _start has address 0x430, but after running it, this address changes to 0x400430.

$ readelf -l ./hello | grep LOAD

 LOAD           0x000000 0x00000000 0x00000000 0x007b4 0x007b4 R E 0x1000
 LOAD           0x000eec 0x00001eec 0x00001eec 0x00130 0x00134 RW  0x1000

How this mapping happens?

Kindly Help.

c
x86
gdb
elf
objdump
asked on Stack Overflow Jan 21, 2019 by Ashish Rana • edited Jan 21, 2019 by Ashish Rana

2 Answers

2

Basically, after linking, ELF file format provide all necessary information for the loaders to load the program into memory and run it.

Each piece of code and data is placed within an offset inside a section, like data section, text section, etc. and access of specific function or global variable is done by adding the proper offset to the section start address.

Now, ELF file format also include program header table:

An executable or shared object file's program header table is an array of structures, each describing a segment or other information that the system needs to prepare the program for execution. An object file segment contains one or more sections, as described in "Segment Contents".

Those structures are then used by the OS loader to load the image to memory. The structure:

typedef struct {
        Elf32_Word      p_type;
        Elf32_Off       p_offset;
        Elf32_Addr      p_vaddr;
        Elf32_Addr      p_paddr;
        Elf32_Word      p_filesz;
        Elf32_Word      p_memsz;
        Elf32_Word      p_flags;
        Elf32_Word      p_align;
} Elf32_Phdr;

Note the following fields:

p_vaddr

The virtual address at which the first byte of the segment resides in memory

p_offset

The offset from the beginning of the file at which the first byte of the segment resides.

And p_type

The kind of segment this array element describes or how to interpret the array element's information. Type values and their meanings are specified in Table 7-35.

From Table 7-35, note PT_LOAD:

Specifies a loadable segment, described by p_filesz and p_memsz. The bytes from the file are mapped to the beginning of the memory segment. If the segment's memory size (p_memsz) is larger than the file size (p_filesz), the extra bytes are defined to hold the value 0 and to follow the segment's initialized area. The file size can not be larger than the memory size. Loadable segment entries in the program header table appear in ascending order, sorted on the p_vaddr member.

So, by looking at those fields (and more) the loader can locate the segments (which can contain multiple sections) within the ELF file, and load them (PT_LOAD) into memory at a given virtual address.

Now, can a virtual address of an ELF file segment be changed at runtime (load time)? yes:

The virtual addresses in the program headers might not represent the actual virtual addresses of the program's memory image. See "Program Loading (Processor-Specific)".

So, program header contains the segments the OS loader will load into memory (loadable segments, which contains loadable sections), but the virtual addresses the loader puts them can differ from the addresses in the ELF file.

How?

To understand it, lets first read about Base Address

Executable and shared object files have a base address, which is the lowest virtual address associated with the memory image of the program's object file. One use of the base address is to relocate the memory image of the program during dynamic linking.

An executable or shared object file's base address is calculated during execution from three values: the memory load address, the maximum page size, and the lowest virtual address of a program's loadable segment. The virtual addresses in the program headers might not represent the actual virtual addresses of the program's memory image. See "Program Loading (Processor-Specific)".

So the practice is the following:

position-independent code. This code enables a segment's virtual address change from one process to another, without invalidating execution behavior.

Though the system chooses virtual addresses for individual processes, it maintains the relative positions of the segments. Because position-independent code uses relative addressing between segments, the difference between virtual addresses in memory must match the difference between virtual addresses in the file.

So by using relative addressing, (PIE- position independent executable) the actual placement can differ from the address in the ELF file.

From PeterCordes's answer:

0x400000 is the Linux default base address for loading PIE executables with ASLR disabled (like GDB does by default).

So for your specific case (PIE executable in Linux) loader picks this base address.

Of course position independent is just an option. Program can be compiled without it, and than absolute addressing mode takes place, in which there must not be difference between segment address in ELF to the real memory address segment is loaded to:

Executable file segments typically contain absolute code. For the process to execute correctly, the segments must reside at the virtual addresses used to create the executable file. The system uses the p_vaddr values unchanged as virtual addresses.

I would recommend you to take a look at the linux implementation of elf image loading here, and those two SO threads here and here.

Paragraphs takes from Oracle ELF documents (here and here)

answered on Stack Overflow Jan 21, 2019 by user2162550 • edited Jan 22, 2019 by user2162550
2

You have a PIE executable (Position Independent Executable), so the file only contains an offset relative to the load address, which the OS chooses (and can randomize).

0x400000 is the Linux default base address for loading PIE executables with ASLR disabled (like GDB does by default).

If you compile with -m32 -fno-pie -no-pie hello.c to make a normal position dependent dynamically linked executable that can load from static locations with mov eax, [symname] instead of having to get EIP in a register and use that to do PC-relative addressing without x86-64 RIP-relative addressing modes, objdump -f will say:

./hello-32-nopie:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08048380                   # hard-coded load address, can't be ASLRed

instead of

architecture: i386, flags 0x00000150:   # some different flags set
HAS_SYMS, DYNAMIC, D_PAGED              # different ELF type
start address 0x000003e0

In a "regular" position-dependent executable, the linker chooses that base address by default, and does embed it in the executable. The OS's program loader does not get to choose for ELF executables, only for ELF shared objects. A non-PIE executable can't be loaded at any other address, so only their libraries can be ASLRed, not the executable itself. This is why PIE executables were invented.

A non-PIE is allowed to embed absolute addresses without any metadata that would let an OS try to relocate it. Or it's allowed to contain hand-written asm that takes advantage of whatever it wants to about the numeric values of addresses.


A PIE is an ELF shared object with an entry-point. Until PIEs were invented, ELF shared objects were usually only used for shared libraries. See 32-bit absolute addresses no longer allowed in x86-64 Linux? for more about PIEs.

They're quite inefficient for 32-bit code, I'd recommend not making 32-bit PIEs.


A static executable can't be a PIE, so gcc -static will create a non-PIE elf executable; it implies -no-pie. (So will linking with ld directly, because only gcc changed to making PIEs by default, gcc needs to pass -pie to ld to do that.)

So it's easy to understand why you wrote "static vs. dynamic" in your title, if the only dynamic executables you ever looked at were PIEs. But a dynamically linked non-PIE ELF executable is totally normal, and what you should be doing if you care about performance but for some reason want / need to make 32-bit executables.

Until the last couple years or so, normal binaries like /bin/ls in normal Linux distros were non-PIE dynamic executables. For x86-64 code, being PIE only slows them down by maybe 1% I think I've read. Slightly larger code for putting a static address in a register, or for indexing a static array. Nowhere near the amount of overhead that 32-bit code has for PIC/PIE.

answered on Stack Overflow Jan 22, 2019 by Peter Cordes • edited Jan 22, 2019 by Peter Cordes

User contributions licensed under CC BY-SA 3.0