This note is part of an effort to understand how a PC works. What happens from the writing of code until the actual physical comptation of inside the CPU? In particular:
Here we follow a simple approach. We write a little program and see how far we can dissect it.
We start with a tiny little program which we choose to write in C, because people claim it is nearest to the actual processor logic. We call it “simple.c”:
main(){
int a,b,c;
a=5;
b=8;
c = a+b;
}
We compile this program on a Linux box with the command
$ gcc simple.c
and get out an executable file a.out. This file is actually quiet long it already has 8446 bytes. Lets look inside:
$ hexedit a.out
We see that the file starts like that:
00000000 7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 .ELF............
00000010 02 00 3E 00 01 00 00 00 E0 03 40 00 00 00 00 00 ..>.......@.....
00000020 40 00 00 00 00 00 00 00 60 11 00 00 00 00 00 00 @.......`.......
and goes on and on for quiet a while. The output has the following structure:
What does ELF mean? A quick search on
wikipedia reveals, that there is a UNIX file format called
Executable and Linkable Format which has this very
abreviation. A call of file a.out
reassures that this wilde guess
might be correct.
The best reference on ELF files on the net I could find is the John R. Levine - Linkers and Loaders. Our treatment is similar to LinuxForums.org - ELF using readelf and objdump.
ELF is a container format for executable files suitable for the Linux operating system. Before a program can run on the CPU a loader or dynamic linker program (in our case ld.so) of the OS is called which servers the following tasks:
According to the generic format specification an ELF file consists of:
The ELF file is used by the loader and by the linker in two different ways:
Diving further into the specification we find that the headder consists of the following components:
The first column describes the bit length of the corresponding entries. The first line describes char=byte array of length 16, carrying the ELF Identification In our example this is:
7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00
These bytes have the following meaning:
Fortunately we do not need to do all this byte matching by hand. There is already a linux tool that does that for you:
$ readelf -h a.out
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4003e0
Start of program headers: 64 (bytes into file)
Start of section headers: 4448 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 28
What do we learn from this information:
We see that the file has the following structure (byte offsets in []-braces):
[0 - 63] File headder (64 bytes)
[64-120][..][511-567] Program headders (9*56 bytes)
[568-4447] ??? (4327 bytes)
[4448-4512][..][6368-6431] Section headders (31*64 bytes)
[6432-8445] ??? (3932 bytes)
Lets go ahead and instpect the section headders. Fortunately the byte matching to the reference greatly simplyfied by use ‘readelf –sections’. We see that there are 31 section headers, starting at offset 0x1160:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[..]
[12] .init PROGBITS 00000000004003a8 000003a8
0000000000000018 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 00000000004003c0 000003c0
0000000000000020 0000000000000010 AX 0 0 4
[14] .text PROGBITS 00000000004003e0 000003e0
00000000000001e8 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 00000000004005c8 000005c8
000000000000000e 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 00000000004005d8 000005d8
0000000000000004 0000000000000004 AM 0 0 4
[..]
[22] .dynamic DYNAMIC 0000000000600e40 00000e40
00000000000001a0 0000000000000010 WA 7 0 8
[..]
[25] .data PROGBITS 0000000000601008 00001008
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000601018 00001018
0000000000000010 0000000000000000 WA 0 0 8
[27] .comment PROGBITS 0000000000000000 00001018
0000000000000048 0000000000000001 MS 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00001060
00000000000000fe 0000000000000000 0 0 1
[29] .symtab SYMTAB 0000000000000000 00001920
0000000000000600 0000000000000018 30 47 8
[30] .strtab STRTAB 0000000000000000 00001f20
00000000000001de 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
From this table we first read off the ‘‘offsets’’ of the sections,
which is contained in the last column. The first section starts at
00000238 which is the hex code for 568. And indeed the first gap
[568-4447]
just starts at this point. Comparing the further offests
we see that the full gap is populated by the sections [1 .interp]
-
[28 .shstrtab]
. The latter gap [6432-8845]
is populated by
sections [29 .symtab]
-[30 .strtab]
.
Another information we can get from this table is that only the sections:
contain executable data.
To extract the information about from the Program header we call:
$ readelf --segments a.out
Elf file type is EXEC (Executable file)
Entry point 0x4003e0
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000067c 0x000000000000067c R E 200000
LOAD 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
0x0000000000000200 0x0000000000000210 RW 200000
DYNAMIC 0x0000000000000e40 0x0000000000600e40 0x0000000000600e40
0x00000000000001a0 0x00000000000001a0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000005dc 0x00000000004005dc 0x00000000004005dc
0x0000000000000024 0x0000000000000024 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 8
GNU_RELRO 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
0x00000000000001e8 0x00000000000001e8 R 1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .hash .gnu.hash
.dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn
.rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .ctors .dtors .jcr .dynamic .got
objdump
After identifying the executable parts of the program lets inspect these parts. We disassemble the parts using objdump.
$ objdump -d a.out
Disassembly of section .init:
00000000004003a8 <_init>:
4003a8: 48 83 ec 08 sub $0x8,%rsp
4003ac: e8 5b 00 00 00 callq 40040c <call_gmon_start>
4003b1: e8 ea 00 00 00 callq 4004a0 <frame_dummy>
4003b6: e8 d5 01 00 00 callq 400590 <__do_global_ctors_aux>
4003bb: 48 83 c4 08 add $0x8,%rsp
4003bf: c3 retq
Disassembly of section .plt:
00000000004003c0 <__libc_start_main@plt-0x10>:
4003c0: ff 35 2a 0c 20 00 pushq 0x200c2a(%rip)
4003c6: ff 25 2c 0c 20 00 jmpq *0x200c2c(%rip)
4003cc: 0f 1f 40 00 nopl 0x0(%rax)
00000000004003d0 <__libc_start_main@plt>:
4003d0: ff 25 2a 0c 20 00 jmpq *0x200c2a(%rip)
4003d6: 68 00 00 00 00 pushq $0x0
4003db: e9 e0 ff ff ff jmpq 4003c0 <_init+0x18>
Disassembly of section .text:
00000000004003e0 <_start>:
4003e0: 31 ed xor %ebp,%ebp
4003e2: 49 89 d1 mov %rdx,%r9
4003e5: 5e pop %rsi
4003e6: 48 89 e2 mov %rsp,%rdx
4003e9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
4003ed: 50 push %rax
4003ee: 54 push %rsp
4003ef: 49 c7 c0 f0 04 40 00 mov $0x4004f0,%r8
4003f6: 48 c7 c1 00 05 40 00 mov $0x400500,%rcx
4003fd: 48 c7 c7 c4 04 40 00 mov $0x4004c4,%rdi
400404: e8 c7 ff ff ff callq 4003d0 <__libc_start_main@plt>
400409: f4 hlt
40040a: 90 nop
40040b: 90 nop
000000000040040c <call_gmon_start>:
40040c: 48 83 ec 08 sub $0x8,%rsp
400410: 48 8b 05 c9 0b 20 00 mov 0x200bc9(%rip),%rax
400417: 48 85 c0 test %rax,%rax
40041a: 74 02 je 40041e <call_gmon_start+0x12>
40041c: ff d0 callq *%rax
...
0000000000400430 <__do_global_dtors_aux>:
400430: 55 push %rbp
400431: 48 89 e5 mov %rsp,%rbp
400434: 53 push %rbx
...
00000000004004a0 <frame_dummy>:
4004a0: 55 push %rbp
4004a1: 48 83 3d 8f 09 20 00 cmpq $0x0,0x20098f(%rip)
4004a8: 00
00000000004004c4 <main>:
4004c4: 55 push %rbp
4004c5: 48 89 e5 mov %rsp,%rbp
4004c8: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp)
4004cf: c7 45 f8 08 00 00 00 movl $0x8,-0x8(%rbp)
4004d6: 8b 45 f8 mov -0x8(%rbp),%eax
4004d9: 8b 55 fc mov -0x4(%rbp),%edx
4004dc: 8d 04 02 lea (%rdx,%rax,1),%eax
4004df: 89 45 f4 mov %eax,-0xc(%rbp)
4004e2: c9 leaveq
4004e3: c3 retq
...
00000000004004f0 <__libc_csu_fini>:
...
0000000000400500 <__libc_csu_init>:
...
0000000000400590 <__do_global_ctors_aux>:
...
Disassembly of section .fini:
00000000004005c8 <_fini>:
4005c8: 48 83 ec 08 sub $0x8,%rsp
4005cc: e8 5f fe ff ff callq 400430 <__do_global_dtors_aux>
4005d1: 48 83 c4 08 add $0x8,%rsp
4005d5: c3 retq
In this view the content of our executable sections grouped into machine operations in the middle column. The colum to the left is an offset counter and on the right we find a translation of the operations to the Assembly Language.
Observations:
< .... >
<_start>
function<main>
, which is also the name of the function in our c Progam.Let’s look inside the <main>
function:
push %rbp
mov %rsp,%rbp
movl $0x5,-0x4(%rbp)
movl $0x8,-0x8(%rbp)
mov -0x8(%rbp),%eax
mov -0x4(%rbp),%edx
lea (%rdx,%rax,1),%eax
mov %eax,-0xc(%rbp)
leaveq
We find our numbers 5 and 8 declared as constants ($) in hexadecimal
format. These are stored into a memory location specified by the %rbp
register with a certain offset. Then the values are read from memory
and copied into the %eax
and %edx
registers. Afterwards these
registers are added (lea
) and the result is stored in %eax
.
We can use the gnu debugger
gdb to see how the program
is executed. Call gdb a.out
to start a debuggiong session. Our
comments appear at after (#):
$ gdb a.out
GNU gdb (GDB) 7.1-ubuntu
Copyright (C) 2010 Free Software Foundation, Inc.
[...]
(gdb) b _start # Insert a breakpoint at function _start
Breakpoint 1 at 0x4003e0
(gdb) r # run the program
Starting program: /home/heinrich/Desktop/C_experiments/b.out
Breakpoint 1, 0x00000000004003e0 in _start ()
(gdb) stepi # one step forwards
0x00000000004003e2 in _start ()
Pressing [enter] now repeatedly we can step forwards trough the program. It will display the offset of the current instruction and the function being executed.
The following functions are called:
Now we show last command in assembly language using gdb:
(gdb) display/i $pc
=> 0x4003e2 <_start+2>: mov %rdx,%r9
(gdb) stepi
0x00000000004003e5 in _start ()
1: x/i $pc
=> 0x4003e5 <_start+5>: pop %rsi
(gdb) info registers # show contents of registers
rax 0x1c 28
rbx 0x0 0
rcx 0x7ffff7ffd040 140737354125376
rdx 0x7ffff7dec250 140737351959120
rsi 0x7ffff7df7a83 140737352006275
...
r8 0xb 11
r9 0x7ffff7dec250 140737351959120
r10 0xb 11
...
gs 0x0 0
We have been told, that the segments of the ELF file are loaded into the memory of the computer. Can we see how that really works?
A nice explanation of the physical lyout of the memory can be found inside the GDB documentation.
As explained in the note, each program assumes that it can access all the memory of the computer and starts writing it offset 0. This is of course not the case. The instead the OS provides reserved spaces inside the memory (virtual memory pages) which are typically 4kb in size Wikipedia - Virtual Memory.
The translation of the adresses to the physical location is made on the fly by a special coprocessor called Memory Mamagement Unit (MMU).
The OS binds these VMP to a process. Unless explicitly stated, no other process is allowed to read out this memory.
So how do whe memory pages look like that we got for our sweet little program? Linux helps us with that. We first run the program inside the debugger in order to have it in the memory.
$ gdb a.out
(gdb) b main
(gdb) r
Then we open another terminal and type (cf. [http://linux.die.net/man/5/proc man proc])
$ cat /proc/`pgrep a.out`/maps
and get:
Address Per. Offset Dev. inode. Path name
00400000-00401000 r-xp 00000000 08:05 21775994 /home/.../a.out
00600000-00601000 r--p 00000000 08:05 21775994 /home/.../a.out
00601000-00602000 rw-p 00001000 08:05 21775994 /home/.../a.out
7ffff7a5a000-7ffff7bd4000 r-xp 00000000 08:05 15341246 /lib/libc-2.11.1.so
7ffff7bd4000-7ffff7dd3000 ---p 0017a000 08:05 15341246 /lib/libc-2.11.1.so
7ffff7dd3000-7ffff7dd7000 r--p 00179000 08:05 15341246 /lib/libc-2.11.1.so
7ffff7dd7000-7ffff7dd8000 rw-p 0017d000 08:05 15341246 /lib/libc-2.11.1.so
7ffff7dd8000-7ffff7ddd000 rw-p 00000000 00:00 0
7ffff7ddd000-7ffff7dfd000 r-xp 00000000 08:05 15341231 /lib/ld-2.11.1.so
7ffff7fcf000-7ffff7fd2000 rw-p 00000000 00:00 0
7ffff7ff9000-7ffff7ffb000 rw-p 00000000 00:00 0
7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 0001f000 08:05 15341231 /lib/ld-2.11.1.so
7ffff7ffd000-7ffff7ffe000 rw-p 00020000 08:05 15341231 /lib/ld-2.11.1.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
An better readble version is produced by
$ cat /proc/`pgrep a.out`/smaps
but uses up too much space to reproduce it here. We need to compare this to the output of the segmet view:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000067c 0x000000000000067c R E 200000
LOAD 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
0x0000000000000200 0x0000000000000210 RW 200000
DYNAMIC 0x0000000000000e40 0x0000000000600e40 0x0000000000600e40
0x00000000000001a0 0x00000000000001a0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000005dc 0x00000000004005dc 0x00000000004005dc
0x0000000000000024 0x0000000000000024 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 8
GNU_RELRO 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
0x00000000000001e8 0x00000000000001e8 R 1
Comparing the VirtAddr colum to the Address colum above suggests, that the segments:
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
GNU_EH_FRAME 0x00000000000005dc 0x00000000004005dc 0x00000000004005dc
Are mapped into the the first memory page. And segments:
LOAD 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
DYNAMIC 0x0000000000000e40 0x0000000000600e40 0x0000000000600e40
GNU_RELRO 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
into the second one. The segment:
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
has only zero entries and seems to have something to do with the program stack, which has its own VM page:
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
We can use the GNU debugger to look inside these memory locations! Lets first display the beginning of the first memory page starting at 0x400000:
(gdb) x/24x 0x00400000
0x400000: 0x7f 0x45 0x4c 0x46 0x02 0x01 0x01 0x00
0x400008: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400010: 0x02 0x00 0x3e 0x00 0x01 0x00 0x00 0x00
Do you see what this is?? Precisely the beginning of the ELF file: ‘0x7f ELF’